Add push notification functionality with tests and digest scheduler
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
All checks were successful
Chore App Build, Test, and Push Docker Images / build-and-push (push) Successful in 2m14s
- Implemented push subscription API with tests for subscribing and unsubscribing users. - Created web push notification tests triggered by child actions. - Added digest scheduler to send email digests to users at 9 PM local time. - Developed utility functions for creating and validating digest action tokens. - Integrated web push sender to handle sending notifications to users. - Added service worker for handling push notifications in the frontend. - Created a push opt-in component for user notification preferences. - Implemented tests for the push opt-in component to ensure correct behavior. - Updated frontend services to manage push subscriptions and permissions.
This commit is contained in:
@@ -5,7 +5,7 @@ import os
|
||||
from flask import Flask
|
||||
from api.child_api import child_api
|
||||
from api.auth_api import auth_api
|
||||
from db.db import child_db, reward_db, task_db, users_db, chore_schedules_db, task_extensions_db
|
||||
from db.db import child_db, reward_db, task_db, users_db, chore_schedules_db, task_extensions_db, pending_confirmations_db
|
||||
from tinydb import Query
|
||||
from models.child import Child
|
||||
import jwt
|
||||
@@ -514,4 +514,100 @@ def test_list_child_tasks_no_server_side_filtering(client):
|
||||
returned_ids = {t['id'] for t in resp.get_json()['tasks']}
|
||||
# Both good tasks must be present; server never filters based on schedule/time
|
||||
assert TASK_GOOD_ID in returned_ids
|
||||
assert extra_id in returned_ids
|
||||
assert extra_id in returned_ids
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# request-reward: duplicate guard
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_request_reward_duplicate_returns_409(client):
|
||||
"""Requesting the same reward twice without resolution returns 409 Conflict."""
|
||||
reward_db.insert({'id': 'r_dup', 'name': 'Duplicate Reward', 'cost': 5, 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'child_dup',
|
||||
'name': 'Dupe Kid',
|
||||
'age': 8,
|
||||
'points': 20,
|
||||
'tasks': [],
|
||||
'rewards': ['r_dup'],
|
||||
'user_id': 'testuserid',
|
||||
})
|
||||
|
||||
first = client.post('/child/child_dup/request-reward', json={'reward_id': 'r_dup'})
|
||||
assert first.status_code == 200
|
||||
|
||||
second = client.post('/child/child_dup/request-reward', json={'reward_id': 'r_dup'})
|
||||
assert second.status_code == 409
|
||||
assert second.get_json()['code'] == 'DUPLICATE_REWARD_REQUEST'
|
||||
|
||||
# Cleanup
|
||||
pending_confirmations_db.remove(Query().child_id == 'child_dup')
|
||||
child_db.remove(Query().id == 'child_dup')
|
||||
reward_db.remove(Query().id == 'r_dup')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# deny-reward-request endpoint
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_deny_reward_request_removes_pending(client):
|
||||
"""Denying a reward request removes the pending confirmation and fires REQUEST_CANCELLED."""
|
||||
reward_db.insert({'id': 'r_deny1', 'name': 'Deny Reward', 'cost': 5, 'user_id': 'testuserid'})
|
||||
child_db.insert({
|
||||
'id': 'child_deny1',
|
||||
'name': 'Deny Kid',
|
||||
'age': 9,
|
||||
'points': 10,
|
||||
'tasks': [],
|
||||
'rewards': ['r_deny1'],
|
||||
'user_id': 'testuserid',
|
||||
})
|
||||
pending_confirmations_db.insert({
|
||||
'id': 'pend_deny1',
|
||||
'child_id': 'child_deny1',
|
||||
'entity_id': 'r_deny1',
|
||||
'entity_type': 'reward',
|
||||
'user_id': 'testuserid',
|
||||
'status': 'pending',
|
||||
'approved_at': None,
|
||||
})
|
||||
|
||||
resp = client.post('/child/child_deny1/deny-reward-request', json={'reward_id': 'r_deny1'})
|
||||
assert resp.status_code == 200
|
||||
|
||||
remaining = pending_confirmations_db.get(
|
||||
(Query().child_id == 'child_deny1') & (Query().entity_id == 'r_deny1')
|
||||
)
|
||||
assert remaining is None
|
||||
|
||||
# Cleanup
|
||||
child_db.remove(Query().id == 'child_deny1')
|
||||
reward_db.remove(Query().id == 'r_deny1')
|
||||
|
||||
|
||||
def test_deny_reward_request_already_resolved_returns_200(client):
|
||||
"""If no pending request exists, denying returns 200 with an informational message."""
|
||||
resp = client.post('/child/child_gone/deny-reward-request', json={'reward_id': 'r_gone'})
|
||||
assert resp.status_code == 200
|
||||
data = resp.get_json()
|
||||
assert 'already been resolved' in data['message']
|
||||
|
||||
|
||||
def test_deny_reward_request_requires_auth(client):
|
||||
"""Deny-reward-request endpoint rejects unauthenticated requests."""
|
||||
# Create a fresh unauthenticated client
|
||||
from flask import Flask
|
||||
from api.child_api import child_api as child_api_bp
|
||||
from api.auth_api import auth_api as auth_api_bp
|
||||
from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS
|
||||
|
||||
app2 = Flask(__name__)
|
||||
app2.register_blueprint(child_api_bp)
|
||||
app2.register_blueprint(auth_api_bp, url_prefix='/auth')
|
||||
app2.config['TESTING'] = True
|
||||
app2.config['SECRET_KEY'] = TEST_SECRET_KEY
|
||||
app2.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS
|
||||
with app2.test_client() as anon:
|
||||
resp = anon.post('/child/someid/deny-reward-request', json={'reward_id': 'r1'})
|
||||
assert resp.status_code == 401
|
||||
Reference in New Issue
Block a user