206 lines
5.7 KiB
Python
206 lines
5.7 KiB
Python
from datetime import datetime, timedelta
|
|
|
|
from sqlalchemy import select
|
|
|
|
from app.auth.services import create_user
|
|
from app.tasks.models import Task
|
|
from app.tasks.services import (
|
|
add_subtask,
|
|
create_task,
|
|
list_today_tasks,
|
|
toggle_subtask,
|
|
)
|
|
|
|
|
|
def create_account_and_login(client, suffix: str = "") -> None:
|
|
email = f"person{suffix}@example.com"
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": f"steady_user{suffix}",
|
|
"email": email,
|
|
"password": "a calm secure password",
|
|
"confirm_password": "a calm secure password",
|
|
},
|
|
)
|
|
client.post(
|
|
"/auth/login",
|
|
data={"email": email, "password": "a calm secure password"},
|
|
)
|
|
|
|
|
|
def task_form_data(**overrides):
|
|
data = {
|
|
"title": "Write a gentle plan",
|
|
"description": "Start with one paragraph.",
|
|
"status": "not_started",
|
|
"priority": "normal",
|
|
"due_date": "",
|
|
}
|
|
data.update(overrides)
|
|
return data
|
|
|
|
|
|
def test_task_pages_require_login(client):
|
|
response = client.get("/tasks/")
|
|
|
|
assert response.status_code == 302
|
|
assert "/auth/login" in response.headers["Location"]
|
|
|
|
|
|
def test_create_edit_and_delete_task(client, db):
|
|
create_account_and_login(client)
|
|
|
|
create_response = client.post(
|
|
"/tasks/new",
|
|
data=task_form_data(due_date="2026-08-09T14:30"),
|
|
follow_redirects=True,
|
|
)
|
|
task = db.session.scalar(
|
|
select(Task).where(Task.title == "Write a gentle plan")
|
|
)
|
|
assert create_response.status_code == 200
|
|
assert b"Task captured" in create_response.data
|
|
assert task is not None
|
|
assert task.due_date == datetime(2026, 8, 9, 14, 30)
|
|
|
|
edit_response = client.post(
|
|
f"/tasks/{task.id}/edit",
|
|
data=task_form_data(
|
|
title="Write the first paragraph",
|
|
status="done",
|
|
priority="important",
|
|
),
|
|
follow_redirects=True,
|
|
)
|
|
db.session.refresh(task)
|
|
assert b"Task updated" in edit_response.data
|
|
assert task.title == "Write the first paragraph"
|
|
assert task.completed_at is not None
|
|
|
|
delete_response = client.post(
|
|
f"/tasks/{task.id}/delete",
|
|
follow_redirects=True,
|
|
)
|
|
assert b"Task deleted" in delete_response.data
|
|
assert db.session.get(Task, task.id) is None
|
|
|
|
|
|
def test_status_filter_only_shows_selected_tasks(client, db):
|
|
create_account_and_login(client)
|
|
client.post("/tasks/new", data=task_form_data(title="Visible active task"))
|
|
client.post(
|
|
"/tasks/new",
|
|
data=task_form_data(title="Hidden finished task", status="done"),
|
|
)
|
|
|
|
response = client.get("/tasks/?status=not_started")
|
|
|
|
assert response.status_code == 200
|
|
assert b"Visible active task" in response.data
|
|
assert b"Hidden finished task" not in response.data
|
|
|
|
|
|
def test_invalid_status_filter_is_rejected(client):
|
|
create_account_and_login(client)
|
|
|
|
response = client.get("/tasks/?status=unexpected")
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_today_is_limited_and_hides_future_and_completed_tasks(db):
|
|
user = create_user("today_user", "today@example.com", "secure password")
|
|
now = datetime(2026, 8, 8, 12, 0)
|
|
|
|
for number in range(8):
|
|
create_task(
|
|
user.id,
|
|
title=f"Inbox task {number}",
|
|
description=None,
|
|
status="not_started",
|
|
priority="normal",
|
|
due_date=None,
|
|
)
|
|
future = create_task(
|
|
user.id,
|
|
title="Future task",
|
|
description=None,
|
|
status="not_started",
|
|
priority="urgent",
|
|
due_date=now + timedelta(days=2),
|
|
)
|
|
completed = create_task(
|
|
user.id,
|
|
title="Completed task",
|
|
description=None,
|
|
status="done",
|
|
priority="urgent",
|
|
due_date=now,
|
|
)
|
|
|
|
tasks = list_today_tasks(user.id, now=now)
|
|
|
|
assert len(tasks) == 7
|
|
assert future not in tasks
|
|
assert completed not in tasks
|
|
|
|
|
|
def test_users_cannot_access_each_others_tasks(client, db):
|
|
owner = create_user("owner", "owner@example.com", "secure password")
|
|
intruder = create_user("intruder", "intruder@example.com", "secure password")
|
|
task = create_task(
|
|
owner.id,
|
|
title="Owner-only task",
|
|
description=None,
|
|
status="not_started",
|
|
priority="normal",
|
|
due_date=None,
|
|
)
|
|
client.post(
|
|
"/auth/login",
|
|
data={"email": intruder.email, "password": "secure password"},
|
|
)
|
|
|
|
assert client.get(f"/tasks/{task.id}").status_code == 404
|
|
assert client.post(f"/tasks/{task.id}/delete").status_code == 404
|
|
assert db.session.get(Task, task.id) is not None
|
|
|
|
|
|
def test_subtasks_update_derived_progress(client, db):
|
|
user = create_user("steps_user", "steps@example.com", "secure password")
|
|
task = create_task(
|
|
user.id,
|
|
title="Two-step task",
|
|
description=None,
|
|
status="in_progress",
|
|
priority="important",
|
|
due_date=None,
|
|
)
|
|
first = add_subtask(task, "First small step")
|
|
add_subtask(task, "Second small step")
|
|
|
|
assert task.progress_percentage == 0
|
|
toggle_subtask(task, first.id)
|
|
assert task.progress_percentage == 50
|
|
|
|
client.post(
|
|
"/auth/login",
|
|
data={"email": user.email, "password": "secure password"},
|
|
)
|
|
response = client.get(f"/tasks/{task.id}")
|
|
assert b"First small step" in response.data
|
|
assert b'value="50"' in response.data
|
|
|
|
|
|
def test_blank_task_title_is_rejected(client, db):
|
|
create_account_and_login(client)
|
|
|
|
response = client.post(
|
|
"/tasks/new",
|
|
data=task_form_data(title=" "),
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert b"This field cannot be blank" in response.data
|
|
assert db.session.scalar(select(Task)) is None
|