Files
VRC-Circle/tests/errors.test.ts
T

71 lines
2.6 KiB
TypeScript
Raw Normal View History

2026-07-01 12:19:27 +07:00
import { describe, it, expect } from "vitest";
import { httpStatusOf, isTransientError, toApiError } from "../src/main/vrchat/errors";
describe("httpStatusOf", () => {
it("reads status from the many shapes vrchat/SDK errors take", () => {
expect(httpStatusOf({ status: 404 })).toBe(404);
expect(httpStatusOf({ statusCode: 401 })).toBe(401);
expect(httpStatusOf({ status_code: 500 })).toBe(500);
expect(httpStatusOf({ response: { status: 429 } })).toBe(429);
expect(httpStatusOf({ error: { status_code: 403 } })).toBe(403);
});
it("returns undefined for a shapeless error", () => {
expect(httpStatusOf(null)).toBeUndefined();
expect(httpStatusOf({})).toBeUndefined();
expect(httpStatusOf("boom")).toBeUndefined();
});
});
describe("isTransientError", () => {
it("treats 429 and 5xx as transient", () => {
expect(isTransientError({ status: 429 })).toBe(true);
expect(isTransientError({ status: 500 })).toBe(true);
expect(isTransientError({ status: 503 })).toBe(true);
});
it("treats 4xx (other than 429) as permanent", () => {
expect(isTransientError({ status: 404 })).toBe(false);
expect(isTransientError({ status: 401 })).toBe(false);
});
it("treats a status-less error (network failure) as transient", () => {
expect(isTransientError(new Error("ECONNRESET"))).toBe(true);
});
});
describe("toApiError", () => {
it("maps well-known statuses to codes", () => {
expect(toApiError({ status: 401 }).code).toBe("unauthorized");
expect(toApiError({ status: 404 }).code).toBe("not_found");
expect(toApiError({ status: 429 }).code).toBe("rate_limited");
});
it("classifies status-less network-ish messages as network", () => {
expect(toApiError({ message: "fetch failed: ENOTFOUND" }).code).toBe("network");
});
it("passes through a pre-coded error and its 2FA methods", () => {
const e = toApiError({ code: "two_factor_required", methods: ["totp"] });
expect(e.code).toBe("two_factor_required");
expect(e.methods).toEqual(["totp"]);
});
it("uses a friendly rate-limit message and parses retry-after", () => {
const e = toApiError({ status: 429, response: { headers: { "retry-after": "12" } } });
expect(e.code).toBe("rate_limited");
expect(e.retryAfter).toBe(12);
expect(e.message).toMatch(/rate limit/i);
});
it("gives a temporary-unavailable message for 5xx", () => {
expect(toApiError({ status: 502 }).message).toMatch(/temporarily unavailable/i);
});
it("falls back to unknown with the original message", () => {
const e = toApiError({ message: "weird" });
expect(e.code).toBe("unknown");
expect(e.message).toBe("weird");
});
});