@nocobase/app-template-default 1.0.0-beta.73 → 1.0.0-beta.74
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/AGENTS.md +1 -1
- package/README.MD +2 -0
- package/client/components/default-access-editor.tsx +183 -0
- package/client/components/permission-editor.tsx +216 -150
- package/client/components/role-list.tsx +61 -0
- package/client/lib/navigation-transform.ts +28 -1
- package/client/locales/system/settings-en-US.ts +73 -2
- package/client/locales/system/settings-zh-CN.ts +65 -2
- package/client/pages/settings/default-access-form.tsx +499 -0
- package/client/pages/settings/default-access-model.ts +154 -0
- package/client/pages/settings/default-access.tsx +407 -0
- package/client/pages/settings/permission-filters.tsx +43 -37
- package/client/pages/settings/permission-management-page.tsx +105 -0
- package/client/pages/settings/permissions-model.ts +37 -1
- package/client/pages/settings/role-form.tsx +46 -18
- package/client/pages/settings/roles.tsx +23 -43
- package/client/pages/settings/routes.ts +78 -19
- package/client/pages/settings/shared.tsx +3 -0
- package/client/pages/settings/users.tsx +1 -1
- package/client/routing/route-navigation.ts +21 -1
- package/package.json +19 -19
- package/tests/components/default-access.test.tsx +554 -0
- package/tests/components/page-harness.test.tsx +132 -112
- package/tests/components/permission-editor.test.tsx +233 -73
- package/tests/components/permission-management-tabs.test.tsx +128 -0
- package/tests/components/role-list.test.tsx +71 -0
- package/tests/components/roles.test.tsx +78 -0
- package/tests/components/user-permissions.test.tsx +12 -0
- package/tests/logic/client-routes.test.ts +8 -0
- package/tests/logic/default-access-model.test.ts +167 -0
- package/tests/logic/permission-management-routes.test.ts +64 -0
- package/tests/logic/settings-permissions.test.ts +89 -0
- package/vitest.config.ts +2 -2
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
import { answerApi, renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
|
+
import {
|
|
4
|
+
AuthorizationClient,
|
|
5
|
+
authorizationClientToken,
|
|
6
|
+
type AuthorizationOptionsResponse,
|
|
7
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
8
|
+
import { screen, waitFor, within } from '@testing-library/react';
|
|
9
|
+
import userEvent from '@testing-library/user-event';
|
|
10
|
+
import { Routes, Route } from 'react-router';
|
|
11
|
+
import { expect, it, vi } from 'vitest';
|
|
12
|
+
import Page from '../../client/pages/settings/default-access.js';
|
|
13
|
+
import Form from '../../client/pages/settings/default-access-form.js';
|
|
14
|
+
import enUS from '../../client/locales/en-US.js';
|
|
15
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
16
|
+
const namespace = '@nocobase/default-access-test';
|
|
17
|
+
const services = (app: ClientApplication) =>
|
|
18
|
+
app.container.instance(
|
|
19
|
+
authorizationClientToken,
|
|
20
|
+
new AuthorizationClient(app.container.resolve(apiClientToken)),
|
|
21
|
+
);
|
|
22
|
+
const namespaces = {
|
|
23
|
+
[namespace]: {
|
|
24
|
+
'en-US': async () => ({ default: enUS }),
|
|
25
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const catalog: AuthorizationOptionsResponse = {
|
|
29
|
+
sections: [
|
|
30
|
+
{
|
|
31
|
+
name: 'business',
|
|
32
|
+
title: 'Business',
|
|
33
|
+
order: 0,
|
|
34
|
+
subsections: [
|
|
35
|
+
{
|
|
36
|
+
name: 'projects',
|
|
37
|
+
title: 'Projects',
|
|
38
|
+
resources: [
|
|
39
|
+
{
|
|
40
|
+
type: 'composite',
|
|
41
|
+
id: 'projects',
|
|
42
|
+
title: 'Projects',
|
|
43
|
+
actions: [
|
|
44
|
+
{ name: 'view', title: 'View' },
|
|
45
|
+
{ name: 'edit', title: 'Edit' },
|
|
46
|
+
],
|
|
47
|
+
dataScopes: {
|
|
48
|
+
view: [
|
|
49
|
+
{
|
|
50
|
+
key: 'projects',
|
|
51
|
+
title: 'Projects',
|
|
52
|
+
collection: 'projects',
|
|
53
|
+
fields: ['name'],
|
|
54
|
+
recordAccess: ['own', 'customFilter'],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
edit: [
|
|
58
|
+
{
|
|
59
|
+
key: 'projects',
|
|
60
|
+
title: 'Projects',
|
|
61
|
+
collection: 'projects',
|
|
62
|
+
fields: ['name'],
|
|
63
|
+
recordAccess: ['own', 'customFilter'],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
recordAccess: [
|
|
74
|
+
{ key: 'own', title: 'Own records', collections: ['projects'] },
|
|
75
|
+
{ key: 'customFilter', title: 'Custom filter', collections: ['projects'] },
|
|
76
|
+
],
|
|
77
|
+
subjectTypes: [],
|
|
78
|
+
collections: [],
|
|
79
|
+
};
|
|
80
|
+
const existing = {
|
|
81
|
+
key: 'project-default',
|
|
82
|
+
resource: { type: 'composite', id: 'projects' },
|
|
83
|
+
actions: [
|
|
84
|
+
{
|
|
85
|
+
action: 'view',
|
|
86
|
+
scopeKey: 'projects',
|
|
87
|
+
selection: {
|
|
88
|
+
type: 'recordAccess',
|
|
89
|
+
key: 'customFilter',
|
|
90
|
+
params: { filter: { $and: [{ name: { $eq: 'Original' } }] } },
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
action: 'edit',
|
|
95
|
+
scopeKey: 'projects',
|
|
96
|
+
selection: { type: 'recordAccess', key: 'own' },
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
function tree() {
|
|
101
|
+
return (
|
|
102
|
+
<Routes>
|
|
103
|
+
<Route path='/default-access' element={<Page />}>
|
|
104
|
+
<Route path='create' element={<Form />} />
|
|
105
|
+
<Route path=':key/edit' element={<Form />} />
|
|
106
|
+
</Route>
|
|
107
|
+
</Routes>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
it('loads and inspects existing scopes without writes, preserving nested filters on opening and cancelling', async () => {
|
|
112
|
+
const user = userEvent.setup();
|
|
113
|
+
const calls = vi.fn(({ path }: { path: string }) =>
|
|
114
|
+
path === 'authorization/permissions'
|
|
115
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
116
|
+
: path.endsWith('/options')
|
|
117
|
+
? { data: catalog }
|
|
118
|
+
: { data: [existing] },
|
|
119
|
+
);
|
|
120
|
+
await renderWithApp(tree(), {
|
|
121
|
+
route: '/default-access/project-default/edit?q=Projects',
|
|
122
|
+
namespace,
|
|
123
|
+
namespaces,
|
|
124
|
+
services,
|
|
125
|
+
fetch: answerApi(calls),
|
|
126
|
+
});
|
|
127
|
+
expect(await screen.findByRole('textbox', { name: 'Key *' })).toHaveValue(
|
|
128
|
+
existing.key,
|
|
129
|
+
);
|
|
130
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' });
|
|
131
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
|
|
132
|
+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
|
|
133
|
+
await waitFor(() =>
|
|
134
|
+
expect(
|
|
135
|
+
screen.queryByRole('textbox', { name: 'Key *' }),
|
|
136
|
+
).not.toBeInTheDocument(),
|
|
137
|
+
);
|
|
138
|
+
expect(
|
|
139
|
+
calls.mock.calls.every(
|
|
140
|
+
([request]) =>
|
|
141
|
+
!('method' in request) || Reflect.get(request, 'method') === 'GET',
|
|
142
|
+
),
|
|
143
|
+
).toBe(true);
|
|
144
|
+
expect(
|
|
145
|
+
screen.getByRole('textbox', { name: 'Search business objects or keys' }),
|
|
146
|
+
).toHaveValue('Projects');
|
|
147
|
+
});
|
|
148
|
+
it('patches only the chosen scope and keeps the advanced filter in the other action', async () => {
|
|
149
|
+
const user = userEvent.setup();
|
|
150
|
+
let saved: unknown;
|
|
151
|
+
const calls = vi.fn(
|
|
152
|
+
(request: { path: string; method?: string; json?: unknown }) => {
|
|
153
|
+
if (request.path === 'authorization/permissions')
|
|
154
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
155
|
+
if (request.path.endsWith('/options')) return { data: catalog };
|
|
156
|
+
if (request.method === 'PATCH') {
|
|
157
|
+
saved = request.json;
|
|
158
|
+
return { data: existing };
|
|
159
|
+
}
|
|
160
|
+
return { data: [existing] };
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
await renderWithApp(tree(), {
|
|
164
|
+
route: '/default-access/project-default/edit',
|
|
165
|
+
namespace,
|
|
166
|
+
namespaces,
|
|
167
|
+
services,
|
|
168
|
+
fetch: answerApi(calls),
|
|
169
|
+
});
|
|
170
|
+
await user.click(
|
|
171
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
172
|
+
);
|
|
173
|
+
await user.click(await screen.findByRole('option', { name: 'All records' }));
|
|
174
|
+
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
175
|
+
await waitFor(() =>
|
|
176
|
+
expect(saved).toEqual({
|
|
177
|
+
actions: [
|
|
178
|
+
existing.actions[0],
|
|
179
|
+
{ action: 'edit', scopeKey: 'projects', selection: { type: 'all' } },
|
|
180
|
+
],
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
it('supports read-only inspection and does not show write or delete actions without permission', async () => {
|
|
185
|
+
await renderWithApp(tree(), {
|
|
186
|
+
route: '/default-access/project-default/edit',
|
|
187
|
+
namespace,
|
|
188
|
+
namespaces,
|
|
189
|
+
services,
|
|
190
|
+
fetch: answerApi(({ path }) =>
|
|
191
|
+
path === 'authorization/permissions'
|
|
192
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
193
|
+
: path.endsWith('/options')
|
|
194
|
+
? { data: catalog }
|
|
195
|
+
: { data: [existing] },
|
|
196
|
+
),
|
|
197
|
+
});
|
|
198
|
+
expect(
|
|
199
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
200
|
+
).toBeDisabled();
|
|
201
|
+
expect(
|
|
202
|
+
screen.queryByRole('button', { name: 'Save' }),
|
|
203
|
+
).not.toBeInTheDocument();
|
|
204
|
+
expect(
|
|
205
|
+
screen.queryByRole('button', { name: 'New default scope' }),
|
|
206
|
+
).not.toBeInTheDocument();
|
|
207
|
+
});
|
|
208
|
+
it('shows unknown rules read-only and refuses overwriting a rule changed after loading', async () => {
|
|
209
|
+
const user = userEvent.setup();
|
|
210
|
+
let count = 0;
|
|
211
|
+
let writes = 0;
|
|
212
|
+
await renderWithApp(tree(), {
|
|
213
|
+
route: '/default-access/project-default/edit',
|
|
214
|
+
namespace,
|
|
215
|
+
namespaces,
|
|
216
|
+
services,
|
|
217
|
+
fetch: answerApi(({ path, method }) => {
|
|
218
|
+
if (path === 'authorization/permissions')
|
|
219
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
220
|
+
if (path.endsWith('/options')) return { data: catalog };
|
|
221
|
+
if (method === 'PATCH') writes++;
|
|
222
|
+
count++;
|
|
223
|
+
return { data: [count > 2 ? { ...existing, actions: [] } : existing] };
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
await user.click(
|
|
227
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
228
|
+
);
|
|
229
|
+
await user.click(await screen.findByRole('option', { name: 'All records' }));
|
|
230
|
+
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
231
|
+
expect(
|
|
232
|
+
await screen.findByText(enUS.settings.defaultAccess.changed),
|
|
233
|
+
).toBeVisible();
|
|
234
|
+
expect(writes).toBe(0);
|
|
235
|
+
});
|
|
236
|
+
it('keeps unavailable catalog selections visible and read-only rather than creating a replacement rule', async () => {
|
|
237
|
+
await renderWithApp(tree(), {
|
|
238
|
+
route: '/default-access/project-default/edit',
|
|
239
|
+
namespace,
|
|
240
|
+
namespaces,
|
|
241
|
+
services,
|
|
242
|
+
fetch: answerApi(({ path }) =>
|
|
243
|
+
path === 'authorization/permissions'
|
|
244
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
245
|
+
: path.endsWith('/options')
|
|
246
|
+
? { data: { ...catalog, sections: [] } }
|
|
247
|
+
: { data: [existing] },
|
|
248
|
+
),
|
|
249
|
+
});
|
|
250
|
+
expect(
|
|
251
|
+
await screen.findByText(enUS.settings.defaultAccess.unsupported),
|
|
252
|
+
).toBeVisible();
|
|
253
|
+
expect(screen.getByRole('button', { name: 'Save' })).toBeDisabled();
|
|
254
|
+
expect(screen.getByRole('textbox', { name: 'Key *' })).toHaveValue(
|
|
255
|
+
existing.key,
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it.each(['en-US', 'zh-CN'])(
|
|
260
|
+
'creates an explicitly configured rule in %s without adding all-records grants',
|
|
261
|
+
async (locale) => {
|
|
262
|
+
const copy = locale === 'zh-CN' ? zhCN : enUS;
|
|
263
|
+
const user = userEvent.setup();
|
|
264
|
+
let rules: unknown[] = [];
|
|
265
|
+
let saved: unknown;
|
|
266
|
+
await renderWithApp(tree(), {
|
|
267
|
+
route: '/default-access/create',
|
|
268
|
+
locale,
|
|
269
|
+
namespace,
|
|
270
|
+
namespaces,
|
|
271
|
+
services,
|
|
272
|
+
fetch: answerApi((request) => {
|
|
273
|
+
if (request.path === 'authorization/permissions')
|
|
274
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
275
|
+
if (request.path.endsWith('/options')) return { data: catalog };
|
|
276
|
+
if (request.method === 'POST') {
|
|
277
|
+
saved = request.json;
|
|
278
|
+
rules = [request.json];
|
|
279
|
+
return { data: request.json };
|
|
280
|
+
}
|
|
281
|
+
return { data: rules };
|
|
282
|
+
}),
|
|
283
|
+
});
|
|
284
|
+
await user.type(
|
|
285
|
+
await screen.findByRole('textbox', { name: `${copy.settings.key} *` }),
|
|
286
|
+
'new-baseline',
|
|
287
|
+
);
|
|
288
|
+
await user.click(
|
|
289
|
+
screen.getByRole('combobox', {
|
|
290
|
+
name: `${copy.settings.defaultAccess.resource} *`,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
await user.click(await screen.findByRole('option', { name: 'Projects' }));
|
|
294
|
+
await user.click(screen.getByRole('button', { name: copy.settings.save }));
|
|
295
|
+
await waitFor(() =>
|
|
296
|
+
expect(saved).toEqual({
|
|
297
|
+
key: 'new-baseline',
|
|
298
|
+
resource: { type: 'composite', id: 'projects' },
|
|
299
|
+
actions: [],
|
|
300
|
+
}),
|
|
301
|
+
);
|
|
302
|
+
await waitFor(() =>
|
|
303
|
+
expect(screen.getByRole('link', { name: 'Projects' })).toBeVisible(),
|
|
304
|
+
);
|
|
305
|
+
},
|
|
306
|
+
);
|
|
307
|
+
it('keeps failed deletion in its confirmation and deletes only after explicit confirmation', async () => {
|
|
308
|
+
const user = userEvent.setup();
|
|
309
|
+
let rules = [existing];
|
|
310
|
+
let deletes = 0;
|
|
311
|
+
await renderWithApp(tree(), {
|
|
312
|
+
route: '/default-access',
|
|
313
|
+
namespace,
|
|
314
|
+
namespaces,
|
|
315
|
+
services,
|
|
316
|
+
fetch: answerApi((request) => {
|
|
317
|
+
if (request.path === 'authorization/permissions')
|
|
318
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
319
|
+
if (request.path.endsWith('/options')) return { data: catalog };
|
|
320
|
+
if (request.method === 'DELETE') {
|
|
321
|
+
deletes++;
|
|
322
|
+
if (deletes === 1)
|
|
323
|
+
return Response.json(
|
|
324
|
+
{ error: { message: 'Private server detail' } },
|
|
325
|
+
{ status: 500 },
|
|
326
|
+
);
|
|
327
|
+
rules = [];
|
|
328
|
+
return new Response(null, { status: 204 });
|
|
329
|
+
}
|
|
330
|
+
return { data: rules };
|
|
331
|
+
}),
|
|
332
|
+
});
|
|
333
|
+
await user.click(
|
|
334
|
+
await screen.findByRole('button', { name: 'Actions for Projects' }),
|
|
335
|
+
);
|
|
336
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Delete' }));
|
|
337
|
+
expect(deletes).toBe(0);
|
|
338
|
+
await user.click(screen.getByRole('button', { name: 'Delete', exact: true }));
|
|
339
|
+
expect(await screen.findByRole('alert')).toHaveTextContent(
|
|
340
|
+
enUS.settings.failed,
|
|
341
|
+
);
|
|
342
|
+
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
343
|
+
expect(screen.queryByText('Private server detail')).not.toBeInTheDocument();
|
|
344
|
+
await user.click(screen.getByRole('button', { name: 'Delete', exact: true }));
|
|
345
|
+
await screen.findByText(enUS.settings.defaultAccess.empty);
|
|
346
|
+
expect(deletes).toBe(2);
|
|
347
|
+
});
|
|
348
|
+
it('retains the create draft when the server rejects a duplicate and shows a contextual error', async () => {
|
|
349
|
+
const user = userEvent.setup();
|
|
350
|
+
await renderWithApp(tree(), {
|
|
351
|
+
route: '/default-access/create',
|
|
352
|
+
namespace,
|
|
353
|
+
namespaces,
|
|
354
|
+
services,
|
|
355
|
+
fetch: answerApi((request) =>
|
|
356
|
+
request.path === 'authorization/permissions'
|
|
357
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
358
|
+
: request.path.endsWith('/options')
|
|
359
|
+
? { data: catalog }
|
|
360
|
+
: request.method === 'POST'
|
|
361
|
+
? Response.json(
|
|
362
|
+
{ error: { reason: 'DEFAULT_ACCESS_CONFLICT' } },
|
|
363
|
+
{ status: 409 },
|
|
364
|
+
)
|
|
365
|
+
: { data: [] },
|
|
366
|
+
),
|
|
367
|
+
});
|
|
368
|
+
await user.type(
|
|
369
|
+
await screen.findByRole('textbox', { name: 'Key *' }),
|
|
370
|
+
'new-rule',
|
|
371
|
+
);
|
|
372
|
+
await user.click(screen.getByRole('combobox', { name: 'Business object *' }));
|
|
373
|
+
await user.click(await screen.findByRole('option', { name: 'Projects' }));
|
|
374
|
+
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
375
|
+
expect(
|
|
376
|
+
await screen.findByText(enUS.settings.defaultAccess.duplicate),
|
|
377
|
+
).toBeVisible();
|
|
378
|
+
expect(screen.getByRole('textbox', { name: 'Key *' })).toHaveValue(
|
|
379
|
+
'new-rule',
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('configures custom conditions in a temporary draft and cancels without saving or replacing the previous scope', async () => {
|
|
384
|
+
const user = userEvent.setup();
|
|
385
|
+
const calls = vi.fn((request: { path: string; method?: string }) =>
|
|
386
|
+
request.path === 'authorization/permissions'
|
|
387
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
388
|
+
: request.path.endsWith('/options')
|
|
389
|
+
? { data: catalog }
|
|
390
|
+
: { data: [existing] },
|
|
391
|
+
);
|
|
392
|
+
await renderWithApp(tree(), {
|
|
393
|
+
route: '/default-access/project-default/edit',
|
|
394
|
+
namespace,
|
|
395
|
+
namespaces,
|
|
396
|
+
services,
|
|
397
|
+
fetch: answerApi(calls),
|
|
398
|
+
});
|
|
399
|
+
await user.click(
|
|
400
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
401
|
+
);
|
|
402
|
+
await user.click(
|
|
403
|
+
await screen.findByRole('option', { name: 'Custom filter' }),
|
|
404
|
+
);
|
|
405
|
+
await screen.findByText('Configure record filter');
|
|
406
|
+
expect(
|
|
407
|
+
screen.getByRole('button', { name: 'Save', exact: true }),
|
|
408
|
+
).toBeDisabled();
|
|
409
|
+
const dialog = screen.getByRole('dialog');
|
|
410
|
+
await user.click(
|
|
411
|
+
within(dialog).getByRole('button', { name: 'Cancel', exact: true }),
|
|
412
|
+
);
|
|
413
|
+
await waitFor(() =>
|
|
414
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
|
|
415
|
+
);
|
|
416
|
+
expect(
|
|
417
|
+
screen.getByRole('combobox', { name: 'Edit · Projects' }),
|
|
418
|
+
).toHaveTextContent('Own records');
|
|
419
|
+
expect(calls.mock.calls.every(([request]) => request.method === 'GET')).toBe(
|
|
420
|
+
true,
|
|
421
|
+
);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('validates a missing key on submit and focuses its inline error without sending a create', async () => {
|
|
425
|
+
const user = userEvent.setup();
|
|
426
|
+
let writes = 0;
|
|
427
|
+
await renderWithApp(tree(), {
|
|
428
|
+
route: '/default-access/create',
|
|
429
|
+
namespace,
|
|
430
|
+
namespaces,
|
|
431
|
+
services,
|
|
432
|
+
fetch: answerApi((request) => {
|
|
433
|
+
if (request.method === 'POST') writes++;
|
|
434
|
+
return request.path === 'authorization/permissions'
|
|
435
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
436
|
+
: request.path.endsWith('/options')
|
|
437
|
+
? { data: catalog }
|
|
438
|
+
: { data: [] };
|
|
439
|
+
}),
|
|
440
|
+
});
|
|
441
|
+
await user.click(
|
|
442
|
+
await screen.findByRole('combobox', { name: 'Business object *' }),
|
|
443
|
+
);
|
|
444
|
+
await user.click(await screen.findByRole('option', { name: 'Projects' }));
|
|
445
|
+
expect(
|
|
446
|
+
screen.queryByText(enUS.settings.defaultAccess.keyRequired),
|
|
447
|
+
).not.toBeInTheDocument();
|
|
448
|
+
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
449
|
+
expect(
|
|
450
|
+
await screen.findByText(enUS.settings.defaultAccess.keyRequired),
|
|
451
|
+
).toBeVisible();
|
|
452
|
+
await waitFor(() =>
|
|
453
|
+
expect(screen.getByRole('textbox', { name: 'Key *' })).toHaveFocus(),
|
|
454
|
+
);
|
|
455
|
+
expect(writes).toBe(0);
|
|
456
|
+
});
|
|
457
|
+
it('refuses deleting a changed rule and refreshes the list for explicit re-confirmation', async () => {
|
|
458
|
+
const user = userEvent.setup();
|
|
459
|
+
let reads = 0;
|
|
460
|
+
let writes = 0;
|
|
461
|
+
await renderWithApp(tree(), {
|
|
462
|
+
route: '/default-access',
|
|
463
|
+
namespace,
|
|
464
|
+
namespaces,
|
|
465
|
+
services,
|
|
466
|
+
fetch: answerApi((request) => {
|
|
467
|
+
if (request.path === 'authorization/permissions')
|
|
468
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
469
|
+
if (request.path.endsWith('/options')) return { data: catalog };
|
|
470
|
+
if (request.method === 'DELETE') writes++;
|
|
471
|
+
reads++;
|
|
472
|
+
return { data: [reads > 1 ? { ...existing, actions: [] } : existing] };
|
|
473
|
+
}),
|
|
474
|
+
});
|
|
475
|
+
await user.click(
|
|
476
|
+
await screen.findByRole('button', { name: 'Actions for Projects' }),
|
|
477
|
+
);
|
|
478
|
+
await user.click(await screen.findByRole('menuitem', { name: 'Delete' }));
|
|
479
|
+
await user.click(screen.getByRole('button', { name: 'Delete', exact: true }));
|
|
480
|
+
expect(
|
|
481
|
+
await screen.findByText(enUS.settings.defaultAccess.deleteChanged),
|
|
482
|
+
).toBeVisible();
|
|
483
|
+
expect(writes).toBe(0);
|
|
484
|
+
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('disables New rule with a reason when every available resource is already configured', async () => {
|
|
488
|
+
await renderWithApp(tree(), {
|
|
489
|
+
route: '/default-access',
|
|
490
|
+
namespace,
|
|
491
|
+
namespaces,
|
|
492
|
+
services,
|
|
493
|
+
fetch: answerApi(({ path }) =>
|
|
494
|
+
path === 'authorization/permissions'
|
|
495
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
496
|
+
: path.endsWith('/options')
|
|
497
|
+
? { data: catalog }
|
|
498
|
+
: { data: [existing] },
|
|
499
|
+
),
|
|
500
|
+
});
|
|
501
|
+
expect(
|
|
502
|
+
await screen.findByText(enUS.settings.defaultAccess.noResourcesShort),
|
|
503
|
+
).toBeVisible();
|
|
504
|
+
expect(
|
|
505
|
+
screen.getByRole('button', { name: 'New default scope' }),
|
|
506
|
+
).toBeDisabled();
|
|
507
|
+
expect(
|
|
508
|
+
screen.queryByRole('link', { name: 'New default scope' }),
|
|
509
|
+
).not.toBeInTheDocument();
|
|
510
|
+
});
|
|
511
|
+
it('maps a server scope validation error to the affected selector and focuses it while retaining the draft', async () => {
|
|
512
|
+
const user = userEvent.setup();
|
|
513
|
+
await renderWithApp(tree(), {
|
|
514
|
+
route: '/default-access/project-default/edit',
|
|
515
|
+
namespace,
|
|
516
|
+
namespaces,
|
|
517
|
+
services,
|
|
518
|
+
fetch: answerApi((request) =>
|
|
519
|
+
request.path === 'authorization/permissions'
|
|
520
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
521
|
+
: request.path.endsWith('/options')
|
|
522
|
+
? { data: catalog }
|
|
523
|
+
: request.method === 'PATCH'
|
|
524
|
+
? Response.json(
|
|
525
|
+
{
|
|
526
|
+
error: {
|
|
527
|
+
reason: 'INVALID_AUTHORIZATION_INPUT',
|
|
528
|
+
fieldViolations: [
|
|
529
|
+
{
|
|
530
|
+
field: 'actions.1.selection.key',
|
|
531
|
+
description: 'Private detail',
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
{ status: 400 },
|
|
537
|
+
)
|
|
538
|
+
: { data: [existing] },
|
|
539
|
+
),
|
|
540
|
+
});
|
|
541
|
+
await user.click(
|
|
542
|
+
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
543
|
+
);
|
|
544
|
+
await user.click(await screen.findByRole('option', { name: 'All records' }));
|
|
545
|
+
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
546
|
+
expect(
|
|
547
|
+
await screen.findByText(enUS.settings.defaultAccess.scopeInvalid),
|
|
548
|
+
).toBeVisible();
|
|
549
|
+
const scope = screen.getByRole('combobox', { name: 'Edit · Projects' });
|
|
550
|
+
expect(scope).toHaveAttribute('aria-invalid', 'true');
|
|
551
|
+
await waitFor(() => expect(scope).toHaveFocus());
|
|
552
|
+
expect(scope).toHaveTextContent('All records');
|
|
553
|
+
expect(screen.queryByText('Private detail')).not.toBeInTheDocument();
|
|
554
|
+
});
|