WIP! A BB-style forum, on the ATmosphere!
We're still working... we'll be back soon when we have something to show off!
node
typescript
hono
htmx
atproto
1import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2import { logger } from "../../lib/logger.js";
3import { createAuthRoutes } from "../auth.js";
4
5vi.mock("../../lib/logger.js", () => ({
6 logger: {
7 debug: vi.fn(),
8 info: vi.fn(),
9 warn: vi.fn(),
10 error: vi.fn(),
11 fatal: vi.fn(),
12 },
13}));
14
15const mockFetch = vi.fn();
16
17describe("createAuthRoutes", () => {
18 beforeEach(() => {
19 vi.stubGlobal("fetch", mockFetch);
20 vi.stubEnv("APPVIEW_URL", "http://localhost:3000");
21 vi.mocked(logger.error).mockClear();
22 });
23
24 afterEach(() => {
25 vi.unstubAllGlobals();
26 vi.unstubAllEnvs();
27 mockFetch.mockReset();
28 });
29
30 function loadAuthRoutes() {
31 return createAuthRoutes("http://localhost:3000");
32 }
33
34 describe("POST /logout", () => {
35 it("calls AppView logout, clears cookie, and redirects to /", async () => {
36 mockFetch.mockResolvedValueOnce({
37 ok: true,
38 status: 200,
39 });
40
41 const authRoutes = await loadAuthRoutes();
42 const res = await authRoutes.request("/logout", {
43 method: "POST",
44 headers: { cookie: "atbb_session=user-token" },
45 });
46
47 expect(res.status).toBe(303);
48 expect(res.headers.get("location")).toBe("/");
49
50 // Verify AppView logout was called with the forwarded cookie
51 expect(mockFetch).toHaveBeenCalledOnce();
52 const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit];
53 expect(url).toBe("http://localhost:3000/api/auth/logout");
54 // Full Cookie header forwarded verbatim
55 expect((init.headers as Record<string, string>)["Cookie"]).toBe(
56 "atbb_session=user-token"
57 );
58 });
59
60 it("clears atbb_session cookie via Set-Cookie header", async () => {
61 mockFetch.mockResolvedValueOnce({ ok: true, status: 200 });
62
63 const authRoutes = await loadAuthRoutes();
64 const res = await authRoutes.request("/logout", { method: "POST" });
65
66 const setCookie = res.headers.get("set-cookie");
67 expect(setCookie).toContain("atbb_session=");
68 expect(setCookie).toContain("Max-Age=0");
69 });
70
71 it("still clears cookie and redirects even if AppView logout fails", async () => {
72 mockFetch.mockRejectedValueOnce(new Error("fetch failed: ECONNREFUSED"));
73
74 const authRoutes = await loadAuthRoutes();
75 const res = await authRoutes.request("/logout", { method: "POST" });
76
77 // Should still redirect home (graceful degradation)
78 expect(res.status).toBe(303);
79 expect(res.headers.get("location")).toBe("/");
80 const setCookie = res.headers.get("set-cookie");
81 expect(setCookie).toContain("Max-Age=0");
82 });
83
84 it("logs error when AppView logout returns non-ok status", async () => {
85 mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });
86
87 const authRoutes = await loadAuthRoutes();
88 const res = await authRoutes.request("/logout", { method: "POST" });
89
90 // Still redirects home despite non-ok response
91 expect(res.status).toBe(303);
92 expect(logger.error).toHaveBeenCalledWith(
93 expect.stringContaining("non-ok status"),
94 expect.objectContaining({ status: 500 })
95 );
96 });
97
98 it("re-throws programming errors (ReferenceError) without clearing the cookie", async () => {
99 mockFetch.mockRejectedValueOnce(new ReferenceError("fetch is not defined"));
100
101 const authRoutes = await loadAuthRoutes();
102 const res = await authRoutes.request("/logout", { method: "POST" });
103
104 // Programming error escapes the catch block — Hono's default handler returns 500
105 // rather than the expected 303 redirect, and the cookie is never cleared
106 expect(res.status).toBe(500);
107 expect(res.headers.get("set-cookie")).toBeNull();
108 });
109
110 it("logs error when AppView logout throws a network error", async () => {
111 mockFetch.mockRejectedValueOnce(new Error("fetch failed: ECONNREFUSED"));
112
113 const authRoutes = await loadAuthRoutes();
114 const res = await authRoutes.request("/logout", { method: "POST" });
115
116 expect(res.status).toBe(303);
117 expect(logger.error).toHaveBeenCalledWith(
118 expect.stringContaining("Failed to call AppView logout"),
119 expect.objectContaining({ error: expect.stringContaining("ECONNREFUSED") })
120 );
121 });
122 });
123});