// A canned Gitea instance served through a fetch() stub. const BASE = "https://gitea.test/api/v1"; const now = new Date(); const iso = (minutesAgo) => new Date(now.getTime() - minutesAgo * 60000).toISOString(); const RUNS = [ { id: 101, run_number: 12, display_title: "Fix the flaky test", path: ".gitea/workflows/ci.yml", event: "push", status: "completed", conclusion: "success", head_branch: "feature/login", head_sha: "abc1234def", created_at: iso(30), started_at: iso(29), completed_at: iso(26), html_url: "https://gitea.test/acme/widget/actions/runs/101", actor: { login: "alice" } }, { id: 102, run_number: 13, display_title: "Add login form", path: ".gitea/workflows/ci.yml", event: "pull_request", status: "running", conclusion: "", head_branch: "feature/login", head_sha: "def5678abc", created_at: iso(4), started_at: iso(4), completed_at: null, html_url: "https://gitea.test/acme/widget/actions/runs/102", actor: { login: "bob" } }, { id: 103, run_number: 5, display_title: "Nightly", path: ".gitea/workflows/nightly.yml", event: "schedule", status: "completed", conclusion: "failure", head_branch: "main", head_sha: "999aaa111", created_at: iso(300), started_at: iso(300), completed_at: iso(295), html_url: "https://gitea.test/acme/widget/actions/runs/103", actor: { login: "ci" } }, ]; const JOBS = { 101: [{ id: 9001, name: "build", status: "completed", conclusion: "success", started_at: iso(29), completed_at: iso(27), runner_name: "runner-1", html_url: "https://gitea.test/acme/widget/actions/runs/101/jobs/0", steps: [{ number: 1, name: "Checkout", status: "completed", conclusion: "success", started_at: iso(29), completed_at: iso(29) }, { number: 2, name: "Test", status: "completed", conclusion: "success", started_at: iso(29), completed_at: iso(27) }] }], 102: [{ id: 9002, name: "build", status: "running", conclusion: "", started_at: iso(4), completed_at: null, steps: [] }], 103: [{ id: 9003, name: "nightly", status: "completed", conclusion: "failure", started_at: iso(300), completed_at: iso(295), steps: [] }], }; const ARTIFACTS = { 101: [{ id: 555, name: "coverage", size_in_bytes: 20480, expired: false, archive_download_url: BASE + "/repos/acme/widget/actions/artifacts/555/zip" }], }; const DIFF_HUNK = [ "@@ -10,4 +10,7 @@ function login() {", " const form = document.querySelector('form');", "- form.submit();", "+ if (!validate(form)) {", "+ return;", "+ }", "+ form.submit();", ].join("\n"); const PULLS = [ { number: 7, title: "Add login form", state: "open", draft: false, merged: false, mergeable: true, user: { login: "bob" }, updated_at: iso(3), created_at: iso(120), head: { ref: "feature/login", label: "acme:feature/login", sha: "def5678abc" }, base: { ref: "main", label: "acme:main" }, body: "Adds a login form and validates it before submit.", html_url: "https://gitea.test/acme/widget/pulls/7" }, { number: 6, title: "Bump deps", state: "open", draft: true, merged: false, mergeable: true, user: { login: "alice" }, updated_at: iso(600), created_at: iso(900), head: { ref: "chore/deps", label: "acme:chore/deps", sha: "111222333" }, base: { ref: "main", label: "acme:main" }, body: "", html_url: "https://gitea.test/acme/widget/pulls/6" }, ]; const REVIEWS = { 7: [{ id: 4001, state: "REQUEST_CHANGES", body: "Please guard the submit.", comments_count: 1, user: { login: "alice" }, submitted_at: iso(10), html_url: "https://gitea.test/acme/widget/pulls/7#pullrequestreview-4001" }, { id: 4002, state: "APPROVED", body: "LGTM now", comments_count: 0, user: { login: "carol" }, submitted_at: iso(2) }], 6: [], }; const REVIEW_COMMENTS = { 4001: [{ id: 7001, body: "Guard this before submitting.", path: "src/login.js", position: 13, pull_request_review_id: 4001, user: { login: "alice" }, created_at: iso(10), diff_hunk: DIFF_HUNK, resolver: null, html_url: "https://gitea.test/acme/widget/pulls/7#issuecomment-7001" }], }; const FILES = { 7: [{ filename: "src/login.js", status: "modified", additions: 4, deletions: 1, html_url: "https://gitea.test/acme/widget/pulls/7/files#src-login-js" }, { filename: "src/new.js", status: "added", additions: 12, deletions: 0 }], 6: [{ filename: "package.json", status: "modified", additions: 2, deletions: 2 }], }; const PR_DIFF = [ "diff --git a/src/login.js b/src/login.js", "index 1111111..2222222 100644", "--- a/src/login.js", "+++ b/src/login.js", DIFF_HUNK, "diff --git a/src/new.js b/src/new.js", "new file mode 100644", "index 0000000..3333333", "--- /dev/null", "+++ b/src/new.js", "@@ -0,0 +1,2 @@", "+export function noop() {}", "", ].join("\n"); const SECRETS = [{ name: "DEPLOY_KEY", created_at: iso(5000), description: "" }]; const VARIABLES = [{ name: "REGISTRY", data: "registry.example.com" }]; const calls = []; function json(body, status = 200) { const payload = JSON.stringify(body); return { ok: status < 400, status, text: async () => payload, json: async () => body, arrayBuffer: async () => new TextEncoder().encode(payload).buffer, }; } function plain(body, status = 200) { return { ok: status < 400, status, text: async () => body, json: async () => JSON.parse(body), arrayBuffer: async () => new TextEncoder().encode(body).buffer, }; } global.fetch = async (url, init) => { const method = (init && init.method) || "GET"; const parsed = new URL(url); const p = parsed.pathname.replace("/api/v1", ""); const q = parsed.searchParams; calls.push(method + " " + p + (q.toString() ? "?" + q.toString() : "")); const auth = init && init.headers && init.headers["Authorization"]; if (!auth) return json({ message: "token required" }, 401); const page = Number(q.get("page") || 1); const first = (list) => (page > 1 ? [] : list); if (p === "/version") return json({ version: "1.27.2" }); if (p === "/user") return json({ login: "tester", id: 1 }); if (p === "/user/repos") return json(first([ { name: "widget", full_name: "acme/widget", owner: { login: "acme" }, html_url: "https://gitea.test/acme/widget", default_branch: "main" }])); const m = p.match(/^\/repos\/([^/]+)\/([^/]+)(\/.*)?$/); if (!m) return json({ message: "no route for " + p }, 404); const rest = m[3] || ""; if (rest === "") { // The instance answers SSH on a different hostname than its web UI, // which is what host-alias detection keys off. return json({ name: m[2], full_name: m[1] + "/" + m[2], html_url: "https://gitea.test/" + m[1] + "/" + m[2], clone_url: "https://gitea.test/" + m[1] + "/" + m[2] + ".git", ssh_url: "git@ssh.gitea.test:" + m[1] + "/" + m[2] + ".git", }); } if (rest === "/branches") return json(first([{ name: "main" }, { name: "feature/login" }])); if (rest === "/actions/runs") { let runs = RUNS; if (q.get("branch")) runs = runs.filter((r) => r.head_branch === q.get("branch")); return json({ total_count: runs.length, workflow_runs: runs }); } if (rest === "/actions/workflows") { return json({ workflows: [{ id: 1, name: "CI", path: ".gitea/workflows/ci.yml" }] }); } let sub = rest.match(/^\/actions\/runs\/(\d+)\/jobs$/); if (sub) return json({ jobs: JOBS[sub[1]] || [] }); sub = rest.match(/^\/actions\/runs\/(\d+)\/artifacts$/); if (sub) return json({ artifacts: ARTIFACTS[sub[1]] || [] }); sub = rest.match(/^\/actions\/jobs\/(\d+)\/logs$/); if (sub) return plain("Starting job " + sub[1] + "\nDone\n"); sub = rest.match(/^\/actions\/artifacts\/(\d+)\/zip$/); if (sub) return plain("PKfake-zip-bytes"); if (/^\/actions\/runs\/\d+\/(rerun|rerun-failed-jobs|cancel)$/.test(rest)) return json(null, 204); if (/^\/actions\/runs\/\d+\/jobs\/\d+\/rerun$/.test(rest)) return json(null, 204); if (rest === "/actions/secrets") return json(first(SECRETS)); if (rest === "/actions/variables") return json(first(VARIABLES)); if (/^\/actions\/secrets\/[^/]+$/.test(rest)) return json(null, 204); if (/^\/actions\/variables\/[^/]+$/.test(rest)) return json(null, 204); if (rest === "/pulls" && method === "GET") return json(first(PULLS)); if (rest === "/pulls" && method === "POST") { return json({ number: 8, html_url: "https://gitea.test/acme/widget/pulls/8" }); } sub = rest.match(/^\/pulls\/(\d+)$/); if (sub) return json(PULLS.find((x) => x.number === Number(sub[1])) || null); sub = rest.match(/^\/pulls\/(\d+)\.diff$/); if (sub) return plain(PR_DIFF); sub = rest.match(/^\/pulls\/(\d+)\/files$/); if (sub) return json(first(FILES[sub[1]] || [])); sub = rest.match(/^\/pulls\/(\d+)\/reviews$/); if (sub && method === "GET") return json(first(REVIEWS[sub[1]] || [])); if (sub && method === "POST") return json({ id: 4003 }); sub = rest.match(/^\/pulls\/(\d+)\/reviews\/(\d+)\/comments$/); if (sub) return json(REVIEW_COMMENTS[sub[2]] || []); if (/^\/pulls\/\d+\/merge$/.test(rest)) return json(null, 204); if (/^\/pulls\/\d+\/comments\/\d+\/replies$/.test(rest)) return json({ id: 7002 }); if (/^\/pulls\/comments\/\d+\/(un)?resolve$/.test(rest)) return json(null, 204); sub = rest.match(/^\/issues\/(\d+)$/); if (sub) return json({ number: Number(sub[1]), state: "closed" }); if (/^\/issues\/\d+\/timeline$/.test(rest)) { return json(first([{ type: "comment", body: "Looks good", user: { login: "carol" }, created_at: iso(5) }])); } if (/^\/issues\/\d+\/comments$/.test(rest)) return json(first([])); return json({ message: "no route for " + p }, 404); }; module.exports = { calls, RUNS, PULLS, DIFF_HUNK, PR_DIFF };