#!/usr/bin/env python3 """A stand-in for the part of Gitea 1.27's API that tools/gitea-release uses. Tags, releases and assets in memory. Writes need the token "not-a-real-token", as Gitea needs a real one. Like Gitea, it takes a second asset under a name a release already has: refusing that is tools/gitea-release's job, and the tests check that it does. GET /test/tamper/TAG/NAME changes an asset's bytes, the way somebody could in the web UI. Prints the port it listens on, then serves until killed. """ import json, re from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs, unquote TOKEN = "token not-a-real-token" PREFIX = "/api/v1/repos/church/classroom" state = {"tags": {"v0.1.5": "1" * 40}, "releases": [], "next": 1} def find(tag): return next((r for r in state["releases"] if r["tag_name"] == tag), None) class Gitea(BaseHTTPRequestHandler): def log_message(self, *args): pass def reply(self, status, body=None, raw=None): data = raw if raw is not None else json.dumps(body).encode() if body is not None else b"" self.send_response(status) self.send_header("Content-Length", str(len(data))) self.end_headers() self.wfile.write(data) def authorised(self): if self.headers.get("Authorization") != TOKEN: self.reply(401, {"message": "token is required"}) return False return True def release_json(self, r): port = self.server.server_port return {**{k: v for k, v in r.items() if k != "assets"}, "html_url": f"http://127.0.0.1:{port}/church/classroom/releases/tag/{r['tag_name']}"} def do_GET(self): path = urlparse(self.path).path if m := re.fullmatch(PREFIX + r"/releases/tags/(.+)", path): r = find(unquote(m.group(1))) return self.reply(200, self.release_json(r)) if r else self.reply(404, {}) if m := re.fullmatch(PREFIX + r"/tags/(.+)", path): sha = state["tags"].get(unquote(m.group(1))) return self.reply(200, {"name": m.group(1), "commit": {"sha": sha}}) if sha else self.reply(404, {}) if m := re.fullmatch(PREFIX + r"/releases/(\d+)/assets", path): r = next((r for r in state["releases"] if r["id"] == int(m.group(1))), None) if not r: return self.reply(404, {}) port = self.server.server_port return self.reply(200, [{"id": i, "name": n, "size": len(d), "browser_download_url": f"http://127.0.0.1:{port}/download/{r['id']}/{n}"} for i, (n, d) in enumerate(r["assets"].items())]) if m := re.fullmatch(r"/download/(\d+)/(.+)", path): r = next((r for r in state["releases"] if r["id"] == int(m.group(1))), None) data = r and r["assets"].get(unquote(m.group(2))) return self.reply(200, raw=data) if data is not None else self.reply(404, {}) # For the test: somebody replaces an asset by hand in the web UI. if m := re.fullmatch(r"/test/tamper/(.+)/(.+)", path): find(unquote(m.group(1)))["assets"][unquote(m.group(2))] = b"tampered" return self.reply(200, {}) return self.reply(404, {}) def do_POST(self): if not self.authorised(): return url = urlparse(self.path) length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if url.path == PREFIX + "/releases": data = json.loads(body) if find(data["tag_name"]): return self.reply(409, {"message": "release exists"}) state["tags"].setdefault(data["tag_name"], data["target_commitish"]) r = {"id": state["next"], "tag_name": data["tag_name"], "name": data["name"], "body": data["body"], "prerelease": data["prerelease"], "draft": False, "assets": {}} state["next"] += 1 state["releases"].append(r) return self.reply(201, self.release_json(r)) if m := re.fullmatch(PREFIX + r"/releases/(\d+)/assets", url.path): r = next(r for r in state["releases"] if r["id"] == int(m.group(1))) name = parse_qs(url.query)["name"][0] boundary = self.headers["Content-Type"].split("boundary=")[1].encode() part = body.split(b"--" + boundary)[1] content = part.split(b"\r\n\r\n", 1)[1].rsplit(b"\r\n", 1)[0] # Gitea itself happily takes a second asset of the same name. r["assets"][name] = content return self.reply(201, {"name": name}) return self.reply(404, {}) def do_PATCH(self): if not self.authorised(): return path = urlparse(self.path).path if m := re.fullmatch(PREFIX + r"/releases/(\d+)", path): r = next(r for r in state["releases"] if r["id"] == int(m.group(1))) data = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) r.update({k: v for k, v in data.items() if k in ("body", "prerelease")}) return self.reply(200, self.release_json(r)) return self.reply(404, {}) server = HTTPServer(("127.0.0.1", 0), Gitea) print(server.server_port, flush=True) server.serve_forever()