"""Server-owned settings fields must survive full-object PUTs from the renderer. Reproduces the production bug where a Settings save built from a pre-activation snapshot (the renderer PUTs the ENTIRE AppSettings object) silently wiped openswarm_bearer_token + connection_mode, disconnecting paying subscribers minutes after a successful Stripe activation. The fix: subscription/identity fields are written only by their dedicated endpoints (activate, signin-activate, signout, disconnect); PUT /api/settings preserves whatever is on disk for them. """ from __future__ import annotations import pytest from unittest.mock import patch, AsyncMock from fastapi.testclient import TestClient from backend.main import app @pytest.fixture def client(): import backend.auth as auth_mod if not auth_mod.TOKEN: import secrets auth_mod.TOKEN = secrets.token_urlsafe(32) return TestClient(app, headers={"Authorization": f"Bearer {auth_mod.TOKEN}"}) @pytest.fixture def reset_settings(): from backend.apps.settings.settings import load_settings, save_settings original = load_settings().model_copy(deep=True) yield save_settings(original) def p_activate_pro(client, token="repro-bearer-0123456789abcdef"): """Drive the real /api/subscription/activate with a mocked cloud /api/me.""" fake_me = AsyncMock() fake_me.status_code = 200 fake_me.json = lambda: { "email": "payer@example.com", "plan": "pro", "status": "active", "current_period_end": 4102444800000, "usage": {"utilization": 0}, } with patch("httpx.AsyncClient") as MockClient: instance = MockClient.return_value.__aenter__.return_value instance.get = AsyncMock(return_value=fake_me) r = client.post("/api/subscription/activate", json={"token": token}) assert r.status_code == 200, r.text return token def test_stale_settings_put_cannot_wipe_activation(client, reset_settings): """The exact production sequence: snapshot settings, activate Pro, PUT the stale snapshot back (renderer Save of a pre-activation draft). The bearer and pro mode must survive; the user's editable change must still apply.""" snapshot = client.get("/api/settings").json() assert snapshot is not None token = p_activate_pro(client) from backend.apps.settings.settings import load_settings s = load_settings() assert s.openswarm_bearer_token == token assert s.connection_mode == "openswarm-pro" assert s.openswarm_subscription_plan == "pro" stale = dict(snapshot) stale["user_name"] = "Stale Draft Save" r = client.put("/api/settings", json=stale) assert r.status_code == 200 s = load_settings() assert s.user_name == "Stale Draft Save" assert s.openswarm_bearer_token == token, "stale PUT wiped the bearer" assert s.connection_mode == "openswarm-pro", "stale PUT reverted connection_mode" assert s.openswarm_subscription_plan == "pro" assert s.openswarm_subscription_expires is not None body = r.json()["settings"] assert body["openswarm_bearer_token"] == token assert body["connection_mode"] == "openswarm-pro" def test_put_cannot_inject_server_owned_fields(client, reset_settings): """The inverse direction: a client PUT must not be able to SET subscription state either (it would imply entitlement the cloud never granted).""" snapshot = client.get("/api/settings").json() forged = dict(snapshot) forged["connection_mode"] = "openswarm-pro" forged["openswarm_bearer_token"] = "forged-bearer-fedcba9876543210" forged["openswarm_subscription_plan"] = "ultra" forged["user_id"] = "u-forged" r = client.put("/api/settings", json=forged) assert r.status_code == 200 from backend.apps.settings.settings import load_settings s = load_settings() assert s.openswarm_bearer_token == snapshot.get("openswarm_bearer_token") assert s.connection_mode == snapshot.get("connection_mode") assert s.openswarm_subscription_plan == snapshot.get("openswarm_subscription_plan") assert s.user_id == snapshot.get("user_id") def test_dedicated_endpoints_still_mutate(client, reset_settings): """Freezing PUT must not freeze the real owners: disconnect still reverts routing, and a fresh activate still re-connects afterwards.""" p_activate_pro(client) r = client.post("/api/subscription/disconnect") assert r.status_code == 200 from backend.apps.settings.settings import load_settings s = load_settings() assert s.connection_mode == "own_key" assert s.openswarm_bearer_token is not None # disconnect keeps sign-in p_activate_pro(client, token="second-bearer-aaaabbbbccccdddd") s = load_settings() assert s.connection_mode == "openswarm-pro" assert s.openswarm_bearer_token == "second-bearer-aaaabbbbccccdddd"