# tests/test_gui_export_yaml.py
import pytest
pytest.importorskip("fastapi")
import yaml
from fastapi.testclient import TestClient
from touchstone.gui import app as gui_app
from touchstone.gui.control import RunRegistry
class _FakeLauncher:
def spawn(self, argv):
return 1
def alive(self, pid):
return False
def terminate(self, pid):
return None
def _client(tmp_path):
reg = RunRegistry(runs_dir=tmp_path, launcher=_FakeLauncher())
return TestClient(gui_app.create_app(runs_dir=tmp_path, registry=reg))
_VALID_PACK = """
id: x
title: X
scale: {1: a, 2: b, 3: c, 4: d, 5: e}
dimensions: [{id: Q1, name: Q, weight: 1}]
ko_rule: {dimension: Q1, threshold: 2}
prompt_variants: [{id: none, system_prompt: null}]
categories: [{id: A, name: A, prompts: [{id: A1, title: T, prompt: "p"}]}]
"""
def test_export_yaml_rejects_symlink_escape(tmp_path, monkeypatch):
# a symlink in packs/ pointing to a VALID pack OUTSIDE packs/ must not be served
monkeypatch.chdir(tmp_path)
(tmp_path / "packs").mkdir()
(tmp_path / "outside.yaml").write_text(_VALID_PACK, encoding="utf-8")
(tmp_path / "packs" / "evil.yaml").symlink_to(tmp_path / "outside.yaml")
r = _client(tmp_path).get("/export-yaml?kind=pack&path=packs/evil.yaml")
assert r.status_code == 404
def test_export_yaml_config_roundtrips(tmp_path):
r = _client(tmp_path).get("/export-yaml?kind=config&path=config.m5.yaml")
assert r.status_code == 200
assert "yaml" in r.headers["content-type"]
parsed = yaml.safe_load(r.text)
assert isinstance(parsed, dict)
assert parsed["machine"] == "M5-32GB"
assert "config.m5.yaml" in r.headers.get("content-disposition", "")
def test_export_yaml_pack_roundtrips(tmp_path):
r = _client(tmp_path).get("/export-yaml?kind=pack&path=packs/ndassist.yaml")
assert r.status_code == 200
assert "yaml" in r.headers["content-type"]
parsed = yaml.safe_load(r.text)
assert isinstance(parsed, dict)
assert parsed["id"]
def test_export_yaml_rejects_bad_kind(tmp_path):
r = _client(tmp_path).get("/export-yaml?kind=secret&path=config.m5.yaml")
assert r.status_code == 404
def test_export_yaml_rejects_unoffered_path(tmp_path):
r = _client(tmp_path).get("/export-yaml?kind=config&path=../etc/passwd")
assert r.status_code == 404
def test_export_yaml_rejects_pack_as_config(tmp_path):
# A pack path offered under the pack glob must not be readable via kind=config.
r = _client(tmp_path).get("/export-yaml?kind=config&path=packs/ndassist.yaml")
assert r.status_code == 404
def test_export_yaml_config_redacts_api_key(tmp_path):
# SECURITY: the downloadable config must never carry the endpoint api_key in cleartext
# (the on-screen viewer masks it; the download must not be a side-channel).
r = _client(tmp_path).get("/export-yaml?kind=config&path=config.m5.yaml")
assert r.status_code == 200
parsed = yaml.safe_load(r.text)
assert parsed["endpoint"]["api_key"] == "<redacted>"