Files
ntfy/web/src/app/errors.test.js
T
2026-06-24 05:18:22 -04:00

94 lines
3.2 KiB
JavaScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
UnauthorizedError,
UserExistsError,
TopicReservedError,
AccountActionLimitReachedError,
IncorrectPasswordError,
EmailVerificationCodeInvalidError,
EmailPrimaryElsewhereError,
throwAppError,
fetchOrThrow,
} from "./errors";
// A minimal stand-in for a fetch Response: just the bits errors.js reads.
const fakeResponse = (status, body) => ({
status,
json: async () => {
if (body === undefined) throw new Error("no body");
return body;
},
});
beforeEach(() => {
// errors.js logs to console.log on every error path; keep test output clean.
vi.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("error classes", () => {
it("carry the server error codes from errors.go", () => {
expect(UserExistsError.CODE).toBe(40901);
expect(TopicReservedError.CODE).toBe(40902);
expect(AccountActionLimitReachedError.CODE).toBe(42906);
expect(IncorrectPasswordError.CODE).toBe(40026);
expect(EmailVerificationCodeInvalidError.CODE).toBe(40051);
expect(EmailPrimaryElsewhereError.CODE).toBe(40908);
});
it("are Error subclasses with human-readable messages", () => {
expect(new UnauthorizedError()).toBeInstanceOf(Error);
expect(new UnauthorizedError().message).toBe("Unauthorized");
expect(new UserExistsError().message).toBe("Username already exists");
});
});
describe("throwAppError", () => {
it("maps 401 and 403 to UnauthorizedError", async () => {
await expect(throwAppError(fakeResponse(401))).rejects.toBeInstanceOf(UnauthorizedError);
await expect(throwAppError(fakeResponse(403))).rejects.toBeInstanceOf(UnauthorizedError);
});
it("maps known ntfy error codes to their specific error classes", async () => {
await expect(throwAppError(fakeResponse(409, { code: UserExistsError.CODE }))).rejects.toBeInstanceOf(UserExistsError);
await expect(throwAppError(fakeResponse(409, { code: TopicReservedError.CODE }))).rejects.toBeInstanceOf(TopicReservedError);
await expect(throwAppError(fakeResponse(429, { code: AccountActionLimitReachedError.CODE }))).rejects.toBeInstanceOf(
AccountActionLimitReachedError
);
});
it("wraps an unknown code that carries an error message in a generic Error", async () => {
await expect(throwAppError(fakeResponse(400, { code: 12345, error: "boom" }))).rejects.toThrow("Error 12345: boom");
});
it("throws a generic 'Unexpected response' error when there is no ntfy error body", async () => {
await expect(throwAppError(fakeResponse(500))).rejects.toThrow("Unexpected response 500");
});
});
describe("fetchOrThrow", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("returns the response unchanged on HTTP 200", async () => {
const response = { status: 200 };
vi.stubGlobal(
"fetch",
vi.fn(async () => response)
);
await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).resolves.toBe(response);
});
it("throws on a non-200 response", async () => {
vi.stubGlobal(
"fetch",
vi.fn(async () => fakeResponse(401))
);
await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).rejects.toBeInstanceOf(UnauthorizedError);
});
});