134 lines
5.1 KiB
Python
134 lines
5.1 KiB
Python
#!/usr/bin/env python3
|
|
"""M1 smoke test for scenegod.server — stdlib only, real uvicorn on a test port.
|
|
|
|
python3 scripts/test_server.py # green = M1 code-done
|
|
|
|
Spins up the server against throwaway asset/scene dirs, then asserts tree
|
|
grouping, file streaming, path-traversal rejection, scene round-trip, and
|
|
validation failures.
|
|
"""
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PY = ROOT / ".venv" / "bin" / "python"
|
|
PORT = 8099
|
|
BASE = f"http://127.0.0.1:{PORT}"
|
|
|
|
|
|
def req(method, path, body=None):
|
|
data = body.encode() if isinstance(body, str) else body
|
|
r = urllib.request.Request(BASE + path, data=data, method=method)
|
|
try:
|
|
with urllib.request.urlopen(r) as resp:
|
|
return resp.status, resp.read()
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read()
|
|
|
|
|
|
def get_json(path):
|
|
st, b = req("GET", path)
|
|
assert st == 200, f"{path} -> {st}"
|
|
return json.loads(b)
|
|
|
|
|
|
def main():
|
|
tmp = Path(tempfile.mkdtemp(prefix="scenegod_test_"))
|
|
assets, scenes = tmp / "assets", tmp / "scenes"
|
|
# lady.glb + lady.fbx + lady.jpg share a stem -> one entry, thumb split off
|
|
(assets / "characters" / "pack01").mkdir(parents=True)
|
|
for f in ("lady.glb", "lady.fbx", "lady.jpg"):
|
|
(assets / "characters" / "pack01" / f).write_bytes(b"x")
|
|
(assets / "backdrops").mkdir(parents=True)
|
|
(assets / "backdrops" / "street.jpg").write_bytes(b"img")
|
|
for d in ("animations", "props", "audio"):
|
|
(assets / d).mkdir()
|
|
|
|
env = {**os.environ, "SCENEGOD_ASSETS": str(assets), "SCENEGOD_SCENES": str(scenes)}
|
|
proc = subprocess.Popen(
|
|
[str(PY), "-m", "uvicorn", "scenegod.server:app", "--port", str(PORT)],
|
|
cwd=ROOT, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
try:
|
|
for _ in range(50): # wait for boot
|
|
try:
|
|
req("GET", "/scenes")
|
|
break
|
|
except urllib.error.URLError:
|
|
time.sleep(0.2)
|
|
else:
|
|
raise SystemExit("server did not start")
|
|
|
|
n = 0
|
|
# --- tree grouping ---
|
|
tree = get_json("/assets/tree")
|
|
chars = tree["characters"]
|
|
assert len(chars) == 1 and chars[0]["name"] == "lady", chars
|
|
assert chars[0]["path"] == "characters/pack01/lady.glb", chars # glb preferred
|
|
assert set(chars[0]["formats"]) == {"glb", "fbx"}, chars # jpg not a format
|
|
assert chars[0]["thumb"] == "characters/pack01/lady.jpg", chars
|
|
assert tree["backdrops"][0]["path"] == "backdrops/street.jpg", tree["backdrops"]
|
|
n += 1
|
|
|
|
# --- file streaming ---
|
|
st, b = req("GET", "/assets/file?path=backdrops/street.jpg")
|
|
assert st == 200 and b == b"img", (st, b)
|
|
n += 1
|
|
|
|
# --- path traversal rejected ---
|
|
for bad in ("../../etc/hosts", "/etc/hosts", "..%2f..%2fetc%2fhosts",
|
|
"characters/../../../etc/hosts"):
|
|
st, _ = req("GET", "/assets/file?path=" + bad)
|
|
assert st >= 400, f"traversal {bad!r} not rejected: {st}"
|
|
n += 1
|
|
|
|
# --- scene round-trip ---
|
|
scene = {"version": 1, "name": "s1", "fps": 30, "duration": 5.0,
|
|
"entities": [{"id": "e1", "kind": "camera", "label": "cam",
|
|
"tracks": {"transform": [{"t": 0}, {"t": 1}]}}],
|
|
"cameraCuts": [{"t": 0, "camera": "e1"}]}
|
|
st, _ = req("POST", "/scenes/" + urllib.parse.quote("My Scene!"), json.dumps(scene))
|
|
assert st == 200, st
|
|
assert get_json("/scenes/my-scene")["duration"] == 5.0 # slugified
|
|
assert any(s["name"] == "my-scene" for s in get_json("/scenes"))
|
|
n += 1
|
|
|
|
# --- validation failures ---
|
|
bad_scenes = [
|
|
{"version": 2, "entities": []}, # bad version
|
|
{"version": 1, "entities": [{"id": "a"}, {"id": "a"}]}, # dup id
|
|
{"version": 1, "entities": [{"id": "a", "tracks": # unsorted
|
|
{"transform": [{"t": 1}, {"t": 0}]}}]},
|
|
{"version": 1, "entities": [], "cameraCuts": [{"camera": "nope"}]}, # bad cut
|
|
{"version": 1, "entities": [{"id": "a", "tracks": {"clips": [ # overlap
|
|
{"start": 0, "in": 0, "out": 2, "loop": 1, "fade": 0},
|
|
{"start": 1, "in": 0, "out": 2}]}}]},
|
|
]
|
|
for bs in bad_scenes:
|
|
st, b = req("POST", "/scenes/bad", json.dumps(bs))
|
|
assert st == 422, f"expected 422 for {bs}, got {st}"
|
|
assert json.loads(b)["errors"], b
|
|
# a fade-covered overlap is allowed
|
|
ok = {"version": 1, "entities": [{"id": "a", "tracks": {"clips": [
|
|
{"start": 0, "in": 0, "out": 2, "loop": 1, "fade": 0.5},
|
|
{"start": 1.6, "in": 0, "out": 2}]}}]}
|
|
st, _ = req("POST", "/scenes/ok-fade", json.dumps(ok))
|
|
assert st == 200, st
|
|
n += 1
|
|
|
|
print(f"OK: {n} test groups passed")
|
|
finally:
|
|
proc.terminate()
|
|
proc.wait()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|