Files
gitea-nova/Tests/units.test.js
T
thatguygriffandClaude Opus 5 694609a3cc Implement the Gitea extension
The repository was a bare Nova scaffold. This builds it out to match the
capabilities of the official Gitea VS Code extension.

Sidebar sections:
  - Current Branch: pull requests and workflow runs for the checked-out
    branch, with a current/all/pinned branch filter
  - Workflows: runs grouped by workflow file, expanding into jobs, steps,
    and artifacts
  - Pull Requests: open pull requests across repositories, expanding into
    reviews, review comments, and changed files
  - Settings: per-instance connection state, plus repository Actions
    secrets and variables

Commands cover run control (re-run, re-run failed jobs, re-run a job,
cancel), job logs, artifact download/reveal/open, pull request overview,
diff, checkout, creation, merge and close, the full review cycle, and
secret and variable management. Multiple instances are supported, routed
by git remote host, with tokens held per instance in the Keychain.

Nova exposes no webview, diff editor, editor decorations, or extension
status bar, so four features are shaped differently from the VS Code
original: the pull request timeline renders as Markdown, diffs open as
unified .diff documents, review comments are published through an
IssueCollection so they appear in the gutter and the Issues sidebar, and
a failed run posts a notification. OAuth and insecureSkipVerify have no
Nova equivalent and are omitted. README.md records all of this.

Endpoints were taken from Gitea's published swagger.v1.json. Servers
predating the workflow runs API fall back to /actions/tasks.

Tests/ runs the extension's real code under Node against a stubbed Nova
runtime and a canned Gitea instance: 88 checks, no install step and no
network. Images are generated by Tools/make-icons.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01MQuusXgZC2dzwpJJ1qhtti
2026-08-28 19:55:54 -03:00

257 lines
11 KiB
JavaScript

//
// Units and edge cases: remote URL parsing, diff arithmetic, time formatting,
// preference precedence, HTTP error surfacing, pagination, and the unconfigured
// and legacy-server paths.
//
const { EXTENSION_ROOT } = require("./nova-stub.js");
const { PR_DIFF } = require("./fake-gitea.js");
const { Checker } = require("./assert.js");
const path = require("node:path");
const script = (...parts) => path.join(EXTENSION_ROOT, "Scripts", ...parts);
const t = new Checker();
/** Swaps in a fetch implementation for the duration of `body`. */
async function withResponder(responder, body) {
const saved = global.fetch;
global.fetch = responder;
try {
return await body();
} finally {
global.fetch = saved;
}
}
const ok = (payload) => ({ ok: true, status: 200, text: async () => JSON.stringify(payload) });
const fail = (status, message) => ({
ok: false,
status,
text: async () => JSON.stringify({ message }),
});
(async () => {
t.section("remote URL parsing");
const remotes = require(script("gitea", "remotes.js"));
const parsed = (host, owner, name, port = null) => ({ host, port, owner, name });
t.equals("https", remotes.parse("https://gitea.com/acme/widget.git"), parsed("gitea.com", "acme", "widget"));
t.equals("https without .git", remotes.parse("https://gitea.com/acme/widget"), parsed("gitea.com", "acme", "widget"));
t.equals(
"https with credentials",
remotes.parse("https://user:[email protected]/acme/widget.git"),
parsed("git.example.com", "acme", "widget"),
);
t.equals(
"https with port",
remotes.parse("https://git.example.com:3000/acme/widget.git"),
parsed("git.example.com", "acme", "widget", 3000),
);
t.equals("scp-style ssh", remotes.parse("[email protected]:acme/widget.git"), parsed("gitea.com", "acme", "widget"));
t.equals(
"ssh:// with port",
remotes.parse("ssh://[email protected]:2222/acme/widget.git"),
parsed("git.example.com", "acme", "widget", 2222),
);
t.equals(
"instance served from a sub-path",
remotes.parse("https://example.com/gitea/acme/widget.git"),
parsed("example.com", "acme", "widget"),
);
t.equals("host lower-cased", remotes.parse("[email protected]:Acme/Widget.git").host, "gitea.com");
t.equals("garbage rejected", remotes.parse("not a url"), null);
t.equals("empty rejected", remotes.parse(""), null);
t.equals("hostOf drops the port", remotes.hostOf("https://git.example.com:3000"), "git.example.com");
t.equals("hostOf on a plain URL", remotes.hostOf("https://gitea.com"), "gitea.com");
t.section("diff hunk to file line");
const review = require(script("controllers", "review.js"));
t.equals("additions only", review.lineFromDiffHunk("@@ -0,0 +1,2 @@\n+first\n+second", null), 2);
t.equals("removed lines do not advance the new file", review.lineFromDiffHunk("@@ -10,4 +10,7 @@\n ctx\n-gone\n+a\n+b", null), 12);
t.equals("single-line hunk header", review.lineFromDiffHunk("@@ -5 +5 @@\n+changed", null), 5);
t.equals("no hunk falls back to position", review.lineFromDiffHunk(null, 42), 42);
t.equals("unparseable hunk falls back", review.lineFromDiffHunk("no header here", 7), 7);
t.section("per-file diff extraction");
const pulls = require(script("controllers", "pullRequests.js"));
const first = pulls.extractFileDiff(PR_DIFF, "src/login.js");
t.check(
"slices out only the first file",
first.startsWith("diff --git a/src/login.js") && !first.includes("src/new.js"),
JSON.stringify(first?.slice(0, 60)),
);
const last = pulls.extractFileDiff(PR_DIFF, "src/new.js");
t.check("slices out the last file", last.startsWith("diff --git a/src/new.js") && last.includes("noop"));
t.equals("unknown file yields null", pulls.extractFileDiff(PR_DIFF, "nope.js"), null);
t.section("time formatting");
const time = require(script("util", "time.js"));
t.equals("Go zero time renders blank", time.relative("0001-01-01T00:00:00Z"), "");
t.equals("null renders blank", time.relative(null), "");
t.check("a past time reads as ago", time.relative(new Date(Date.now() - 7200e3).toISOString()).endsWith("ago"));
t.equals("minutes and seconds", time.duration("2026-08-28T10:00:00Z", "2026-08-28T10:01:23Z"), "1m 23s");
t.equals("hours and minutes", time.duration("2026-08-28T10:00:00Z", "2026-08-28T12:30:00Z"), "2h 30m");
t.check(
"a zero end time still yields a duration",
typeof time.duration("2026-08-28T10:00:00Z", "0001-01-01T00:00:00Z") === "string",
);
t.section("preference precedence");
const config = require(script("util", "config.js"));
nova.config.set("gitea.baseUrl", "https://global.example.com");
t.equals("global value used", config.get("gitea.baseUrl"), "https://global.example.com");
nova.workspace.config.set("gitea.baseUrl", "https://workspace.example.com");
t.equals("workspace overrides global", config.get("gitea.baseUrl"), "https://workspace.example.com");
nova.workspace.config.remove("gitea.baseUrl");
t.equals("falls back to global", config.get("gitea.baseUrl"), "https://global.example.com");
nova.config.remove("gitea.baseUrl");
t.equals("falls back to the built-in default", config.get("gitea.baseUrl"), "https://gitea.com");
t.equals("boolean default when unset", config.bool("gitea.reviewComments.enabled"), true);
nova.config.set("gitea.reviewComments.enabled", false);
t.equals("an explicit false is respected", config.bool("gitea.reviewComments.enabled"), false);
nova.config.remove("gitea.reviewComments.enabled");
t.equals("trailing slashes stripped", config.normalizeBaseUrl("https://gitea.com///"), "https://gitea.com");
t.equals("missing scheme filled in", config.normalizeBaseUrl("gitea.example.com"), "https://gitea.example.com");
t.equals("blank rejected", config.normalizeBaseUrl(" "), null);
nova.config.set("gitea.baseUrl", "https://a.example.com");
nova.config.set("gitea.instances", ["https://b.example.com/", "https://a.example.com", "https://b.example.com"]);
t.equals("instances de-duplicated, default first", config.instances(), [
"https://a.example.com",
"https://b.example.com",
]);
t.section("HTTP error surfacing");
const { GiteaClient, GiteaError } = require(script("gitea", "client.js"));
const { GiteaApi } = require(script("gitea", "api.js"));
const api = new GiteaApi(new GiteaClient("https://gitea.test", () => "tok"));
const repo = { owner: "acme", name: "widget" };
await withResponder(async () => fail(401, "token is invalid"), async () => {
try {
await api.currentUser();
t.check("401 throws", false);
} catch (error) {
t.check("401 raises a GiteaError", error instanceof GiteaError);
t.check("401 flagged as an auth failure", error.isAuth === true);
t.check("401 tells the user what to do", error.message.includes("Set a valid token"), error.message);
}
});
await withResponder(async () => fail(403, "missing scope"), async () => {
try {
await api.listSecrets(repo);
t.check("403 throws", false);
} catch (error) {
t.check("403 names the cause", error.message.includes("scope"), error.message);
t.check("403 keeps the server's own detail", error.message.includes("missing scope"), error.message);
}
});
t.section("older Gitea servers");
await withResponder(
async (url) => {
if (url.includes("/actions/runs")) return fail(404, "Not Found");
if (url.includes("/actions/tasks")) {
return ok({ total_count: 1, workflow_runs: [{ id: 1, name: "legacy" }] });
}
return fail(404, "Not Found");
},
async () => {
const result = await api.listRuns(repo, { limit: 10 });
t.check(
"runs fall back to /actions/tasks",
result.legacy === true && result.runs.length === 1,
JSON.stringify(result),
);
},
);
await withResponder(async () => fail(404, "Not Found"), async () => {
t.equals("a missing artifacts endpoint yields an empty list", await api.listArtifacts(repo, 1), []);
});
t.section("query building and pagination");
const paged = [];
await withResponder(
async (url) => {
paged.push(url);
const page = Number(new URL(url).searchParams.get("page"));
return ok(
page === 1
? Array.from({ length: 50 }, (_, i) => ({ number: i + 1 }))
: Array.from({ length: 5 }, (_, i) => ({ number: 51 + i })),
);
},
async () => {
t.equals("collects across pages", (await api.listPullRequests(repo, { limit: 60 })).length, 55);
t.check("page and limit sent", paged[0].includes("page=1") && paged[0].includes("limit=50"), paged[0]);
t.check(
"state and sort sent",
paged[0].includes("state=open") && paged[0].includes("sort=recentupdate"),
paged[0],
);
t.check("stops after a short page", paged.length === 2, String(paged.length));
},
);
const built = [];
await withResponder(
async (url) => {
built.push(url);
return ok({ workflow_runs: [] });
},
async () => {
await api.listRuns(repo, { limit: 20, branch: null, event: null });
t.check(
"null query parameters omitted entirely",
!built[0].includes("branch=") && !built[0].includes("event=") && built[0].includes("limit=20"),
built[0],
);
},
);
const escaped = [];
await withResponder(
async (url) => {
escaped.push(url);
return ok({});
},
async () => {
await api.repository({ owner: "my org", name: "a+b" });
t.check("path segments percent-encoded", escaped[0].includes("/repos/my%20org/a%2Bb"), escaped[0]);
},
);
t.section("nothing configured");
const { Store } = require(script("store.js"));
nova.config.set("gitea.baseUrl", "https://untokened.example.com");
nova.config.set("gitea.instances", []);
const store = new Store();
t.check("store reports no token", store.hasAnyToken === false);
await store.refresh({ rediscover: true });
store.stop();
const status = store.instanceStatus.get("https://untokened.example.com");
t.check("instance marked as needing a token", status?.needsToken === true, JSON.stringify(status));
const { BranchProvider } = require(script("views", "branchView.js"));
const rows = await new BranchProvider(store).getChildren(null);
t.check(
"the sidebar explains how to sign in",
rows.length === 1 && rows[0].name.includes("Set Token"),
rows.map((row) => row.name).join(" | "),
);
store.dispose();
t.finish("UNIT CHECKS");
})().catch((error) => {
console.error("HARNESS ERROR", error);
process.exit(2);
});