@aiquants/auth-react-router 0.6.0 → 0.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,165 +0,0 @@
1
- /**
2
- * Tests for the session storage factory (cookie shape, backdoor port, guards).
3
- * セッションストレージファクトリのテスト (cookie 形状・バックドアポート・ガード)。
4
- */
5
- import type { SessionProfile } from "@aiquants/auth-core"
6
- import { describe, expect, it } from "vitest"
7
- import type { MockUserPort } from "./ports"
8
- import { createAuthSessionStorage } from "./session"
9
-
10
- const SECRET = "test-secret"
11
-
12
- const profile: SessionProfile = {
13
- id: "107791905418691074770",
14
- displayName: "宏治",
15
- name: { familyName: "Mock", givenName: "User" },
16
- emails: [{ value: "mock@example.com" }],
17
- accessToken: "at",
18
- refreshToken: "rt",
19
- expirationDateMs: 1750000000000,
20
- provider: "google",
21
- }
22
-
23
- const request = (cookie?: string) => new Request("http://localhost/", cookie ? { headers: { Cookie: cookie } } : undefined)
24
-
25
- describe("createAuthSessionStorage", () => {
26
- it("throws when secrets are missing (fail-closed)", () => {
27
- expect(() => createAuthSessionStorage({ secrets: [] })).toThrow("REMIX_SESSION_SECRET must be set")
28
- expect(() => createAuthSessionStorage({ secrets: [""] })).toThrow("REMIX_SESSION_SECRET must be set")
29
- })
30
-
31
- it("commits a byte-compatible __session cookie (httpOnly/lax/30d)", async () => {
32
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
33
- const cookie = await storage.saveSession(request(), profile)
34
-
35
- expect(cookie.startsWith("__session=")).toBe(true)
36
- expect(cookie).toContain("HttpOnly")
37
- expect(cookie).toContain("SameSite=Lax")
38
- expect(cookie).toContain(`Max-Age=${60 * 60 * 24 * 30}`)
39
- expect(cookie).toContain("Path=/")
40
- })
41
-
42
- it("roundtrips the session user under the 'user' key without extra fields", async () => {
43
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
44
- const cookie = await storage.saveSession(request(), profile)
45
- const user = await storage.getSessionUser(request(cookie))
46
-
47
- expect(user).toEqual(profile)
48
- // photo/_json/photos を含まない byte 互換形状
49
- expect(user && "photo" in user).toBe(false)
50
- })
51
-
52
- it("returns undefined without a session cookie", async () => {
53
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
54
- expect(await storage.getSessionUser(request())).toBeUndefined()
55
- })
56
-
57
- it("uses the injected mockUser port when enabled", async () => {
58
- const mockUser: MockUserPort = {
59
- enabled: (req) => req.headers.get("Cookie")?.includes("mock_user=true") ?? false,
60
- resolve: () => ({ ...profile, displayName: "太郎" }),
61
- }
62
- const storage = createAuthSessionStorage({ secrets: [SECRET] }, { mockUser })
63
-
64
- const mocked = await storage.getSessionUser(request("mock_user=true"))
65
- expect(mocked?.displayName).toBe("太郎")
66
- // バックドア条件を満たさないリクエストは通常経路 (cookie 無し → undefined)
67
- expect(await storage.getSessionUser(request())).toBeUndefined()
68
- })
69
-
70
- it("is backdoor-disabled by default (no port injected)", async () => {
71
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
72
- expect(await storage.getSessionUser(request("mock_user=true; mock_user_id=8"))).toBeUndefined()
73
- })
74
-
75
- it("requireUser redirects to the login path when unauthenticated", async () => {
76
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
77
- try {
78
- await storage.requireUser(request())
79
- expect.unreachable("should have thrown a redirect")
80
- } catch (error) {
81
- expect(error).toBeInstanceOf(Response)
82
- expect((error as Response).headers.get("Location")).toBe("/auth/login")
83
- }
84
- })
85
-
86
- it("requireAdminUser redirects non-admin users to the root", async () => {
87
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
88
- const cookie = await storage.saveSession(request(), profile)
89
- try {
90
- await storage.requireAdminUser(request(cookie))
91
- expect.unreachable("should have thrown a redirect")
92
- } catch (error) {
93
- expect(error).toBeInstanceOf(Response)
94
- expect((error as Response).headers.get("Location")).toBe("/")
95
- }
96
- })
97
-
98
- it("requireAdminUser passes admin users through", async () => {
99
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
100
- const cookie = await storage.saveSession(request(), { ...profile, role: "admin" })
101
- const user = await storage.requireAdminUser(request(cookie))
102
- expect(user.role).toBe("admin")
103
- })
104
-
105
- it("logout destroys the session cookie", async () => {
106
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
107
- const cookie = await storage.saveSession(request(), profile)
108
- const destroyed = await storage.logout(request(cookie))
109
- // 破棄 cookie は即時失効する
110
- expect(destroyed).toContain("__session=")
111
- expect(destroyed).toContain("Expires=Thu, 01 Jan 1970")
112
- })
113
- })
114
-
115
- /**
116
- * 無効化済みアカウントの即時失効。
117
- * `getSessionUser` は `requireUser` と `authenticateInLoader` の双方が通る唯一の絞り込み点であり、
118
- * ここで弾くことで既存セッション Cookie を持っていても全ルートで未認証扱いになる。
119
- */
120
- describe("createAuthSessionStorage — isUserActive による無効化の即時反映", () => {
121
- const profile = { id: "sub-1", displayName: "U", name: { familyName: "U", givenName: "U" }, emails: [{ value: "u@example.com" }] } as never
122
-
123
- /** セッション Cookie を持つリクエストを組み立てる。 */
124
- const requestWithSession = async (storage: ReturnType<typeof createAuthSessionStorage>) => {
125
- const cookie = await storage.saveSession(new Request("http://x/"), profile)
126
- return new Request("http://x/", { headers: { Cookie: cookie.split(";")[0] } })
127
- }
128
-
129
- it("有効なアカウントはこれまでどおりセッションから解決される", async () => {
130
- const storage = createAuthSessionStorage({ secrets: [SECRET] }, { isUserActive: () => true })
131
- expect(await storage.getSessionUser(await requestWithSession(storage))).toMatchObject({ id: "sub-1" })
132
- })
133
-
134
- it("無効化済みアカウントは有効な Cookie を持っていても未認証として扱われる", async () => {
135
- const storage = createAuthSessionStorage({ secrets: [SECRET] }, { isUserActive: () => false })
136
- expect(await storage.getSessionUser(await requestWithSession(storage))).toBeUndefined()
137
- })
138
-
139
- it("無効化済みなら requireUser はログインへリダイレクトする", async () => {
140
- const storage = createAuthSessionStorage({ secrets: [SECRET] }, { isUserActive: async () => false })
141
- const request = await requestWithSession(storage)
142
- await expect(storage.requireUser(request)).rejects.toBeInstanceOf(Response)
143
- })
144
-
145
- it("ポート未指定なら従来どおり (既存構成の挙動を変えない)", async () => {
146
- const storage = createAuthSessionStorage({ secrets: [SECRET] })
147
- expect(await storage.getSessionUser(await requestWithSession(storage))).toMatchObject({ id: "sub-1" })
148
- })
149
-
150
- it("ポートには解決したプロファイルが渡される", async () => {
151
- const seen: unknown[] = []
152
- const storage = createAuthSessionStorage(
153
- { secrets: [SECRET] },
154
- {
155
- isUserActive: (p) => {
156
- seen.push(p)
157
- return true
158
- },
159
- },
160
- )
161
- await storage.getSessionUser(await requestWithSession(storage))
162
- expect(seen).toHaveLength(1)
163
- expect(seen[0]).toMatchObject({ id: "sub-1" })
164
- })
165
- })