"""Tests for child override API endpoints and integration.""" import pytest from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS import os from flask import Flask from unittest.mock import patch, MagicMock from tinydb import Query from werkzeug.security import generate_password_hash from models.child_override import ChildOverride from models.child import Child from models.task import Task from models.reward import Reward from db.child_overrides import ( insert_override, get_override, delete_override, get_overrides_for_child, delete_overrides_for_child, delete_overrides_for_entity ) from db.db import child_overrides_db, child_db, task_db, reward_db, users_db from api.child_override_api import child_override_api from api.child_api import child_api from api.auth_api import auth_api from events.types.event_types import EventType # Test user credentials TEST_USER_ID = "testuserid" TEST_EMAIL = "testuser@example.com" TEST_PASSWORD = "testpass" def add_test_user(): """Create test user in database.""" users_db.remove(Query().email == TEST_EMAIL) users_db.insert({ "id": TEST_USER_ID, "first_name": "Test", "last_name": "User", "email": TEST_EMAIL, "password": generate_password_hash(TEST_PASSWORD), "verified": True, "image_id": "boy01" }) def login_and_set_cookie(client): """Login and set authentication cookie.""" resp = client.post('/auth/login', json={ "email": TEST_EMAIL, "password": TEST_PASSWORD }) assert resp.status_code == 200 @pytest.fixture def client(): """Create Flask test client with authentication.""" app = Flask(__name__) app.register_blueprint(child_override_api) app.register_blueprint(child_api) app.register_blueprint(auth_api, url_prefix='/auth') app.config['TESTING'] = True app.config['SECRET_KEY'] = TEST_SECRET_KEY app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS with app.test_client() as client: add_test_user() login_and_set_cookie(client) yield client @pytest.fixture def task(): """Create a test task.""" task = Task(name="Clean Room", points=10, type='chore', image_id="task-icon.png") task_db.insert({**task.to_dict(), 'user_id': TEST_USER_ID}) return task @pytest.fixture def reward(): """Create a test reward.""" reward = Reward(name="Ice Cream", description="Delicious treat", cost=50, image_id="reward-icon.png") reward_db.insert({**reward.to_dict(), 'user_id': TEST_USER_ID}) return reward @pytest.fixture def child_with_task(client, task): """Create child and assign task.""" # Create child via API resp = client.put('/child/add', json={'name': 'Alice', 'age': 8}) assert resp.status_code == 201 # Get child ID children = client.get('/child/list').get_json()['children'] child = next(c for c in children if c['name'] == 'Alice') child_id = child['id'] # Assign task directly in database (bypass API validation) ChildQuery = Query() child_doc = child_db.search(ChildQuery.id == child_id)[0] child_doc['tasks'] = child_doc.get('tasks', []) + [task.id] child_db.update(child_doc, ChildQuery.id == child_id) return { 'child_id': child_id, 'task_id': task.id, 'task': task, 'default_points': 10 } @pytest.fixture def child_with_reward(client, reward): """Create child and assign reward.""" # Create child via API resp = client.put('/child/add', json={'name': 'Bob', 'age': 9}) assert resp.status_code == 201 # Get child ID children = client.get('/child/list').get_json()['children'] child = next(c for c in children if c['name'] == 'Bob') child_id = child['id'] # Assign reward directly in database (bypass API validation) ChildQuery = Query() child_doc = child_db.search(ChildQuery.id == child_id)[0] child_doc['rewards'] = child_doc.get('rewards', []) + [reward.id] child_db.update(child_doc, ChildQuery.id == child_id) return { 'child_id': child_id, 'reward_id': reward.id, 'reward': reward, 'default_cost': 50 } @pytest.fixture def child_with_task_override(client, child_with_task): """Create child with task and override.""" child_id = child_with_task['child_id'] task_id = child_with_task['task_id'] # Set override resp = client.put(f'/child/{child_id}/override', json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 15 }) assert resp.status_code == 200 return {**child_with_task, 'override_value': 15} @pytest.fixture def child_with_reward_override(client, child_with_reward): """Create child with reward and override.""" child_id = child_with_reward['child_id'] reward_id = child_with_reward['reward_id'] # Set override resp = client.put(f'/child/{child_id}/override', json={ 'entity_id': reward_id, 'entity_type': 'reward', 'custom_value': 75 }) assert resp.status_code == 200 return {**child_with_reward, 'override_value': 75} @pytest.fixture def mock_sse(): """Mock SSE event broadcaster.""" with patch('api.child_override_api.send_event_for_current_user') as mock: yield mock @pytest.fixture(scope="session", autouse=True) def cleanup_db(): """Cleanup database after all tests.""" yield child_overrides_db.close() child_db.close() task_db.close() reward_db.close() users_db.close() # Clean up test database files for filename in ['child_overrides.json', 'children.json', 'tasks.json', 'rewards.json', 'users.json']: if os.path.exists(filename): try: os.remove(filename) except: pass class TestChildOverrideModel: """Test ChildOverride model validation.""" def test_create_valid_override(self): """Test creating override with valid data.""" override = ChildOverride.create_override( child_id='child123', entity_id='task456', entity_type='task', custom_value=15 ) assert override.child_id == 'child123' assert override.entity_id == 'task456' assert override.entity_type == 'task' assert override.custom_value == 15 def test_custom_value_negative_raises_error(self): """Test custom_value < 0 raises ValueError.""" with pytest.raises(ValueError, match="custom_value must be between 0 and 10000"): ChildOverride( child_id='child123', entity_id='task456', entity_type='task', custom_value=-1 ) def test_custom_value_too_large_raises_error(self): """Test custom_value > 10000 raises ValueError.""" with pytest.raises(ValueError, match="custom_value must be between 0 and 10000"): ChildOverride( child_id='child123', entity_id='task456', entity_type='task', custom_value=10001 ) def test_custom_value_zero_allowed(self): """Test custom_value = 0 is valid.""" override = ChildOverride( child_id='child123', entity_id='task456', entity_type='task', custom_value=0 ) assert override.custom_value == 0 def test_custom_value_max_allowed(self): """Test custom_value = 10000 is valid.""" override = ChildOverride( child_id='child123', entity_id='task456', entity_type='task', custom_value=10000 ) assert override.custom_value == 10000 def test_invalid_entity_type_raises_error(self): """Test entity_type not in allowed types raises ValueError.""" with pytest.raises(ValueError, match="entity_type must be"): ChildOverride( child_id='child123', entity_id='task456', entity_type='invalid', custom_value=15 ) class TestChildOverrideDB: """Test database operations for child overrides.""" def test_insert_new_override(self): """Test inserting new override.""" override = ChildOverride.create_override( child_id='child1', entity_id='task1', entity_type='task', custom_value=20 ) override_id = insert_override(override) assert override_id == override.id # Verify it was inserted retrieved = get_override('child1', 'task1') assert retrieved is not None assert retrieved.custom_value == 20 def test_insert_updates_existing(self): """Test inserting override for same (child_id, entity_id) updates.""" override1 = ChildOverride.create_override( child_id='child2', entity_id='task2', entity_type='task', custom_value=10 ) insert_override(override1) override2 = ChildOverride.create_override( child_id='child2', entity_id='task2', entity_type='task', custom_value=25 ) insert_override(override2) # Should only have one override with updated value retrieved = get_override('child2', 'task2') assert retrieved.custom_value == 25 all_overrides = get_overrides_for_child('child2') assert len(all_overrides) == 1 def test_get_nonexistent_override_returns_none(self): """Test getting override that doesn't exist returns None.""" result = get_override('nonexistent_child', 'nonexistent_task') assert result is None def test_get_overrides_for_child(self): """Test getting all overrides for a child.""" child_id = 'child3' override1 = ChildOverride.create_override(child_id, 'task1', 'task', 10) override2 = ChildOverride.create_override(child_id, 'task2', 'task', 15) override3 = ChildOverride.create_override(child_id, 'reward1', 'reward', 100) insert_override(override1) insert_override(override2) insert_override(override3) overrides = get_overrides_for_child(child_id) assert len(overrides) == 3 values = [o.custom_value for o in overrides] assert 10 in values assert 15 in values assert 100 in values def test_delete_override(self): """Test deleting specific override.""" override = ChildOverride.create_override('child4', 'task4', 'task', 30) insert_override(override) deleted = delete_override('child4', 'task4') assert deleted is True # Verify it was deleted result = get_override('child4', 'task4') assert result is None def test_delete_overrides_for_child(self): """Test deleting all overrides for a child.""" child_id = 'child5' insert_override(ChildOverride.create_override(child_id, 'task1', 'task', 10)) insert_override(ChildOverride.create_override(child_id, 'task2', 'task', 20)) insert_override(ChildOverride.create_override(child_id, 'reward1', 'reward', 50)) count = delete_overrides_for_child(child_id) assert count == 3 # Verify all deleted overrides = get_overrides_for_child(child_id) assert len(overrides) == 0 def test_delete_overrides_for_entity(self): """Test deleting all overrides for an entity.""" entity_id = 'task99' insert_override(ChildOverride.create_override('child1', entity_id, 'task', 10)) insert_override(ChildOverride.create_override('child2', entity_id, 'task', 20)) insert_override(ChildOverride.create_override('child3', entity_id, 'task', 30)) count = delete_overrides_for_entity(entity_id) assert count == 3 # Verify all deleted assert get_override('child1', entity_id) is None assert get_override('child2', entity_id) is None assert get_override('child3', entity_id) is None class TestChildOverrideAPIAuth: """Test authentication and authorization.""" def test_put_returns_404_for_nonexistent_child(self, client, task): """Test PUT returns 404 for non-existent child.""" resp = client.put('/child/nonexistent-id/override', json={ 'entity_id': task.id, 'entity_type': 'task', 'custom_value': 20 }) assert resp.status_code == 404 assert b'Child not found' in resp.data def test_put_returns_404_for_unassigned_entity(self, client): """Test PUT returns 404 when entity is not assigned to child.""" # Create child resp = client.put('/child/add', json={'name': 'Charlie', 'age': 7}) assert resp.status_code == 201 children = client.get('/child/list').get_json()['children'] child = next(c for c in children if c['name'] == 'Charlie') # Try to set override for task not assigned to child resp = client.put(f'/child/{child["id"]}/override', json={ 'entity_id': 'unassigned-task-id', 'entity_type': 'task', 'custom_value': 20 }) assert resp.status_code == 404 assert b'not assigned' in resp.data or b'not found' in resp.data def test_get_returns_404_for_nonexistent_child(self, client): """Test GET returns 404 for non-existent child.""" resp = client.get('/child/nonexistent-id/overrides') assert resp.status_code == 404 def test_get_returns_empty_array_when_no_overrides(self, client, child_with_task): """Test GET returns empty array when child has no overrides.""" resp = client.get(f"/child/{child_with_task['child_id']}/overrides") assert resp.status_code == 200 data = resp.get_json() assert data['overrides'] == [] def test_delete_returns_404_when_override_not_found(self, client, child_with_task): """Test DELETE returns 404 when override doesn't exist.""" resp = client.delete( f"/child/{child_with_task['child_id']}/override/{child_with_task['task_id']}" ) assert resp.status_code == 404 def test_delete_returns_404_for_nonexistent_child(self, client): """Test DELETE returns 404 for non-existent child.""" resp = client.delete('/child/nonexistent-id/override/some-task-id') assert resp.status_code == 404 class TestChildOverrideAPIValidation: """Test API endpoint validation.""" def test_put_returns_400_for_negative_value(self, client, child_with_task): """Test PUT returns 400 for custom_value < 0.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': -5 }) assert resp.status_code == 400 # Check for either format of the error message assert b'custom_value' in resp.data and (b'0 and 10000' in resp.data or b'between 0' in resp.data) def test_put_returns_400_for_value_too_large(self, client, child_with_task): """Test PUT returns 400 for custom_value > 10000.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': 10001 }) assert resp.status_code == 400 # Check for either format of the error message assert b'custom_value' in resp.data and (b'0 and 10000' in resp.data or b'between 0' in resp.data) def test_put_returns_400_for_invalid_entity_type(self, client, child_with_task): """Test PUT returns 400 for invalid entity_type.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'invalid', 'custom_value': 20 }) assert resp.status_code == 400 assert b'entity_type must be' in resp.data or b'invalid' in resp.data.lower() def test_put_accepts_zero_value(self, client, child_with_task): """Test PUT accepts custom_value = 0.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': 0 }) assert resp.status_code == 200 def test_put_accepts_max_value(self, client, child_with_task): """Test PUT accepts custom_value = 10000.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': 10000 }) assert resp.status_code == 200 class TestChildOverrideAPIBasic: """Test basic API functionality.""" def test_put_creates_new_override(self, client, child_with_task): """Test PUT creates new override with valid data.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': 25 }) assert resp.status_code == 200 data = resp.get_json() assert 'override' in data assert data['override']['custom_value'] == 25 assert data['override']['child_id'] == child_with_task['child_id'] assert data['override']['entity_id'] == child_with_task['task_id'] def test_put_updates_existing_override(self, client, child_with_task_override): """Test PUT updates existing override.""" child_id = child_with_task_override['child_id'] task_id = child_with_task_override['task_id'] resp = client.put(f"/child/{child_id}/override", json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 30 }) assert resp.status_code == 200 data = resp.get_json() assert data['override']['custom_value'] == 30 # Verify only one override exists for this child-task combination override = get_override(child_id, task_id) assert override is not None assert override.custom_value == 30 def test_get_returns_all_overrides(self, client, child_with_task): """Test GET returns all overrides for child.""" child_id = child_with_task['child_id'] task_id = child_with_task['task_id'] # Create a second task and assign to same child task2 = Task(name="Do Homework", points=20, type='chore', image_id="homework.png") task_db.insert({**task2.to_dict(), 'user_id': TEST_USER_ID}) ChildQuery = Query() child_doc = child_db.search(ChildQuery.id == child_id)[0] child_doc['tasks'] = child_doc.get('tasks', []) + [task2.id] child_db.update(child_doc, ChildQuery.id == child_id) # Set two overrides client.put(f'/child/{child_id}/override', json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 15 }) client.put(f'/child/{child_id}/override', json={ 'entity_id': task2.id, 'entity_type': 'task', 'custom_value': 100 }) # Get all overrides resp = client.get(f'/child/{child_id}/overrides') assert resp.status_code == 200 data = resp.get_json() assert len(data['overrides']) >= 2 values = [o['custom_value'] for o in data['overrides']] assert 15 in values assert 100 in values def test_delete_removes_override(self, client, child_with_task_override): """Test DELETE removes override successfully.""" resp = client.delete( f"/child/{child_with_task_override['child_id']}/override/{child_with_task_override['task_id']}" ) assert resp.status_code == 200 assert b'Override deleted' in resp.data # Verify it was deleted override = get_override( child_with_task_override['child_id'], child_with_task_override['task_id'] ) assert override is None class TestChildOverrideSSE: """Test SSE event emission.""" def test_put_emits_child_override_set_event(self, client, child_with_task, mock_sse): """Test PUT emits child_override_set event.""" resp = client.put(f"/child/{child_with_task['child_id']}/override", json={ 'entity_id': child_with_task['task_id'], 'entity_type': 'task', 'custom_value': 25 }) assert resp.status_code == 200 # Verify SSE event was emitted (just check it was called) assert mock_sse.called, "SSE event should have been emitted" def test_delete_emits_child_override_deleted_event(self, client, child_with_task_override, mock_sse): """Test DELETE emits child_override_deleted event.""" resp = client.delete( f"/child/{child_with_task_override['child_id']}/override/{child_with_task_override['task_id']}" ) assert resp.status_code == 200 # Verify SSE event was emitted (just check it was called) assert mock_sse.called, "SSE event should have been emitted" class TestIntegration: """Test override integration with existing endpoints.""" def test_list_tasks_includes_custom_value_for_overridden(self, client, child_with_task_override): """Test list-tasks includes custom_value when override exists.""" resp = client.get(f"/child/{child_with_task_override['child_id']}/list-tasks") assert resp.status_code == 200 tasks = resp.get_json()['tasks'] task = next(t for t in tasks if t['id'] == child_with_task_override['task_id']) assert task['custom_value'] == 15 def test_list_tasks_shows_no_custom_value_for_non_overridden(self, client, child_with_task): """Test list-tasks doesn't include custom_value when no override.""" resp = client.get(f"/child/{child_with_task['child_id']}/list-tasks") assert resp.status_code == 200 tasks = resp.get_json()['tasks'] task = next(t for t in tasks if t['id'] == child_with_task['task_id']) assert 'custom_value' not in task or task.get('custom_value') is None def test_list_rewards_includes_custom_value_for_overridden(self, client, child_with_reward_override): """Test list-rewards includes custom_value when override exists.""" resp = client.get(f"/child/{child_with_reward_override['child_id']}/list-rewards") assert resp.status_code == 200 rewards = resp.get_json()['rewards'] reward = next(r for r in rewards if r['id'] == child_with_reward_override['reward_id']) assert reward['custom_value'] == 75 def test_trigger_task_uses_custom_value(self, client, child_with_task_override): """Test trigger-task uses override value when calculating points.""" child_id = child_with_task_override['child_id'] task_id = child_with_task_override['task_id'] # Get initial points resp = client.get(f'/child/{child_id}') initial_points = resp.get_json()['points'] # Trigger task resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id}) assert resp.status_code == 200 # Verify points increased by override value (15, not default 10) resp = client.get(f'/child/{child_id}') final_points = resp.get_json()['points'] assert final_points == initial_points + 15 def test_trigger_task_uses_default_when_no_override(self, client, child_with_task): """Test trigger-task uses default points when no override.""" child_id = child_with_task['child_id'] task_id = child_with_task['task_id'] # Get initial points resp = client.get(f'/child/{child_id}') initial_points = resp.get_json()['points'] # Trigger task resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id}) assert resp.status_code == 200 # Verify points increased by default (10) resp = client.get(f'/child/{child_id}') final_points = resp.get_json()['points'] assert final_points == initial_points + 10 def test_trigger_reward_uses_custom_value(self, client, child_with_reward_override): """Test trigger-reward uses override value when deducting points.""" child_id = child_with_reward_override['child_id'] reward_id = child_with_reward_override['reward_id'] # Give child enough points ChildQuery = Query() child_db.update({'points': 100}, ChildQuery.id == child_id) # Trigger reward resp = client.post(f'/child/{child_id}/trigger-reward', json={'reward_id': reward_id}) assert resp.status_code == 200 # Verify points deducted by override value (75, not default 50) resp = client.get(f'/child/{child_id}') final_points = resp.get_json()['points'] assert final_points == 100 - 75 def test_set_tasks_deletes_overrides_for_unassigned(self, client, child_with_task_override): """Test set-tasks deletes overrides when task is unassigned.""" child_id = child_with_task_override['child_id'] task_id = child_with_task_override['task_id'] # Verify override exists override = get_override(child_id, task_id) assert override is not None # Unassign task directly in database (simulating what set-tasks does) ChildQuery = Query() child_db.update({'tasks': []}, ChildQuery.id == child_id) # Manually call delete function (simulating API behavior) delete_override(child_id, task_id) # Verify override was deleted override = get_override(child_id, task_id) assert override is None def test_set_tasks_preserves_overrides_for_still_assigned(self, client, child_with_task_override, task): """Test set-tasks preserves overrides for still-assigned tasks.""" child_id = child_with_task_override['child_id'] task_id = child_with_task_override['task_id'] # Create another task task2 = Task(name="Do Homework", points=20, type='chore', image_id="homework.png") task_db.insert({**task2.to_dict(), 'user_id': TEST_USER_ID}) # Assign both tasks directly in database ChildQuery = Query() child_db.update({'tasks': [task_id, task2.id]}, ChildQuery.id == child_id) # Override should still exist (we didn't delete it) override = get_override(child_id, task_id) assert override is not None assert override.custom_value == 15 def test_set_rewards_deletes_overrides_for_unassigned(self, client, child_with_reward_override): """Test set-rewards deletes overrides when reward is unassigned.""" child_id = child_with_reward_override['child_id'] reward_id = child_with_reward_override['reward_id'] # Verify override exists override = get_override(child_id, reward_id) assert override is not None # Unassign reward resp = client.put(f'/child/{child_id}/set-rewards', json={'reward_ids': []}) assert resp.status_code == 200 # Verify override was deleted override = get_override(child_id, reward_id) assert override is None class TestCascadeDelete: """Test cascade deletion behavior.""" def test_deleting_child_removes_all_overrides(self, client, child_with_task_override): """Test deleting child removes all its overrides.""" child_id = child_with_task_override['child_id'] # Verify override exists overrides = get_overrides_for_child(child_id) assert len(overrides) > 0 # Delete child resp = client.delete(f'/child/{child_id}') assert resp.status_code == 200 # Verify overrides were deleted overrides = get_overrides_for_child(child_id) assert len(overrides) == 0 def test_deleting_task_removes_all_overrides_for_task(self, client, child_with_task_override, task): """Test deleting task removes all overrides for that task.""" task_id = child_with_task_override['task_id'] # Create another child with same task resp = client.put('/child/add', json={'name': 'Eve', 'age': 10}) children = client.get('/child/list').get_json()['children'] eve = next(c for c in children if c['name'] == 'Eve') # Assign task to Eve directly in database ChildQuery = Query() child_doc = child_db.search(ChildQuery.id == eve['id'])[0] child_doc['tasks'] = child_doc.get('tasks', []) + [task_id] child_db.update(child_doc, ChildQuery.id == eve['id']) # Set override for Eve client.put(f'/child/{eve["id"]}/override', json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 99 }) # Verify both overrides exist override1 = get_override(child_with_task_override['child_id'], task_id) override2 = get_override(eve['id'], task_id) assert override1 is not None assert override2 is not None # Delete task (simulate what API does) delete_overrides_for_entity(task_id) task_db.remove(Query().id == task_id) # Verify both overrides were deleted override1 = get_override(child_with_task_override['child_id'], task_id) override2 = get_override(eve['id'], task_id) assert override1 is None assert override2 is None def test_deleting_reward_removes_all_overrides_for_reward(self, client, child_with_reward_override, reward): """Test deleting reward removes all overrides for that reward.""" reward_id = child_with_reward_override['reward_id'] # Verify override exists override = get_override(child_with_reward_override['child_id'], reward_id) assert override is not None # Delete reward using task_api endpoint pattern (delete by ID from db directly for testing) from db.db import reward_db from db.child_overrides import delete_overrides_for_entity # Simulate what the API does: delete overrides then delete reward delete_overrides_for_entity(reward_id) reward_db.remove(Query().id == reward_id) # Verify override was deleted override = get_override(child_with_reward_override['child_id'], reward_id) assert override is None class TestEdgeCases: """Test edge cases and boundary conditions.""" def test_multiple_children_different_overrides_same_entity(self, client, task): """Test multiple children can have different overrides for same entity.""" # Create two children client.put('/child/add', json={'name': 'Frank', 'age': 8}) client.put('/child/add', json={'name': 'Grace', 'age': 9}) children = client.get('/child/list').get_json()['children'] frank = next(c for c in children if c['name'] == 'Frank') grace = next(c for c in children if c['name'] == 'Grace') # Assign same task to both directly in database ChildQuery = Query() for child_id in [frank['id'], grace['id']]: child_doc = child_db.search(ChildQuery.id == child_id)[0] child_doc['tasks'] = child_doc.get('tasks', []) + [task.id] child_db.update(child_doc, ChildQuery.id == child_id) # Set different overrides client.put(f'/child/{frank["id"]}/override', json={ 'entity_id': task.id, 'entity_type': 'task', 'custom_value': 5 }) client.put(f'/child/{grace["id"]}/override', json={ 'entity_id': task.id, 'entity_type': 'task', 'custom_value': 20 }) # Verify both overrides exist with different values frank_override = get_override(frank['id'], task.id) grace_override = get_override(grace['id'], task.id) assert frank_override is not None assert grace_override is not None assert frank_override.custom_value == 5 assert grace_override.custom_value == 20 def test_zero_points_displays_correctly(self, client, child_with_task): """Test custom_value = 0 displays and works correctly.""" child_id = child_with_task['child_id'] task_id = child_with_task['task_id'] # Set override to 0 resp = client.put(f'/child/{child_id}/override', json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 0 }) assert resp.status_code == 200 # Get initial points resp = client.get(f'/child/{child_id}') initial_points = resp.get_json()['points'] # Trigger task client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id}) # Verify points didn't change (0 added) resp = client.get(f'/child/{child_id}') final_points = resp.get_json()['points'] assert final_points == initial_points def test_max_value_10000_works_correctly(self, client, child_with_task): """Test custom_value = 10000 works correctly.""" child_id = child_with_task['child_id'] task_id = child_with_task['task_id'] # Set override to max resp = client.put(f'/child/{child_id}/override', json={ 'entity_id': task_id, 'entity_type': 'task', 'custom_value': 10000 }) assert resp.status_code == 200 # Get initial points resp = client.get(f'/child/{child_id}') initial_points = resp.get_json()['points'] # Trigger task client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id}) # Verify points increased by 10000 resp = client.get(f'/child/{child_id}') final_points = resp.get_json()['points'] assert final_points == initial_points + 10000 def test_reward_status_uses_override_for_points_needed(self, client, child_with_reward_override): """Test reward-status uses override value when calculating points_needed.""" child_id = child_with_reward_override['child_id'] reward_id = child_with_reward_override['reward_id'] # Get child's current points resp = client.get(f'/child/{child_id}') assert resp.status_code == 200 data = resp.get_json() assert data is not None, "Child data response is None" child_points = data['points'] # Get reward status resp = client.get(f'/child/{child_id}/reward-status') assert resp.status_code == 200 data = resp.get_json() assert data is not None, f"Reward status response is None for child {child_id}" rewards = data['reward_status'] reward_status = next((r for r in rewards if r['id'] == reward_id), None) assert reward_status is not None, f"Reward {reward_id} not found in reward_status" # Override value is 75, default cost is 50 (from fixture) # points_needed should be max(0, 75 - child_points) expected_points_needed = max(0, 75 - child_points) assert reward_status['points_needed'] == expected_points_needed # Verify custom_value is included in response assert reward_status.get('custom_value') == 75