Add routine management features for child and parent views
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 3m8s
- Implemented routine child-mode flow tests to ensure proper functionality of routine assignment and task completion. - Created notification tests for parent view to verify routine completion notifications for children. - Developed ChildRoutineOverlay component for displaying routine tasks and handling user interactions. - Added RoutineApproveDialog component for approving or rejecting completed routines. - Created unit tests for ChildRoutineOverlay and RoutineEditView components to ensure correct behavior and rendering. - Enhanced RoutineEditView with proper handling of task addition and form submission. Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import os
|
||||
|
||||
from flask import Flask
|
||||
from api.child_api import child_api
|
||||
import api.child_api as child_api_module
|
||||
from api.auth_api import auth_api
|
||||
from db.db import child_db, reward_db, task_db, users_db, chore_schedules_db, task_extensions_db, pending_confirmations_db
|
||||
from tinydb import Query
|
||||
@@ -585,6 +586,39 @@ def test_list_child_tasks_clears_stale_approved_and_pending(client):
|
||||
assert tasks2[TASK_GOOD_ID]['approved_at'] is None
|
||||
|
||||
|
||||
def test_list_child_tasks_local_day_keeps_approved_across_utc_rollover(client, monkeypatch):
|
||||
"""Approved chore should remain completed when UTC date differs but user-local day matches.
|
||||
|
||||
Example: 2026-05-11T22:30:00Z is 2026-05-12 local day in Pacific/Kiritimati (UTC+14).
|
||||
"""
|
||||
_setup_sched_child_and_tasks(task_db, child_db)
|
||||
|
||||
# Force deterministic local-day basis for this endpoint call.
|
||||
monkeypatch.setattr(
|
||||
child_api_module,
|
||||
'_get_user_today_local',
|
||||
lambda user_id: ('2026-05-12', 'Pacific/Kiritimati'),
|
||||
)
|
||||
|
||||
pending_confirmations_db.insert({
|
||||
'id': 'pend_local_day_approved',
|
||||
'child_id': CHILD_SCHED_ID,
|
||||
'entity_id': TASK_GOOD_ID,
|
||||
'entity_type': 'chore',
|
||||
'user_id': 'testuserid',
|
||||
'status': 'approved',
|
||||
'approved_at': '2026-05-11T22:30:00+00:00',
|
||||
'created_at': 1778538600,
|
||||
'updated_at': 1778538600,
|
||||
})
|
||||
|
||||
resp = client.get(f'/child/{CHILD_SCHED_ID}/list-tasks')
|
||||
assert resp.status_code == 200
|
||||
tasks = {t['id']: t for t in resp.get_json()['tasks']}
|
||||
assert tasks[TASK_GOOD_ID]['pending_status'] == 'approved'
|
||||
assert tasks[TASK_GOOD_ID]['approved_at'] == '2026-05-11T22:30:00+00:00'
|
||||
|
||||
|
||||
def test_confirm_chore_allows_when_previous_pending_is_stale(client):
|
||||
"""A stale pending chore record from a prior day must not block confirm-chore."""
|
||||
old_ts = (datetime.now(timezone.utc) - timedelta(days=2)).timestamp()
|
||||
|
||||
@@ -3,6 +3,7 @@ from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
from datetime import date as date_type
|
||||
import api.child_api as child_api_module
|
||||
|
||||
from flask import Flask
|
||||
from api.child_api import child_api
|
||||
@@ -71,6 +72,24 @@ def setup_child_and_chore(child_name='TestChild', age=8, chore_points=10):
|
||||
return child['id'], 'chore1'
|
||||
|
||||
|
||||
def test_local_day_iso_check_handles_utc_rollover():
|
||||
# 22:30 UTC on 2026-05-11 is 12:30 local on 2026-05-12 in Pacific/Kiritimati (UTC+14).
|
||||
assert child_api_module._is_iso_timestamp_on_local_day(
|
||||
'2026-05-11T22:30:00+00:00',
|
||||
'2026-05-12',
|
||||
'Pacific/Kiritimati',
|
||||
)
|
||||
|
||||
|
||||
def test_local_day_epoch_check_handles_utc_rollover():
|
||||
# Same instant as above represented as epoch seconds.
|
||||
assert child_api_module._is_epoch_timestamp_on_local_day(
|
||||
1778538600,
|
||||
'2026-05-12',
|
||||
'Pacific/Kiritimati',
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Child Confirm Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
296
backend/tests/test_routine_api.py
Normal file
296
backend/tests/test_routine_api.py
Normal file
@@ -0,0 +1,296 @@
|
||||
"""Unit tests for routine API endpoints."""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from tinydb import Query
|
||||
from models.routine import Routine
|
||||
from models.routine_item import RoutineItem
|
||||
from models.routine_schedule import RoutineSchedule
|
||||
from models.routine_extension import RoutineExtension
|
||||
from models.pending_confirmation import PendingConfirmation
|
||||
from api.error_codes import ErrorCodes
|
||||
from db.db import routine_db, routine_items_db, routine_schedules_db, routine_extensions_db, pending_confirmations_db
|
||||
|
||||
|
||||
class TestRoutineModel:
|
||||
"""Test Routine model serialization and validation."""
|
||||
|
||||
def test_routine_creation(self):
|
||||
"""Test creating a routine instance."""
|
||||
routine = Routine(name="Morning Routine", points=50, image_id="img123", user_id="user1")
|
||||
assert routine.name == "Morning Routine"
|
||||
assert routine.points == 50
|
||||
assert routine.image_id == "img123"
|
||||
assert routine.user_id == "user1"
|
||||
assert routine.id is not None
|
||||
|
||||
def test_routine_to_dict(self):
|
||||
"""Test routine serialization."""
|
||||
routine = Routine(name="Test", points=10, image_id=None, user_id="user1")
|
||||
data = routine.to_dict()
|
||||
assert data["name"] == "Test"
|
||||
assert data["points"] == 10
|
||||
assert data["id"] == routine.id
|
||||
|
||||
def test_routine_from_dict(self):
|
||||
"""Test routine deserialization."""
|
||||
routine_dict = {"id": "r1", "name": "Test", "points": 20, "image_id": "img1", "user_id": "u1"}
|
||||
routine = Routine.from_dict(routine_dict)
|
||||
assert routine.id == "r1"
|
||||
assert routine.name == "Test"
|
||||
assert routine.points == 20
|
||||
|
||||
|
||||
class TestRoutineItemModel:
|
||||
"""Test RoutineItem model."""
|
||||
|
||||
def test_routine_item_creation(self):
|
||||
"""Test creating a routine item."""
|
||||
item = RoutineItem(routine_id="r1", name="Make Bed", image_id=None, order=0)
|
||||
assert item.routine_id == "r1"
|
||||
assert item.name == "Make Bed"
|
||||
assert item.order == 0
|
||||
|
||||
def test_routine_item_to_dict(self):
|
||||
"""Test item serialization."""
|
||||
item = RoutineItem(routine_id="r1", name="Get Dressed", image_id="img1", order=1)
|
||||
data = item.to_dict()
|
||||
assert data["routine_id"] == "r1"
|
||||
assert data["name"] == "Get Dressed"
|
||||
assert data["order"] == 1
|
||||
|
||||
|
||||
class TestRoutineScheduleModel:
|
||||
"""Test RoutineSchedule model."""
|
||||
|
||||
def test_days_mode_schedule_creation(self):
|
||||
"""Test creating days mode schedule."""
|
||||
day_configs = [
|
||||
{"day": 0, "hour": 8, "minute": 0},
|
||||
{"day": 1, "hour": 9, "minute": 30},
|
||||
]
|
||||
schedule = RoutineSchedule(
|
||||
child_id="c1",
|
||||
routine_id="r1",
|
||||
mode="days",
|
||||
enabled=True,
|
||||
day_configs=day_configs,
|
||||
default_hour=8,
|
||||
default_minute=0,
|
||||
default_has_deadline=True,
|
||||
)
|
||||
assert schedule.mode == "days"
|
||||
assert len(schedule.day_configs) == 2
|
||||
assert schedule.enabled is True
|
||||
|
||||
def test_interval_mode_schedule_creation(self):
|
||||
"""Test creating interval mode schedule."""
|
||||
schedule = RoutineSchedule(
|
||||
child_id="c1",
|
||||
routine_id="r1",
|
||||
mode="interval",
|
||||
enabled=True,
|
||||
interval_days=3,
|
||||
anchor_date="2026-05-01",
|
||||
interval_has_deadline=True,
|
||||
interval_hour=10,
|
||||
interval_minute=30,
|
||||
)
|
||||
assert schedule.mode == "interval"
|
||||
assert schedule.interval_days == 3
|
||||
assert schedule.interval_hour == 10
|
||||
|
||||
|
||||
class TestRoutineExtensionModel:
|
||||
"""Test RoutineExtension model."""
|
||||
|
||||
def test_extension_creation(self):
|
||||
"""Test creating a routine extension."""
|
||||
extension = RoutineExtension(child_id="c1", routine_id="r1", date="2026-05-10")
|
||||
assert extension.child_id == "c1"
|
||||
assert extension.routine_id == "r1"
|
||||
assert extension.date == "2026-05-10"
|
||||
|
||||
|
||||
class TestRoutineDB:
|
||||
"""Test routine database operations."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear routine db before each test."""
|
||||
routine_db.truncate()
|
||||
|
||||
def test_add_routine(self):
|
||||
"""Test adding a routine to database."""
|
||||
routine = Routine(name="Test Routine", points=50, image_id=None, user_id="user1")
|
||||
routine_db.insert(routine.to_dict())
|
||||
|
||||
result = routine_db.search(Query().id == routine.id)
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "Test Routine"
|
||||
|
||||
def test_get_routine_by_id(self):
|
||||
"""Test fetching routine by ID."""
|
||||
routine = Routine(name="Fetch Test", points=30, image_id=None, user_id="user1")
|
||||
routine_db.insert(routine.to_dict())
|
||||
|
||||
result = routine_db.get(Query().id == routine.id)
|
||||
assert result is not None
|
||||
assert result["name"] == "Fetch Test"
|
||||
|
||||
def test_list_user_routines(self):
|
||||
"""Test listing routines for a user."""
|
||||
r1 = Routine(name="R1", points=10, image_id=None, user_id="user1")
|
||||
r2 = Routine(name="R2", points=20, image_id=None, user_id="user1")
|
||||
r3 = Routine(name="R3", points=15, image_id=None, user_id="user2")
|
||||
|
||||
routine_db.insert(r1.to_dict())
|
||||
routine_db.insert(r2.to_dict())
|
||||
routine_db.insert(r3.to_dict())
|
||||
|
||||
results = routine_db.search(Query().user_id == "user1")
|
||||
assert len(results) == 2
|
||||
|
||||
def test_update_routine(self):
|
||||
"""Test updating a routine."""
|
||||
routine = Routine(name="Original", points=50, image_id=None, user_id="user1")
|
||||
routine_db.insert(routine.to_dict())
|
||||
|
||||
routine_db.update({"name": "Updated", "points": 100}, Query().id == routine.id)
|
||||
result = routine_db.get(Query().id == routine.id)
|
||||
assert result["name"] == "Updated"
|
||||
assert result["points"] == 100
|
||||
|
||||
|
||||
class TestRoutineItemDB:
|
||||
"""Test routine item database operations."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear item db before each test."""
|
||||
routine_items_db.truncate()
|
||||
|
||||
def test_add_routine_item(self):
|
||||
"""Test adding a routine item."""
|
||||
item = RoutineItem(routine_id="r1", name="Make Bed", image_id=None, order=0)
|
||||
routine_items_db.insert(item.to_dict())
|
||||
|
||||
result = routine_items_db.search(Query().routine_id == "r1")
|
||||
assert len(result) == 1
|
||||
assert result[0]["name"] == "Make Bed"
|
||||
|
||||
def test_get_items_for_routine(self):
|
||||
"""Test fetching all items for a routine."""
|
||||
i1 = RoutineItem(routine_id="r1", name="Item1", image_id=None, order=0)
|
||||
i2 = RoutineItem(routine_id="r1", name="Item2", image_id=None, order=1)
|
||||
i3 = RoutineItem(routine_id="r2", name="Item3", image_id=None, order=0)
|
||||
|
||||
routine_items_db.insert(i1.to_dict())
|
||||
routine_items_db.insert(i2.to_dict())
|
||||
routine_items_db.insert(i3.to_dict())
|
||||
|
||||
results = routine_items_db.search(Query().routine_id == "r1")
|
||||
assert len(results) == 2
|
||||
|
||||
def test_delete_item(self):
|
||||
"""Test deleting a routine item."""
|
||||
item = RoutineItem(routine_id="r1", name="Test", image_id=None, order=0)
|
||||
routine_items_db.insert(item.to_dict())
|
||||
|
||||
routine_items_db.remove(Query().id == item.id)
|
||||
result = routine_items_db.search(Query().id == item.id)
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
class TestRoutineScheduleDB:
|
||||
"""Test routine schedule database operations."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear schedule db before each test."""
|
||||
routine_schedules_db.truncate()
|
||||
|
||||
def test_upsert_schedule(self):
|
||||
"""Test upserting a routine schedule."""
|
||||
day_configs = [{"day": 0, "hour": 8, "minute": 0}]
|
||||
schedule = RoutineSchedule(
|
||||
child_id="c1",
|
||||
routine_id="r1",
|
||||
mode="days",
|
||||
enabled=True,
|
||||
day_configs=day_configs,
|
||||
default_hour=8,
|
||||
default_minute=0,
|
||||
default_has_deadline=True,
|
||||
)
|
||||
routine_schedules_db.insert(schedule.to_dict())
|
||||
|
||||
result = routine_schedules_db.get(
|
||||
(Query().child_id == "c1") & (Query().routine_id == "r1")
|
||||
)
|
||||
assert result is not None
|
||||
assert result["mode"] == "days"
|
||||
|
||||
def test_delete_schedule(self):
|
||||
"""Test deleting a schedule."""
|
||||
schedule = RoutineSchedule(
|
||||
child_id="c1",
|
||||
routine_id="r1",
|
||||
mode="interval",
|
||||
enabled=True,
|
||||
interval_days=2,
|
||||
anchor_date="2026-05-01",
|
||||
interval_has_deadline=True,
|
||||
interval_hour=10,
|
||||
interval_minute=0,
|
||||
)
|
||||
routine_schedules_db.insert(schedule.to_dict())
|
||||
|
||||
routine_schedules_db.remove(
|
||||
(Query().child_id == "c1") & (Query().routine_id == "r1")
|
||||
)
|
||||
result = routine_schedules_db.search(Query().child_id == "c1")
|
||||
assert len(result) == 0
|
||||
|
||||
|
||||
class TestPendingRoutineConfirmation:
|
||||
"""Test pending routine confirmation workflow."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Clear db before each test."""
|
||||
pending_confirmations_db.truncate()
|
||||
|
||||
def test_create_routine_confirmation(self):
|
||||
"""Test creating a pending routine confirmation."""
|
||||
confirmation = PendingConfirmation(
|
||||
child_id="c1",
|
||||
entity_id="r1",
|
||||
entity_type="routine",
|
||||
user_id="u1",
|
||||
status="pending",
|
||||
)
|
||||
pending_confirmations_db.insert(confirmation.to_dict())
|
||||
|
||||
result = pending_confirmations_db.get(
|
||||
(Query().child_id == "c1") & (Query().entity_id == "r1") & (Query().entity_type == "routine")
|
||||
)
|
||||
assert result is not None
|
||||
assert result["status"] == "pending"
|
||||
|
||||
def test_approve_routine_confirmation(self):
|
||||
"""Test approving a routine confirmation."""
|
||||
confirmation = PendingConfirmation(
|
||||
child_id="c1",
|
||||
entity_id="r1",
|
||||
entity_type="routine",
|
||||
user_id="u1",
|
||||
status="pending",
|
||||
)
|
||||
pending_confirmations_db.insert(confirmation.to_dict())
|
||||
|
||||
today_utc = datetime.now(timezone.utc).isoformat()
|
||||
pending_confirmations_db.update(
|
||||
{"status": "approved", "approved_at": today_utc},
|
||||
Query().id == confirmation.id,
|
||||
)
|
||||
|
||||
result = pending_confirmations_db.get(Query().id == confirmation.id)
|
||||
assert result["status"] == "approved"
|
||||
assert result["approved_at"] is not None
|
||||
Reference in New Issue
Block a user