import http.client
import json
import sys
import tempfile
import threading
import unittest
import urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
import quill_core # noqa: E402
import server # noqa: E402
FILES = {
"README.md": "# demo\n\nA demo space.\n",
"posts/2026-10-01-hello.md": "---\ntitle: Hello\nauthor: test-agent\n---\nFirst **post**. See [the other](second.md).\n",
"posts/second.md": "# Second post\n\nPlain text \n",
}
class FakeGitea(BaseHTTPRequestHandler):
def log_message(self, *a):
pass
def do_GET(self):
p = urllib.parse.urlsplit(self.path)
path = p.path
def out(obj, raw=False):
body = obj.encode() if raw else json.dumps(obj).encode()
self.send_response(200); self.send_header("Content-Length", str(len(body))); self.end_headers(); self.wfile.write(body)
if path == "/api/v1/repos/demo/space":
return out({"default_branch": "main"})
if path == "/api/v1/repos/demo/space/branches/main":
return out({"commit": {"id": "abc123def456", "message": "add posts", "author": {"name": "test-agent"}, "timestamp": "2026-10-01T00:00:00Z"}})
if path.startswith("/api/v1/repos/demo/space/git/trees/"):
return out({"tree": [{"path": k, "type": "blob"} for k in FILES]})
if path.startswith("/api/v1/repos/demo/space/raw/"):
f = urllib.parse.unquote(path[len("/api/v1/repos/demo/space/raw/"):])
if f in FILES:
return out(FILES[f], raw=True)
self.send_response(404); self.send_header("Content-Length", "0"); self.end_headers()
def serve(handler):
srv = ThreadingHTTPServer(("127.0.0.1", 0), handler)
threading.Thread(target=srv.serve_forever, daemon=True).start()
return srv
class QuillTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.gitea = serve(FakeGitea)
cls.tmp = tempfile.TemporaryDirectory()
cfg = {
"name": "Test Quill", "gitea_url": f"http://127.0.0.1:{cls.gitea.server_port}",
"git_web_base": "https://git.example.test", "access": "code",
"view_code_sha256": server.hash_code("letmein"), "data_dir": cls.tmp.name,
"nav": [{"label": "Git", "href": "https://git.example.test/"}],
"spaces": [{"key": "demo", "name": "Demo", "repo": "demo/space", "description": "d"}],
}
p = Path(cls.tmp.name) / "config.json"
p.write_text(json.dumps(cfg))
server.CONFIG = server.load_config(p)
server._SECRET = server._secret(Path(cls.tmp.name))
cls.srv = serve(server.Handler)
@classmethod
def tearDownClass(cls):
cls.srv.shutdown(); cls.gitea.shutdown(); cls.tmp.cleanup()
def req(self, method, path, body=None, headers=None):
c = http.client.HTTPConnection("127.0.0.1", self.srv.server_port, timeout=10)
c.request(method, path, body=body, headers=headers or {})
r = c.getresponse()
return r.status, dict(r.getheaders()), r.read()
def test_core_listing_and_post(self):
status, data = quill_core.api("/v1/quill/spaces/demo", server.gitea_get)
self.assertEqual(status, 200)
titles = [p["title"] for p in data["posts"]]
self.assertIn("Hello", titles)
self.assertIn("Second post", titles)
self.assertTrue(all("html" not in p for p in data["posts"]))
slug = next(p["slug"] for p in data["posts"] if p["title"] == "Second post")
status, data = quill_core.api("/v1/quill/spaces/demo/posts/" + slug, server.gitea_get)
self.assertEqual(status, 200)
self.assertNotIn("