personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_push_routes.py
8.1 kB 256 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6from flask import Flask, g, request 7 8from solstone.convey import create_app 9from solstone.convey.push import push_bp 10from solstone.convey.secure_listener.identity import ConveyIdentity 11from solstone.convey.sol_initiated.copy import APNS_CATEGORY_SOL_CHAT_REQUEST 12from solstone.think.push.devices import load_devices 13from solstone.think.push.runtime import stop_all_push_runtime 14 15 16def _identity(fingerprint: str = "fp-1") -> ConveyIdentity: 17 return ConveyIdentity( 18 mode="pl-direct", 19 fingerprint=fingerprint, 20 device_label="iPhone", 21 paired_at="2026-05-20T00:00:00Z", 22 session_id="s1", 23 ) 24 25 26def _register_app() -> Flask: 27 app = Flask(__name__) 28 app.config["TESTING"] = True 29 30 @app.before_request 31 def stamp_identity() -> None: 32 stamped = request.environ.get("pl.identity") 33 if stamped is not None: 34 g.identity = stamped 35 36 app.register_blueprint(push_bp) 37 return app 38 39 40def _register_body(token: str = "A" * 64) -> dict[str, str]: 41 return { 42 "device_token": token, 43 "bundle_id": "org.solpbc.solstone-swift", 44 "environment": "development", 45 "platform": "ios", 46 } 47 48 49def _post_register( 50 client, *, identity: ConveyIdentity | None = None, token: str = "A" * 64 51): 52 environ_overrides = {"pl.identity": identity or _identity()} 53 return client.post( 54 "/api/push/register", 55 json=_register_body(token), 56 environ_overrides=environ_overrides, 57 ) 58 59 60def _device_row() -> dict[str, object]: 61 return { 62 "fingerprint": "fp-1", 63 "token": "a" * 64, 64 "bundle_id": "org.solpbc.solstone-swift", 65 "environment": "development", 66 "platform": "ios", 67 "registered_at": 1, 68 } 69 70 71def test_register_push_device_happy_path(monkeypatch, tmp_path): 72 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 73 ensure_calls: list[bool] = [] 74 monkeypatch.setattr( 75 "solstone.convey.push.ensure_reach_token", 76 lambda: ensure_calls.append(True) or None, 77 ) 78 client = _register_app().test_client() 79 80 response = _post_register(client, identity=_identity("fp-1"), token="A" * 64) 81 82 assert response.status_code == 200 83 assert response.get_json() == {"registered": True, "device_count": 1} 84 assert ensure_calls == [True] 85 assert load_devices() == [ 86 { 87 "fingerprint": "fp-1", 88 "token": "a" * 64, 89 "bundle_id": "org.solpbc.solstone-swift", 90 "environment": "development", 91 "platform": "ios", 92 "registered_at": load_devices()[0]["registered_at"], 93 } 94 ] 95 96 97def test_register_push_device_replaces_same_fingerprint(monkeypatch, tmp_path): 98 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 99 client = _register_app().test_client() 100 101 first = _post_register(client, identity=_identity("fp-1"), token="A" * 64) 102 second = _post_register(client, identity=_identity("fp-1"), token="B" * 64) 103 104 assert first.get_json()["device_count"] == 1 105 assert second.get_json() == {"registered": True, "device_count": 1} 106 stored = load_devices() 107 assert len(stored) == 1 108 assert stored[0]["fingerprint"] == "fp-1" 109 assert stored[0]["token"] == "b" * 64 110 111 112def test_delete_push_device_by_fingerprint(monkeypatch, tmp_path): 113 monkeypatch.setenv("SOLSTONE_JOURNAL", str(tmp_path)) 114 client = _register_app().test_client() 115 identity = _identity("fp-1") 116 _post_register(client, identity=identity) 117 118 response = client.delete( 119 "/api/push/register", 120 environ_overrides={"pl.identity": identity}, 121 ) 122 123 assert response.status_code == 200 124 assert response.get_json() == {"removed": True, "device_count": 0} 125 assert load_devices() == [] 126 127 128def test_register_refuses_none_fingerprint_on_loopback(journal_copy): 129 app = create_app(str(journal_copy)) 130 app.config["TESTING"] = True 131 client = app.test_client() 132 try: 133 response = client.post("/api/push/register", json=_register_body()) 134 finally: 135 stop_all_push_runtime() 136 137 assert response.status_code == 400 138 assert response.get_json()["reason_code"] == "push_request_invalid" 139 140 141def test_register_refuses_missing_paired_identity(journal_copy): 142 app = create_app(str(journal_copy)) 143 app.config["TESTING"] = True 144 client = app.test_client() 145 try: 146 response = client.post( 147 "/api/push/register", 148 json=_register_body(), 149 environ_overrides={"REMOTE_ADDR": "203.0.113.7"}, 150 ) 151 finally: 152 stop_all_push_runtime() 153 154 assert response.status_code == 400 155 data = response.get_json() 156 assert data["reason_code"] == "push_request_invalid" 157 assert data["detail"] == "push registration requires a paired device" 158 159 160def test_status_shape(monkeypatch): 161 client = _register_app().test_client() 162 monkeypatch.setattr( 163 "solstone.convey.push.load_devices", 164 lambda: [ 165 { 166 "fingerprint": "fp-1", 167 "token": "a" * 64, 168 "bundle_id": "org.solpbc.solstone-swift", 169 "environment": "development", 170 "platform": "ios", 171 "registered_at": 1713528000, 172 } 173 ], 174 ) 175 monkeypatch.setattr("solstone.convey.push.read_reach_token", lambda: "tok") 176 177 response = client.get("/api/push/status") 178 179 assert response.status_code == 200 180 data = response.get_json() 181 assert set(data) == {"device_count", "relay_available", "devices"} 182 assert data["device_count"] == 1 183 assert data["relay_available"] is True 184 assert data["devices"] == [ 185 { 186 "token_suffix": "...aaaa", 187 "bundle_id": "org.solpbc.solstone-swift", 188 "environment": "development", 189 "platform": "ios", 190 "registered_at": "2024-04-19T12:00:00Z", 191 } 192 ] 193 194 195def test_status_relay_unavailable(monkeypatch): 196 client = _register_app().test_client() 197 monkeypatch.setattr("solstone.convey.push.load_devices", lambda: []) 198 monkeypatch.setattr("solstone.convey.push.read_reach_token", lambda: "") 199 200 response = client.get("/api/push/status") 201 202 assert response.status_code == 200 203 assert response.get_json() == { 204 "device_count": 0, 205 "relay_available": False, 206 "devices": [], 207 } 208 209 210def test_push_test_relays(monkeypatch): 211 client = _register_app().test_client() 212 calls: list[dict[str, str]] = [] 213 monkeypatch.setattr("solstone.convey.push.load_devices", lambda: [_device_row()]) 214 monkeypatch.setattr( 215 "solstone.convey.push.dispatch_via_portal", 216 lambda **kwargs: calls.append(kwargs) or {"ok": True}, 217 ) 218 219 response = client.post("/api/push/test", json={"body": "hi"}) 220 221 assert response.status_code == 200 222 data = response.get_json() 223 assert data["dispatched"] is True 224 assert data["request_id"].startswith("push-test-") 225 assert calls == [ 226 { 227 "request_id": data["request_id"], 228 "summary": "hi", 229 "category": APNS_CATEGORY_SOL_CHAT_REQUEST, 230 } 231 ] 232 233 234def test_push_test_returns_503_when_no_devices(monkeypatch): 235 client = _register_app().test_client() 236 monkeypatch.setattr("solstone.convey.push.load_devices", lambda: []) 237 238 response = client.post("/api/push/test") 239 240 assert response.status_code == 503 241 data = response.get_json() 242 assert data["reason_code"] == "feature_unavailable" 243 assert data["detail"] == "no devices to reach" 244 245 246def test_push_test_returns_503_when_relay_dispatch_fails(monkeypatch): 247 client = _register_app().test_client() 248 monkeypatch.setattr("solstone.convey.push.load_devices", lambda: [_device_row()]) 249 monkeypatch.setattr("solstone.convey.push.dispatch_via_portal", lambda **_: None) 250 251 response = client.post("/api/push/test") 252 253 assert response.status_code == 503 254 data = response.get_json() 255 assert data["reason_code"] == "feature_unavailable" 256 assert data["detail"] == "push relay dispatch failed"