|
|
|
@@ -0,0 +1,71 @@
|
|
|
|
|
import { afterEach, expect, it, vi } from "vitest";
|
|
|
|
|
const auth = vi.hoisted(() => ({ session: vi.fn() }));
|
|
|
|
|
vi.mock("next-auth", () => ({ getServerSession: auth.session }));
|
|
|
|
|
vi.mock("@/lib/auth/admin-auth", () => ({ adminAuthOptions: {}, requiredAdminRole: "ops" }));
|
|
|
|
|
import { GET, POST } from "./route";
|
|
|
|
|
import { GET as detail } from "./[id]/route";
|
|
|
|
|
import { GET as messages } from "./[id]/messages/route";
|
|
|
|
|
const context = { params: Promise.resolve({ id: "100000000000000009" }) };
|
|
|
|
|
|
|
|
|
|
const request = () => new Request("https://portal.example/api/suggestions");
|
|
|
|
|
afterEach(() => { vi.resetAllMocks(); vi.unstubAllGlobals(); vi.unstubAllEnvs(); });
|
|
|
|
|
it("serves suggestions to the existing admin session using runtime env configuration", async () => {
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
|
|
|
vi.stubEnv("DISCORD_BOT_TOKEN", "test-token");
|
|
|
|
|
vi.stubEnv("DISCORD_GUILD_ID", "100000000000000001");
|
|
|
|
|
vi.stubEnv("DISCORD_SUGGESTIONS_FORUM_ID", "100000000000000002");
|
|
|
|
|
const fetcher = vi.fn().mockResolvedValueOnce(Response.json({ id: "100000000000000002", guild_id: "100000000000000001", type: 15, available_tags: [] })).mockResolvedValueOnce(Response.json({ threads: [] }));
|
|
|
|
|
vi.stubGlobal("fetch", fetcher);
|
|
|
|
|
const response = await GET(request());
|
|
|
|
|
expect(response.status).toBe(200);
|
|
|
|
|
expect(await response.json()).toEqual({ items: [], nextCursor: null });
|
|
|
|
|
expect(response.headers.get("cache-control")).toBe("no-store");
|
|
|
|
|
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
|
|
|
});
|
|
|
|
|
it.each([detail, messages])("independently protects detail and message routes", async (handler) => {
|
|
|
|
|
const fetcher = vi.fn();
|
|
|
|
|
vi.stubGlobal("fetch", fetcher);
|
|
|
|
|
auth.session.mockResolvedValue(null);
|
|
|
|
|
expect((await handler(request(), context)).status).toBe(401);
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["player"] } });
|
|
|
|
|
expect((await handler(request(), context)).status).toBe(403);
|
|
|
|
|
expect(fetcher).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
it.each(["?limit=0", "?limit=101", "?limit=1.2", "?limit=", "?limit=1&limit=2", "?channel=100000000000000099", "?status=all", "?cursor=../secret"])('rejects invalid query %s without contacting Discord', async (query) => {
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
|
|
|
const fetcher = vi.fn();
|
|
|
|
|
vi.stubGlobal("fetch", fetcher);
|
|
|
|
|
const response = await GET(new Request(`https://portal.example/api/suggestions${query}`));
|
|
|
|
|
expect(response.status).toBe(400);
|
|
|
|
|
expect(fetcher).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
it("returns a read-only problem for writes", async () => {
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
|
|
|
const response = await POST(request());
|
|
|
|
|
expect(response.status).toBe(405);
|
|
|
|
|
expect(response.headers.get("allow")).toBe("GET, HEAD");
|
|
|
|
|
});
|
|
|
|
|
it("reports missing configuration without exposing environment values", async () => {
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["ops"] } });
|
|
|
|
|
vi.stubEnv("DISCORD_SUGGESTIONS_FORUM_ID", "");
|
|
|
|
|
const response = await GET(request());
|
|
|
|
|
expect(response.status).toBe(503);
|
|
|
|
|
expect(await response.json()).toMatchObject({ type: "urn:error:suggestions-not-configured", status: 503, instance: "/api/suggestions" });
|
|
|
|
|
});
|
|
|
|
|
it("returns sanitized problems for unexpected failures", async () => {
|
|
|
|
|
auth.session.mockRejectedValue(new Error("private session details"));
|
|
|
|
|
const response = await GET(request());
|
|
|
|
|
expect(response.status).toBe(503);
|
|
|
|
|
expect(await response.text()).not.toContain("private session details");
|
|
|
|
|
});
|
|
|
|
|
it("rejects a signed-in user without the required admin role", async () => {
|
|
|
|
|
auth.session.mockResolvedValue({ user: { roles: ["player"] } });
|
|
|
|
|
expect((await GET(request())).status).toBe(403);
|
|
|
|
|
});
|
|
|
|
|
it("rejects unauthenticated readers with a JSON problem instead of a redirect", async () => {
|
|
|
|
|
auth.session.mockResolvedValue(null);
|
|
|
|
|
const response = await GET(request());
|
|
|
|
|
expect(response.status).toBe(401);
|
|
|
|
|
expect(response.headers.get("content-type")).toBe("application/problem+json");
|
|
|
|
|
expect(response.headers.get("location")).toBeNull();
|
|
|
|
|
});
|