100 lines
4.4 KiB
Python
100 lines
4.4 KiB
Python
"""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()
|