# retoor import uuid import requests from tests.conftest import BASE_URL def _session_push(): s = requests.Session() name = f"push_{uuid.uuid4().hex[:10]}" s.post( f"{BASE_URL}/auth/signup", data={ "username": name, "email": f"{name}@test.dev", "password": "secret123", "confirm_password": "secret123", "birth_date": "1990-01-01", "accept_terms": "1", }, allow_redirects=True, ) return s def test_browser_base64_is_url_safe_unpadded(): import base64 from devplacepy import push encoded = push.browser_base64(b"\xff\xfe\x00 hello") assert "=" not in encoded assert "+" not in encoded and "/" not in encoded assert base64.urlsafe_b64decode(encoded + "==") == b"\xff\xfe\x00 hello" def test_hkdf_returns_requested_length(): from devplacepy import push derived = push.hkdf(b"input-key-material", b"salt", b"info", 16) assert isinstance(derived, bytes) assert len(derived) == 16 def test_public_key_standard_b64_non_empty(): from devplacepy import push assert push.public_key_standard_b64() def test_create_notification_authorization_is_jwt(): from devplacepy import push token = push.create_notification_authorization("https://push.example.com/endpoint") assert token.count(".") == 2 def test_provider_registry_resolves_default_for_missing_name(): from devplacepy.push import providers assert providers.get(None) is providers.PROVIDERS["webpush"] assert providers.get("") is providers.PROVIDERS["webpush"] assert providers.get(" APNS ") is providers.PROVIDERS["apns"] assert providers.get("nope") is None def test_webpush_parse_registration_accepts_subscription_shape(): from devplacepy.push import providers webpush = providers.PROVIDERS["webpush"] fields = webpush.parse_registration( { "endpoint": "https://push.example.com/sub", "expirationTime": None, "keys": {"p256dh": "p", "auth": "a"}, } ) assert fields == { "endpoint": "https://push.example.com/sub", "key_auth": "a", "key_p256dh": "p", } def test_webpush_parse_registration_rejects_incomplete_bodies(): from devplacepy.push import providers webpush = providers.PROVIDERS["webpush"] assert webpush.parse_registration({"endpoint": "https://push.example.com/x"}) is None assert webpush.parse_registration({"keys": {"p256dh": "p", "auth": "a"}}) is None assert ( webpush.parse_registration( {"endpoint": "https://push.example.com/x", "keys": {"p256dh": "p"}} ) is None ) def test_apns_parse_registration_validates_device_token(): from devplacepy.push import providers apns = providers.PROVIDERS["apns"] token = "a1b2c3d4" * 8 assert apns.parse_registration({"token": f" {token} "}) == {"token": token} assert apns.parse_registration({"token": "abc"}) is None assert apns.parse_registration({"token": "z" * 64}) is None assert apns.parse_registration({"token": "a" * 500}) is None assert apns.parse_registration({"token": None}) is None assert apns.parse_registration({}) is None def test_apns_prepare_translates_the_shared_payload(): import json from devplacepy.push import providers body = json.loads( providers.PROVIDERS["apns"].prepare( { "title": "DevPlace", "message": "You have a new notification.", "icon": "/static/apple-touch-icon.png", "url": "/notifications", } ) ) assert body["aps"]["alert"] == { "title": "DevPlace", "body": "You have a new notification.", } assert body["aps"]["thread-id"] == "devplace-notification" assert body["url"] == "/notifications" assert body["icon"] == "/static/apple-touch-icon.png" def test_apns_prepare_survives_an_empty_payload(): import json from devplacepy.push import providers body = json.loads(providers.PROVIDERS["apns"].prepare({})) assert body["aps"]["alert"]["title"] == "DevPlace" assert body["url"] == "/notifications" def _apns_settings(monkeypatch, **values): from devplacepy.push.providers import apns defaults = { apns.TEAM_ID_KEY: "", apns.KEY_ID_KEY: "", apns.AUTH_KEY_KEY: "", apns.TOPIC_KEY: "", apns.ENVIRONMENT_KEY: "", } defaults.update(values) monkeypatch.setattr(apns, "_setting", lambda key: defaults.get(key, "")) apns._token_state.clear() return defaults def _ec_private_key_pem(): from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec key = ec.generate_private_key(ec.SECP256R1()) return key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.PKCS8, encryption_algorithm=serialization.NoEncryption(), ).decode("utf-8") def test_apns_is_configured_requires_every_credential(monkeypatch): from devplacepy.push import providers from devplacepy.push.providers import apns provider = providers.PROVIDERS["apns"] _apns_settings(monkeypatch) assert provider.is_configured() is False assert providers.is_active(provider) is False _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: "pem", }, ) assert provider.is_configured() is False _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: "pem", apns.TOPIC_KEY: "nl.molodetz.devplace", }, ) assert provider.is_configured() is True def test_apns_host_falls_back_to_production(monkeypatch): from devplacepy.push.providers import apns _apns_settings(monkeypatch) assert apns.host() == "api.push.apple.com" _apns_settings(monkeypatch, **{apns.ENVIRONMENT_KEY: "sandbox"}) assert apns.host() == "api.sandbox.push.apple.com" _apns_settings(monkeypatch, **{apns.ENVIRONMENT_KEY: "nonsense"}) assert apns.host() == "api.push.apple.com" def test_apns_provider_token_is_signed_and_cached(monkeypatch): import jwt from devplacepy.push.providers import apns _apns_settings(monkeypatch) pem = _ec_private_key_pem() token = apns.provider_token("TEAMID1234", "KEYID12345", pem) assert apns.provider_token("TEAMID1234", "KEYID12345", pem) == token header = jwt.get_unverified_header(token) claims = jwt.decode(token, options={"verify_signature": False}) assert header["alg"] == "ES256" assert header["kid"] == "KEYID12345" assert claims["iss"] == "TEAMID1234" assert isinstance(claims["iat"], int) other = apns.provider_token("TEAMID1234", "KEYID12345", _ec_private_key_pem()) assert other != token def test_apns_provider_token_rejects_a_broken_auth_key(monkeypatch): import pytest from devplacepy.push.providers import apns _apns_settings(monkeypatch) with pytest.raises(ValueError): apns.provider_token("TEAMID1234", "KEYID12345", "not-a-pem") with pytest.raises(ValueError): apns.provider_token("TEAMID1234", "KEYID12345", "not-a-pem") def _apns_response_status(monkeypatch, status, body): import httpx from tests.conftest import run_async from devplacepy.push import providers from devplacepy.push.providers import apns _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: _ec_private_key_pem(), apns.TOPIC_KEY: "nl.molodetz.devplace", }, ) provider = providers.PROVIDERS["apns"] seen = {} def handler(request): seen["url"] = str(request.url) seen["headers"] = dict(request.headers) return httpx.Response(status, json=body) async def run(): transport = httpx.MockTransport(handler) async with httpx.AsyncClient(transport=transport) as client: return await provider.deliver( client, {"token": "a" * 64}, provider.prepare({"message": "hi"}) ) return run_async(run()), seen def test_apns_delivery_maps_statuses(monkeypatch): from devplacepy.push import providers accepted, seen = _apns_response_status(monkeypatch, 200, {}) assert accepted.status == providers.ACCEPTED assert seen["url"] == f"https://api.push.apple.com/3/device/{'a' * 64}" assert seen["headers"]["apns-topic"] == "nl.molodetz.devplace" assert seen["headers"]["apns-push-type"] == "alert" assert seen["headers"]["apns-priority"] == "10" assert seen["headers"]["authorization"].startswith("bearer ") assert int(seen["headers"]["apns-expiration"]) > 0 assert seen["headers"]["apns-id"] gone, _ = _apns_response_status(monkeypatch, 410, {"reason": "Unregistered"}) assert gone.status == providers.DEAD bad_token, _ = _apns_response_status(monkeypatch, 400, {"reason": "BadDeviceToken"}) assert bad_token.status == providers.DEAD payload_error, _ = _apns_response_status( monkeypatch, 400, {"reason": "PayloadTooLarge"} ) assert payload_error.status == providers.REJECTED throttled, _ = _apns_response_status(monkeypatch, 429, {"reason": "TooManyRequests"}) assert throttled.status == providers.REJECTED def test_apns_dead_reasons_excludes_topic_misconfiguration(monkeypatch): from devplacepy.push import providers not_for_topic, _ = _apns_response_status( monkeypatch, 400, {"reason": "DeviceTokenNotForTopic"} ) assert not_for_topic.status == providers.REJECTED disallowed, _ = _apns_response_status(monkeypatch, 400, {"reason": "TopicDisallowed"}) assert disallowed.status == providers.REJECTED expired, _ = _apns_response_status(monkeypatch, 410, {"reason": "ExpiredToken"}) assert expired.status == providers.DEAD def test_apns_dead_carries_apns_confirmation_timestamp(monkeypatch): from datetime import datetime, timezone from devplacepy.push import providers gone, _ = _apns_response_status( monkeypatch, 410, {"reason": "Unregistered", "timestamp": 1700000000000} ) assert gone.status == providers.DEAD assert gone.dead_before == datetime.fromtimestamp( 1700000000000 / 1000, tz=timezone.utc ).isoformat() bad_token, _ = _apns_response_status(monkeypatch, 400, {"reason": "BadDeviceToken"}) assert bad_token.status == providers.DEAD assert bad_token.dead_before is None def test_apns_auth_failure_invalidates_cached_token(monkeypatch, local_db): import httpx from tests.conftest import run_async from devplacepy.push import providers from devplacepy.push.providers import apns _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: _ec_private_key_pem(), apns.TOPIC_KEY: "nl.molodetz.devplace", }, ) provider = providers.PROVIDERS["apns"] pem = _ec_private_key_pem() first = apns.provider_token("TEAMID1234", "KEYID12345", pem) assert apns._token_state["current"]["token"] == first assert apns._read_shared_token()["token"] == first def handler(request): return httpx.Response(403, json={"reason": "InvalidProviderToken"}) async def run(): transport = httpx.MockTransport(handler) async with httpx.AsyncClient(transport=transport) as client: return await provider.deliver( client, {"token": "a" * 64}, provider.prepare({"message": "hi"}) ) outcome = run_async(run()) assert outcome.status == providers.REJECTED assert "current" not in apns._token_state assert apns._read_shared_token() is None second = apns.provider_token("TEAMID1234", "KEYID12345", pem) assert second != first def test_apns_provider_token_is_shared_across_workers(monkeypatch, local_db): from devplacepy.push.providers import apns _apns_settings(monkeypatch) pem = _ec_private_key_pem() token = apns.provider_token("TEAMID1234", "KEYID12345", pem) apns._token_state.clear() reused = apns.provider_token("TEAMID1234", "KEYID12345", pem) assert reused == token assert apns._token_state["current"]["token"] == token def test_apns_delivery_without_configuration_never_raises(monkeypatch): import httpx from tests.conftest import run_async from devplacepy.push import providers _apns_settings(monkeypatch) provider = providers.PROVIDERS["apns"] async def run(): transport = httpx.MockTransport(lambda request: httpx.Response(200, json={})) async with httpx.AsyncClient(transport=transport) as client: return await provider.deliver(client, {"token": "a" * 64}, "{}") assert run_async(run()).status == providers.REJECTED def test_group_by_provider_treats_a_missing_provider_as_webpush(): from devplacepy.push.delivery import group_by_provider grouped = group_by_provider( [ {"id": 1, "provider": None}, {"id": 2, "provider": ""}, {"id": 3, "provider": "webpush"}, {"id": 4, "provider": "apns"}, ] ) assert sorted(grouped) == ["apns", "webpush"] assert len(grouped["webpush"]) == 3 assert len(grouped["apns"]) == 1 def test_delivery_timeout_is_clamped(monkeypatch): from devplacepy.push import delivery monkeypatch.setattr(delivery, "get_int_setting", lambda key, default: default) assert delivery.timeout_seconds() == float(delivery.DEFAULT_TIMEOUT_SECONDS) monkeypatch.setattr(delivery, "get_int_setting", lambda key, default: 0) assert delivery.timeout_seconds() == float(delivery.MIN_TIMEOUT_SECONDS) monkeypatch.setattr(delivery, "get_int_setting", lambda key, default: 100000) assert delivery.timeout_seconds() == float(delivery.MAX_TIMEOUT_SECONDS) def test_apns_parse_registration_accepts_client_id_and_normalizes_token(): from devplacepy.push import providers apns = providers.PROVIDERS["apns"] token = "A1B2C3D4" * 8 assert apns.parse_registration( {"token": f"<{token[:8]} {token[8:]}>", "client_id": " device-1 "} ) == {"token": token.lower(), "client_id": "device-1"} assert apns.parse_registration({"token": token, "client_id": ""}) == { "token": token.lower() } assert apns.parse_registration({"token": token, "client_id": {"uid": "x"}}) is None assert apns.parse_registration({"token": token, "client_id": "x" * 200}) is None assert apns.parse_registration({"client_id": "device-1"}) is None def test_apns_client_config_advertises_environment(monkeypatch): from devplacepy.push import providers from devplacepy.push.providers import apns _apns_settings(monkeypatch, **{apns.ENVIRONMENT_KEY: "sandbox"}) assert providers.PROVIDERS["apns"].client_config() == {"environment": "sandbox"} _apns_settings(monkeypatch, **{apns.ENVIRONMENT_KEY: "nonsense"}) assert providers.PROVIDERS["apns"].client_config() == {"environment": "production"} def test_apns_stamp_registration_sets_environment(monkeypatch): from devplacepy.push import providers from devplacepy.push.providers import apns _apns_settings(monkeypatch, **{apns.ENVIRONMENT_KEY: "sandbox"}) stamped = providers.PROVIDERS["apns"].stamp_registration({"token": "a" * 64}) assert stamped["environment"] == "sandbox" def test_apns_delivery_uses_registration_environment(monkeypatch): from devplacepy.push import providers from devplacepy.push.providers import apns _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: _ec_private_key_pem(), apns.TOPIC_KEY: "nl.molodetz.devplace", apns.ENVIRONMENT_KEY: "production", }, ) seen = {} def handler(request): seen["url"] = str(request.url) return __import__("httpx").Response(200, json={}) async def run(): import httpx transport = httpx.MockTransport(handler) async with httpx.AsyncClient(transport=transport) as client: return await providers.PROVIDERS["apns"].deliver( client, {"token": "a" * 64, "environment": "sandbox"}, "{}", ) from tests.conftest import run_async outcome = run_async(run()) assert outcome.status == providers.ACCEPTED assert seen["url"].startswith("https://api.sandbox.push.apple.com/3/device/") def test_apns_delivery_client_is_plain_http2(): import httpx from devplacepy.curl_transport import CurlTransport from devplacepy.push.providers import apns from tests.conftest import run_async client = apns.gateway_client(5.0) assert isinstance(client, httpx.AsyncClient) assert not isinstance(client._transport, CurlTransport) run_async(client.aclose()) def test_apns_delivery_client_persists_across_calls(): from devplacepy.push import providers from devplacepy.push.providers import apns from tests.conftest import run_async provider = providers.PROVIDERS["apns"] assert provider.closes_delivery_client() is False run_async(apns.close_client()) try: first = provider.delivery_client(5.0) second = provider.delivery_client(5.0) assert first is second assert not first.is_closed run_async(apns.close_client()) assert first.is_closed third = provider.delivery_client(5.0) assert third is not first finally: run_async(apns.close_client()) def test_webpush_closes_delivery_client_by_default(): from devplacepy.push import providers provider = providers.PROVIDERS["webpush"] assert provider.closes_delivery_client() is True def test_store_upserts_by_client_id_and_revives_dead_tokens(local_db): from devplacepy.push import store user_uid = "user-apns-1" token = "a" * 64 write = store.register(user_uid, "apns", {"token": token, "client_id": "phone"}) assert write.created is True assert write.revived is False assert write.probe is True same = store.register(user_uid, "apns", {"token": token, "client_id": "phone"}) assert same.created is False assert same.revived is False assert same.record["uid"] == write.record["uid"] store.mark_dead(write.record["id"]) revived = store.register(user_uid, "apns", {"token": token, "client_id": "phone"}) assert revived.created is False assert revived.revived is True assert revived.record["deleted_at"] is None assert len(store.active_for_user(user_uid)) == 1 rotated = store.register( user_uid, "apns", {"token": "b" * 64, "client_id": "phone"} ) assert rotated.created is False assert rotated.record["token"] == "b" * 64 assert rotated.record["uid"] == write.record["uid"] assert len(store.active_for_user(user_uid)) == 1 def test_store_revives_token_only_registration(local_db): from devplacepy.push import store user_uid = "user-apns-2" token = "c" * 64 write = store.register(user_uid, "apns", {"token": token}) store.mark_dead(write.record["id"]) revived = store.register(user_uid, "apns", {"token": token, "client_id": "phone-2"}) assert revived.revived is True assert revived.record["client_id"] == "phone-2" assert len(store.active_for_user(user_uid)) == 1 def test_store_attaches_client_id_to_existing_token_row(local_db): from devplacepy.push import store user_uid = "user-apns-3" token = "d" * 64 first = store.register(user_uid, "apns", {"token": token}) second = store.register(user_uid, "apns", {"token": token, "client_id": "phone-3"}) assert second.created is False assert second.record["uid"] == first.record["uid"] assert second.record["client_id"] == "phone-3" def test_store_mark_dead_skips_a_registration_revived_after_confirmation(local_db): from datetime import datetime, timedelta, timezone from devplacepy.push import store user_uid = "user-apns-4" token = "f" * 64 write = store.register(user_uid, "apns", {"token": token}) registered_at = write.record["registered_at"] assert registered_at stale_confirmation = ( datetime.fromisoformat(registered_at) - timedelta(minutes=5) ).isoformat() store.mark_dead(write.record["id"], stale_confirmation) assert len(store.active_for_user(user_uid)) == 1 fresh_confirmation = ( datetime.now(timezone.utc) + timedelta(minutes=5) ).isoformat() store.mark_dead(write.record["id"], fresh_confirmation) assert store.active_for_user(user_uid) == [] def test_dead_delivery_logs_the_reason(monkeypatch, caplog): import logging import httpx from tests.conftest import run_async from devplacepy.push import providers from devplacepy.push.delivery import _deliver_one class _FakeStore: def __init__(self): self.dead = [] def mark_dead(self, registration_id, dead_before=None): self.dead.append(registration_id) fake = _FakeStore() monkeypatch.setattr("devplacepy.push.delivery.store", fake) async def dead_deliver(client, registration, prepared): return providers.Delivery(providers.DEAD, "400 BadDeviceToken") provider = providers.PROVIDERS["apns"] monkeypatch.setattr(provider, "deliver", dead_deliver) async def run(): async with httpx.AsyncClient() as client: return await _deliver_one( provider, client, {"id": 9, "token": "a" * 64}, "{}", "user-x" ) with caplog.at_level(logging.WARNING): outcome = run_async(run()) assert outcome.status == providers.DEAD assert fake.dead == [9] assert "400 BadDeviceToken" in caplog.text def test_notify_user_opens_apns_gateway_not_stealth(monkeypatch, local_db): import httpx from tests.conftest import run_async from devplacepy.push import store from devplacepy.push.delivery import notify_user from devplacepy.push.providers import apns _apns_settings( monkeypatch, **{ apns.TEAM_ID_KEY: "TEAMID1234", apns.KEY_ID_KEY: "KEYID12345", apns.AUTH_KEY_KEY: _ec_private_key_pem(), apns.TOPIC_KEY: "nl.molodetz.devplace", }, ) store.register("user-gw", "apns", {"token": "e" * 64, "environment": "production"}) opened = [] def fake_gateway(timeout): opened.append(timeout) return httpx.AsyncClient( transport=httpx.MockTransport(lambda request: httpx.Response(200, json={})) ) def fail_stealth(**kwargs): raise AssertionError("APNs must not use stealth") monkeypatch.setattr(apns, "gateway_client", fake_gateway) monkeypatch.setattr( "devplacepy.stealth.stealth_async_client", fail_stealth ) run_async(apns.close_client()) try: run_async(notify_user("user-gw", {"title": "t", "message": "m"})) finally: run_async(apns.close_client()) assert opened