Cover the previously untested credential and error-classification paths: - tea.ts: failing/invalid/non-array `tea login list`, a failing token helper, and an empty token, all driven through the CLI seam via the fake tea harness (now parametrizable for exit codes and output). - errors.ts: a unit-tier test of classifyHttpError for the pure-classifier branches no shipped command reaches yet (issue/pull 404s, the already-classified passthrough, transport failures, no-body defaults). errors.ts is now fully covered and tea.ts is at 96% (the remainder is defensive code unreachable through the seam). Overall coverage rose from ~87% to ~93% statements / ~89% branches; the ratchet is raised to 92/87/95/92 accordingly.
96 lines
4.0 KiB
TypeScript
96 lines
4.0 KiB
TypeScript
import { AxiError } from "axi-sdk-js";
|
|
import { describe, expect, it } from "vitest";
|
|
import { classifyHttpError } from "../src/errors.js";
|
|
|
|
/**
|
|
* Unit-tier coverage of the HTTP error classifier (a pure function, no I/O).
|
|
* The integration tier drives the classifier through the issue-list seam, but
|
|
* some branches belong to paths no shipped command reaches yet — pull-request
|
|
* 404s, the already-classified passthrough, and non-HTTP transport failures —
|
|
* so they are exercised directly here against crafted inputs.
|
|
*/
|
|
function httpError(status: number, url: string, body?: unknown) {
|
|
return { status, url, error: body };
|
|
}
|
|
|
|
const REPO = "http://gitea.example/api/v1/repos/o/r";
|
|
|
|
describe("classifyHttpError", () => {
|
|
it("passes an already-classified AxiError through unchanged", () => {
|
|
const original = new AxiError("boom", "FORBIDDEN", ["hint"]);
|
|
expect(classifyHttpError(original)).toBe(original);
|
|
});
|
|
|
|
it("classifies a 404 on an issue path as ISSUE_NOT_FOUND", () => {
|
|
const result = classifyHttpError(httpError(404, `${REPO}/issues/42`));
|
|
expect(result.code).toBe("ISSUE_NOT_FOUND");
|
|
expect(result.message).toContain("#42");
|
|
});
|
|
|
|
it("classifies a 404 on a pull path as PR_NOT_FOUND", () => {
|
|
const result = classifyHttpError(httpError(404, `${REPO}/pulls/7`));
|
|
expect(result.code).toBe("PR_NOT_FOUND");
|
|
expect(result.message).toContain("#7");
|
|
});
|
|
|
|
it("classifies a 404 on the repo subtree as REPO_NOT_FOUND", () => {
|
|
const result = classifyHttpError(httpError(404, `${REPO}/issues`));
|
|
expect(result.code).toBe("REPO_NOT_FOUND");
|
|
expect(result.message).toContain("o/r");
|
|
});
|
|
|
|
it("classifies a 404 on a non-repo path as UNKNOWN", () => {
|
|
const result = classifyHttpError(httpError(404, "http://gitea.example/api/v1/version"));
|
|
expect(result.code).toBe("UNKNOWN");
|
|
});
|
|
|
|
it("falls back to the raw url when the response url does not parse", () => {
|
|
const result = classifyHttpError(httpError(404, "::not a url::"));
|
|
expect(result.code).toBe("UNKNOWN");
|
|
expect(result.message).toContain("::not a url::");
|
|
});
|
|
|
|
it("uses the body message when present, and a default when absent", () => {
|
|
expect(classifyHttpError(httpError(401, REPO, { message: "token expired" })).message).toBe(
|
|
"token expired",
|
|
);
|
|
expect(classifyHttpError(httpError(401, REPO, {})).message).toBe("Authentication required");
|
|
});
|
|
|
|
it("maps 403 and every validation status (405/409/422) and 429 to their codes", () => {
|
|
expect(classifyHttpError(httpError(403, REPO, {})).code).toBe("FORBIDDEN");
|
|
expect(classifyHttpError(httpError(405, REPO, {})).code).toBe("VALIDATION_ERROR");
|
|
expect(classifyHttpError(httpError(409, REPO, {})).code).toBe("VALIDATION_ERROR");
|
|
expect(classifyHttpError(httpError(422, REPO, {})).code).toBe("VALIDATION_ERROR");
|
|
expect(classifyHttpError(httpError(429, REPO, {})).code).toBe("RATE_LIMITED");
|
|
});
|
|
|
|
it("maps an unexpected status to UNKNOWN, with and without a body message", () => {
|
|
expect(classifyHttpError(httpError(500, REPO, { message: "kaboom" })).message).toContain(
|
|
"kaboom",
|
|
);
|
|
const bare = classifyHttpError(httpError(503, REPO, {}));
|
|
expect(bare.code).toBe("UNKNOWN");
|
|
expect(bare.message).toContain("503");
|
|
});
|
|
|
|
it("classifies a plain Error transport failure as UNKNOWN", () => {
|
|
const result = classifyHttpError(new Error("network down"));
|
|
expect(result.code).toBe("UNKNOWN");
|
|
expect(result.message).toContain("network down");
|
|
});
|
|
|
|
it("includes the underlying cause when a transport failure carries one", () => {
|
|
const error = new Error("fetch failed", { cause: new Error("ECONNREFUSED") });
|
|
const result = classifyHttpError(error);
|
|
expect(result.message).toContain("fetch failed");
|
|
expect(result.message).toContain("ECONNREFUSED");
|
|
});
|
|
|
|
it("stringifies a non-Error thrown value", () => {
|
|
const result = classifyHttpError("just a string");
|
|
expect(result.code).toBe("UNKNOWN");
|
|
expect(result.message).toContain("just a string");
|
|
});
|
|
});
|