@aiquants/auth-react-router 0.4.1 → 0.6.0
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.
- package/README.md +39 -1
- package/dist/admin.d.mts +267 -0
- package/dist/admin.d.ts +267 -0
- package/dist/admin.js +2 -0
- package/dist/admin.js.map +1 -0
- package/dist/admin.mjs +2 -0
- package/dist/admin.mjs.map +1 -0
- package/dist/client.d.mts +2 -2
- package/dist/client.d.ts +2 -2
- package/dist/server.d.mts +23 -4
- package/dist/server.d.ts +23 -4
- package/dist/server.js +1 -1
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1 -1
- package/dist/server.mjs.map +1 -1
- package/dist/styles/auth-react-router.standalone.css +1 -1
- package/package.json +10 -3
- package/src/admin/app.ts +4 -0
- package/src/admin/labels.ts +274 -0
- package/src/admin/server.spec.ts +300 -0
- package/src/admin/server.ts +354 -0
- package/src/admin/ui.tsx +121 -0
- package/src/admin/views.spec.tsx +307 -0
- package/src/admin/views.tsx +696 -0
- package/src/admin.ts +5 -0
- package/src/server/auth-server.ts +1 -0
- package/src/server/google-strategy.spec.ts +56 -0
- package/src/server/google-strategy.ts +10 -1
- package/src/server/ports.ts +22 -4
- package/src/server/session.spec.ts +52 -0
- package/src/server/session.ts +7 -2
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|
2
|
+
import { createRoutesStub } from "react-router"
|
|
3
|
+
import { describe, expect, it, vi } from "vitest"
|
|
4
|
+
import type { UserAdminLabels } from "./labels"
|
|
5
|
+
import { defaultUserAdminLabels, resolveUserAdminLabels } from "./labels"
|
|
6
|
+
import { createInMemoryUserAdminStore } from "./server"
|
|
7
|
+
import { UserAdminShell } from "./ui"
|
|
8
|
+
import { AllowlistView, AuthUserAdminAppView, GroupMembersView, GroupsView, UsersView } from "./views"
|
|
9
|
+
|
|
10
|
+
/** 1 回のフォーム送信内容(intent + 各フィールド)。 */
|
|
11
|
+
type Submission = Record<string, string>
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mounts a view under a route stub and records every form submission it makes.
|
|
15
|
+
* ビューをルートスタブ配下にマウントし、送信された全フォーム内容を記録する処理。
|
|
16
|
+
*
|
|
17
|
+
* 記録配列を返すことで「クリックしたら何が送信されたか」をテストで検証できる
|
|
18
|
+
* (送信内容を観測しないとハンドラを壊しても気づけないため)。
|
|
19
|
+
*/
|
|
20
|
+
function mount(path: string, Component: React.ComponentType<Record<string, unknown>>, loader: () => unknown, props = {}): Submission[] {
|
|
21
|
+
const submissions: Submission[] = []
|
|
22
|
+
const action = async ({ request }: { request: Request }) => {
|
|
23
|
+
const fd = await request.formData()
|
|
24
|
+
const entry: Submission = {}
|
|
25
|
+
for (const [k, v] of fd.entries()) entry[k] = String(v)
|
|
26
|
+
submissions.push(entry)
|
|
27
|
+
return { ok: true }
|
|
28
|
+
}
|
|
29
|
+
const Stub = createRoutesStub([{ path: "/users/*", Component: () => <Component {...props} />, loader, action }])
|
|
30
|
+
render(<Stub initialEntries={[path]} />)
|
|
31
|
+
return submissions
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("UserAdminShell & UI Components", () => {
|
|
35
|
+
it("renders default header when renderHeader is omitted and supports tab counts", async () => {
|
|
36
|
+
const customLabels = resolveUserAdminLabels({ title: "カスタム管理", heading: "カスタム注釈", tabs: { users: "ユーザー", groups: "グループ", groupMembers: "所属", allowlist: "許可" } })
|
|
37
|
+
mount(
|
|
38
|
+
"/users",
|
|
39
|
+
() => (
|
|
40
|
+
<UserAdminShell
|
|
41
|
+
title="カスタム管理"
|
|
42
|
+
annotation={<span>カスタム注釈</span>}
|
|
43
|
+
activeTab="users"
|
|
44
|
+
onTabChange={() => {}}
|
|
45
|
+
tabs={[
|
|
46
|
+
{ id: "users", label: "ユーザー", count: 5 },
|
|
47
|
+
{ id: "groups", label: "グループ" },
|
|
48
|
+
]}>
|
|
49
|
+
<div>コンテンツ</div>
|
|
50
|
+
</UserAdminShell>
|
|
51
|
+
),
|
|
52
|
+
() => ({ segment: "users", labels: customLabels }),
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(await screen.findByText("カスタム管理")).toBeDefined()
|
|
56
|
+
expect(screen.getByText("カスタム注釈")).toBeDefined()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("AuthUserAdminAppView & Views Coverage Suite", () => {
|
|
61
|
+
it("renders AuthUserAdminAppView with custom renderHeader prop and custom labels", async () => {
|
|
62
|
+
const store = createInMemoryUserAdminStore()
|
|
63
|
+
const customLabels: UserAdminLabels = {
|
|
64
|
+
title: "ユーザー & 組織管理",
|
|
65
|
+
annotation: "各種設定",
|
|
66
|
+
tabs: { users: "ユーザー", groups: "グループ", groupMembers: "所属", allowlist: "許可リスト" },
|
|
67
|
+
usersView: { title: "U", addUser: "追加", editUser: "編集", searchPlaceholder: "検索...", displayName: "名前", email: "メール", status: "状態", active: "有効", inactive: "無効", actions: "操作" },
|
|
68
|
+
groupsView: { title: "G", addGroup: "グループ追加", editGroup: "グループ編集", groupKey: "キー", name: "名前", description: "説明", membersCount: "名", actions: "操作" },
|
|
69
|
+
groupMembersView: { title: "GM", selectGroup: "選択", currentMembers: "メンバー", member: "名", nonMember: "非", actionAdd: "追加", actionRemove: "除外" },
|
|
70
|
+
allowlistView: { title: "AL", addEntry: "パターン追加", pattern: "パターン", type: "種別", description: "説明", actions: "操作" },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const data = {
|
|
74
|
+
segment: "users",
|
|
75
|
+
users: await store.listUsers(),
|
|
76
|
+
groups: await store.listGroups(),
|
|
77
|
+
groupMembers: await store.listGroupMembers(),
|
|
78
|
+
allowlist: await store.listAllowlist(),
|
|
79
|
+
labels: customLabels,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
mount("/users", AuthUserAdminAppView, () => data, {
|
|
83
|
+
renderHeader: ({ title }: { title: string }) => <h1 data-testid="custom-header-heading">{title} (Custom)</h1>,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
expect(await screen.findByTestId("custom-header-heading")).toBeDefined()
|
|
87
|
+
expect(screen.getByTestId("user-admin-shell")).toBeDefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("handles user create and edit modals, search filtering, and active toggle in UsersView", async () => {
|
|
91
|
+
const store = createInMemoryUserAdminStore({
|
|
92
|
+
users: [
|
|
93
|
+
{ id: 1, email: "alpha@example.com", displayName: "Alpha User", isActive: true },
|
|
94
|
+
{ id: 2, email: "beta@example.com", displayName: "Beta User", isActive: false },
|
|
95
|
+
],
|
|
96
|
+
})
|
|
97
|
+
const data = {
|
|
98
|
+
segment: "users",
|
|
99
|
+
users: await store.listUsers(),
|
|
100
|
+
groups: [],
|
|
101
|
+
groupMembers: [],
|
|
102
|
+
allowlist: [],
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
mount("/users", UsersView, () => data)
|
|
106
|
+
|
|
107
|
+
expect(await screen.findByTestId("users-view-section")).toBeDefined()
|
|
108
|
+
|
|
109
|
+
// Test search filter
|
|
110
|
+
const searchInput = screen.getByTestId("users-search-input")
|
|
111
|
+
fireEvent.change(searchInput, { target: { value: "Alpha" } })
|
|
112
|
+
expect(screen.getByTestId("user-row-1")).toBeDefined()
|
|
113
|
+
expect(screen.queryByTestId("user-row-2")).toBeNull()
|
|
114
|
+
|
|
115
|
+
// Clear search filter
|
|
116
|
+
fireEvent.change(searchInput, { target: { value: "" } })
|
|
117
|
+
expect(screen.getByTestId("user-row-2")).toBeDefined()
|
|
118
|
+
|
|
119
|
+
// Open create modal & test cancel
|
|
120
|
+
const addBtn = screen.getByTestId("users-add-button")
|
|
121
|
+
fireEvent.click(addBtn)
|
|
122
|
+
expect(screen.getByTestId("user-create-modal")).toBeDefined()
|
|
123
|
+
|
|
124
|
+
// Empty submit guard
|
|
125
|
+
const createForm = screen.getByTestId("user-create-modal").querySelector("form")
|
|
126
|
+
if (createForm) fireEvent.submit(createForm)
|
|
127
|
+
|
|
128
|
+
const cancelCreateBtn = screen.getByText("キャンセル")
|
|
129
|
+
fireEvent.click(cancelCreateBtn)
|
|
130
|
+
expect(screen.queryByTestId("user-create-modal")).toBeNull()
|
|
131
|
+
|
|
132
|
+
// Re-open create modal & submit
|
|
133
|
+
fireEvent.click(addBtn)
|
|
134
|
+
const createName = screen.getByTestId("create-user-display-name")
|
|
135
|
+
const createEmail = screen.getByTestId("create-user-email")
|
|
136
|
+
fireEvent.change(createName, { target: { value: "New User" } })
|
|
137
|
+
fireEvent.change(createEmail, { target: { value: "new@example.com" } })
|
|
138
|
+
|
|
139
|
+
const submitCreate = screen.getByTestId("submit-create-user")
|
|
140
|
+
fireEvent.click(submitCreate)
|
|
141
|
+
|
|
142
|
+
// Open edit modal & test cancel
|
|
143
|
+
const editBtn = screen.getByTestId("edit-user-btn-1")
|
|
144
|
+
fireEvent.click(editBtn)
|
|
145
|
+
expect(screen.getByTestId("user-edit-modal")).toBeDefined()
|
|
146
|
+
|
|
147
|
+
const cancelEditBtn = screen.getByTestId("user-edit-modal").querySelector('button[type="button"]')
|
|
148
|
+
if (cancelEditBtn) fireEvent.click(cancelEditBtn)
|
|
149
|
+
expect(screen.queryByTestId("user-edit-modal")).toBeNull()
|
|
150
|
+
|
|
151
|
+
// Re-open edit modal, test empty submit guard, fill & submit
|
|
152
|
+
fireEvent.click(editBtn)
|
|
153
|
+
const editNameInput = screen.getByTestId("edit-user-display-name")
|
|
154
|
+
fireEvent.change(editNameInput, { target: { value: "" } })
|
|
155
|
+
fireEvent.click(screen.getByTestId("submit-edit-user"))
|
|
156
|
+
|
|
157
|
+
fireEvent.change(editNameInput, { target: { value: "Alpha Updated" } })
|
|
158
|
+
fireEvent.click(screen.getByTestId("submit-edit-user"))
|
|
159
|
+
|
|
160
|
+
// Toggle active status
|
|
161
|
+
const toggleBtn = screen.getByTestId("toggle-user-btn-1")
|
|
162
|
+
fireEvent.click(toggleBtn)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("handles group create, edit modals, and delete confirmations in GroupsView", async () => {
|
|
166
|
+
const store = createInMemoryUserAdminStore({
|
|
167
|
+
groups: [{ id: 1, groupKey: "dev-team", name: "開発チーム", description: "開発用", isActive: true }],
|
|
168
|
+
})
|
|
169
|
+
const data = {
|
|
170
|
+
segment: "groups",
|
|
171
|
+
users: [],
|
|
172
|
+
groups: await store.listGroups(),
|
|
173
|
+
groupMembers: [],
|
|
174
|
+
allowlist: [],
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const submissions = mount("/users/groups", GroupsView, () => data)
|
|
178
|
+
|
|
179
|
+
expect(await screen.findByTestId("groups-view-section")).toBeDefined()
|
|
180
|
+
|
|
181
|
+
// Create group modal open & test cancel
|
|
182
|
+
const addBtn = screen.getByTestId("groups-add-button")
|
|
183
|
+
fireEvent.click(addBtn)
|
|
184
|
+
expect(screen.getByTestId("group-create-modal")).toBeDefined()
|
|
185
|
+
|
|
186
|
+
// Empty submit guard
|
|
187
|
+
const groupForm = screen.getByTestId("group-create-modal").querySelector("form")
|
|
188
|
+
if (groupForm) fireEvent.submit(groupForm)
|
|
189
|
+
|
|
190
|
+
const cancelGroupCreateBtn = screen.getByTestId("group-create-modal").querySelector('button[type="button"]')
|
|
191
|
+
if (cancelGroupCreateBtn) fireEvent.click(cancelGroupCreateBtn)
|
|
192
|
+
expect(screen.queryByTestId("group-create-modal")).toBeNull()
|
|
193
|
+
|
|
194
|
+
// Re-open create group, fill & submit
|
|
195
|
+
fireEvent.click(addBtn)
|
|
196
|
+
fireEvent.change(screen.getByTestId("create-group-key"), { target: { value: "design" } })
|
|
197
|
+
fireEvent.change(screen.getByTestId("create-group-name"), { target: { value: "デザインチーム" } })
|
|
198
|
+
fireEvent.change(screen.getByTestId("create-group-description"), { target: { value: "UI/UX" } })
|
|
199
|
+
|
|
200
|
+
const newGroupForm = screen.getByTestId("group-create-modal").querySelector("form")
|
|
201
|
+
if (newGroupForm) fireEvent.submit(newGroupForm)
|
|
202
|
+
|
|
203
|
+
// Edit group modal open & test cancel
|
|
204
|
+
fireEvent.click(screen.getByTestId("edit-group-btn-1"))
|
|
205
|
+
expect(screen.getByTestId("group-edit-modal")).toBeDefined()
|
|
206
|
+
|
|
207
|
+
const cancelGroupEditBtn = screen.getByTestId("group-edit-modal").querySelector('button[type="button"]')
|
|
208
|
+
if (cancelGroupEditBtn) fireEvent.click(cancelGroupEditBtn)
|
|
209
|
+
expect(screen.queryByTestId("group-edit-modal")).toBeNull()
|
|
210
|
+
|
|
211
|
+
// Re-open edit group, fill & submit (including empty name submit guard)
|
|
212
|
+
fireEvent.click(screen.getByTestId("edit-group-btn-1"))
|
|
213
|
+
const editGroupName = screen.getByTestId("edit-group-name")
|
|
214
|
+
fireEvent.change(editGroupName, { target: { value: "" } })
|
|
215
|
+
fireEvent.click(screen.getByTestId("submit-edit-group"))
|
|
216
|
+
|
|
217
|
+
fireEvent.change(editGroupName, { target: { value: "開発・インフラチーム" } })
|
|
218
|
+
fireEvent.change(screen.getByTestId("edit-group-description"), { target: { value: "Dev & Ops" } })
|
|
219
|
+
fireEvent.click(screen.getByTestId("submit-edit-group"))
|
|
220
|
+
|
|
221
|
+
// 削除確認ダイアログ: キャンセル → 送信されない / OK → 送信される。
|
|
222
|
+
// 「キャンセル後の非送信」を単独で待つと非同期送信が届く前に真になってしまうため、
|
|
223
|
+
// キャンセル→OK と続けて操作し、最終的な deleteGroup 送信が **1 回だけ** であることで
|
|
224
|
+
// ガードの有無を判定する (ガードを外すと 2 回になり落ちる)。
|
|
225
|
+
const confirmSpy = vi.spyOn(window, "confirm")
|
|
226
|
+
confirmSpy.mockReturnValueOnce(false)
|
|
227
|
+
fireEvent.click(screen.getByTestId("delete-group-btn-1"))
|
|
228
|
+
confirmSpy.mockReturnValueOnce(true)
|
|
229
|
+
fireEvent.click(screen.getByTestId("delete-group-btn-1"))
|
|
230
|
+
|
|
231
|
+
await waitFor(() => expect(submissions.filter((s) => s.intent === "deleteGroup")).toHaveLength(1))
|
|
232
|
+
expect(submissions.find((s) => s.intent === "deleteGroup")).toEqual(expect.objectContaining({ intent: "deleteGroup", groupId: "1" }))
|
|
233
|
+
confirmSpy.mockRestore()
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("handles GroupMembersView toggle buttons and empty group list edge case", async () => {
|
|
237
|
+
const store = createInMemoryUserAdminStore({
|
|
238
|
+
users: [{ id: 1, email: "u1@example.com", displayName: "User One", isActive: true }],
|
|
239
|
+
groups: [{ id: 10, groupKey: "team-a", name: "Team A", isActive: true }],
|
|
240
|
+
groupMembers: [{ id: 100, groupId: 10, userId: 1 }],
|
|
241
|
+
})
|
|
242
|
+
const data = {
|
|
243
|
+
segment: "group_members",
|
|
244
|
+
users: await store.listUsers(),
|
|
245
|
+
groups: await store.listGroups(),
|
|
246
|
+
groupMembers: await store.listGroupMembers(),
|
|
247
|
+
allowlist: [],
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const submissions = mount("/users/group_members", GroupMembersView, () => data)
|
|
251
|
+
|
|
252
|
+
expect(await screen.findByTestId("group-members-view-section")).toBeDefined()
|
|
253
|
+
|
|
254
|
+
// Select group
|
|
255
|
+
fireEvent.click(screen.getByTestId("select-group-btn-10"))
|
|
256
|
+
|
|
257
|
+
// Toggle member removal → 既存メンバーなので removeGroupMember が送信されること
|
|
258
|
+
fireEvent.click(screen.getByTestId("toggle-member-btn-1"))
|
|
259
|
+
await waitFor(() => expect(submissions.at(-1)).toEqual(expect.objectContaining({ intent: "removeGroupMember", groupId: "10", userId: "1" })))
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it("renders empty group state message in GroupMembersView when no groups exist", async () => {
|
|
263
|
+
const data = {
|
|
264
|
+
segment: "group_members",
|
|
265
|
+
users: [],
|
|
266
|
+
groups: [],
|
|
267
|
+
groupMembers: [],
|
|
268
|
+
allowlist: [],
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
mount("/users/group_members", GroupMembersView, () => data)
|
|
272
|
+
|
|
273
|
+
expect(await screen.findByTestId("group-members-view-section")).toBeDefined()
|
|
274
|
+
expect(screen.getByText(defaultUserAdminLabels.groupMembersView.selectPrompt)).toBeDefined()
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
it("handles AllowlistView form inputs, submit guards, and entry deletion", async () => {
|
|
278
|
+
const store = createInMemoryUserAdminStore({
|
|
279
|
+
allowlist: [{ id: 1, pattern: "@company.com", type: "domain", description: "Company Domain" }],
|
|
280
|
+
})
|
|
281
|
+
const data = {
|
|
282
|
+
segment: "allowlist",
|
|
283
|
+
users: [],
|
|
284
|
+
groups: [],
|
|
285
|
+
groupMembers: [],
|
|
286
|
+
allowlist: await store.listAllowlist(),
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
mount("/users/allowlist", AllowlistView, () => data)
|
|
290
|
+
|
|
291
|
+
expect(await screen.findByTestId("allowlist-view-section")).toBeDefined()
|
|
292
|
+
|
|
293
|
+
// Empty submit guard
|
|
294
|
+
const allowlistForm = screen.getByTestId("allowlist-view-section").querySelector("form")
|
|
295
|
+
if (allowlistForm) fireEvent.submit(allowlistForm)
|
|
296
|
+
|
|
297
|
+
// Add entry form fill
|
|
298
|
+
fireEvent.change(screen.getByTestId("allowlist-pattern-input"), { target: { value: "special@domain.com" } })
|
|
299
|
+
fireEvent.change(screen.getByTestId("allowlist-type-select"), { target: { value: "email" } })
|
|
300
|
+
fireEvent.change(screen.getByTestId("allowlist-desc-input"), { target: { value: "特別ユーザー" } })
|
|
301
|
+
|
|
302
|
+
fireEvent.click(screen.getByTestId("submit-allowlist-add"))
|
|
303
|
+
|
|
304
|
+
// Delete allowlist entry
|
|
305
|
+
fireEvent.click(screen.getByTestId("remove-allowlist-btn-1"))
|
|
306
|
+
})
|
|
307
|
+
})
|