rebuild(stage-3): establish accounts permissions and secure settings

This commit is contained in:
leefer
2026-07-30 01:33:19 +08:00
parent 2ff35eb6df
commit f69972c3c0
31 changed files with 2742 additions and 18 deletions
+159
View File
@@ -0,0 +1,159 @@
from __future__ import annotations
import sqlite3
import httpx
from backend.bootstrap.application import create_application
from backend.bootstrap.settings import Settings
from tests.support import run_scenario
from tests.test_accounts import (
ADMIN_PASSWORD,
USER_PASSWORD,
csrf_headers,
register,
use_session,
)
def model_payload(index: int, api_key: str | None = None) -> dict[str, str]:
return {
"display_name": f"模型 {index}",
"base_url": f"https://model-{index}.example.com/v1",
"model_identifier": f"model-{index}",
"api_key": api_key or f"secret-key-{index}",
}
def test_model_pool_is_admin_only_and_never_exposes_keys(tmp_path) -> None:
application = create_application(Settings.for_test(tmp_path))
async def scenario(client: httpx.AsyncClient) -> None:
_, admin_session = await register(client, "model-admin", ADMIN_PASSWORD)
client.cookies.clear()
_, user_session = await register(client, "model-user", USER_PASSWORD)
denied_read = await client.get("/api/admin/models")
assert denied_read.status_code == 403
denied_write = await client.post(
"/api/admin/models",
headers=csrf_headers(user_session),
json=model_payload(1),
)
assert denied_write.status_code == 403
use_session(client, admin_session)
created = await client.post(
"/api/admin/models",
headers=csrf_headers(admin_session),
json=model_payload(1, "private-alpha-key"),
)
assert created.status_code == 201
assert created.json()["is_primary"] is True
assert created.json()["is_fallback"] is False
assert created.json()["has_api_key"] is True
assert "api_key" not in created.json()
assert "private-alpha-key" not in created.text
with sqlite3.connect(application.state.settings.database_path) as connection:
encrypted_before = connection.execute(
"SELECT encrypted_api_key FROM llm_models WHERE id = 1"
).fetchone()[0]
assert encrypted_before != "private-alpha-key"
assert "private-alpha-key" not in encrypted_before
updated_payload = model_payload(1)
updated_payload.pop("api_key")
updated_payload["display_name"] = "主模型"
updated = await client.put(
"/api/admin/models/1",
headers=csrf_headers(admin_session),
json=updated_payload,
)
assert updated.status_code == 200
assert updated.json()["display_name"] == "主模型"
with sqlite3.connect(application.state.settings.database_path) as connection:
encrypted_after = connection.execute(
"SELECT encrypted_api_key FROM llm_models WHERE id = 1"
).fetchone()[0]
assert encrypted_after == encrypted_before
run_scenario(application, scenario)
def test_model_selection_deletion_guards_and_runtime_config(tmp_path) -> None:
application = create_application(Settings.for_test(tmp_path))
async def scenario(client: httpx.AsyncClient) -> None:
_, admin_session = await register(client, "selection-admin", ADMIN_PASSWORD)
for index in (1, 2, 3):
response = await client.post(
"/api/admin/models",
headers=csrf_headers(admin_session),
json=model_payload(index),
)
assert response.status_code == 201
duplicate_roles = await client.put(
"/api/admin/models/selection",
headers=csrf_headers(admin_session),
json={"primary_model_id": 2, "fallback_model_id": 2},
)
assert duplicate_roles.status_code == 400
assert duplicate_roles.json()["error"]["code"] == "duplicate_model_role"
selected = await client.put(
"/api/admin/models/selection",
headers=csrf_headers(admin_session),
json={"primary_model_id": 2, "fallback_model_id": 1},
)
assert selected.status_code == 200
models = (await client.get("/api/admin/models")).json()
assert next(item for item in models if item["id"] == 2)["is_primary"] is True
assert next(item for item in models if item["id"] == 1)["is_fallback"] is True
selected_delete = await client.delete(
"/api/admin/models/1", headers=csrf_headers(admin_session)
)
assert selected_delete.status_code == 409
assert selected_delete.json()["error"]["code"] == "model_in_use"
unselected_delete = await client.delete(
"/api/admin/models/3", headers=csrf_headers(admin_session)
)
assert unselected_delete.status_code == 200
runtime = application.state.container.model_pool.runtime_config()
assert runtime.primary.id == 2
assert runtime.fallback is not None
assert runtime.fallback.id == 1
assert (
application.state.container.model_pool.decrypt_api_key(runtime.primary)
== "secret-key-2"
)
run_scenario(application, scenario)
def test_model_pool_rejects_twenty_first_model(tmp_path) -> None:
application = create_application(Settings.for_test(tmp_path))
async def scenario(client: httpx.AsyncClient) -> None:
_, admin_session = await register(client, "capacity-admin", ADMIN_PASSWORD)
for index in range(1, 21):
response = await client.post(
"/api/admin/models",
headers=csrf_headers(admin_session),
json=model_payload(index),
)
assert response.status_code == 201
rejected = await client.post(
"/api/admin/models",
headers=csrf_headers(admin_session),
json=model_payload(21),
)
assert rejected.status_code == 409
assert rejected.json()["error"]["code"] == "model_pool_full"
run_scenario(application, scenario)