@thepalaceproject/circulation-admin 1.36.0 → 1.37.0-post.1
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/.eslintrc +9 -1
- package/dist/circulation-admin.css +1 -1
- package/dist/circulation-admin.js +1 -1
- package/package.json +1 -1
- package/tests/jest/api/patronBlockingRules.test.ts +78 -0
- package/tests/jest/components/DebugAuthentication.test.tsx +281 -0
- package/tests/jest/components/DebugResultListItem.test.tsx +162 -0
- package/tests/jest/components/PasswordInput.test.tsx +81 -0
- package/tests/jest/components/PatronAuthServiceEditForm.test.tsx +458 -0
- package/tests/jest/components/PatronBlockingRulesEditor.test.tsx +833 -0
- package/tests/jest/utils/patronBlockingRulesCapability.test.ts +23 -0
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import * as fetchMock from "fetch-mock-jest";
|
|
5
|
+
import PatronAuthServiceEditForm from "../../../src/components/PatronAuthServiceEditForm";
|
|
6
|
+
import {
|
|
7
|
+
PatronAuthServicesData,
|
|
8
|
+
PatronBlockingRule,
|
|
9
|
+
} from "../../../src/interfaces";
|
|
10
|
+
import { SIP2_PROTOCOL } from "../../../src/utils/patronBlockingRules";
|
|
11
|
+
|
|
12
|
+
const VALIDATE_URL = "/admin/patron_auth_service_validate_patron_blocking_rule";
|
|
13
|
+
|
|
14
|
+
async function expandLibrariesPanel(user: ReturnType<typeof userEvent.setup>) {
|
|
15
|
+
const toggle = screen
|
|
16
|
+
.getAllByRole("button")
|
|
17
|
+
.find((btn) => btn.getAttribute("aria-controls")?.includes("libraries"));
|
|
18
|
+
if (toggle && toggle.getAttribute("aria-expanded") === "false") {
|
|
19
|
+
await user.click(toggle);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SIP2_PROTOCOL_DATA = {
|
|
24
|
+
name: SIP2_PROTOCOL,
|
|
25
|
+
label: "SIP2",
|
|
26
|
+
description: "SIP2 authentication",
|
|
27
|
+
sitewide: false,
|
|
28
|
+
settings: [{ key: "server", label: "Server", required: true }],
|
|
29
|
+
library_settings: [],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const OTHER_PROTOCOL_DATA = {
|
|
33
|
+
name: "api.saml.provider",
|
|
34
|
+
label: "SAML",
|
|
35
|
+
description: "SAML authentication",
|
|
36
|
+
sitewide: false,
|
|
37
|
+
settings: [{ key: "idp_url", label: "IdP URL", required: true }],
|
|
38
|
+
library_settings: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const LIBRARY_SHORT_NAME = "test-lib";
|
|
42
|
+
|
|
43
|
+
const servicesData: PatronAuthServicesData = {
|
|
44
|
+
protocols: [SIP2_PROTOCOL_DATA, OTHER_PROTOCOL_DATA],
|
|
45
|
+
allLibraries: [{ short_name: LIBRARY_SHORT_NAME, name: "Test Library" }],
|
|
46
|
+
patron_auth_services: [],
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const baseProps = {
|
|
50
|
+
data: servicesData,
|
|
51
|
+
disabled: false,
|
|
52
|
+
save: jest.fn(),
|
|
53
|
+
urlBase: "/admin/web/config/patronAuth/",
|
|
54
|
+
listDataKey: "patron_auth_services",
|
|
55
|
+
adminLevel: 10,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function buildSIP2Item(rules: PatronBlockingRule[] = []) {
|
|
59
|
+
return {
|
|
60
|
+
id: 1,
|
|
61
|
+
protocol: SIP2_PROTOCOL,
|
|
62
|
+
settings: { server: "sip.example.com" },
|
|
63
|
+
libraries: [
|
|
64
|
+
{ short_name: LIBRARY_SHORT_NAME, patron_blocking_rules: rules },
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Guard: any blur on the Rule Expression textarea calls validatePatronBlockingRuleExpression.
|
|
70
|
+
// All describe blocks get a default 200 mock so tests that incidentally trigger blur
|
|
71
|
+
// don't fail with "only absolute URLs are supported" from the fetch polyfill.
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
fetchMock.post(VALIDATE_URL, { status: 200 });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
fetchMock.mockReset();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("PatronAuthServiceEditForm – capability gating", () => {
|
|
81
|
+
it("shows PatronBlockingRulesEditor in expanded library settings for SIP2", async () => {
|
|
82
|
+
const user = userEvent.setup();
|
|
83
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
84
|
+
|
|
85
|
+
await expandLibrariesPanel(user);
|
|
86
|
+
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
87
|
+
await user.click(editButton);
|
|
88
|
+
|
|
89
|
+
// The PatronBlockingRulesEditor label and Add Rule button should be visible
|
|
90
|
+
expect(screen.getByText("Patron Blocking Rules")).toBeTruthy();
|
|
91
|
+
expect(screen.getByRole("button", { name: /Add Rule/i })).toBeTruthy();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("does not show PatronBlockingRulesEditor for non-SIP2 protocol", async () => {
|
|
95
|
+
const user = userEvent.setup();
|
|
96
|
+
const item = {
|
|
97
|
+
id: 2,
|
|
98
|
+
protocol: "api.saml.provider",
|
|
99
|
+
settings: { idp_url: "https://idp.example.com" },
|
|
100
|
+
libraries: [{ short_name: LIBRARY_SHORT_NAME }],
|
|
101
|
+
};
|
|
102
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
103
|
+
await expandLibrariesPanel(user);
|
|
104
|
+
|
|
105
|
+
// For non-SIP2 with no library_settings, the Edit button should not render
|
|
106
|
+
expect(screen.queryByRole("button", { name: /Edit/i })).toBeNull();
|
|
107
|
+
expect(screen.queryByText(/Patron Blocking Rules/i)).toBeNull();
|
|
108
|
+
expect(screen.queryByRole("button", { name: /Add Rule/i })).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("PatronAuthServiceEditForm – load / initial value", () => {
|
|
113
|
+
it("populates the editor with existing patron_blocking_rules from saved library settings", async () => {
|
|
114
|
+
const user = userEvent.setup();
|
|
115
|
+
const existingRules: PatronBlockingRule[] = [
|
|
116
|
+
{
|
|
117
|
+
name: "Block expired",
|
|
118
|
+
rule: "status == expired",
|
|
119
|
+
message: "Card expired",
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
render(
|
|
123
|
+
<PatronAuthServiceEditForm
|
|
124
|
+
{...baseProps}
|
|
125
|
+
item={buildSIP2Item(existingRules)}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
await expandLibrariesPanel(user);
|
|
130
|
+
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
131
|
+
await user.click(editButton);
|
|
132
|
+
|
|
133
|
+
const nameInput = screen.getByLabelText(/Rule Name/i) as HTMLInputElement;
|
|
134
|
+
expect(nameInput.value).toBe("Block expired");
|
|
135
|
+
|
|
136
|
+
const ruleTextarea = screen.getByLabelText(
|
|
137
|
+
/Rule Expression/i
|
|
138
|
+
) as HTMLTextAreaElement;
|
|
139
|
+
expect(ruleTextarea.value).toBe("status == expired");
|
|
140
|
+
|
|
141
|
+
const messageInput = screen.getByLabelText(/Message/i) as HTMLInputElement;
|
|
142
|
+
expect(messageInput.value).toBe("Card expired");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("shows empty editor when library has no existing patron_blocking_rules", async () => {
|
|
146
|
+
const user = userEvent.setup();
|
|
147
|
+
render(
|
|
148
|
+
<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item([])} />
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await expandLibrariesPanel(user);
|
|
152
|
+
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
153
|
+
await user.click(editButton);
|
|
154
|
+
|
|
155
|
+
expect(screen.getByText(/No patron blocking rules defined/i)).toBeTruthy();
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("PatronAuthServiceEditForm – error display", () => {
|
|
160
|
+
it("passes error prop down to PatronBlockingRulesEditor", async () => {
|
|
161
|
+
const user = userEvent.setup();
|
|
162
|
+
const error = { status: 400, response: "Validation failed", url: "" };
|
|
163
|
+
render(
|
|
164
|
+
<PatronAuthServiceEditForm
|
|
165
|
+
{...baseProps}
|
|
166
|
+
item={buildSIP2Item([{ name: "", rule: "expr", message: null }])}
|
|
167
|
+
error={error}
|
|
168
|
+
/>
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
await expandLibrariesPanel(user);
|
|
172
|
+
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
173
|
+
await user.click(editButton);
|
|
174
|
+
|
|
175
|
+
// The PatronBlockingRulesEditor should be present (error prop is wired in)
|
|
176
|
+
expect(screen.getByText(/Patron Blocking Rules/i)).toBeTruthy();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("PatronAuthServiceEditForm – serialization", () => {
|
|
181
|
+
it("includes patron_blocking_rules in the library state when editLibrary is called", async () => {
|
|
182
|
+
const user = userEvent.setup();
|
|
183
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
184
|
+
|
|
185
|
+
await expandLibrariesPanel(user);
|
|
186
|
+
const editButton = screen.getByRole("button", { name: /Edit/i });
|
|
187
|
+
await user.click(editButton);
|
|
188
|
+
|
|
189
|
+
// Add a rule via the editor UI
|
|
190
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
191
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "Test Rule");
|
|
192
|
+
await user.type(
|
|
193
|
+
screen.getByLabelText(/Rule Expression/i),
|
|
194
|
+
"field == value"
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Tab out of the expression field to trigger blur → validation (mocked 200).
|
|
198
|
+
// The Save button is disabled until validation succeeds.
|
|
199
|
+
await user.tab();
|
|
200
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
201
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
202
|
+
|
|
203
|
+
await user.click(saveButton);
|
|
204
|
+
|
|
205
|
+
// The library should now be collapsed (indicating editLibrary was called)
|
|
206
|
+
expect(screen.queryByRole("button", { name: /Add Rule/i })).toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("includes patron_blocking_rules in payload when adding a new library", async () => {
|
|
210
|
+
const user = userEvent.setup();
|
|
211
|
+
// No existing libraries associated with service
|
|
212
|
+
const item = {
|
|
213
|
+
id: 3,
|
|
214
|
+
protocol: SIP2_PROTOCOL,
|
|
215
|
+
settings: {},
|
|
216
|
+
libraries: [],
|
|
217
|
+
};
|
|
218
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
219
|
+
|
|
220
|
+
await expandLibrariesPanel(user);
|
|
221
|
+
// Select the library from the dropdown
|
|
222
|
+
const select = screen.getByRole("combobox", { name: /Add Library/i });
|
|
223
|
+
await user.selectOptions(select, LIBRARY_SHORT_NAME);
|
|
224
|
+
|
|
225
|
+
// Add a patron blocking rule
|
|
226
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
227
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "New Rule");
|
|
228
|
+
await user.type(
|
|
229
|
+
screen.getByLabelText(/Rule Expression/i),
|
|
230
|
+
"some expression"
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Tab out to trigger blur → validation (mocked 200).
|
|
234
|
+
// The Add Library button is disabled until validation succeeds.
|
|
235
|
+
await user.tab();
|
|
236
|
+
const addButton = screen.getByRole("button", { name: /Add Library/i });
|
|
237
|
+
await waitFor(() => expect(addButton).not.toBeDisabled());
|
|
238
|
+
|
|
239
|
+
await user.click(addButton);
|
|
240
|
+
|
|
241
|
+
// After adding, the library should appear in the list, editor no longer in "new library" form
|
|
242
|
+
expect(screen.queryByLabelText(/Rule Name/i)).toBeNull();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("PatronAuthServiceEditForm – save button gating", () => {
|
|
247
|
+
it("disables the per-library Save button immediately when a new rule is added", async () => {
|
|
248
|
+
const user = userEvent.setup();
|
|
249
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
250
|
+
|
|
251
|
+
await expandLibrariesPanel(user);
|
|
252
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
253
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
254
|
+
|
|
255
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
256
|
+
await waitFor(() => expect(saveButton).toBeDisabled());
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("re-enables the per-library Save button once validation succeeds", async () => {
|
|
260
|
+
const user = userEvent.setup();
|
|
261
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
262
|
+
|
|
263
|
+
await expandLibrariesPanel(user);
|
|
264
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
265
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
266
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "My Rule");
|
|
267
|
+
await user.type(screen.getByLabelText(/Rule Expression/i), "{{fines}} > 0");
|
|
268
|
+
// Tab out triggers blur → server validation → 200 OK (from beforeEach mock)
|
|
269
|
+
await user.tab();
|
|
270
|
+
|
|
271
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
272
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("keeps the per-library Save button disabled when validation fails", async () => {
|
|
276
|
+
const user = userEvent.setup();
|
|
277
|
+
fetchMock.mockReset();
|
|
278
|
+
fetchMock.post(VALIDATE_URL, {
|
|
279
|
+
status: 400,
|
|
280
|
+
body: { detail: "Unknown placeholder" },
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
284
|
+
|
|
285
|
+
await expandLibrariesPanel(user);
|
|
286
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
287
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
288
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "My Rule");
|
|
289
|
+
await user.type(screen.getByLabelText(/Rule Expression/i), "bad_syntax");
|
|
290
|
+
await user.tab();
|
|
291
|
+
|
|
292
|
+
await screen.findByText(/Unknown placeholder/i);
|
|
293
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
294
|
+
expect(saveButton).toBeDisabled();
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it("disables the Add Library button immediately when an incomplete rule is added to the new library section", async () => {
|
|
298
|
+
const user = userEvent.setup();
|
|
299
|
+
const item = {
|
|
300
|
+
id: 3,
|
|
301
|
+
protocol: SIP2_PROTOCOL,
|
|
302
|
+
settings: {},
|
|
303
|
+
libraries: [],
|
|
304
|
+
};
|
|
305
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={item} />);
|
|
306
|
+
|
|
307
|
+
await expandLibrariesPanel(user);
|
|
308
|
+
const select = screen.getByRole("combobox", { name: /Add Library/i });
|
|
309
|
+
await user.selectOptions(select, LIBRARY_SHORT_NAME);
|
|
310
|
+
|
|
311
|
+
const addButton = screen.getByRole("button", { name: /Add Library/i });
|
|
312
|
+
// Before adding a rule the button is enabled
|
|
313
|
+
expect(addButton).not.toBeDisabled();
|
|
314
|
+
|
|
315
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
316
|
+
|
|
317
|
+
// Incomplete rule (empty name + expression) must block the Add Library button
|
|
318
|
+
await waitFor(() => expect(addButton).toBeDisabled());
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("disables the per-library Save button when two rules share the same name", async () => {
|
|
322
|
+
const user = userEvent.setup();
|
|
323
|
+
const existingRules = [
|
|
324
|
+
{ name: "Rule A", rule: "expr_a" },
|
|
325
|
+
{ name: "Rule B", rule: "expr_b" },
|
|
326
|
+
];
|
|
327
|
+
render(
|
|
328
|
+
<PatronAuthServiceEditForm
|
|
329
|
+
{...baseProps}
|
|
330
|
+
item={buildSIP2Item(existingRules)}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
await expandLibrariesPanel(user);
|
|
335
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
336
|
+
|
|
337
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
338
|
+
// Both rules are complete and unique — Save should be enabled
|
|
339
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
340
|
+
|
|
341
|
+
// Rename rule B to match rule A → duplicate → Save must disable
|
|
342
|
+
const nameInputs = screen.getAllByLabelText(
|
|
343
|
+
/Rule Name/i
|
|
344
|
+
) as HTMLInputElement[];
|
|
345
|
+
await user.clear(nameInputs[1]);
|
|
346
|
+
await user.type(nameInputs[1], "Rule A");
|
|
347
|
+
|
|
348
|
+
await waitFor(() => expect(saveButton).toBeDisabled());
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
it("re-enables the per-library Save button after a duplicate name is resolved", async () => {
|
|
352
|
+
const user = userEvent.setup();
|
|
353
|
+
const existingRules = [
|
|
354
|
+
{ name: "Rule A", rule: "expr_a" },
|
|
355
|
+
{ name: "Rule A", rule: "expr_b" }, // starts as duplicate
|
|
356
|
+
];
|
|
357
|
+
render(
|
|
358
|
+
<PatronAuthServiceEditForm
|
|
359
|
+
{...baseProps}
|
|
360
|
+
item={buildSIP2Item(existingRules)}
|
|
361
|
+
/>
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
await expandLibrariesPanel(user);
|
|
365
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
366
|
+
|
|
367
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
368
|
+
await waitFor(() => expect(saveButton).toBeDisabled());
|
|
369
|
+
|
|
370
|
+
// Fix the duplicate
|
|
371
|
+
const nameInputs = screen.getAllByLabelText(
|
|
372
|
+
/Rule Name/i
|
|
373
|
+
) as HTMLInputElement[];
|
|
374
|
+
await user.clear(nameInputs[1]);
|
|
375
|
+
await user.type(nameInputs[1], "Rule B");
|
|
376
|
+
|
|
377
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("re-blocks and then re-enables the Save button when an existing rule's expression is edited and re-validated", async () => {
|
|
381
|
+
const user = userEvent.setup();
|
|
382
|
+
const existingRules = [{ name: "Rule A", rule: "expr_a" }];
|
|
383
|
+
render(
|
|
384
|
+
<PatronAuthServiceEditForm
|
|
385
|
+
{...baseProps}
|
|
386
|
+
item={buildSIP2Item(existingRules)}
|
|
387
|
+
/>
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
await expandLibrariesPanel(user);
|
|
391
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
392
|
+
|
|
393
|
+
const saveButton = screen.getByRole("button", { name: /^Save$/i });
|
|
394
|
+
// Existing rule is not pending — Save should be enabled
|
|
395
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
396
|
+
|
|
397
|
+
// Edit the expression — Save must re-block
|
|
398
|
+
const ruleTextarea = screen.getByLabelText(
|
|
399
|
+
/Rule Expression/i
|
|
400
|
+
) as HTMLTextAreaElement;
|
|
401
|
+
await user.clear(ruleTextarea);
|
|
402
|
+
await user.type(ruleTextarea, "new_expr");
|
|
403
|
+
await waitFor(() => expect(saveButton).toBeDisabled());
|
|
404
|
+
|
|
405
|
+
// Blur → 200 OK from beforeEach mock → Save re-enabled
|
|
406
|
+
await user.tab();
|
|
407
|
+
await waitFor(() => expect(saveButton).not.toBeDisabled());
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
describe("PatronAuthServiceEditForm – csrfToken / serviceId wiring", () => {
|
|
412
|
+
it("passes the csrfToken from additionalData to the validation API request", async () => {
|
|
413
|
+
const user = userEvent.setup();
|
|
414
|
+
render(
|
|
415
|
+
<PatronAuthServiceEditForm
|
|
416
|
+
{...baseProps}
|
|
417
|
+
item={buildSIP2Item()}
|
|
418
|
+
additionalData={{ csrfToken: "test-csrf-token" }}
|
|
419
|
+
/>
|
|
420
|
+
);
|
|
421
|
+
|
|
422
|
+
await expandLibrariesPanel(user);
|
|
423
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
424
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
425
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "My Rule");
|
|
426
|
+
await user.type(screen.getByLabelText(/Rule Expression/i), "expr");
|
|
427
|
+
await user.tab();
|
|
428
|
+
|
|
429
|
+
await waitFor(() =>
|
|
430
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
431
|
+
VALIDATE_URL,
|
|
432
|
+
expect.objectContaining({
|
|
433
|
+
headers: expect.objectContaining({
|
|
434
|
+
"X-CSRF-Token": "test-csrf-token",
|
|
435
|
+
}),
|
|
436
|
+
})
|
|
437
|
+
)
|
|
438
|
+
);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("passes the item id as service_id in the validation API request body", async () => {
|
|
442
|
+
const user = userEvent.setup();
|
|
443
|
+
// item.id = 1 (from buildSIP2Item)
|
|
444
|
+
render(<PatronAuthServiceEditForm {...baseProps} item={buildSIP2Item()} />);
|
|
445
|
+
|
|
446
|
+
await expandLibrariesPanel(user);
|
|
447
|
+
await user.click(screen.getByRole("button", { name: /Edit/i }));
|
|
448
|
+
await user.click(screen.getByRole("button", { name: /Add Rule/i }));
|
|
449
|
+
await user.type(screen.getByLabelText(/Rule Name/i), "My Rule");
|
|
450
|
+
await user.type(screen.getByLabelText(/Rule Expression/i), "expr");
|
|
451
|
+
await user.tab();
|
|
452
|
+
|
|
453
|
+
await waitFor(() => expect(fetchMock).toHaveBeenCalled());
|
|
454
|
+
const [, options] = fetchMock.calls(VALIDATE_URL)[0];
|
|
455
|
+
const body = (options as RequestInit).body as FormData;
|
|
456
|
+
expect(body.get("service_id")).toBe("1");
|
|
457
|
+
});
|
|
458
|
+
});
|