Skip to content

Commit d79e542

Browse files
authored
test(navbar): guard display_name rendering against rename regressions (#34)
Mirrors the same regression guard added in criticalbit-web#23. The bug itself is upstream in criticalbit-auth-api#26 — this test pins the frontend contract (snake_case display_name from /auth/me, navbar renders displayName ?? email) so a rename or accidental swap to the SteamID64 user id cannot silently reintroduce the symptom here. Two supporting fixes piggy-back on this change: - Export the MSW server from src/test/setup.ts so tests can do per-test handler overrides via server.use(...). - Add VITE_API_URL=http://localhost/api to vitest.config.ts test.env. Without an absolute URL, ky/undici reject /api/... as unparseable and MSW never intercepts; auth.tsx's try/catch swallows the failure and tests appear to pass while every /auth/me silently 500s. Latent test-infra bug — fixing it is what made this guard meaningful.
1 parent b235d51 commit d79e542

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

src/components/navbar.test.tsx

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { screen } from "@testing-library/react"
2+
import { http, HttpResponse } from "msw"
3+
import { describe, expect, it } from "vitest"
4+
import { renderWithFileRoutes } from "@/test/renderers"
5+
import { server } from "@/test/setup"
6+
7+
// Regression guard for criticalbit-web#23 (recurring): the navbar must
8+
// render the human-readable display_name returned by /auth/me, never the
9+
// raw user id (which for Steam OAuth is the SteamID64). The actual root
10+
// cause of the linked bug lives in criticalbit-auth-api (#26) — but this
11+
// test pins the *frontend* contract so a rename like `display_name` →
12+
// `displayName`, a dropped setDisplayName call, or a navbar swap to
13+
// userId/email can't silently reintroduce the same visible regression
14+
// here.
15+
16+
const STEAM_ID = "76561198000000000"
17+
18+
const unauthContext = {
19+
auth: {
20+
isAuthenticated: false,
21+
isLoading: false,
22+
email: null,
23+
userId: null,
24+
displayName: null,
25+
avatarUrl: null,
26+
tosAcceptedAt: null,
27+
consents: null,
28+
login: () => {},
29+
logout: async () => {},
30+
checkAuth: async () => {},
31+
setConsents: () => {},
32+
},
33+
}
34+
35+
function authMeHandler(body: {
36+
id: string
37+
email: string
38+
display_name: string | null
39+
avatar_url: string | null
40+
tos_accepted_at: string | null
41+
}) {
42+
return http.get("*/auth/me", () => HttpResponse.json(body))
43+
}
44+
45+
// The consent fetch fires right after /auth/me succeeds. Stub it so the
46+
// AuthProvider's catch-all doesn't surface as an unhandled-request warning.
47+
const consentsHandler = http.get("*/user/consents", () =>
48+
HttpResponse.json({ current_policy_version: "test", consents: {} })
49+
)
50+
51+
describe("Navbar", () => {
52+
it("renders the Steam display_name, not the SteamID64", async () => {
53+
server.use(
54+
authMeHandler({
55+
id: STEAM_ID,
56+
email: "player@example.com",
57+
display_name: "ZeroEmpires",
58+
avatar_url: null,
59+
tos_accepted_at: null,
60+
}),
61+
consentsHandler
62+
)
63+
64+
await renderWithFileRoutes(<></>, {
65+
initialLocation: "/login",
66+
routerContext: unauthContext,
67+
})
68+
69+
expect(await screen.findByText("ZeroEmpires")).toBeInTheDocument()
70+
expect(screen.queryByText(STEAM_ID)).not.toBeInTheDocument()
71+
expect(screen.queryByText("player@example.com")).not.toBeInTheDocument()
72+
})
73+
74+
it("falls back to email when display_name is null", async () => {
75+
server.use(
76+
authMeHandler({
77+
id: STEAM_ID,
78+
email: "player@example.com",
79+
display_name: null,
80+
avatar_url: null,
81+
tos_accepted_at: null,
82+
}),
83+
consentsHandler
84+
)
85+
86+
await renderWithFileRoutes(<></>, {
87+
initialLocation: "/login",
88+
routerContext: unauthContext,
89+
})
90+
91+
expect(await screen.findByText("player@example.com")).toBeInTheDocument()
92+
expect(screen.queryByText(STEAM_ID)).not.toBeInTheDocument()
93+
})
94+
})

src/test/setup.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { cleanup } from "@testing-library/react"
44
import { setupServer } from "msw/node"
55
import { afterAll, afterEach, beforeAll, vi } from "vitest"
66

7-
const server = setupServer(...handlers)
7+
export const server = setupServer(...handlers)
88

99
beforeAll(() => server.listen())
1010
afterAll(() => server.close())

vitest.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export default defineConfig({
99
environment: "jsdom",
1010
env: {
1111
VITE_LOG_LEVEL: "warn",
12+
// ky/undici reject relative URLs as unparseable. Without an absolute
13+
// base, /auth/me requests in tests throw before reaching MSW and the
14+
// AuthProvider's catch swallows the failure — tests appear to pass
15+
// while every API call silently 500s.
16+
VITE_API_URL: "http://localhost/api",
1217
},
1318
setupFiles: ["./src/test/setup.ts"],
1419
include: ["src/**/*.{test,spec}.{ts,tsx}"],

0 commit comments

Comments
 (0)