File
Blob: tests/client/stores/auth-store.dom.test.ts
| 1 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; |
| 2 | import { createUser } from "@tests/client/util/fixtures"; |
| 3 | import { SESSION_MODES, STORAGE_KEYS } from "@/client/lib/constants"; |
| 4 | |
| 5 | let useAuthStore: typeof import("@/client/stores/auth-store").useAuthStore; |
| 6 | let selectIsAuthenticated: typeof import("@/client/stores/auth-store").selectIsAuthenticated; |
| 7 | let selectHasLocalSession: typeof import("@/client/stores/auth-store").selectHasLocalSession; |
| 8 | |
| 9 | beforeEach(async () => { |
| 10 | localStorage.clear(); |
| 11 | vi.resetModules(); |
| 12 | const mod = await import("@/client/stores/auth-store"); |
| 13 | useAuthStore = mod.useAuthStore; |
| 14 | selectIsAuthenticated = mod.selectIsAuthenticated; |
| 15 | selectHasLocalSession = mod.selectHasLocalSession; |
| 16 | }); |
| 17 | |
| 18 | afterEach(() => { |
| 19 | vi.restoreAllMocks(); |
| 20 | localStorage.clear(); |
| 21 | }); |
| 22 | |
| 23 | describe("auth-store", () => { |
| 24 | describe("initial state", () => { |
| 25 | it("starts with ANONYMOUS session mode and no auth", () => { |
| 26 | const state = useAuthStore.getState(); |
| 27 | expect(state.sessionMode).toBe(SESSION_MODES.ANONYMOUS); |
| 28 | expect(state.accessToken).toBeNull(); |
| 29 | expect(state.refreshState).toBe("idle"); |
| 30 | expect(selectIsAuthenticated(state)).toBe(false); |
| 31 | expect(selectHasLocalSession(state)).toBe(false); |
| 32 | }); |
| 33 | |
| 34 | it("rehydrates cached user from localStorage", async () => { |
| 35 | const user = createUser(); |
| 36 | localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify({ version: 1, value: user })); |
| 37 | vi.resetModules(); |
| 38 | const mod = await import("@/client/stores/auth-store"); |
| 39 | expect(mod.useAuthStore.getState().user).toEqual(user); |
| 40 | expect(mod.useAuthStore.getState().sessionMode).toBe(SESSION_MODES.LOCAL_ONLY); |
| 41 | }); |
| 42 | |
| 43 | it("handles malformed user JSON gracefully", async () => { |
| 44 | localStorage.setItem(STORAGE_KEYS.USER, "{bad-json"); |
| 45 | vi.resetModules(); |
| 46 | const mod = await import("@/client/stores/auth-store"); |
| 47 | expect(mod.useAuthStore.getState().user).toBeNull(); |
| 48 | }); |
| 49 | }); |
| 50 | |
| 51 | describe("setAuth", () => { |
| 52 | it("sets AUTHENTICATED mode with correct derived flags", () => { |
| 53 | const user = createUser(); |
| 54 | useAuthStore.getState().setAuth("tok-123", user); |
| 55 | |
| 56 | const state = useAuthStore.getState(); |
| 57 | expect(state.accessToken).toBe("tok-123"); |
| 58 | expect(state.user).toEqual(user); |
| 59 | expect(state.sessionMode).toBe(SESSION_MODES.AUTHENTICATED); |
| 60 | expect(selectIsAuthenticated(state)).toBe(true); |
| 61 | expect(selectHasLocalSession(state)).toBe(true); |
| 62 | }); |
| 63 | |
| 64 | it("persists user to localStorage", () => { |
| 65 | const user = createUser(); |
| 66 | useAuthStore.getState().setAuth("tok-123", user); |
| 67 | expect(JSON.parse(localStorage.getItem(STORAGE_KEYS.USER)!)).toEqual({ version: 1, value: user }); |
| 68 | }); |
| 69 | |
| 70 | it("does not throw when localStorage writes fail", () => { |
| 71 | const user = createUser(); |
| 72 | vi.spyOn(localStorage, "setItem").mockImplementation(() => { |
| 73 | throw new Error("quota"); |
| 74 | }); |
| 75 | expect(() => useAuthStore.getState().setAuth("tok-123", user)).not.toThrow(); |
| 76 | }); |
| 77 | }); |
| 78 | |
| 79 | describe("markExpired", () => { |
| 80 | it("clears token but retains user", () => { |
| 81 | const user = createUser(); |
| 82 | useAuthStore.getState().setAuth("tok-123", user); |
| 83 | useAuthStore.getState().markExpired(); |
| 84 | |
| 85 | const state = useAuthStore.getState(); |
| 86 | expect(state.accessToken).toBeNull(); |
| 87 | expect(state.user).toEqual(user); |
| 88 | expect(state.sessionMode).toBe(SESSION_MODES.EXPIRED); |
| 89 | expect(selectIsAuthenticated(state)).toBe(false); |
| 90 | expect(selectHasLocalSession(state)).toBe(true); |
| 91 | }); |
| 92 | }); |
| 93 | |
| 94 | describe("markLocalOnly", () => { |
| 95 | it("clears token but keeps the cached user for local-only flows", () => { |
| 96 | const user = createUser(); |
| 97 | useAuthStore.getState().setAuth("tok-123", user); |
| 98 | useAuthStore.getState().markLocalOnly(); |
| 99 | |
| 100 | const state = useAuthStore.getState(); |
| 101 | expect(state.accessToken).toBeNull(); |
| 102 | expect(state.user).toEqual(user); |
| 103 | expect(state.sessionMode).toBe(SESSION_MODES.LOCAL_ONLY); |
| 104 | expect(selectIsAuthenticated(state)).toBe(false); |
| 105 | expect(selectHasLocalSession(state)).toBe(true); |
| 106 | }); |
| 107 | }); |
| 108 | |
| 109 | describe("clearAuth", () => { |
| 110 | it("resets to ANONYMOUS and removes all persisted data", () => { |
| 111 | const user = createUser(); |
| 112 | useAuthStore.getState().setAuth("tok-123", user); |
| 113 | useAuthStore.getState().clearAuth(); |
| 114 | |
| 115 | const state = useAuthStore.getState(); |
| 116 | expect(state.accessToken).toBeNull(); |
| 117 | expect(state.user).toBeNull(); |
| 118 | expect(state.sessionMode).toBe(SESSION_MODES.ANONYMOUS); |
| 119 | expect(selectIsAuthenticated(state)).toBe(false); |
| 120 | expect(selectHasLocalSession(state)).toBe(false); |
| 121 | expect(localStorage.getItem(STORAGE_KEYS.USER)).toBeNull(); |
| 122 | expect(localStorage.getItem(STORAGE_KEYS.CACHED_DOCS)).toBeNull(); |
| 123 | }); |
| 124 | }); |
| 125 | |
| 126 | describe("setSessionMode", () => { |
| 127 | it.each([ |
| 128 | [SESSION_MODES.AUTHENTICATED, true, true], |
| 129 | [SESSION_MODES.LOCAL_ONLY, false, true], |
| 130 | [SESSION_MODES.EXPIRED, false, true], |
| 131 | [SESSION_MODES.ANONYMOUS, false, false], |
| 132 | ] as const)("mode %s -> isAuthenticated=%s, hasLocalSession=%s", (mode, expectAuth, expectLocal) => { |
| 133 | useAuthStore.getState().setSessionMode(mode); |
| 134 | |
| 135 | const state = useAuthStore.getState(); |
| 136 | expect(state.sessionMode).toBe(mode); |
| 137 | expect(selectIsAuthenticated(state)).toBe(expectAuth); |
| 138 | expect(selectHasLocalSession(state)).toBe(expectLocal); |
| 139 | }); |
| 140 | }); |
| 141 | |
| 142 | describe("setRefreshState", () => { |
| 143 | it("tracks the non-persisted refresh lifecycle", () => { |
| 144 | useAuthStore.getState().setRefreshState("refreshing"); |
| 145 | expect(useAuthStore.getState().refreshState).toBe("refreshing"); |
| 146 | useAuthStore.getState().setRefreshState("idle"); |
| 147 | expect(useAuthStore.getState().refreshState).toBe("idle"); |
| 148 | }); |
| 149 | }); |
| 150 | }); |