Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/amogadget_gamma.json
T

1 line
24 KiB
JSON

{"repo": "amogadget/gamma", "files": {"backend/tests/test_prefs_ai_settings.py": {"a1135e2bde355d792ae3d4af27b83d5bd51eb275": {"t": 1785746926.9172726, "n": 8035, "h": "e6f190e451a9e27b", "c": "\"\"\"Per-user prefs KV store (tab sync) + GUI-configured AI provider keys.\"\"\"\n\nimport io\nimport zipfile\n\nimport bcrypt\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"module\")\ndef alice(client):\n \"\"\"A separate TestClient logged in as a real (non-guest) user.\"\"\"\n from gamma.app import app\n from gamma.db import connect_users_db, page_now\n from gamma.seed import create_user_dbs\n\n with connect_users_db() as conn:\n if not conn.execute(\"SELECT 1 FROM users WHERE username = 'alice'\").fetchone():\n conn.execute(\n \"INSERT INTO users (username, password_hash, is_guest, created_at) VALUES (?, ?, 0, ?)\",\n (\"alice\", bcrypt.hashpw(b\"pw\", bcrypt.gensalt()).decode(), page_now()),\n )\n conn.commit()\n create_user_dbs(\"alice\")\n c = TestClient(app)\n r = c.post(\"/api/login\", json={\"username\": \"alice\", \"password\": \"pw\"})\n assert r.status_code == 200, r.text\n return c\n\n\n# --- prefs -------------------------------------------------------------------\n\ndef test_prefs_unset_key_reads_empty(guest):\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.status_code == 200\n body = r.json()\n assert body[\"value\"] is None and body[\"updated_at\"] == \"\"\n\n\ndef test_prefs_roundtrip_and_updated_at(guest):\n tabs = [{\"id\": \"b1\", \"title\": \"Paper A\"}, {\"id\": \"b2\", \"title\": \"Paper B\"}]\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": tabs})\n assert r.status_code == 200\n first = r.json()[\"updated_at\"]\n assert first\n\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.json()[\"value\"] == tabs\n assert r.json()[\"updated_at\"] == first\n\n # Last write wins, updated_at moves forward\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": []})\n assert r.json()[\"updated_at\"] > first\n assert guest.get(\"/api/prefs/open-tabs\").json()[\"value\"] == []\n\n\ndef test_prefs_rejects_bad_keys_and_huge_values(guest):\n assert guest.get(\"/api/prefs/No%20Spaces\").status_code == 400\n assert guest.put(\"/api/prefs/UPPER\", json={\"value\": 1}).status_code == 400\n big = \"x\" * (70 * 1024)\n assert guest.put(\"/api/prefs/open-tabs\", json={\"value\": big}).status_code == 413\n\n\ndef test_prefs_never_serve_the_reserved_ai_settings_key(guest):\n # The raw AI keys live under this prefs key — only the masked\n # /api/ai/settings endpoint may read it.\n assert guest.get(\"/api/prefs/ai-settings\").status_code == 400\n assert guest.put(\"/api/prefs/ai-settings\", json={\"value\": {}}).status_code == 400\n\n\ndef test_prefs_require_session(client):\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/prefs/open-tabs\").status_code == 401\n assert anon.put(\"/api/prefs/open-tabs\", json={\"value\": []}).status_code == 401\n\n\n# --- AI provider entries (GUI key management) ---------------------------------\n\ndef test_guest_cannot_store_keys(guest):\n r = guest.get(\"/api/ai/settings\")\n assert r.status_code == 200\n assert r.json()[\"can_edit\"] is False\n assert r.json()[\"providers\"] == []\n r = guest.post(\"/api/ai/providers\", json={\"protocol\": \"anthropic\", \"api_key\": \"sk-x-guest-key\"})\n assert r.status_code == 403\n\n\ndef test_added_provider_is_masked_and_enables_ai(alice):\n # No env keys exist at all — AI starts disabled until the user adds a provider\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n key = \"sk-ant-api03-test-key-12345678\"\n r = alice.post(\"/api/ai/providers\", json={\n \"protocol\": \"anthropic\", \"name\": \"My DeepSeek\", \"api_key\": key,\n \"base_url\": \"https://example.com/v1x\", \"models\": \"claude-test-model, claude-other\",\n })\n assert r.status_code == 200, r.text\n provs = r.json()[\"providers\"]\n assert len(provs) == 1\n p = provs[0]\n # Never echo the key — only a short hint\n assert key not in r.text\n assert p[\"key_hint\"] == \"…5678\"\n assert p[\"name\"] == \"My DeepSeek\" and p[\"protocol\"] == \"anthropic\"\n assert p[\"base_url\"] == \"https://example.com/v1x\"\n assert p[\"created_at\"]\n\n models = alice.get(\"/api/ai/models\").json()\n assert models[\"enabled\"] is True\n assert models[\"default\"] == f\"{p['id']}:claude-test-model\"\n assert [m[\"model\"] for m in models[\"models\"]] == [\"claude-test-model\", \"claude-other\"]\n assert models[\"models\"][0][\"provider_name\"] == \"My DeepSeek\"\n\n # ...and the same masked view comes back on GET\n g = alice.get(\"/api/ai/settings\").json()\n assert key not in str(g)\n assert g[\"providers\"][0][\"key_hint\"] == \"…5678\"\n\n\ndef test_edit_without_key_keeps_the_stored_one(alice):\n pid = alice.get(\"/api/ai/settings\").json()[\"providers\"][0][\"id\"]\n r = alice.put(f\"/api/ai/providers/{pid}\", json={\"name\": \"Renamed\", \"models\": \"claude-solo\"})\n assert r.status_code == 200, r.text\n p = r.json()[\"providers\"][0]\n assert p[\"name\"] == \"Renamed\" and p[\"key_hint\"] == \"…5678\"\n models = alice.get(\"/api/ai/models\").json()\n assert models[\"enabled\"] is True # key survived the edit\n assert models[\"default\"] == f\"{pid}:claude-solo\"\n\n\ndef test_second_provider_adds_its_models(alice):\n r = alice.post(\"/api/ai/providers\", json={\n \"protocol\": \"openai\", \"api_key\": \"sk-openai-test-key-9876\",\n })\n assert r.status_code == 200, r.text\n assert len(r.json()[\"providers\"]) == 2\n models = alice.get(\"/api/ai/models\").json()[\"models\"]\n # openai entry has no model list — its protocol default appears\n assert [m[\"model\"] for m in models] == [\"claude-solo\", \"gpt-4o-mini\"]\n\n\ndef test_provider_validation(alice):\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"nope\", \"api_key\": \"k\" * 20}).status_code == 400\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"openai\"}).status_code == 400 # no key\n assert alice.post(\"/api/ai/providers\",\n json={\"protocol\": \"openai\", \"api_key\": \"sk-ok-key-123\", \"base_url\": \"ftp://x\"}).status_code == 400\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"openai\", \"api_key\": \"has space\"}).status_code == 400\n assert alice.put(\"/api/ai/providers/does-not-exist\", json={\"name\": \"x\"}).status_code == 404\n\n\ndef test_export_stays_owner_only(alice):\n # Keys ride along inside data.db in the owner's backup — which is fine\n # exactly because only the owner's session can request it.\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/export\").status_code == 401\n r = alice.get(\"/api/export\")\n assert r.status_code == 200\n with zipfile.ZipFile(io.BytesIO(r.content)) as z:\n assert \"data.db\" in z.namelist()\n\n\ndef test_deleting_all_providers_disables_ai(alice):\n for p in alice.get(\"/api/ai/settings\").json()[\"providers\"]:\n assert alice.delete(f\"/api/ai/providers/{p['id']}\").status_code == 200\n assert alice.get(\"/api/ai/settings\").json()[\"providers\"] == []\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n\n# --- manage.py rename-user ----------------------------------------------------\n\ndef test_rename_user_moves_rows_and_directory(client):\n import manage\n from gamma.config import USERS_DIR\n from gamma.app import app\n\n manage.create_user(\"bob\", \"pw2\")\n assert (USERS_DIR / \"bob\" / \"pages.db\").exists()\n\n manage.rename_user(\"bob\", \"bobby\")\n assert not (USERS_DIR / \"bob\").exists()\n assert (USERS_DIR / \"bobby\" / \"pages.db\").exists()\n\n c = TestClient(app)\n assert c.post(\"/api/login\", json={\"username\": \"bob\", \"password\": \"pw2\"}).status_code == 401\n assert c.post(\"/api/login\", json={\"username\": \"bobby\", \"password\": \"pw2\"}).status_code == 200\n\n\ndef test_rename_user_refuses_guest_and_collisions(client, capsys):\n import manage\n manage.rename_user(\"guest\", \"someone\")\n assert \"cannot be renamed\" in capsys.readouterr().out\n manage.create_user(\"carol\", \"pw3\")\n manage.rename_user(\"carol\", \"bobby\") # bobby exists from the test above\n assert \"already exists\" in capsys.readouterr().out\n manage.rename_user(\"carol\", \"bad/name\")\n assert \"must be\" in capsys.readouterr().out\n"}, "eae016c0ba37395cc8049701134d0ed185e80dd9": {"t": 1785769099.2098663, "n": 8035, "h": "e6f190e451a9e27b", "c": "\"\"\"Per-user prefs KV store (tab sync) + GUI-configured AI provider keys.\"\"\"\n\nimport io\nimport zipfile\n\nimport bcrypt\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"module\")\ndef alice(client):\n \"\"\"A separate TestClient logged in as a real (non-guest) user.\"\"\"\n from gamma.app import app\n from gamma.db import connect_users_db, page_now\n from gamma.seed import create_user_dbs\n\n with connect_users_db() as conn:\n if not conn.execute(\"SELECT 1 FROM users WHERE username = 'alice'\").fetchone():\n conn.execute(\n \"INSERT INTO users (username, password_hash, is_guest, created_at) VALUES (?, ?, 0, ?)\",\n (\"alice\", bcrypt.hashpw(b\"pw\", bcrypt.gensalt()).decode(), page_now()),\n )\n conn.commit()\n create_user_dbs(\"alice\")\n c = TestClient(app)\n r = c.post(\"/api/login\", json={\"username\": \"alice\", \"password\": \"pw\"})\n assert r.status_code == 200, r.text\n return c\n\n\n# --- prefs -------------------------------------------------------------------\n\ndef test_prefs_unset_key_reads_empty(guest):\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.status_code == 200\n body = r.json()\n assert body[\"value\"] is None and body[\"updated_at\"] == \"\"\n\n\ndef test_prefs_roundtrip_and_updated_at(guest):\n tabs = [{\"id\": \"b1\", \"title\": \"Paper A\"}, {\"id\": \"b2\", \"title\": \"Paper B\"}]\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": tabs})\n assert r.status_code == 200\n first = r.json()[\"updated_at\"]\n assert first\n\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.json()[\"value\"] == tabs\n assert r.json()[\"updated_at\"] == first\n\n # Last write wins, updated_at moves forward\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": []})\n assert r.json()[\"updated_at\"] > first\n assert guest.get(\"/api/prefs/open-tabs\").json()[\"value\"] == []\n\n\ndef test_prefs_rejects_bad_keys_and_huge_values(guest):\n assert guest.get(\"/api/prefs/No%20Spaces\").status_code == 400\n assert guest.put(\"/api/prefs/UPPER\", json={\"value\": 1}).status_code == 400\n big = \"x\" * (70 * 1024)\n assert guest.put(\"/api/prefs/open-tabs\", json={\"value\": big}).status_code == 413\n\n\ndef test_prefs_never_serve_the_reserved_ai_settings_key(guest):\n # The raw AI keys live under this prefs key — only the masked\n # /api/ai/settings endpoint may read it.\n assert guest.get(\"/api/prefs/ai-settings\").status_code == 400\n assert guest.put(\"/api/prefs/ai-settings\", json={\"value\": {}}).status_code == 400\n\n\ndef test_prefs_require_session(client):\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/prefs/open-tabs\").status_code == 401\n assert anon.put(\"/api/prefs/open-tabs\", json={\"value\": []}).status_code == 401\n\n\n# --- AI provider entries (GUI key management) ---------------------------------\n\ndef test_guest_cannot_store_keys(guest):\n r = guest.get(\"/api/ai/settings\")\n assert r.status_code == 200\n assert r.json()[\"can_edit\"] is False\n assert r.json()[\"providers\"] == []\n r = guest.post(\"/api/ai/providers\", json={\"protocol\": \"anthropic\", \"api_key\": \"sk-x-guest-key\"})\n assert r.status_code == 403\n\n\ndef test_added_provider_is_masked_and_enables_ai(alice):\n # No env keys exist at all — AI starts disabled until the user adds a provider\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n key = \"sk-ant-api03-test-key-12345678\"\n r = alice.post(\"/api/ai/providers\", json={\n \"protocol\": \"anthropic\", \"name\": \"My DeepSeek\", \"api_key\": key,\n \"base_url\": \"https://example.com/v1x\", \"models\": \"claude-test-model, claude-other\",\n })\n assert r.status_code == 200, r.text\n provs = r.json()[\"providers\"]\n assert len(provs) == 1\n p = provs[0]\n # Never echo the key — only a short hint\n assert key not in r.text\n assert p[\"key_hint\"] == \"…5678\"\n assert p[\"name\"] == \"My DeepSeek\" and p[\"protocol\"] == \"anthropic\"\n assert p[\"base_url\"] == \"https://example.com/v1x\"\n assert p[\"created_at\"]\n\n models = alice.get(\"/api/ai/models\").json()\n assert models[\"enabled\"] is True\n assert models[\"default\"] == f\"{p['id']}:claude-test-model\"\n assert [m[\"model\"] for m in models[\"models\"]] == [\"claude-test-model\", \"claude-other\"]\n assert models[\"models\"][0][\"provider_name\"] == \"My DeepSeek\"\n\n # ...and the same masked view comes back on GET\n g = alice.get(\"/api/ai/settings\").json()\n assert key not in str(g)\n assert g[\"providers\"][0][\"key_hint\"] == \"…5678\"\n\n\ndef test_edit_without_key_keeps_the_stored_one(alice):\n pid = alice.get(\"/api/ai/settings\").json()[\"providers\"][0][\"id\"]\n r = alice.put(f\"/api/ai/providers/{pid}\", json={\"name\": \"Renamed\", \"models\": \"claude-solo\"})\n assert r.status_code == 200, r.text\n p = r.json()[\"providers\"][0]\n assert p[\"name\"] == \"Renamed\" and p[\"key_hint\"] == \"…5678\"\n models = alice.get(\"/api/ai/models\").json()\n assert models[\"enabled\"] is True # key survived the edit\n assert models[\"default\"] == f\"{pid}:claude-solo\"\n\n\ndef test_second_provider_adds_its_models(alice):\n r = alice.post(\"/api/ai/providers\", json={\n \"protocol\": \"openai\", \"api_key\": \"sk-openai-test-key-9876\",\n })\n assert r.status_code == 200, r.text\n assert len(r.json()[\"providers\"]) == 2\n models = alice.get(\"/api/ai/models\").json()[\"models\"]\n # openai entry has no model list — its protocol default appears\n assert [m[\"model\"] for m in models] == [\"claude-solo\", \"gpt-4o-mini\"]\n\n\ndef test_provider_validation(alice):\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"nope\", \"api_key\": \"k\" * 20}).status_code == 400\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"openai\"}).status_code == 400 # no key\n assert alice.post(\"/api/ai/providers\",\n json={\"protocol\": \"openai\", \"api_key\": \"sk-ok-key-123\", \"base_url\": \"ftp://x\"}).status_code == 400\n assert alice.post(\"/api/ai/providers\", json={\"protocol\": \"openai\", \"api_key\": \"has space\"}).status_code == 400\n assert alice.put(\"/api/ai/providers/does-not-exist\", json={\"name\": \"x\"}).status_code == 404\n\n\ndef test_export_stays_owner_only(alice):\n # Keys ride along inside data.db in the owner's backup — which is fine\n # exactly because only the owner's session can request it.\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/export\").status_code == 401\n r = alice.get(\"/api/export\")\n assert r.status_code == 200\n with zipfile.ZipFile(io.BytesIO(r.content)) as z:\n assert \"data.db\" in z.namelist()\n\n\ndef test_deleting_all_providers_disables_ai(alice):\n for p in alice.get(\"/api/ai/settings\").json()[\"providers\"]:\n assert alice.delete(f\"/api/ai/providers/{p['id']}\").status_code == 200\n assert alice.get(\"/api/ai/settings\").json()[\"providers\"] == []\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n\n# --- manage.py rename-user ----------------------------------------------------\n\ndef test_rename_user_moves_rows_and_directory(client):\n import manage\n from gamma.config import USERS_DIR\n from gamma.app import app\n\n manage.create_user(\"bob\", \"pw2\")\n assert (USERS_DIR / \"bob\" / \"pages.db\").exists()\n\n manage.rename_user(\"bob\", \"bobby\")\n assert not (USERS_DIR / \"bob\").exists()\n assert (USERS_DIR / \"bobby\" / \"pages.db\").exists()\n\n c = TestClient(app)\n assert c.post(\"/api/login\", json={\"username\": \"bob\", \"password\": \"pw2\"}).status_code == 401\n assert c.post(\"/api/login\", json={\"username\": \"bobby\", \"password\": \"pw2\"}).status_code == 200\n\n\ndef test_rename_user_refuses_guest_and_collisions(client, capsys):\n import manage\n manage.rename_user(\"guest\", \"someone\")\n assert \"cannot be renamed\" in capsys.readouterr().out\n manage.create_user(\"carol\", \"pw3\")\n manage.rename_user(\"carol\", \"bobby\") # bobby exists from the test above\n assert \"already exists\" in capsys.readouterr().out\n manage.rename_user(\"carol\", \"bad/name\")\n assert \"must be\" in capsys.readouterr().out\n"}, "068ce38e02a967c2a38520d1c2f93aec4c9e3977": {"t": 1785769100.5603142, "n": 6541, "h": "3797f79b3d4db72c", "c": "\"\"\"Per-user prefs KV store (tab sync) + GUI-configured AI provider keys.\"\"\"\n\nimport io\nimport zipfile\n\nimport bcrypt\nimport pytest\nfrom fastapi.testclient import TestClient\n\n\n@pytest.fixture(scope=\"module\")\ndef alice(client):\n \"\"\"A separate TestClient logged in as a real (non-guest) user.\"\"\"\n from gamma.app import app\n from gamma.db import connect_users_db, page_now\n from gamma.seed import create_user_dbs\n\n with connect_users_db() as conn:\n if not conn.execute(\"SELECT 1 FROM users WHERE username = 'alice'\").fetchone():\n conn.execute(\n \"INSERT INTO users (username, password_hash, is_guest, created_at) VALUES (?, ?, 0, ?)\",\n (\"alice\", bcrypt.hashpw(b\"pw\", bcrypt.gensalt()).decode(), page_now()),\n )\n conn.commit()\n create_user_dbs(\"alice\")\n c = TestClient(app)\n r = c.post(\"/api/login\", json={\"username\": \"alice\", \"password\": \"pw\"})\n assert r.status_code == 200, r.text\n return c\n\n\n# --- prefs -------------------------------------------------------------------\n\ndef test_prefs_unset_key_reads_empty(guest):\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.status_code == 200\n body = r.json()\n assert body[\"value\"] is None and body[\"updated_at\"] == \"\"\n\n\ndef test_prefs_roundtrip_and_updated_at(guest):\n tabs = [{\"id\": \"b1\", \"title\": \"Paper A\"}, {\"id\": \"b2\", \"title\": \"Paper B\"}]\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": tabs})\n assert r.status_code == 200\n first = r.json()[\"updated_at\"]\n assert first\n\n r = guest.get(\"/api/prefs/open-tabs\")\n assert r.json()[\"value\"] == tabs\n assert r.json()[\"updated_at\"] == first\n\n # Last write wins, updated_at moves forward\n r = guest.put(\"/api/prefs/open-tabs\", json={\"value\": []})\n assert r.json()[\"updated_at\"] > first\n assert guest.get(\"/api/prefs/open-tabs\").json()[\"value\"] == []\n\n\ndef test_prefs_rejects_bad_keys_and_huge_values(guest):\n assert guest.get(\"/api/prefs/No%20Spaces\").status_code == 400\n assert guest.put(\"/api/prefs/UPPER\", json={\"value\": 1}).status_code == 400\n big = \"x\" * (70 * 1024)\n assert guest.put(\"/api/prefs/open-tabs\", json={\"value\": big}).status_code == 413\n\n\ndef test_prefs_never_serve_the_reserved_ai_settings_key(guest):\n # The raw AI keys live under this prefs key — only the masked\n # /api/ai/settings endpoint may read it.\n assert guest.get(\"/api/prefs/ai-settings\").status_code == 400\n assert guest.put(\"/api/prefs/ai-settings\", json={\"value\": {}}).status_code == 400\n\n\ndef test_prefs_require_session(client):\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/prefs/open-tabs\").status_code == 401\n assert anon.put(\"/api/prefs/open-tabs\", json={\"value\": []}).status_code == 401\n\n\n# --- AI provider settings ----------------------------------------------------\n\ndef test_guest_cannot_store_keys(guest):\n r = guest.get(\"/api/ai/settings\")\n assert r.status_code == 200\n assert r.json()[\"can_edit\"] is False\n r = guest.put(\"/api/ai/settings\", json={\"providers\": {\"anthropic\": {\"api_key\": \"sk-x\"}}})\n assert r.status_code == 403\n\n\ndef test_user_key_is_stored_masked_and_enables_ai(alice):\n # No env keys exist at all — AI starts disabled until the user adds a key\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n key = \"sk-ant-api03-test-key-12345678\"\n r = alice.put(\"/api/ai/settings\", json={\n \"providers\": {\"anthropic\": {\"api_key\": key, \"base_url\": \"https://example.com/v1x\"}},\n \"models\": \"anthropic:claude-test-model\",\n })\n assert r.status_code == 200, r.text\n body = r.json()\n prov = body[\"providers\"][\"anthropic\"]\n # Never echo the key — only a short hint\n assert key not in r.text\n assert prov[\"configured\"] is True\n assert prov[\"key_hint\"] == \"…5678\"\n assert prov[\"base_url\"] == \"https://example.com/v1x\"\n\n models = alice.get(\"/api/ai/models\").json()\n assert models[\"enabled\"] is True\n assert models[\"default\"] == \"anthropic:claude-test-model\"\n\n # ...and the same masked view comes back on GET\n g = alice.get(\"/api/ai/settings\").json()\n assert key not in str(g)\n assert g[\"providers\"][\"anthropic\"][\"key_hint\"] == \"…5678\"\n\n\ndef test_settings_validation(alice):\n assert alice.put(\"/api/ai/settings\", json={\"providers\": {\"nope\": {}}}).status_code == 400\n assert alice.put(\"/api/ai/settings\",\n json={\"providers\": {\"openai\": {\"base_url\": \"ftp://x\"}}}).status_code == 400\n assert alice.put(\"/api/ai/settings\",\n json={\"providers\": {\"openai\": {\"api_key\": \"has space\"}}}).status_code == 400\n\n\ndef test_export_stays_owner_only(alice):\n # Keys ride along inside data.db in the owner's backup — which is fine\n # exactly because only the owner's session can request it.\n from gamma.app import app\n anon = TestClient(app)\n assert anon.get(\"/api/export\").status_code == 401\n r = alice.get(\"/api/export\")\n assert r.status_code == 200\n with zipfile.ZipFile(io.BytesIO(r.content)) as z:\n assert \"data.db\" in z.namelist()\n\n\ndef test_removing_key_disables_ai_again(alice):\n r = alice.put(\"/api/ai/settings\", json={\"providers\": {\"anthropic\": {\"api_key\": \"\"}}, \"models\": \"\"})\n assert r.status_code == 200\n assert r.json()[\"providers\"][\"anthropic\"][\"configured\"] is False\n assert alice.get(\"/api/ai/models\").json()[\"enabled\"] is False\n\n\n# --- manage.py rename-user ----------------------------------------------------\n\ndef test_rename_user_moves_rows_and_directory(client):\n import manage\n from gamma.config import USERS_DIR\n from gamma.app import app\n\n manage.create_user(\"bob\", \"pw2\")\n assert (USERS_DIR / \"bob\" / \"pages.db\").exists()\n\n manage.rename_user(\"bob\", \"bobby\")\n assert not (USERS_DIR / \"bob\").exists()\n assert (USERS_DIR / \"bobby\" / \"pages.db\").exists()\n\n c = TestClient(app)\n assert c.post(\"/api/login\", json={\"username\": \"bob\", \"password\": \"pw2\"}).status_code == 401\n assert c.post(\"/api/login\", json={\"username\": \"bobby\", \"password\": \"pw2\"}).status_code == 200\n\n\ndef test_rename_user_refuses_guest_and_collisions(client, capsys):\n import manage\n manage.rename_user(\"guest\", \"someone\")\n assert \"cannot be renamed\" in capsys.readouterr().out\n manage.create_user(\"carol\", \"pw3\")\n manage.rename_user(\"carol\", \"bobby\") # bobby exists from the test above\n assert \"already exists\" in capsys.readouterr().out\n manage.rename_user(\"carol\", \"bad/name\")\n assert \"must be\" in capsys.readouterr().out\n"}}}}