"""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 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) if __name__ == "__main__": unittest.main()