"""Fast, dependency-light regression tests for P0 request protections.""" from __future__ import annotations import asyncio import os import tempfile import unittest os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-test-config-")) from fastapi import HTTPException from core.validation import _validate_character_record, _validate_http_url from core.config import _load_settings, _save_settings from server import RequestSizeLimitMiddleware class SecurityValidationTests(unittest.TestCase): def test_character_record_rejects_missing_id_and_bad_sheet(self): for record in ( {"name": "Missing id", "sheet": {}}, {"id": "c1", "name": "Bad sheet", "sheet": []}, ): with self.assertRaises(HTTPException): _validate_character_record(record) def test_character_record_accepts_minimal_record(self): record = {"id": "c1", "book": "Book", "name": "Name", "tags": "", "sheet": {}} self.assertIs(_validate_character_record(record), record) def test_private_url_is_rejected_when_downloads_are_public_only(self): with self.assertRaises(HTTPException): _validate_http_url("http://127.0.0.1:8188/image", allow_private=False) def test_request_size_middleware_rejects_oversized_json(self): sent = [] async def downstream(scope, receive, send): sent.append({"type": "downstream"}) async def send(message): sent.append(message) scope = { "type": "http", "method": "POST", "path": "/api/test", "headers": [(b"content-type", b"application/json"), (b"content-length", b"40000000")], } asyncio.run(RequestSizeLimitMiddleware(downstream)(scope, None, send)) self.assertEqual(sent[0]["status"], 413) self.assertNotIn({"type": "downstream"}, sent) def test_settings_never_returns_provider_credentials(self): from fastapi.testclient import TestClient from server import app settings = _load_settings() settings.update({ "llm_api_key": "provider-secret", "engine_api_keys": {"local": "engine-secret"}, }) _save_settings(settings) with TestClient(app) as client: response = client.get("/api/settings", headers={"origin": "http://testserver"}) self.assertEqual(response.status_code, 200) payload = response.json() self.assertEqual(payload.get("llm_api_key"), "__configured__") self.assertEqual(payload.get("engine_api_keys", {}).get("local"), "__configured__") self.assertNotIn("provider-secret", response.text) self.assertNotIn("engine-secret", response.text) def test_empty_masked_secret_does_not_overwrite_saved_value(self): from fastapi.testclient import TestClient from server import app settings = _load_settings() settings["llm_api_key"] = "keep-this-secret" _save_settings(settings) with TestClient(app) as client: response = client.post("/api/settings", json={"llm_api_key": ""}, headers={"origin": "http://testserver"}) self.assertEqual(response.status_code, 200) self.assertEqual(_load_settings().get("llm_api_key"), "keep-this-secret") if __name__ == "__main__": unittest.main()