173 lines
5.7 KiB
Python
173 lines
5.7 KiB
Python
from datetime import date, datetime
|
|
|
|
from app.auth.services import create_user
|
|
from app.tasks.services import (
|
|
calculate_forgiving_streak,
|
|
complete_task,
|
|
create_task,
|
|
)
|
|
|
|
|
|
def login(client, email: str, password: str = "secure password") -> None:
|
|
client.post(
|
|
"/auth/login",
|
|
data={"email": email, "password": password},
|
|
)
|
|
|
|
|
|
def make_task(user_id: int, title: str, *, priority: str = "normal"):
|
|
return create_task(
|
|
user_id,
|
|
title=title,
|
|
description=None,
|
|
status="not_started",
|
|
priority=priority,
|
|
due_date=None,
|
|
)
|
|
|
|
|
|
def test_focus_mode_surfaces_only_highest_ranked_today_task(client, db):
|
|
user = create_user("focus_user", "focus@example.com", "secure password")
|
|
make_task(user.id, "Lower priority choice")
|
|
primary = make_task(user.id, "Primary urgent choice", priority="urgent")
|
|
login(client, user.email)
|
|
|
|
response = client.get("/tasks/focus")
|
|
|
|
assert response.status_code == 200
|
|
assert b"Primary urgent choice" in response.data
|
|
assert b"Lower priority choice" not in response.data
|
|
assert f'data-task-id="{primary.id}"'.encode() not in response.data
|
|
|
|
|
|
def test_focus_settings_store_timer_context_and_chunks(client, db):
|
|
user = create_user("setup_user", "setup@example.com", "secure password")
|
|
task = make_task(user.id, "Configured focus task")
|
|
login(client, user.email)
|
|
|
|
response = client.post(
|
|
f"/tasks/{task.id}/focus-settings",
|
|
data={
|
|
"timer_minutes": "25",
|
|
"context": "At my desk with headphones",
|
|
"chunk_plan": "5, 5, 10",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
db.session.refresh(task)
|
|
|
|
assert response.status_code == 200
|
|
assert b"Focus setup saved" in response.data
|
|
assert b"Visual countdown" in response.data
|
|
assert b"At my desk with headphones" in response.data
|
|
assert task.timer_seconds == 1500
|
|
assert task.context == "At my desk with headphones"
|
|
assert task.chunk_sessions == [
|
|
{"minutes": 5, "completed": False},
|
|
{"minutes": 5, "completed": False},
|
|
{"minutes": 10, "completed": False},
|
|
]
|
|
|
|
|
|
def test_invalid_chunk_plan_is_rejected_without_changes(client, db):
|
|
user = create_user("invalid_user", "invalid@example.com", "secure password")
|
|
task = make_task(user.id, "Unchanged focus task")
|
|
login(client, user.email)
|
|
|
|
response = client.post(
|
|
f"/tasks/{task.id}/focus-settings",
|
|
data={"timer_minutes": "10", "chunk_plan": "5, later, 10"},
|
|
follow_redirects=True,
|
|
)
|
|
db.session.refresh(task)
|
|
|
|
assert b"Use comma-separated whole minutes" in response.data
|
|
assert task.timer_seconds == 0
|
|
assert task.chunk_sessions == []
|
|
|
|
|
|
def test_chunk_toggle_updates_only_requested_block(client, db):
|
|
user = create_user("chunk_user", "chunk@example.com", "secure password")
|
|
task = make_task(user.id, "Chunked task")
|
|
task.chunk_sessions = [
|
|
{"minutes": 5, "completed": False},
|
|
{"minutes": 10, "completed": False},
|
|
]
|
|
db.session.commit()
|
|
login(client, user.email)
|
|
|
|
response = client.post(
|
|
f"/tasks/{task.id}/chunks/1/toggle",
|
|
follow_redirects=True,
|
|
)
|
|
db.session.refresh(task)
|
|
|
|
assert response.status_code == 200
|
|
assert task.chunk_sessions[0]["completed"] is False
|
|
assert task.chunk_sessions[1]["completed"] is True
|
|
assert client.post(f"/tasks/{task.id}/chunks/4/toggle").status_code == 404
|
|
|
|
|
|
def test_completion_celebrates_and_advances_to_next_task(client, db):
|
|
user = create_user("complete_user", "complete@example.com", "secure password")
|
|
primary = make_task(user.id, "Finish this first", priority="urgent")
|
|
make_task(user.id, "Then show this task")
|
|
login(client, user.email)
|
|
|
|
response = client.post(
|
|
f"/tasks/{primary.id}/complete",
|
|
follow_redirects=True,
|
|
)
|
|
db.session.refresh(primary)
|
|
|
|
assert response.status_code == 200
|
|
assert b"That step is complete" in response.data
|
|
assert b'id="focus-task-title">Then show this task' in response.data
|
|
assert b'id="focus-task-title">Finish this first' not in response.data
|
|
assert primary.status == "done"
|
|
assert primary.completed_at is not None
|
|
|
|
|
|
def test_focus_mutations_do_not_expose_foreign_tasks(client, db):
|
|
owner = create_user("focus_owner", "focus-owner@example.com", "secure password")
|
|
visitor = create_user(
|
|
"focus_visitor",
|
|
"focus-visitor@example.com",
|
|
"secure password",
|
|
)
|
|
task = make_task(owner.id, "Private focus task")
|
|
login(client, visitor.email)
|
|
|
|
assert client.get(f"/tasks/{task.id}/focus").status_code == 404
|
|
assert client.post(f"/tasks/{task.id}/complete").status_code == 404
|
|
assert (
|
|
client.post(
|
|
f"/tasks/{task.id}/focus-settings",
|
|
data={"timer_minutes": "15"},
|
|
).status_code
|
|
== 404
|
|
)
|
|
assert client.post(f"/tasks/{task.id}/chunks/0/toggle").status_code == 404
|
|
|
|
|
|
def test_forgiving_streak_allows_one_internal_rest_day(db):
|
|
user = create_user("streak_user", "streak@example.com", "secure password")
|
|
for day in (8, 7, 5):
|
|
task = make_task(user.id, f"Completed on day {day}")
|
|
complete_task(task, completed_at=datetime(2026, 8, day, 12, 0))
|
|
|
|
streak = calculate_forgiving_streak(user.id, today=date(2026, 8, 8))
|
|
|
|
assert streak.days == 3
|
|
assert streak.freeze_used is True
|
|
|
|
|
|
def test_current_incomplete_day_does_not_break_streak(db):
|
|
user = create_user("rest_user", "rest@example.com", "secure password")
|
|
yesterday = make_task(user.id, "Yesterday counted")
|
|
complete_task(yesterday, completed_at=datetime(2026, 8, 7, 12, 0))
|
|
|
|
streak = calculate_forgiving_streak(user.id, today=date(2026, 8, 8))
|
|
|
|
assert streak.days == 1
|
|
assert streak.freeze_used is False
|