"""Voice consent/provenance: validation, defaults, persistence, upload route (temp dirs only).""" import asyncio import io import os import sys import tempfile import unittest from pathlib import Path from unittest import mock sys.path.insert(0, str(Path(__file__).resolve().parents[1])) os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-prov-test-config-")) from fastapi import HTTPException, UploadFile from starlette.datastructures import Headers from core.provenance import import_provenance, validate_provenance from core.voice import _load_meta, _save_meta, _voice_package_paths import routes.library as lib class ValidationTests(unittest.TestCase): def test_defaults_and_cleaning(self): p = validate_provenance({}) self.assertEqual(p["basis"], "unknown") p = validate_provenance({"basis": "licensed", "licence": " CC-BY\x00 4.0 ", "source": "https://x.org/a"}) self.assertEqual(p["licence"], "CC-BY 4.0") self.assertTrue(p["set_at"]) def test_rejects(self): for bad in ({"basis": "stolen"}, {"note": "x" * 1001}, {"source": "javascript:alert(1)"}, {"source": "ftp://x.org/a"}, {"source": "https://u:pw@x.org/"}, {"licence": 5}): with self.assertRaises(ValueError, msg=str(bad)): validate_provenance(bad) def test_recording_not_settable_by_client(self): self.assertNotIn("recorded_consent", validate_provenance({"recorded_consent": "../../etc/passwd"})) self.assertEqual(validate_provenance({}, {"recorded_consent": "a.webm"})["recorded_consent"], "a.webm") def test_import_prefill_stays_unknown(self): p = import_provenance("https://voices.example/v/1") self.assertEqual((p["basis"], p["source"]), ("unknown", "https://voices.example/v/1")) self.assertEqual(import_provenance("ftp://bad")["source"], "") class RouteTests(unittest.TestCase): def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.dir = Path(self.tmp.name) self.wav = self.dir / "EN_F_Test.wav" self.wav.write_bytes(b"RIFF") patch = mock.patch.object(lib, "_load_settings", return_value={"voices_scan_dir": str(self.dir)}) patch.start() self.addCleanup(patch.stop) self.addCleanup(self.tmp.cleanup) def _req(self, body): class R: async def json(_): return body return R() def test_put_persists_and_validates(self): out = asyncio.run(lib.put_voice_provenance("EN_F_Test", self._req({"basis": "own_voice", "note": "me"}))) self.assertEqual(_load_meta(self.wav)["provenance"]["basis"], "own_voice") self.assertEqual(out["provenance"]["note"], "me") with self.assertRaises(HTTPException) as cm: asyncio.run(lib.put_voice_provenance("EN_F_Test", self._req({"basis": "nope"}))) self.assertEqual(cm.exception.status_code, 400) with self.assertRaises(HTTPException) as cm: asyncio.run(lib.put_voice_provenance("missing", self._req({}))) self.assertEqual(cm.exception.status_code, 404) def test_legacy_voice_has_no_provenance(self): self.assertNotIn("provenance", _load_meta(self.wav)) # UI treats absent as unknown def _upload(self, name, ctype, data): f = UploadFile(io.BytesIO(data), filename=name, headers=Headers({"content-type": ctype})) return asyncio.run(lib.upload_consent_recording("EN_F_Test", f)) def test_consent_upload(self): out = self._upload("c.webm", "audio/webm;codecs=opus", b"abc") self.assertEqual(out["provenance"]["recorded_consent"], "EN_F_Test.webm") self.assertTrue(self.wav.with_suffix(".consent").exists()) self.assertIn(self.wav.with_suffix(".consent"), _voice_package_paths(self.wav)) self.assertEqual(asyncio.run(lib.serve_consent_recording("EN_F_Test")).media_type, "audio/webm") for name, ctype in (("c.exe", "audio/webm"), ("c.webm", "text/html"), ("../c.webm", "audio/webm")): with self.assertRaises(HTTPException, msg=name): self._upload(name, ctype, b"abc") def test_consent_too_large(self): with mock.patch("core.provenance.MAX_CONSENT_BYTES", 2), self.assertRaises(HTTPException) as cm: self._upload("c.webm", "audio/webm", b"abcdef") self.assertEqual(cm.exception.status_code, 413) self.assertFalse(self.wav.with_suffix(".consent").exists()) if __name__ == "__main__": unittest.main()