import os import pytest from flask import Flask from werkzeug.security import generate_password_hash from tinydb import Query from api.push_subscription_api import push_subscription_api from api.auth_api import auth_api from db.db import users_db, push_subscriptions_db from tests.conftest import TEST_SECRET_KEY, TEST_REFRESH_TOKEN_EXPIRY_DAYS TEST_EMAIL = "pushtest@example.com" TEST_PASSWORD = "pushpassword123" TEST_USER_ID = "push_test_user_id" TEST_ENDPOINT = "https://fcm.googleapis.com/fcm/send/test-endpoint-abc" TEST_KEYS = {"p256dh": "BNgz3XcMv1", "auth": "abc123"} def seed_user(): users_db.remove(Query().email == TEST_EMAIL) users_db.insert({ "id": TEST_USER_ID, "first_name": "Push", "last_name": "Tester", "email": TEST_EMAIL, "password": generate_password_hash(TEST_PASSWORD), "verified": True, "image_id": None, "marked_for_deletion": False, "marked_for_deletion_at": None, "role": "user", "timezone": None, "email_digest_enabled": True, }) def cleanup(): users_db.remove(Query().email == TEST_EMAIL) push_subscriptions_db.remove(Query().user_id == TEST_USER_ID) @pytest.fixture def client(): app = Flask(__name__) app.register_blueprint(push_subscription_api) app.register_blueprint(auth_api, url_prefix='/auth') app.config['TESTING'] = True app.config['SECRET_KEY'] = TEST_SECRET_KEY app.config['REFRESH_TOKEN_EXPIRY_DAYS'] = TEST_REFRESH_TOKEN_EXPIRY_DAYS app.config['VAPID_PUBLIC_KEY'] = 'test-vapid-public-key' app.config['FRONTEND_URL'] = 'http://localhost:5173' cleanup() seed_user() with app.test_client() as c: yield c cleanup() @pytest.fixture def auth_client(client): client.post('/auth/login', json={"email": TEST_EMAIL, "password": TEST_PASSWORD}) return client class TestVapidKey: def test_returns_public_key(self, client): res = client.get('/push-vapid-key') assert res.status_code == 200 assert res.get_json()['public_key'] == 'test-vapid-public-key' def test_unauthenticated_ok(self, client): # VAPID key endpoint is public res = client.get('/push-vapid-key') assert res.status_code == 200 class TestSubscribe: def test_subscribe_stores_subscription(self, auth_client): res = auth_client.post('/push-subscription', json={ "endpoint": TEST_ENDPOINT, "keys": TEST_KEYS, }) assert res.status_code == 200 data = res.get_json() assert 'id' in data saved = push_subscriptions_db.get( (Query().user_id == TEST_USER_ID) & (Query().endpoint == TEST_ENDPOINT) ) assert saved is not None def test_subscribe_updates_timezone(self, auth_client): auth_client.post('/push-subscription', json={ "endpoint": TEST_ENDPOINT, "keys": TEST_KEYS, "timezone": "America/New_York", }) user = users_db.get(Query().id == TEST_USER_ID) assert user['timezone'] == 'America/New_York' def test_subscribe_upserts_same_endpoint(self, auth_client): auth_client.post('/push-subscription', json={"endpoint": TEST_ENDPOINT, "keys": TEST_KEYS}) auth_client.post('/push-subscription', json={"endpoint": TEST_ENDPOINT, "keys": TEST_KEYS}) count = len(push_subscriptions_db.search( (Query().user_id == TEST_USER_ID) & (Query().endpoint == TEST_ENDPOINT) )) assert count == 1 def test_subscribe_requires_endpoint(self, auth_client): res = auth_client.post('/push-subscription', json={"keys": TEST_KEYS}) assert res.status_code == 400 def test_subscribe_requires_keys(self, auth_client): res = auth_client.post('/push-subscription', json={"endpoint": TEST_ENDPOINT}) assert res.status_code == 400 def test_subscribe_requires_p256dh_and_auth(self, auth_client): res = auth_client.post('/push-subscription', json={ "endpoint": TEST_ENDPOINT, "keys": {"p256dh": "only-one-key"}, }) assert res.status_code == 400 def test_subscribe_requires_auth(self, client): res = client.post('/push-subscription', json={ "endpoint": TEST_ENDPOINT, "keys": TEST_KEYS, }) assert res.status_code == 401 def test_subscribe_multiple_endpoints_per_user(self, auth_client): """Multiple subscriptions can coexist for the same user (one per device).""" endpoint2 = "https://fcm.googleapis.com/fcm/send/second-device-endpoint" keys2 = {"p256dh": "BNgz3XcMv2", "auth": "def456"} auth_client.post('/push-subscription', json={"endpoint": TEST_ENDPOINT, "keys": TEST_KEYS}) auth_client.post('/push-subscription', json={"endpoint": endpoint2, "keys": keys2}) subs = push_subscriptions_db.search(Query().user_id == TEST_USER_ID) assert len(subs) == 2 endpoints = {s['endpoint'] for s in subs} assert TEST_ENDPOINT in endpoints assert endpoint2 in endpoints class TestUnsubscribe: def test_unsubscribe_removes_subscription(self, auth_client): auth_client.post('/push-subscription', json={"endpoint": TEST_ENDPOINT, "keys": TEST_KEYS}) res = auth_client.delete('/push-subscription', json={"endpoint": TEST_ENDPOINT}) assert res.status_code == 200 data = res.get_json() assert data['removed'] >= 1 saved = push_subscriptions_db.get( (Query().user_id == TEST_USER_ID) & (Query().endpoint == TEST_ENDPOINT) ) assert saved is None def test_unsubscribe_nonexistent_endpoint_ok(self, auth_client): res = auth_client.delete('/push-subscription', json={"endpoint": "https://nonexistent"}) assert res.status_code == 200 assert res.get_json()['removed'] == 0 def test_unsubscribe_requires_endpoint(self, auth_client): res = auth_client.delete('/push-subscription', json={}) assert res.status_code == 400 def test_unsubscribe_requires_auth(self, client): res = client.delete('/push-subscription', json={"endpoint": TEST_ENDPOINT}) assert res.status_code == 401