feat: add PendingRewardDialog, RewardConfirmDialog, and TaskConfirmDialog components
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 25s

- Implemented PendingRewardDialog for handling pending reward requests.
- Created RewardConfirmDialog for confirming reward redemption.
- Developed TaskConfirmDialog for task confirmation with child name display.

test: add unit tests for ChildView and ParentView components

- Added comprehensive tests for ChildView including task triggering and SSE event handling.
- Implemented tests for ParentView focusing on override modal and SSE event management.

test: add ScrollingList component tests

- Created tests for ScrollingList to verify item fetching, loading states, and custom item classes.
- Included tests for two-step click interactions and edit button display logic.
- Moved toward hashed passwords.
This commit is contained in:
2026-02-10 20:21:05 -05:00
parent 3dee8b80a2
commit 401c21ad82
45 changed files with 4353 additions and 441 deletions

View File

@@ -0,0 +1,142 @@
import pytest
from werkzeug.security import generate_password_hash, check_password_hash
from flask import Flask
from api.auth_api import auth_api
from db.db import users_db
from tinydb import Query
from models.user import User
from datetime import datetime
@pytest.fixture
def client():
"""Setup Flask test client with auth blueprint."""
app = Flask(__name__)
app.register_blueprint(auth_api, url_prefix='/auth')
app.config['TESTING'] = True
app.config['SECRET_KEY'] = 'supersecretkey'
app.config['FRONTEND_URL'] = 'http://localhost:5173'
with app.test_client() as client:
yield client
def test_signup_hashes_password(client):
"""Test that signup hashes the password."""
# Clean up any existing user
users_db.remove(Query().email == 'test@example.com')
data = {
'first_name': 'Test',
'last_name': 'User',
'email': 'test@example.com',
'password': 'password123'
}
response = client.post('/auth/signup', json=data)
assert response.status_code == 201
# Check that password is hashed in DB
user_dict = users_db.get(Query().email == 'test@example.com')
assert user_dict is not None
assert user_dict['password'].startswith('scrypt:')
def test_login_with_correct_password(client):
"""Test login succeeds with correct password."""
# Clean up and create a user with hashed password
users_db.remove(Query().email == 'test@example.com')
hashed_pw = generate_password_hash('password123')
user = User(
first_name='Test',
last_name='User',
email='test@example.com',
password=hashed_pw,
verified=True
)
users_db.insert(user.to_dict())
data = {'email': 'test@example.com', 'password': 'password123'}
response = client.post('/auth/login', json=data)
assert response.status_code == 200
assert 'token' in response.headers.get('Set-Cookie', '')
def test_login_with_incorrect_password(client):
"""Test login fails with incorrect password."""
# Clean up and create a user with hashed password
users_db.remove(Query().email == 'test@example.com')
hashed_pw = generate_password_hash('password123')
user = User(
first_name='Test',
last_name='User',
email='test@example.com',
password=hashed_pw,
verified=True
)
users_db.insert(user.to_dict())
data = {'email': 'test@example.com', 'password': 'wrongpassword'}
response = client.post('/auth/login', json=data)
assert response.status_code == 401
assert response.json['code'] == 'INVALID_CREDENTIALS'
def test_reset_password_hashes_new_password(client):
"""Test that reset-password hashes the new password."""
# Clean up and create a user with reset token
users_db.remove(Query().email == 'test@example.com')
user = User(
first_name='Test',
last_name='User',
email='test@example.com',
password=generate_password_hash('oldpassword'),
verified=True,
reset_token='validtoken',
reset_token_created=datetime.utcnow().isoformat()
)
users_db.insert(user.to_dict())
data = {'token': 'validtoken', 'password': 'newpassword123'}
response = client.post('/auth/reset-password', json=data)
assert response.status_code == 200
# Check that password is hashed in DB
user_dict = users_db.get(Query().email == 'test@example.com')
assert user_dict is not None
assert user_dict['password'].startswith('scrypt:')
assert check_password_hash(user_dict['password'], 'newpassword123')
def test_migration_script_hashes_plain_text_passwords():
"""Test the migration script hashes plain text passwords."""
# Clean up
users_db.remove(Query().email == 'test1@example.com')
users_db.remove(Query().email == 'test2@example.com')
# Create users with plain text passwords
user1 = User(
first_name='Test1',
last_name='User',
email='test1@example.com',
password='plaintext1',
verified=True
)
already_hashed = generate_password_hash('alreadyhashed')
user2 = User(
first_name='Test2',
last_name='User',
email='test2@example.com',
password=already_hashed, # Already hashed
verified=True
)
users_db.insert(user1.to_dict())
users_db.insert(user2.to_dict())
# Run migration script
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from scripts.hash_passwords import main
main()
# Check user1 password is now hashed
user1_dict = users_db.get(Query().email == 'test1@example.com')
assert user1_dict['password'].startswith('scrypt:')
assert check_password_hash(user1_dict['password'], 'plaintext1')
# Check user2 password unchanged
user2_dict = users_db.get(Query().email == 'test2@example.com')
assert user2_dict['password'] == already_hashed

View File

@@ -8,6 +8,7 @@ from db.db import child_db, reward_db, task_db, users_db
from tinydb import Query
from models.child import Child
import jwt
from werkzeug.security import generate_password_hash
# Test user credentials
@@ -22,7 +23,7 @@ def add_test_user():
"first_name": "Test",
"last_name": "User",
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"password": generate_password_hash(TEST_PASSWORD),
"verified": True,
"image_id": "boy01"
})

View File

@@ -0,0 +1,944 @@
"""Tests for child override API endpoints and integration."""
import pytest
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('/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)
app.config['TESTING'] = True
app.config['SECRET_KEY'] = 'supersecretkey'
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, is_good=True, 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 ['task', 'reward'] raises ValueError."""
with pytest.raises(ValueError, match="entity_type must be 'task' or 'reward'"):
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, is_good=True, 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, is_good=True, 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

View File

@@ -5,6 +5,7 @@ import time
from config.paths import get_user_image_dir
from PIL import Image as PILImage
import pytest
from werkzeug.security import generate_password_hash
from flask import Flask
from api.image_api import image_api, UPLOAD_FOLDER
@@ -29,7 +30,7 @@ def add_test_user():
"first_name": "Test",
"last_name": "User",
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"password": generate_password_hash(TEST_PASSWORD),
"verified": True,
"image_id": "boy01"
})

View File

@@ -1,5 +1,6 @@
import pytest
import os
from werkzeug.security import generate_password_hash
from flask import Flask
from api.reward_api import reward_api
@@ -21,7 +22,7 @@ def add_test_user():
"first_name": "Test",
"last_name": "User",
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"password": generate_password_hash(TEST_PASSWORD),
"verified": True,
"image_id": "boy01"
})

View File

@@ -1,5 +1,6 @@
import pytest
import os
from werkzeug.security import generate_password_hash
from flask import Flask
from api.task_api import task_api
@@ -20,7 +21,7 @@ def add_test_user():
"first_name": "Test",
"last_name": "User",
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"password": generate_password_hash(TEST_PASSWORD),
"verified": True,
"image_id": "boy01"
})

View File

@@ -6,6 +6,7 @@ from api.auth_api import auth_api
from db.db import users_db
from tinydb import Query
import jwt
from werkzeug.security import generate_password_hash
# Test user credentials
TEST_EMAIL = "usertest@example.com"
@@ -25,7 +26,7 @@ def add_test_users():
"first_name": "Test",
"last_name": "User",
"email": TEST_EMAIL,
"password": TEST_PASSWORD,
"password": generate_password_hash(TEST_PASSWORD),
"verified": True,
"image_id": "boy01",
"marked_for_deletion": False,
@@ -38,7 +39,7 @@ def add_test_users():
"first_name": "Marked",
"last_name": "User",
"email": MARKED_EMAIL,
"password": MARKED_PASSWORD,
"password": generate_password_hash(MARKED_PASSWORD),
"verified": True,
"image_id": "girl01",
"marked_for_deletion": True,