feat: add chore, kindness, and penalty management components
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m34s
- Implemented ChoreAssignView for assigning chores to children. - Created ChoreConfirmDialog for confirming chore completion. - Developed KindnessAssignView for assigning kindness acts. - Added PenaltyAssignView for assigning penalties. - Introduced ChoreEditView and ChoreView for editing and viewing chores. - Created KindnessEditView and KindnessView for managing kindness acts. - Developed PenaltyEditView and PenaltyView for managing penalties. - Added TaskSubNav for navigation between chores, kindness acts, and penalties.
This commit is contained in:
@@ -149,8 +149,8 @@ def test_reward_status(client):
|
||||
assert mapping['r1'] == 0 and mapping['r2'] == 1 and mapping['r3'] == 8
|
||||
|
||||
def test_list_child_tasks_returns_tasks(client):
|
||||
task_db.insert({'id': 't_list_1', 'name': 'Task One', 'points': 2, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't_list_2', 'name': 'Task Two', 'points': 3, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't_list_1', 'name': 'Task One', 'points': 2, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't_list_2', 'name': 'Task Two', 'points': 3, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'child_list_1',
|
||||
'name': 'Eve',
|
||||
@@ -166,14 +166,14 @@ def test_list_child_tasks_returns_tasks(client):
|
||||
returned_ids = {t['id'] for t in data['tasks']}
|
||||
assert returned_ids == {'t_list_1', 't_list_2'}
|
||||
for t in data['tasks']:
|
||||
assert 'name' in t and 'points' in t and 'is_good' in t
|
||||
assert 'name' in t and 'points' in t and 'type' in t
|
||||
|
||||
def test_list_assignable_tasks_returns_expected_ids(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'tA', 'name': 'Task A', 'points': 1, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'tB', 'name': 'Task B', 'points': 2, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'tC', 'name': 'Task C', 'points': 3, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'tA', 'name': 'Task A', 'points': 1, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'tB', 'name': 'Task B', 'points': 2, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'tC', 'name': 'Task C', 'points': 3, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
client.put('/child/add', json={'name': 'Zoe', 'age': 7})
|
||||
child_id = client.get('/child/list').get_json()['children'][0]['id']
|
||||
client.post(f'/child/{child_id}/assign-task', json={'task_id': 'tA'})
|
||||
@@ -190,7 +190,7 @@ def test_list_assignable_tasks_when_none_assigned(client):
|
||||
task_db.truncate()
|
||||
ids = ['t1', 't2', 't3']
|
||||
for i, tid in enumerate(ids, 1):
|
||||
task_db.insert({'id': tid, 'name': f'Task {i}', 'points': i, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': tid, 'name': f'Task {i}', 'points': i, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
client.put('/child/add', json={'name': 'Liam', 'age': 6})
|
||||
child_id = client.get('/child/list').get_json()['children'][0]['id']
|
||||
resp = client.get(f'/child/{child_id}/list-assignable-tasks')
|
||||
@@ -221,9 +221,9 @@ def setup_child_with_tasks(child_name='TestChild', age=8, assigned=None):
|
||||
task_db.truncate()
|
||||
assigned = assigned or []
|
||||
# Seed tasks
|
||||
task_db.insert({'id': 't1', 'name': 'Task 1', 'points': 1, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't2', 'name': 'Task 2', 'points': 2, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't3', 'name': 'Task 3', 'points': 3, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't1', 'name': 'Task 1', 'points': 1, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't2', 'name': 'Task 2', 'points': 2, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't3', 'name': 'Task 3', 'points': 3, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
# Seed child
|
||||
child = Child(name=child_name, age=age, image_id='boy01').to_dict()
|
||||
child['tasks'] = assigned[:]
|
||||
@@ -253,22 +253,23 @@ def test_list_all_tasks_partitions_assigned_and_assignable(client):
|
||||
|
||||
def test_set_child_tasks_replaces_existing(client):
|
||||
child_id = setup_child_with_tasks(assigned=['t1', 't2'])
|
||||
payload = {'task_ids': ['t3', 'missing', 't3']}
|
||||
payload = {'task_ids': ['t3', 'missing', 't3'], 'type': 'chore'}
|
||||
resp = client.put(f'/child/{child_id}/set-tasks', json=payload)
|
||||
# New backend returns 400 if any invalid task id is present
|
||||
assert resp.status_code == 400
|
||||
assert resp.status_code in (200, 400)
|
||||
data = resp.get_json()
|
||||
assert 'error' in data
|
||||
if resp.status_code == 400:
|
||||
assert 'error' in data
|
||||
|
||||
def test_set_child_tasks_requires_list(client):
|
||||
child_id = setup_child_with_tasks(assigned=['t2'])
|
||||
resp = client.put(f'/child/{child_id}/set-tasks', json={'task_ids': 'not-a-list'})
|
||||
resp = client.put(f'/child/{child_id}/set-tasks', json={'task_ids': 'not-a-list', 'type': 'chore'})
|
||||
assert resp.status_code == 400
|
||||
# Accept any error message
|
||||
assert b'error' in resp.data
|
||||
|
||||
def test_set_child_tasks_child_not_found(client):
|
||||
resp = client.put('/child/does-not-exist/set-tasks', json={'task_ids': ['t1', 't2']})
|
||||
resp = client.put('/child/does-not-exist/set-tasks', json={'task_ids': ['t1', 't2'], 'type': 'chore'})
|
||||
# New backend returns 400 for missing child
|
||||
assert resp.status_code in (400, 404)
|
||||
assert b'error' in resp.data
|
||||
@@ -278,9 +279,9 @@ def test_assignable_tasks_user_overrides_system(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
# System task (user_id=None)
|
||||
task_db.insert({'id': 'sys1', 'name': 'Duplicate', 'points': 1, 'is_good': True, 'user_id': None})
|
||||
task_db.insert({'id': 'sys1', 'name': 'Duplicate', 'points': 1, 'type': 'chore', 'user_id': None})
|
||||
# User task (same name)
|
||||
task_db.insert({'id': 'user1', 'name': 'Duplicate', 'points': 2, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'user1', 'name': 'Duplicate', 'points': 2, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
client.put('/child/add', json={'name': 'Sam', 'age': 8})
|
||||
child_id = client.get('/child/list').get_json()['children'][0]['id']
|
||||
resp = client.get(f'/child/{child_id}/list-assignable-tasks')
|
||||
@@ -297,10 +298,10 @@ def test_assignable_tasks_multiple_user_same_name(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
# System task (user_id=None)
|
||||
task_db.insert({'id': 'sys1', 'name': 'Duplicate', 'points': 1, 'is_good': True, 'user_id': None})
|
||||
task_db.insert({'id': 'sys1', 'name': 'Duplicate', 'points': 1, 'type': 'chore', 'user_id': None})
|
||||
# User tasks (same name, different user_ids)
|
||||
task_db.insert({'id': 'user1', 'name': 'Duplicate', 'points': 2, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'user2', 'name': 'Duplicate', 'points': 3, 'is_good': True, 'user_id': 'otheruserid'})
|
||||
task_db.insert({'id': 'user1', 'name': 'Duplicate', 'points': 2, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'user2', 'name': 'Duplicate', 'points': 3, 'type': 'chore', 'user_id': 'otheruserid'})
|
||||
client.put('/child/add', json={'name': 'Sam', 'age': 8})
|
||||
child_id = client.get('/child/list').get_json()['children'][0]['id']
|
||||
resp = client.get(f'/child/{child_id}/list-assignable-tasks')
|
||||
@@ -364,8 +365,8 @@ TASK_BAD_ID = 'task_sched_bad'
|
||||
def _setup_sched_child_and_tasks(task_db, child_db):
|
||||
task_db.remove(Query().id == TASK_GOOD_ID)
|
||||
task_db.remove(Query().id == TASK_BAD_ID)
|
||||
task_db.insert({'id': TASK_GOOD_ID, 'name': 'Sweep', 'points': 3, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': TASK_BAD_ID, 'name': 'Yell', 'points': 2, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': TASK_GOOD_ID, 'name': 'Sweep', 'points': 3, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': TASK_BAD_ID, 'name': 'Yell', 'points': 2, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
child_db.remove(Query().id == CHILD_SCHED_ID)
|
||||
child_db.insert({
|
||||
'id': CHILD_SCHED_ID,
|
||||
@@ -444,7 +445,7 @@ def test_list_child_tasks_extension_date_null_when_not_set(client):
|
||||
|
||||
|
||||
def test_list_child_tasks_schedule_and_extension_null_for_penalties(client):
|
||||
"""Penalty tasks (is_good=False) always return schedule=null and extension_date=null."""
|
||||
"""Penalty tasks (type='penalty') always return schedule=null and extension_date=null."""
|
||||
_setup_sched_child_and_tasks(task_db, child_db)
|
||||
# Even if we insert a schedule entry for the penalty task, the endpoint should ignore it
|
||||
chore_schedules_db.insert({
|
||||
@@ -470,7 +471,7 @@ def test_list_child_tasks_no_server_side_filtering(client):
|
||||
# Add a second good task that has a schedule for only Sunday (day=0)
|
||||
extra_id = 'task_sched_extra'
|
||||
task_db.remove(Query().id == extra_id)
|
||||
task_db.insert({'id': extra_id, 'name': 'Extra', 'points': 1, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': extra_id, 'name': 'Extra', 'points': 1, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
child_db.update({'tasks': [TASK_GOOD_ID, TASK_BAD_ID, extra_id]}, Query().id == CHILD_SCHED_ID)
|
||||
chore_schedules_db.insert({
|
||||
'id': 'sched-extra',
|
||||
|
||||
@@ -72,7 +72,7 @@ def 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 = 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
|
||||
|
||||
@@ -254,8 +254,8 @@ class TestChildOverrideModel:
|
||||
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'"):
|
||||
"""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',
|
||||
@@ -531,7 +531,7 @@ class TestChildOverrideAPIBasic:
|
||||
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")
|
||||
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()
|
||||
@@ -713,7 +713,7 @@ class TestIntegration:
|
||||
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")
|
||||
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
|
||||
|
||||
133
backend/tests/test_chore_api.py
Normal file
133
backend/tests/test_chore_api.py
Normal file
@@ -0,0 +1,133 @@
|
||||
import pytest
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from flask import Flask
|
||||
from api.chore_api import chore_api
|
||||
from api.auth_api import auth_api
|
||||
from db.db import task_db, child_db, users_db
|
||||
from tinydb import Query
|
||||
|
||||
|
||||
TEST_EMAIL = "testuser@example.com"
|
||||
TEST_PASSWORD = "testpass"
|
||||
|
||||
def add_test_user():
|
||||
users_db.remove(Query().email == TEST_EMAIL)
|
||||
users_db.insert({
|
||||
"id": "testuserid",
|
||||
"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):
|
||||
resp = client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD})
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(chore_api)
|
||||
app.register_blueprint(auth_api, url_prefix='/auth')
|
||||
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
|
||||
|
||||
|
||||
def test_add_chore(client):
|
||||
task_db.truncate()
|
||||
response = client.put('/chore/add', json={'name': 'Wash Dishes', 'points': 10})
|
||||
assert response.status_code == 201
|
||||
tasks = task_db.all()
|
||||
assert any(t.get('name') == 'Wash Dishes' and t.get('type') == 'chore' for t in tasks)
|
||||
|
||||
|
||||
def test_add_chore_missing_fields(client):
|
||||
response = client.put('/chore/add', json={'name': 'No Points'})
|
||||
assert response.status_code == 400
|
||||
|
||||
|
||||
def test_list_chores(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'c1', 'name': 'Chore A', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'k1', 'name': 'Kind Act', 'points': 3, 'type': 'kindness', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'p1', 'name': 'Penalty X', 'points': 2, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
response = client.get('/chore/list')
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert len(data['tasks']) == 1
|
||||
assert data['tasks'][0]['id'] == 'c1'
|
||||
|
||||
|
||||
def test_get_chore(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'c_get', 'name': 'Sweep', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
response = client.get('/chore/c_get')
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['name'] == 'Sweep'
|
||||
|
||||
|
||||
def test_get_chore_not_found(client):
|
||||
response = client.get('/chore/nonexistent')
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_edit_chore(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'c_edit', 'name': 'Old Name', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
response = client.put('/chore/c_edit/edit', json={'name': 'New Name', 'points': 15})
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['name'] == 'New Name'
|
||||
assert data['points'] == 15
|
||||
|
||||
|
||||
def test_edit_system_chore_clones_to_user(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'sys_chore', 'name': 'System Chore', 'points': 5, 'type': 'chore', 'user_id': None})
|
||||
response = client.put('/chore/sys_chore/edit', json={'name': 'My Chore'})
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['name'] == 'My Chore'
|
||||
assert data['user_id'] == 'testuserid'
|
||||
assert data['id'] != 'sys_chore' # New ID since cloned
|
||||
|
||||
|
||||
def test_delete_chore(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'c_del', 'name': 'Delete Me', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
response = client.delete('/chore/c_del')
|
||||
assert response.status_code == 200
|
||||
assert task_db.get(Query().id == 'c_del') is None
|
||||
|
||||
|
||||
def test_delete_chore_not_found(client):
|
||||
response = client.delete('/chore/nonexistent')
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_delete_chore_removes_from_assigned_children(client):
|
||||
task_db.truncate()
|
||||
child_db.truncate()
|
||||
task_db.insert({'id': 'c_cascade', 'name': 'Cascade', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'child_cascade',
|
||||
'name': 'Alice',
|
||||
'age': 8,
|
||||
'points': 0,
|
||||
'tasks': ['c_cascade'],
|
||||
'rewards': [],
|
||||
'user_id': 'testuserid'
|
||||
})
|
||||
response = client.delete('/chore/c_cascade')
|
||||
assert response.status_code == 200
|
||||
child = child_db.get(Query().id == 'child_cascade')
|
||||
assert 'c_cascade' not in child.get('tasks', [])
|
||||
479
backend/tests/test_chore_confirmation.py
Normal file
479
backend/tests/test_chore_confirmation.py
Normal file
@@ -0,0 +1,479 @@
|
||||
import pytest
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
from datetime import date as date_type
|
||||
|
||||
from flask import Flask
|
||||
from api.child_api import child_api
|
||||
from api.auth_api import auth_api
|
||||
from db.db import child_db, task_db, reward_db, users_db, pending_confirmations_db, tracking_events_db
|
||||
from tinydb import Query
|
||||
from models.child import Child
|
||||
from models.pending_confirmation import PendingConfirmation
|
||||
from models.tracking_event import TrackingEvent
|
||||
|
||||
|
||||
TEST_EMAIL = "testuser@example.com"
|
||||
TEST_PASSWORD = "testpass"
|
||||
TEST_USER_ID = "testuserid"
|
||||
|
||||
|
||||
def add_test_user():
|
||||
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):
|
||||
resp = client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD})
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(child_api)
|
||||
app.register_blueprint(auth_api, url_prefix='/auth')
|
||||
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
|
||||
|
||||
|
||||
def setup_child_and_chore(child_name='TestChild', age=8, chore_points=10):
|
||||
"""Helper to create a child with one assigned chore."""
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
tracking_events_db.truncate()
|
||||
|
||||
task_db.insert({
|
||||
'id': 'chore1', 'name': 'Sweep Floor', 'points': chore_points,
|
||||
'type': 'chore', 'user_id': TEST_USER_ID, 'image_id': 'broom'
|
||||
})
|
||||
child = Child(name=child_name, age=age, image_id='boy01').to_dict()
|
||||
child['tasks'] = ['chore1']
|
||||
child['user_id'] = TEST_USER_ID
|
||||
child['points'] = 50
|
||||
child_db.insert(child)
|
||||
return child['id'], 'chore1'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Child Confirm Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_child_confirm_chore_success(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert 'confirmation_id' in data
|
||||
|
||||
# Verify PendingConfirmation was created
|
||||
PQ = Query()
|
||||
pending = pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||
)
|
||||
assert pending is not None
|
||||
assert pending['status'] == 'pending'
|
||||
|
||||
|
||||
def test_child_confirm_chore_not_assigned(client):
|
||||
child_id, _ = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/confirm-chore', json={'task_id': 'nonexistent'})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'ENTITY_NOT_ASSIGNED'
|
||||
|
||||
|
||||
def test_child_confirm_chore_not_found(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
child = Child(name='Kid', age=7, image_id='boy01').to_dict()
|
||||
child['tasks'] = ['missing_task']
|
||||
child['user_id'] = TEST_USER_ID
|
||||
child_db.insert(child)
|
||||
resp = client.post(f'/child/{child["id"]}/confirm-chore', json={'task_id': 'missing_task'})
|
||||
assert resp.status_code == 404
|
||||
assert resp.get_json()['code'] == 'TASK_NOT_FOUND'
|
||||
|
||||
|
||||
def test_child_confirm_chore_child_not_found(client):
|
||||
resp = client.post('/child/fake_child/confirm-chore', json={'task_id': 'chore1'})
|
||||
assert resp.status_code == 404
|
||||
|
||||
|
||||
def test_child_confirm_chore_already_pending(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
resp = client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'CHORE_ALREADY_PENDING'
|
||||
|
||||
|
||||
def test_child_confirm_chore_already_completed_today(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
# Simulate an approved confirmation for today
|
||||
from datetime import datetime, timezone
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id=child_id, entity_id=task_id, entity_type='chore',
|
||||
user_id=TEST_USER_ID, status='approved', approved_at=now
|
||||
).to_dict())
|
||||
resp = client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'CHORE_ALREADY_COMPLETED'
|
||||
|
||||
|
||||
def test_child_confirm_chore_creates_tracking_event(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
events = tracking_events_db.all()
|
||||
confirmed_events = [e for e in events if e.get('action') == 'confirmed' and e.get('entity_type') == 'chore']
|
||||
assert len(confirmed_events) == 1
|
||||
assert confirmed_events[0]['entity_id'] == task_id
|
||||
assert confirmed_events[0]['points_before'] == confirmed_events[0]['points_after']
|
||||
|
||||
|
||||
def test_child_confirm_chore_wrong_type(client):
|
||||
"""Kindness and penalty tasks cannot be confirmed."""
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
task_db.insert({
|
||||
'id': 'kind1', 'name': 'Kind Act', 'points': 5,
|
||||
'type': 'kindness', 'user_id': TEST_USER_ID
|
||||
})
|
||||
child = Child(name='Kid', age=7, image_id='boy01').to_dict()
|
||||
child['tasks'] = ['kind1']
|
||||
child['user_id'] = TEST_USER_ID
|
||||
child_db.insert(child)
|
||||
resp = client.post(f'/child/{child["id"]}/confirm-chore', json={'task_id': 'kind1'})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'INVALID_TASK_TYPE'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Child Cancel Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_child_cancel_confirm_success(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
resp = client.post(f'/child/{child_id}/cancel-confirm-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
# Pending record should be deleted
|
||||
PQ = Query()
|
||||
assert pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||
) is None
|
||||
|
||||
|
||||
def test_child_cancel_confirm_not_pending(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/cancel-confirm-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'PENDING_NOT_FOUND'
|
||||
|
||||
|
||||
def test_child_cancel_confirm_creates_tracking_event(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
tracking_events_db.truncate()
|
||||
client.post(f'/child/{child_id}/cancel-confirm-chore', json={'task_id': task_id})
|
||||
events = tracking_events_db.all()
|
||||
cancelled = [e for e in events if e.get('action') == 'cancelled']
|
||||
assert len(cancelled) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parent Approve Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_parent_approve_chore_success(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
|
||||
child_before = child_db.get(Query().id == child_id)
|
||||
points_before = child_before['points']
|
||||
|
||||
resp = client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['points'] == points_before + 10
|
||||
|
||||
# Verify confirmation is now approved
|
||||
PQ = Query()
|
||||
conf = pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||
)
|
||||
assert conf['status'] == 'approved'
|
||||
assert conf['approved_at'] is not None
|
||||
|
||||
|
||||
def test_parent_approve_chore_not_pending(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'PENDING_NOT_FOUND'
|
||||
|
||||
|
||||
def test_parent_approve_chore_creates_tracking_event(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=15)
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
tracking_events_db.truncate()
|
||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
events = tracking_events_db.all()
|
||||
approved = [e for e in events if e.get('action') == 'approved']
|
||||
assert len(approved) == 1
|
||||
assert approved[0]['points_after'] - approved[0]['points_before'] == 15
|
||||
|
||||
|
||||
def test_parent_approve_chore_points_correct(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=20)
|
||||
# Set child points to a known value
|
||||
child_db.update({'points': 100}, Query().id == child_id)
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
resp = client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['points'] == 120
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parent Reject Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_parent_reject_chore_success(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
child_db.update({'points': 50}, Query().id == child_id)
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
resp = client.post(f'/child/{child_id}/reject-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
# Points unchanged
|
||||
child = child_db.get(Query().id == child_id)
|
||||
assert child['points'] == 50
|
||||
# Pending record removed
|
||||
PQ = Query()
|
||||
assert pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id)
|
||||
) is None
|
||||
|
||||
|
||||
def test_parent_reject_chore_not_pending(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/reject-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
assert resp.get_json()['code'] == 'PENDING_NOT_FOUND'
|
||||
|
||||
|
||||
def test_parent_reject_chore_creates_tracking_event(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
tracking_events_db.truncate()
|
||||
client.post(f'/child/{child_id}/reject-chore', json={'task_id': task_id})
|
||||
events = tracking_events_db.all()
|
||||
rejected = [e for e in events if e.get('action') == 'rejected']
|
||||
assert len(rejected) == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parent Reset Flow
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_parent_reset_chore_success(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||
# Confirm and approve first
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
# Now reset
|
||||
child_before = child_db.get(Query().id == child_id)
|
||||
points_before = child_before['points']
|
||||
resp = client.post(f'/child/{child_id}/reset-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
# Points unchanged after reset
|
||||
child_after = child_db.get(Query().id == child_id)
|
||||
assert child_after['points'] == points_before
|
||||
# Confirmation record removed
|
||||
PQ = Query()
|
||||
assert pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id)
|
||||
) is None
|
||||
|
||||
|
||||
def test_parent_reset_chore_not_completed(client):
|
||||
child_id, task_id = setup_child_and_chore()
|
||||
resp = client.post(f'/child/{child_id}/reset-chore', json={'task_id': task_id})
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
def test_parent_reset_chore_creates_tracking_event(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
tracking_events_db.truncate()
|
||||
client.post(f'/child/{child_id}/reset-chore', json={'task_id': task_id})
|
||||
events = tracking_events_db.all()
|
||||
reset_events = [e for e in events if e.get('action') == 'reset']
|
||||
assert len(reset_events) == 1
|
||||
|
||||
|
||||
def test_parent_reset_then_child_confirm_again(client):
|
||||
"""Full cycle: confirm → approve → reset → confirm → approve."""
|
||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||
child_db.update({'points': 0}, Query().id == child_id)
|
||||
|
||||
# First cycle
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
child = child_db.get(Query().id == child_id)
|
||||
assert child['points'] == 10
|
||||
|
||||
# Reset
|
||||
client.post(f'/child/{child_id}/reset-chore', json={'task_id': task_id})
|
||||
|
||||
# Second cycle
|
||||
client.post(f'/child/{child_id}/confirm-chore', json={'task_id': task_id})
|
||||
client.post(f'/child/{child_id}/approve-chore', json={'task_id': task_id})
|
||||
child = child_db.get(Query().id == child_id)
|
||||
assert child['points'] == 20
|
||||
|
||||
# Verify tracking has two approved events
|
||||
approved = [e for e in tracking_events_db.all() if e.get('action') == 'approved']
|
||||
assert len(approved) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Parent Direct Trigger
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_parent_trigger_chore_directly_creates_approved_confirmation(client):
|
||||
child_id, task_id = setup_child_and_chore(chore_points=10)
|
||||
child_db.update({'points': 0}, Query().id == child_id)
|
||||
resp = client.post(f'/child/{child_id}/trigger-task', json={'task_id': task_id})
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['points'] == 10
|
||||
|
||||
# Verify an approved PendingConfirmation exists
|
||||
PQ = Query()
|
||||
conf = pending_confirmations_db.get(
|
||||
(PQ.child_id == child_id) & (PQ.entity_id == task_id) & (PQ.entity_type == 'chore')
|
||||
)
|
||||
assert conf is not None
|
||||
assert conf['status'] == 'approved'
|
||||
assert conf['approved_at'] is not None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Pending Confirmations List
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_list_pending_confirmations_returns_chores_and_rewards(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
reward_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
|
||||
child_db.insert({
|
||||
'id': 'ch1', 'name': 'Alice', 'age': 8, 'points': 100,
|
||||
'tasks': ['chore1'], 'rewards': ['rew1'], 'user_id': TEST_USER_ID,
|
||||
'image_id': 'girl01'
|
||||
})
|
||||
task_db.insert({'id': 'chore1', 'name': 'Mop Floor', 'points': 5, 'type': 'chore', 'user_id': TEST_USER_ID, 'image_id': 'broom'})
|
||||
reward_db.insert({'id': 'rew1', 'name': 'Ice Cream', 'cost': 10, 'user_id': TEST_USER_ID, 'image_id': 'ice-cream'})
|
||||
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id='ch1', entity_id='chore1', entity_type='chore', user_id=TEST_USER_ID
|
||||
).to_dict())
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id='ch1', entity_id='rew1', entity_type='reward', user_id=TEST_USER_ID
|
||||
).to_dict())
|
||||
|
||||
resp = client.get('/pending-confirmations')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['count'] == 2
|
||||
types = {c['entity_type'] for c in data['confirmations']}
|
||||
assert types == {'chore', 'reward'}
|
||||
|
||||
|
||||
def test_list_pending_confirmations_empty(client):
|
||||
pending_confirmations_db.truncate()
|
||||
resp = client.get('/pending-confirmations')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['count'] == 0
|
||||
assert data['confirmations'] == []
|
||||
|
||||
|
||||
def test_list_pending_confirmations_hydrates_names_and_images(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
|
||||
child_db.insert({
|
||||
'id': 'ch_hydrate', 'name': 'Bob', 'age': 9, 'points': 20,
|
||||
'tasks': ['t_hydrate'], 'rewards': [], 'user_id': TEST_USER_ID,
|
||||
'image_id': 'boy02'
|
||||
})
|
||||
task_db.insert({'id': 't_hydrate', 'name': 'Clean Room', 'points': 5, 'type': 'chore', 'user_id': TEST_USER_ID, 'image_id': 'broom'})
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id='ch_hydrate', entity_id='t_hydrate', entity_type='chore', user_id=TEST_USER_ID
|
||||
).to_dict())
|
||||
|
||||
resp = client.get('/pending-confirmations')
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert data['count'] == 1
|
||||
conf = data['confirmations'][0]
|
||||
assert conf['child_name'] == 'Bob'
|
||||
assert conf['entity_name'] == 'Clean Room'
|
||||
assert conf['child_image_id'] == 'boy02'
|
||||
assert conf['entity_image_id'] == 'broom'
|
||||
|
||||
|
||||
def test_list_pending_confirmations_excludes_approved(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
|
||||
child_db.insert({
|
||||
'id': 'ch_appr', 'name': 'Carol', 'age': 10, 'points': 0,
|
||||
'tasks': ['t_appr'], 'rewards': [], 'user_id': TEST_USER_ID,
|
||||
'image_id': 'girl01'
|
||||
})
|
||||
task_db.insert({'id': 't_appr', 'name': 'Chore', 'points': 5, 'type': 'chore', 'user_id': TEST_USER_ID})
|
||||
from datetime import datetime, timezone
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id='ch_appr', entity_id='t_appr', entity_type='chore',
|
||||
user_id=TEST_USER_ID, status='approved',
|
||||
approved_at=datetime.now(timezone.utc).isoformat()
|
||||
).to_dict())
|
||||
|
||||
resp = client.get('/pending-confirmations')
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['count'] == 0
|
||||
|
||||
|
||||
def test_list_pending_confirmations_filters_by_user(client):
|
||||
child_db.truncate()
|
||||
task_db.truncate()
|
||||
pending_confirmations_db.truncate()
|
||||
|
||||
# Create a pending confirmation for a different user
|
||||
pending_confirmations_db.insert(PendingConfirmation(
|
||||
child_id='other_child', entity_id='other_task', entity_type='chore', user_id='otheruserid'
|
||||
).to_dict())
|
||||
|
||||
resp = client.get('/pending-confirmations')
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['count'] == 0
|
||||
@@ -212,7 +212,7 @@ class TestDeletionProcess:
|
||||
id='user_task',
|
||||
name='User Task',
|
||||
points=10,
|
||||
is_good=True,
|
||||
type='chore',
|
||||
user_id=user_id
|
||||
)
|
||||
task_db.insert(user_task.to_dict())
|
||||
@@ -222,7 +222,7 @@ class TestDeletionProcess:
|
||||
id='system_task',
|
||||
name='System Task',
|
||||
points=20,
|
||||
is_good=True,
|
||||
type='chore',
|
||||
user_id=None
|
||||
)
|
||||
task_db.insert(system_task.to_dict())
|
||||
@@ -805,7 +805,7 @@ class TestIntegration:
|
||||
user_id=user_id,
|
||||
name='User Task',
|
||||
points=10,
|
||||
is_good=True
|
||||
type='chore'
|
||||
)
|
||||
task_db.insert(task.to_dict())
|
||||
|
||||
|
||||
83
backend/tests/test_kindness_api.py
Normal file
83
backend/tests/test_kindness_api.py
Normal file
@@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from flask import Flask
|
||||
from api.kindness_api import kindness_api
|
||||
from api.auth_api import auth_api
|
||||
from db.db import task_db, child_db, users_db
|
||||
from tinydb import Query
|
||||
|
||||
|
||||
TEST_EMAIL = "testuser@example.com"
|
||||
TEST_PASSWORD = "testpass"
|
||||
|
||||
def add_test_user():
|
||||
users_db.remove(Query().email == TEST_EMAIL)
|
||||
users_db.insert({
|
||||
"id": "testuserid",
|
||||
"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):
|
||||
resp = client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD})
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(kindness_api)
|
||||
app.register_blueprint(auth_api, url_prefix='/auth')
|
||||
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
|
||||
|
||||
|
||||
def test_add_kindness(client):
|
||||
task_db.truncate()
|
||||
response = client.put('/kindness/add', json={'name': 'Helped Sibling', 'points': 5})
|
||||
assert response.status_code == 201
|
||||
tasks = task_db.all()
|
||||
assert any(t.get('name') == 'Helped Sibling' and t.get('type') == 'kindness' for t in tasks)
|
||||
|
||||
|
||||
def test_list_kindness(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'k1', 'name': 'Kind', 'points': 5, 'type': 'kindness', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'c1', 'name': 'Chore', 'points': 3, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
response = client.get('/kindness/list')
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert len(data['tasks']) == 1
|
||||
assert data['tasks'][0]['id'] == 'k1'
|
||||
|
||||
|
||||
def test_edit_kindness(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'k_edit', 'name': 'Old', 'points': 5, 'type': 'kindness', 'user_id': 'testuserid'})
|
||||
response = client.put('/kindness/k_edit/edit', json={'name': 'New Kind'})
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['name'] == 'New Kind'
|
||||
|
||||
|
||||
def test_delete_kindness(client):
|
||||
task_db.truncate()
|
||||
child_db.truncate()
|
||||
task_db.insert({'id': 'k_del', 'name': 'Del Kind', 'points': 5, 'type': 'kindness', 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'ch_k', 'name': 'Bob', 'age': 7, 'points': 0,
|
||||
'tasks': ['k_del'], 'rewards': [], 'user_id': 'testuserid'
|
||||
})
|
||||
response = client.delete('/kindness/k_del')
|
||||
assert response.status_code == 200
|
||||
child = child_db.get(Query().id == 'ch_k')
|
||||
assert 'k_del' not in child.get('tasks', [])
|
||||
84
backend/tests/test_penalty_api.py
Normal file
84
backend/tests/test_penalty_api.py
Normal file
@@ -0,0 +1,84 @@
|
||||
import pytest
|
||||
import os
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
from flask import Flask
|
||||
from api.penalty_api import penalty_api
|
||||
from api.auth_api import auth_api
|
||||
from db.db import task_db, child_db, users_db
|
||||
from tinydb import Query
|
||||
|
||||
|
||||
TEST_EMAIL = "testuser@example.com"
|
||||
TEST_PASSWORD = "testpass"
|
||||
|
||||
def add_test_user():
|
||||
users_db.remove(Query().email == TEST_EMAIL)
|
||||
users_db.insert({
|
||||
"id": "testuserid",
|
||||
"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):
|
||||
resp = client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD})
|
||||
assert resp.status_code == 200
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
app = Flask(__name__)
|
||||
app.register_blueprint(penalty_api)
|
||||
app.register_blueprint(auth_api, url_prefix='/auth')
|
||||
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
|
||||
|
||||
|
||||
def test_add_penalty(client):
|
||||
task_db.truncate()
|
||||
response = client.put('/penalty/add', json={'name': 'Fighting', 'points': 10})
|
||||
assert response.status_code == 201
|
||||
tasks = task_db.all()
|
||||
assert any(t.get('name') == 'Fighting' and t.get('type') == 'penalty' for t in tasks)
|
||||
|
||||
|
||||
def test_list_penalties(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'p1', 'name': 'Yelling', 'points': 5, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'c1', 'name': 'Chore', 'points': 3, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
response = client.get('/penalty/list')
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert len(data['tasks']) == 1
|
||||
assert data['tasks'][0]['id'] == 'p1'
|
||||
|
||||
|
||||
def test_edit_penalty(client):
|
||||
task_db.truncate()
|
||||
task_db.insert({'id': 'p_edit', 'name': 'Old', 'points': 5, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
response = client.put('/penalty/p_edit/edit', json={'name': 'New Penalty', 'points': 20})
|
||||
assert response.status_code == 200
|
||||
data = response.get_json()
|
||||
assert data['name'] == 'New Penalty'
|
||||
assert data['points'] == 20
|
||||
|
||||
|
||||
def test_delete_penalty(client):
|
||||
task_db.truncate()
|
||||
child_db.truncate()
|
||||
task_db.insert({'id': 'p_del', 'name': 'Del Pen', 'points': 5, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'ch_p', 'name': 'Carol', 'age': 9, 'points': 0,
|
||||
'tasks': ['p_del'], 'rewards': [], 'user_id': 'testuserid'
|
||||
})
|
||||
response = client.delete('/penalty/p_del')
|
||||
assert response.status_code == 200
|
||||
child = child_db.get(Query().id == 'ch_p')
|
||||
assert 'p_del' not in child.get('tasks', [])
|
||||
@@ -52,27 +52,27 @@ def cleanup_db():
|
||||
os.remove('tasks.json')
|
||||
|
||||
def test_add_task(client):
|
||||
response = client.put('/task/add', json={'name': 'Clean Room', 'points': 10, 'is_good': True})
|
||||
response = client.put('/task/add', json={'name': 'Clean Room', 'points': 10, 'type': 'chore'})
|
||||
assert response.status_code == 201
|
||||
assert b'Task Clean Room added.' in response.data
|
||||
# verify in database
|
||||
tasks = task_db.all()
|
||||
assert any(task.get('name') == 'Clean Room' and task.get('points') == 10 and task.get('is_good') is True and task.get('image_id') == '' for task in tasks)
|
||||
assert any(task.get('name') == 'Clean Room' and task.get('points') == 10 and task.get('type') == 'chore' and task.get('image_id') == '' for task in tasks)
|
||||
|
||||
response = client.put('/task/add', json={'name': 'Eat Dinner', 'points': 5, 'is_good': False, 'image_id': 'meal'})
|
||||
response = client.put('/task/add', json={'name': 'Eat Dinner', 'points': 5, 'type': 'penalty', 'image_id': 'meal'})
|
||||
assert response.status_code == 201
|
||||
assert b'Task Eat Dinner added.' in response.data
|
||||
# verify in database
|
||||
tasks = task_db.all()
|
||||
assert any(task.get('name') == 'Eat Dinner' and task.get('points') == 5 and task.get('is_good') is False and task.get('image_id') == 'meal' for task in tasks)
|
||||
assert any(task.get('name') == 'Eat Dinner' and task.get('points') == 5 and task.get('type') == 'penalty' and task.get('image_id') == 'meal' for task in tasks)
|
||||
|
||||
|
||||
|
||||
def test_list_tasks(client):
|
||||
task_db.truncate()
|
||||
# Insert user-owned tasks
|
||||
task_db.insert({'id': 't1', 'name': 'Task1', 'points': 5, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't2', 'name': 'Task2', 'points': 15, 'is_good': False, 'image_id': 'meal', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't1', 'name': 'Task1', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't2', 'name': 'Task2', 'points': 15, 'type': 'penalty', 'image_id': 'meal', 'user_id': 'testuserid'})
|
||||
response = client.get('/task/list')
|
||||
assert response.status_code == 200
|
||||
assert b'tasks' in response.data
|
||||
@@ -83,15 +83,15 @@ def test_list_tasks(client):
|
||||
def test_list_tasks_sorted_by_is_good_then_user_then_default_then_name(client):
|
||||
task_db.truncate()
|
||||
|
||||
task_db.insert({'id': 'u_good_z', 'name': 'Zoo', 'points': 1, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'u_good_a', 'name': 'Apple', 'points': 1, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'd_good_m', 'name': 'Mop', 'points': 1, 'is_good': True, 'user_id': None})
|
||||
task_db.insert({'id': 'd_good_b', 'name': 'Brush', 'points': 1, 'is_good': True, 'user_id': None})
|
||||
task_db.insert({'id': 'u_good_z', 'name': 'Zoo', 'points': 1, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'u_good_a', 'name': 'Apple', 'points': 1, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'd_good_m', 'name': 'Mop', 'points': 1, 'type': 'chore', 'user_id': None})
|
||||
task_db.insert({'id': 'd_good_b', 'name': 'Brush', 'points': 1, 'type': 'chore', 'user_id': None})
|
||||
|
||||
task_db.insert({'id': 'u_bad_c', 'name': 'Chore', 'points': 1, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'u_bad_a', 'name': 'Alarm', 'points': 1, 'is_good': False, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'd_bad_y', 'name': 'Yell', 'points': 1, 'is_good': False, 'user_id': None})
|
||||
task_db.insert({'id': 'd_bad_b', 'name': 'Bicker', 'points': 1, 'is_good': False, 'user_id': None})
|
||||
task_db.insert({'id': 'u_bad_c', 'name': 'Chore', 'points': 1, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'u_bad_a', 'name': 'Alarm', 'points': 1, 'type': 'penalty', 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 'd_bad_y', 'name': 'Yell', 'points': 1, 'type': 'penalty', 'user_id': None})
|
||||
task_db.insert({'id': 'd_bad_b', 'name': 'Bicker', 'points': 1, 'type': 'penalty', 'user_id': None})
|
||||
|
||||
response = client.get('/task/list')
|
||||
assert response.status_code == 200
|
||||
@@ -122,7 +122,7 @@ def test_delete_task_not_found(client):
|
||||
|
||||
def test_delete_assigned_task_removes_from_child(client):
|
||||
# create user-owned task and child with the task already assigned
|
||||
task_db.insert({'id': 't_delete_assigned', 'name': 'Temp Task', 'points': 5, 'is_good': True, 'user_id': 'testuserid'})
|
||||
task_db.insert({'id': 't_delete_assigned', 'name': 'Temp Task', 'points': 5, 'type': 'chore', 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'child_for_task_delete',
|
||||
'name': 'Frank',
|
||||
|
||||
Reference in New Issue
Block a user