@scality/data-browser-library 1.0.7 → 1.0.9
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/dist/components/__tests__/BucketCorsPage.test.js +67 -9
- package/dist/components/__tests__/BucketDetails.test.js +1 -0
- package/dist/components/__tests__/BucketLifecycleFormPage.test.js +16 -11
- package/dist/components/__tests__/BucketNotificationFormPage.test.js +45 -0
- package/dist/components/__tests__/BucketOverview.test.js +92 -2
- package/dist/components/__tests__/BucketPolicyPage.test.js +70 -51
- package/dist/components/__tests__/BucketReplicationFormPage.test.js +18 -24
- package/dist/components/__tests__/ObjectList.test.js +43 -2
- package/dist/components/buckets/BucketConfigEditButton.d.ts +2 -0
- package/dist/components/buckets/BucketConfigEditButton.js +9 -3
- package/dist/components/buckets/BucketCorsPage.js +57 -20
- package/dist/components/buckets/BucketDetails.js +27 -2
- package/dist/components/buckets/BucketLifecycleFormPage.js +310 -270
- package/dist/components/buckets/BucketOverview.js +21 -18
- package/dist/components/buckets/BucketPolicyPage.js +119 -83
- package/dist/components/buckets/BucketReplicationFormPage.js +39 -29
- package/dist/components/buckets/BucketVersioning.js +16 -10
- package/dist/components/buckets/__tests__/BucketVersioning.test.js +76 -23
- package/dist/components/buckets/notifications/BucketNotificationFormPage.js +13 -5
- package/dist/components/objects/ObjectList.js +22 -25
- package/dist/components/objects/ObjectLock/EditRetentionButton.js +2 -2
- package/dist/components/objects/UploadButton.js +25 -15
- package/dist/config/__tests__/resolveBrandingTheme.test.d.ts +1 -0
- package/dist/config/__tests__/resolveBrandingTheme.test.js +96 -0
- package/dist/config/resolveBrandingTheme.d.ts +16 -0
- package/dist/config/resolveBrandingTheme.js +23 -0
- package/dist/config/types.d.ts +36 -0
- package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -0
- package/dist/hooks/index.d.ts +1 -1
- package/dist/hooks/objectOperations.d.ts +3 -3
- package/dist/hooks/objectOperations.js +3 -3
- package/dist/hooks/useBucketConfigEditor.d.ts +4 -4
- package/dist/hooks/useBucketConfigEditor.js +16 -31
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/test/mocks/esmOnlyModules.js +4 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/utils/__tests__/proxyMiddleware.test.js +34 -0
- package/dist/utils/proxyMiddleware.js +2 -0
- package/package.json +4 -4
- package/dist/components/Editor.d.ts +0 -12
- package/dist/components/Editor.js +0 -28
- package/dist/types/monaco.d.ts +0 -13
- package/dist/types/monaco.js +0 -0
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
4
|
-
import { useDeleteBucketCors, useGetBucketCors, useSetBucketCors } from "../../hooks/index.js";
|
|
4
|
+
import { useDeleteBucketCors, useGetBucketCors, useISVBucketStatus, useSetBucketCors } from "../../hooks/index.js";
|
|
5
5
|
import { createMockMutationResult, createMockQueryResult, createTestWrapper } from "../../test/testUtils.js";
|
|
6
|
+
import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
|
|
6
7
|
import { BucketCorsPage } from "../buckets/BucketCorsPage.js";
|
|
7
8
|
jest.mock('../../hooks', ()=>({
|
|
8
9
|
useGetBucketCors: jest.fn(),
|
|
9
10
|
useSetBucketCors: jest.fn(),
|
|
10
11
|
useDeleteBucketCors: jest.fn(),
|
|
12
|
+
useISVBucketStatus: jest.fn(),
|
|
11
13
|
useBucketConfigEditor: jest.requireActual('../../hooks').useBucketConfigEditor
|
|
12
14
|
}));
|
|
13
|
-
jest.mock('
|
|
15
|
+
jest.mock('@scality/core-ui/dist/next', ()=>({
|
|
16
|
+
...jest.requireActual('@scality/core-ui/dist/next'),
|
|
14
17
|
Editor: ({ value, onChange })=>/*#__PURE__*/ jsx("textarea", {
|
|
15
18
|
"data-testid": "cors-editor",
|
|
16
19
|
value: value,
|
|
@@ -20,6 +23,7 @@ jest.mock('../Editor', ()=>({
|
|
|
20
23
|
const mockUseGetBucketCors = jest.mocked(useGetBucketCors);
|
|
21
24
|
const mockUseSetBucketCors = jest.mocked(useSetBucketCors);
|
|
22
25
|
const mockUseDeleteBucketCors = jest.mocked(useDeleteBucketCors);
|
|
26
|
+
const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
|
|
23
27
|
const mockNavigate = jest.fn();
|
|
24
28
|
jest.mock('react-router', ()=>({
|
|
25
29
|
...jest.requireActual('react-router'),
|
|
@@ -43,11 +47,7 @@ const existingCorsRules = [
|
|
|
43
47
|
MaxAgeSeconds: 3600
|
|
44
48
|
}
|
|
45
49
|
];
|
|
46
|
-
const createNoSuchCorsError = ()=>
|
|
47
|
-
const error = new Error('CORS configuration does not exist');
|
|
48
|
-
error.name = 'NoSuchCORSConfiguration';
|
|
49
|
-
return error;
|
|
50
|
-
};
|
|
50
|
+
const createNoSuchCorsError = ()=>new EnhancedS3Error('CORS configuration does not exist', 'NoSuchCORSConfiguration', ErrorCategory.NOT_FOUND, new Error('CORS configuration does not exist'), 404);
|
|
51
51
|
const mockNoCorsExists = ()=>{
|
|
52
52
|
mockUseGetBucketCors.mockReturnValue(createMockQueryResult({
|
|
53
53
|
status: 'error',
|
|
@@ -86,6 +86,15 @@ describe('BucketCorsPage', ()=>{
|
|
|
86
86
|
jest.clearAllMocks();
|
|
87
87
|
mockUseSetBucketCors.mockReturnValue(createMockMutationResult(mockSaveMutate));
|
|
88
88
|
mockUseDeleteBucketCors.mockReturnValue(createMockMutationResult(mockDeleteMutate));
|
|
89
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
90
|
+
isVeeamBucket: false,
|
|
91
|
+
isCommvaultBucket: false,
|
|
92
|
+
isKastenBucket: false,
|
|
93
|
+
isISVManaged: false,
|
|
94
|
+
isvApplication: void 0,
|
|
95
|
+
isLoading: false,
|
|
96
|
+
bucketTagsStatus: 'success'
|
|
97
|
+
});
|
|
89
98
|
});
|
|
90
99
|
it('shows loading state while fetching CORS configuration', ()=>{
|
|
91
100
|
mockUseGetBucketCors.mockReturnValue(createMockQueryResult({
|
|
@@ -94,14 +103,32 @@ describe('BucketCorsPage', ()=>{
|
|
|
94
103
|
renderBucketCorsPage();
|
|
95
104
|
expect(screen.getByText('Loading CORS configuration...')).toBeInTheDocument();
|
|
96
105
|
});
|
|
97
|
-
it('shows create mode with
|
|
106
|
+
it('shows create mode with empty editor when no CORS exists', async ()=>{
|
|
98
107
|
mockNoCorsExists();
|
|
99
108
|
renderBucketCorsPage();
|
|
100
109
|
await waitFor(()=>{
|
|
101
110
|
expect(screen.getByText('Create Bucket CORS')).toBeInTheDocument();
|
|
102
111
|
});
|
|
103
112
|
const editor = screen.getByTestId('cors-editor');
|
|
104
|
-
expect(editor.value).
|
|
113
|
+
expect(editor.value).toBe('');
|
|
114
|
+
expect(screen.getByText('Empty CORS rule')).toBeInTheDocument();
|
|
115
|
+
});
|
|
116
|
+
it('loads standard template when clicking the template button', async ()=>{
|
|
117
|
+
mockNoCorsExists();
|
|
118
|
+
renderBucketCorsPage();
|
|
119
|
+
await waitFor(()=>{
|
|
120
|
+
expect(screen.getByText('Empty CORS rule')).toBeInTheDocument();
|
|
121
|
+
});
|
|
122
|
+
fireEvent.click(screen.getByRole('button', {
|
|
123
|
+
name: /Load a template/i
|
|
124
|
+
}));
|
|
125
|
+
await waitFor(()=>{
|
|
126
|
+
const editor = screen.getByTestId('cors-editor');
|
|
127
|
+
expect(editor.value).toContain('AllowedMethods');
|
|
128
|
+
expect(editor.value).toContain('GET');
|
|
129
|
+
expect(editor.value).toContain('PUT');
|
|
130
|
+
});
|
|
131
|
+
expect(screen.queryByText('Empty CORS rule')).not.toBeInTheDocument();
|
|
105
132
|
});
|
|
106
133
|
it('shows edit mode with existing CORS rules', async ()=>{
|
|
107
134
|
mockCorsExists();
|
|
@@ -248,6 +275,37 @@ describe('BucketCorsPage', ()=>{
|
|
|
248
275
|
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket');
|
|
249
276
|
});
|
|
250
277
|
});
|
|
278
|
+
it('renders nothing while ISV bucket status is loading', ()=>{
|
|
279
|
+
mockNoCorsExists();
|
|
280
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
281
|
+
isVeeamBucket: false,
|
|
282
|
+
isCommvaultBucket: false,
|
|
283
|
+
isKastenBucket: false,
|
|
284
|
+
isISVManaged: false,
|
|
285
|
+
isvApplication: void 0,
|
|
286
|
+
isLoading: true,
|
|
287
|
+
bucketTagsStatus: 'pending'
|
|
288
|
+
});
|
|
289
|
+
renderBucketCorsPage();
|
|
290
|
+
expect(screen.queryByText('Loading CORS configuration...')).not.toBeInTheDocument();
|
|
291
|
+
expect(screen.queryByTestId('cors-editor')).not.toBeInTheDocument();
|
|
292
|
+
});
|
|
293
|
+
it('redirects to bucket overview when bucket is ISV-managed', ()=>{
|
|
294
|
+
mockNoCorsExists();
|
|
295
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
296
|
+
isVeeamBucket: true,
|
|
297
|
+
isCommvaultBucket: false,
|
|
298
|
+
isKastenBucket: false,
|
|
299
|
+
isISVManaged: true,
|
|
300
|
+
isvApplication: 'Veeam',
|
|
301
|
+
isLoading: false,
|
|
302
|
+
bucketTagsStatus: 'success'
|
|
303
|
+
});
|
|
304
|
+
renderBucketCorsPage();
|
|
305
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket', {
|
|
306
|
+
replace: true
|
|
307
|
+
});
|
|
308
|
+
});
|
|
251
309
|
it('shows error message when fetching CORS fails with unexpected error', ()=>{
|
|
252
310
|
const accessDeniedError = new Error('Access Denied');
|
|
253
311
|
accessDeniedError.name = 'AccessDenied';
|
|
@@ -10,6 +10,7 @@ import * as __rspack_external__contexts_DataBrowserUICustomizationContext_js_f26
|
|
|
10
10
|
jest.mock('../../hooks');
|
|
11
11
|
jest.mock('../../hooks/useISVBucketDetection');
|
|
12
12
|
jest.mock('../../hooks/useFeatures');
|
|
13
|
+
jest.mock('../../hooks/useIsBucketEmpty');
|
|
13
14
|
const mockUseParams = jest.fn();
|
|
14
15
|
const mockUseNavigate = jest.fn();
|
|
15
16
|
jest.mock('react-router', ()=>({
|
|
@@ -51,6 +51,12 @@ const renderBucketLifecycleFormPage = (bucketName = 'test-bucket', ruleId)=>{
|
|
|
51
51
|
})
|
|
52
52
|
}));
|
|
53
53
|
};
|
|
54
|
+
const findStatusToggle = ()=>{
|
|
55
|
+
const label = document.querySelector('[for="status"]');
|
|
56
|
+
let current = label?.parentElement;
|
|
57
|
+
while(current && !current.querySelector('input[type="checkbox"]'))current = current.parentElement;
|
|
58
|
+
return current?.querySelector('input[type="checkbox"]');
|
|
59
|
+
};
|
|
54
60
|
describe('BucketLifecycleFormPage', ()=>{
|
|
55
61
|
const mockMutate = jest.fn();
|
|
56
62
|
const enableExpirationAction = async ()=>{
|
|
@@ -134,7 +140,7 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
134
140
|
it('renders required form fields in create mode', ()=>{
|
|
135
141
|
renderBucketLifecycleFormPage();
|
|
136
142
|
expect(screen.getByLabelText(/rule id/i)).toBeInTheDocument();
|
|
137
|
-
expect(
|
|
143
|
+
expect(document.querySelector('[id="label-status"]')).toBeInTheDocument();
|
|
138
144
|
expect(screen.getByLabelText(/filter/i)).toBeInTheDocument();
|
|
139
145
|
});
|
|
140
146
|
it('renders all lifecycle action toggles', ()=>{
|
|
@@ -358,11 +364,13 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
358
364
|
await waitFor(()=>{
|
|
359
365
|
expect(screen.getByText('Edit Lifecycle Rule')).toBeInTheDocument();
|
|
360
366
|
});
|
|
361
|
-
|
|
362
|
-
await
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
367
|
+
let statusToggle = null;
|
|
368
|
+
await waitFor(()=>{
|
|
369
|
+
statusToggle = findStatusToggle();
|
|
370
|
+
expect(statusToggle).toBeInTheDocument();
|
|
371
|
+
expect(statusToggle).toBeChecked();
|
|
372
|
+
});
|
|
373
|
+
fireEvent.click(statusToggle);
|
|
366
374
|
mockSuccessSubmit(mockMutate);
|
|
367
375
|
await submitForm('save');
|
|
368
376
|
await waitFor(()=>{
|
|
@@ -393,11 +401,8 @@ describe('BucketLifecycleFormPage', ()=>{
|
|
|
393
401
|
await waitFor(()=>{
|
|
394
402
|
expect(screen.getByText('Edit Lifecycle Rule')).toBeInTheDocument();
|
|
395
403
|
});
|
|
396
|
-
const
|
|
397
|
-
|
|
398
|
-
await user_event.click(screen.getByRole('option', {
|
|
399
|
-
name: 'Disabled'
|
|
400
|
-
}));
|
|
404
|
+
const statusToggle = findStatusToggle();
|
|
405
|
+
fireEvent.click(statusToggle);
|
|
401
406
|
mockSuccessSubmit(mockMutate);
|
|
402
407
|
await submitForm('save');
|
|
403
408
|
await waitFor(()=>{
|
|
@@ -3,12 +3,15 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
|
3
3
|
import user_event from "@testing-library/user-event";
|
|
4
4
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
5
5
|
import { useGetBucketNotification, useSetBucketNotification } from "../../hooks/index.js";
|
|
6
|
+
import { useSupportedNotificationEvents } from "../../hooks/useSupportedNotificationEvents.js";
|
|
6
7
|
import { createTestWrapper } from "../../test/testUtils.js";
|
|
7
8
|
import { BucketNotificationFormPage } from "../buckets/notifications/BucketNotificationFormPage.js";
|
|
8
9
|
jest.mock('../../hooks', ()=>({
|
|
9
10
|
useGetBucketNotification: jest.fn(),
|
|
10
11
|
useSetBucketNotification: jest.fn()
|
|
11
12
|
}));
|
|
13
|
+
jest.mock('../../hooks/useSupportedNotificationEvents');
|
|
14
|
+
const mockUseSupportedNotificationEvents = jest.mocked(useSupportedNotificationEvents);
|
|
12
15
|
const mockUseGetBucketNotification = jest.mocked(useGetBucketNotification);
|
|
13
16
|
const mockUseSetBucketNotification = jest.mocked(useSetBucketNotification);
|
|
14
17
|
const mockNavigate = jest.fn();
|
|
@@ -61,6 +64,7 @@ describe('BucketNotificationFormPage', ()=>{
|
|
|
61
64
|
mutate: mockMutate,
|
|
62
65
|
isPending: false
|
|
63
66
|
});
|
|
67
|
+
mockUseSupportedNotificationEvents.mockReturnValue(void 0);
|
|
64
68
|
});
|
|
65
69
|
describe('Create Mode', ()=>{
|
|
66
70
|
it('renders create notification form with all fields', ()=>{
|
|
@@ -379,5 +383,46 @@ describe('BucketNotificationFormPage', ()=>{
|
|
|
379
383
|
fireEvent.click(cancelButton);
|
|
380
384
|
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket?tab=notification');
|
|
381
385
|
});
|
|
386
|
+
it('filters out unsupported events when populating form in edit mode', async ()=>{
|
|
387
|
+
const configWithUnsupportedEvents = {
|
|
388
|
+
QueueConfigurations: [
|
|
389
|
+
{
|
|
390
|
+
Id: 'existing-rule',
|
|
391
|
+
QueueArn: 'arn:aws:sqs:us-east-1:123:existing-queue',
|
|
392
|
+
Events: [
|
|
393
|
+
's3:ObjectCreated:Put',
|
|
394
|
+
's3:ObjectRemoved:Delete',
|
|
395
|
+
's3:IntelligentTiering'
|
|
396
|
+
]
|
|
397
|
+
}
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
mockUseGetBucketNotification.mockReturnValue({
|
|
401
|
+
data: configWithUnsupportedEvents,
|
|
402
|
+
status: 'success'
|
|
403
|
+
});
|
|
404
|
+
mockUseSupportedNotificationEvents.mockReturnValue([
|
|
405
|
+
's3:ObjectCreated:*',
|
|
406
|
+
's3:ObjectCreated:Put',
|
|
407
|
+
's3:ObjectCreated:Post',
|
|
408
|
+
's3:ObjectCreated:Copy',
|
|
409
|
+
's3:ObjectCreated:CompleteMultipartUpload',
|
|
410
|
+
's3:ObjectRemoved:*',
|
|
411
|
+
's3:ObjectRemoved:Delete',
|
|
412
|
+
's3:ObjectRemoved:DeleteMarkerCreated'
|
|
413
|
+
]);
|
|
414
|
+
renderEditPage();
|
|
415
|
+
await waitFor(()=>{
|
|
416
|
+
expect(screen.getByRole('checkbox', {
|
|
417
|
+
name: 's3:ObjectCreated:Put'
|
|
418
|
+
})).toBeChecked();
|
|
419
|
+
});
|
|
420
|
+
expect(screen.getByRole('checkbox', {
|
|
421
|
+
name: 's3:ObjectRemoved:Delete'
|
|
422
|
+
})).toBeChecked();
|
|
423
|
+
expect(screen.queryByRole('checkbox', {
|
|
424
|
+
name: 's3:IntelligentTiering'
|
|
425
|
+
})).not.toBeInTheDocument();
|
|
426
|
+
});
|
|
382
427
|
});
|
|
383
428
|
});
|
|
@@ -4,6 +4,7 @@ import { MemoryRouter } from "react-router";
|
|
|
4
4
|
import { useGetBucketAcl, useGetBucketCors, useGetBucketLocation, useGetBucketObjectLockConfiguration, useGetBucketPolicy, useGetBucketTagging, useGetBucketVersioning, useISVBucketStatus } from "../../hooks/index.js";
|
|
5
5
|
import { useFeatures } from "../../hooks/useFeatures.js";
|
|
6
6
|
import { applyBucketMocks, createTestWrapper } from "../../test/testUtils.js";
|
|
7
|
+
import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
|
|
7
8
|
import { BucketOverview, useBucketOverviewContext } from "../buckets/BucketOverview.js";
|
|
8
9
|
import * as __rspack_external__contexts_DataBrowserUICustomizationContext_js_f267b01c from "../../contexts/DataBrowserUICustomizationContext.js";
|
|
9
10
|
jest.mock('../../hooks');
|
|
@@ -588,8 +589,7 @@ describe('BucketOverview', ()=>{
|
|
|
588
589
|
expect(errorElements.length).toBeGreaterThan(0);
|
|
589
590
|
});
|
|
590
591
|
it("shows 'Not configured' when policy does not exist (NoSuchBucketPolicy)", ()=>{
|
|
591
|
-
const noSuchPolicyError = new Error('Policy does not exist');
|
|
592
|
-
noSuchPolicyError.name = 'NoSuchBucketPolicy';
|
|
592
|
+
const noSuchPolicyError = new EnhancedS3Error('Policy does not exist', 'NoSuchBucketPolicy', ErrorCategory.NOT_FOUND, new Error('Policy does not exist'), 404);
|
|
593
593
|
mockUseGetBucketPolicy.mockReturnValue({
|
|
594
594
|
data: void 0,
|
|
595
595
|
error: noSuchPolicyError,
|
|
@@ -617,6 +617,96 @@ describe('BucketOverview', ()=>{
|
|
|
617
617
|
expect(onEditPolicy).toHaveBeenCalledWith('test-bucket');
|
|
618
618
|
});
|
|
619
619
|
});
|
|
620
|
+
describe('ISV-managed buckets', ()=>{
|
|
621
|
+
it('disables CORS edit button when bucket is ISV-managed', ()=>{
|
|
622
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
623
|
+
isVeeamBucket: true,
|
|
624
|
+
isCommvaultBucket: false,
|
|
625
|
+
isKastenBucket: false,
|
|
626
|
+
isISVManaged: true,
|
|
627
|
+
isvApplication: 'Veeam',
|
|
628
|
+
isLoading: false,
|
|
629
|
+
bucketTagsStatus: 'success'
|
|
630
|
+
});
|
|
631
|
+
mockUseGetBucketCors.mockReturnValue({
|
|
632
|
+
data: {
|
|
633
|
+
CORSRules: [
|
|
634
|
+
{
|
|
635
|
+
AllowedMethods: [
|
|
636
|
+
'GET'
|
|
637
|
+
],
|
|
638
|
+
AllowedOrigins: [
|
|
639
|
+
'*'
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
},
|
|
644
|
+
status: 'success',
|
|
645
|
+
error: null
|
|
646
|
+
});
|
|
647
|
+
renderBucketOverview();
|
|
648
|
+
const corsButton = screen.getByRole('button', {
|
|
649
|
+
name: /bucket CORS/i
|
|
650
|
+
});
|
|
651
|
+
expect(corsButton).toBeDisabled();
|
|
652
|
+
});
|
|
653
|
+
it('disables bucket policy edit button when bucket is ISV-managed', ()=>{
|
|
654
|
+
mockUseISVBucketStatus.mockReturnValue({
|
|
655
|
+
isVeeamBucket: false,
|
|
656
|
+
isCommvaultBucket: true,
|
|
657
|
+
isKastenBucket: false,
|
|
658
|
+
isISVManaged: true,
|
|
659
|
+
isvApplication: 'Commvault',
|
|
660
|
+
isLoading: false,
|
|
661
|
+
bucketTagsStatus: 'success'
|
|
662
|
+
});
|
|
663
|
+
mockUseGetBucketPolicy.mockReturnValue({
|
|
664
|
+
data: {
|
|
665
|
+
Policy: '{"Version":"2012-10-17","Statement":[]}'
|
|
666
|
+
},
|
|
667
|
+
error: null,
|
|
668
|
+
status: 'success'
|
|
669
|
+
});
|
|
670
|
+
renderBucketOverview();
|
|
671
|
+
const policyButton = screen.getByRole('button', {
|
|
672
|
+
name: /bucket policy/i
|
|
673
|
+
});
|
|
674
|
+
expect(policyButton).toBeDisabled();
|
|
675
|
+
expect(screen.getByText('Configured')).toBeInTheDocument();
|
|
676
|
+
});
|
|
677
|
+
it('enables CORS and policy edit buttons when bucket is not ISV-managed', ()=>{
|
|
678
|
+
mockUseGetBucketCors.mockReturnValue({
|
|
679
|
+
data: {
|
|
680
|
+
CORSRules: [
|
|
681
|
+
{
|
|
682
|
+
AllowedMethods: [
|
|
683
|
+
'GET'
|
|
684
|
+
],
|
|
685
|
+
AllowedOrigins: [
|
|
686
|
+
'*'
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
},
|
|
691
|
+
status: 'success',
|
|
692
|
+
error: null
|
|
693
|
+
});
|
|
694
|
+
mockUseGetBucketPolicy.mockReturnValue({
|
|
695
|
+
data: {
|
|
696
|
+
Policy: '{"Version":"2012-10-17","Statement":[]}'
|
|
697
|
+
},
|
|
698
|
+
error: null,
|
|
699
|
+
status: 'success'
|
|
700
|
+
});
|
|
701
|
+
renderBucketOverview();
|
|
702
|
+
expect(screen.getByRole('button', {
|
|
703
|
+
name: /bucket CORS/i
|
|
704
|
+
})).not.toBeDisabled();
|
|
705
|
+
expect(screen.getByRole('button', {
|
|
706
|
+
name: /bucket policy/i
|
|
707
|
+
})).not.toBeDisabled();
|
|
708
|
+
});
|
|
709
|
+
});
|
|
620
710
|
describe('Extra Sections', ()=>{
|
|
621
711
|
it('renders extra sections between General and Data Protection', ()=>{
|
|
622
712
|
const Wrapper = createTestWrapper();
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
3
|
import { MemoryRouter, Route, Routes } from "react-router";
|
|
4
|
-
import { useGetBucketPolicy, useISVBucketStatus, useSetBucketPolicy } from "../../hooks/index.js";
|
|
4
|
+
import { useDeleteBucketPolicy, useGetBucketPolicy, useISVBucketStatus, useSetBucketPolicy } from "../../hooks/index.js";
|
|
5
5
|
import { createMockMutationResult, createMockQueryResult, createTestWrapper } from "../../test/testUtils.js";
|
|
6
|
+
import { EnhancedS3Error, ErrorCategory } from "../../utils/errorHandling.js";
|
|
6
7
|
import { BucketPolicyPage } from "../buckets/BucketPolicyPage.js";
|
|
7
8
|
jest.mock('../../hooks', ()=>({
|
|
8
9
|
useGetBucketPolicy: jest.fn(),
|
|
9
10
|
useSetBucketPolicy: jest.fn(),
|
|
11
|
+
useDeleteBucketPolicy: jest.fn(),
|
|
10
12
|
useISVBucketStatus: jest.fn(),
|
|
11
13
|
useBucketConfigEditor: jest.requireActual('../../hooks').useBucketConfigEditor
|
|
12
14
|
}));
|
|
13
|
-
jest.mock('
|
|
15
|
+
jest.mock('@scality/core-ui/dist/next', ()=>({
|
|
16
|
+
...jest.requireActual('@scality/core-ui/dist/next'),
|
|
14
17
|
Editor: ({ value, onChange })=>/*#__PURE__*/ jsx("textarea", {
|
|
15
18
|
"data-testid": "policy-editor",
|
|
16
19
|
value: value,
|
|
@@ -19,13 +22,10 @@ jest.mock('../Editor', ()=>({
|
|
|
19
22
|
}));
|
|
20
23
|
const mockUseGetBucketPolicy = jest.mocked(useGetBucketPolicy);
|
|
21
24
|
const mockUseSetBucketPolicy = jest.mocked(useSetBucketPolicy);
|
|
25
|
+
const mockUseDeleteBucketPolicy = jest.mocked(useDeleteBucketPolicy);
|
|
22
26
|
const mockUseISVBucketStatus = jest.mocked(useISVBucketStatus);
|
|
23
27
|
const mockNavigate = jest.fn();
|
|
24
|
-
const createNoSuchPolicyError = ()=>
|
|
25
|
-
const error = new Error('Policy does not exist');
|
|
26
|
-
error.name = 'NoSuchBucketPolicy';
|
|
27
|
-
return error;
|
|
28
|
-
};
|
|
28
|
+
const createNoSuchPolicyError = ()=>new EnhancedS3Error('Policy does not exist', 'NoSuchBucketPolicy', ErrorCategory.NOT_FOUND, new Error('Policy does not exist'), 404);
|
|
29
29
|
const mockNoPolicyExists = ()=>{
|
|
30
30
|
mockUseGetBucketPolicy.mockReturnValue(createMockQueryResult({
|
|
31
31
|
status: 'error',
|
|
@@ -66,10 +66,12 @@ const renderBucketPolicyPage = (bucketName = 'test-bucket')=>{
|
|
|
66
66
|
};
|
|
67
67
|
describe('BucketPolicyPage', ()=>{
|
|
68
68
|
const mockMutate = jest.fn();
|
|
69
|
+
const mockDeleteMutate = jest.fn();
|
|
69
70
|
beforeEach(()=>{
|
|
70
71
|
jest.clearAllMocks();
|
|
71
72
|
mockNavigate.mockClear();
|
|
72
73
|
mockUseSetBucketPolicy.mockReturnValue(createMockMutationResult(mockMutate));
|
|
74
|
+
mockUseDeleteBucketPolicy.mockReturnValue(createMockMutationResult(mockDeleteMutate));
|
|
73
75
|
mockUseISVBucketStatus.mockReturnValue({
|
|
74
76
|
isVeeamBucket: false,
|
|
75
77
|
isCommvaultBucket: false,
|
|
@@ -87,14 +89,31 @@ describe('BucketPolicyPage', ()=>{
|
|
|
87
89
|
renderBucketPolicyPage();
|
|
88
90
|
expect(screen.getByText('Loading policy...')).toBeInTheDocument();
|
|
89
91
|
});
|
|
90
|
-
it('renders create mode with
|
|
92
|
+
it('renders create mode with empty editor when no policy exists', async ()=>{
|
|
91
93
|
mockNoPolicyExists();
|
|
92
94
|
renderBucketPolicyPage();
|
|
93
95
|
await waitFor(()=>{
|
|
94
96
|
expect(screen.getByText('Create Bucket Policy')).toBeInTheDocument();
|
|
95
97
|
});
|
|
96
98
|
const editor = screen.getByTestId('policy-editor');
|
|
97
|
-
expect(editor.value).
|
|
99
|
+
expect(editor.value).toBe('');
|
|
100
|
+
expect(screen.getByText('Empty bucket policy')).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
it('loads standard template when clicking the template button', async ()=>{
|
|
103
|
+
mockNoPolicyExists();
|
|
104
|
+
renderBucketPolicyPage();
|
|
105
|
+
await waitFor(()=>{
|
|
106
|
+
expect(screen.getByText('Empty bucket policy')).toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
fireEvent.click(screen.getByRole('button', {
|
|
109
|
+
name: /Load a template/i
|
|
110
|
+
}));
|
|
111
|
+
await waitFor(()=>{
|
|
112
|
+
const editor = screen.getByTestId('policy-editor');
|
|
113
|
+
expect(editor.value).toContain('ExampleStatement');
|
|
114
|
+
expect(editor.value).toContain('s3:GetObject');
|
|
115
|
+
});
|
|
116
|
+
expect(screen.queryByText('Empty bucket policy')).not.toBeInTheDocument();
|
|
98
117
|
});
|
|
99
118
|
it('renders edit mode with existing policy', async ()=>{
|
|
100
119
|
mockUseGetBucketPolicy.mockReturnValue(createMockQueryResult({
|
|
@@ -194,7 +213,43 @@ describe('BucketPolicyPage', ()=>{
|
|
|
194
213
|
});
|
|
195
214
|
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket');
|
|
196
215
|
});
|
|
197
|
-
it('
|
|
216
|
+
it('deletes bucket policy when saving empty content', async ()=>{
|
|
217
|
+
mockUseGetBucketPolicy.mockReturnValue(createMockQueryResult({
|
|
218
|
+
data: {
|
|
219
|
+
Policy: JSON.stringify(existingPolicy),
|
|
220
|
+
$metadata: {}
|
|
221
|
+
},
|
|
222
|
+
status: 'success'
|
|
223
|
+
}));
|
|
224
|
+
renderBucketPolicyPage();
|
|
225
|
+
const editor = await screen.findByTestId('policy-editor');
|
|
226
|
+
await waitFor(()=>{
|
|
227
|
+
expect(editor.value).toContain('ExistingStatement');
|
|
228
|
+
});
|
|
229
|
+
fireEvent.change(editor, {
|
|
230
|
+
target: {
|
|
231
|
+
value: ''
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
mockDeleteMutate.mockImplementation((_, options)=>{
|
|
235
|
+
options?.onSuccess?.();
|
|
236
|
+
});
|
|
237
|
+
await waitFor(()=>{
|
|
238
|
+
expect(screen.getByRole('button', {
|
|
239
|
+
name: /save/i
|
|
240
|
+
})).not.toBeDisabled();
|
|
241
|
+
});
|
|
242
|
+
fireEvent.click(screen.getByRole('button', {
|
|
243
|
+
name: /save/i
|
|
244
|
+
}));
|
|
245
|
+
await waitFor(()=>{
|
|
246
|
+
expect(mockDeleteMutate).toHaveBeenCalledWith({
|
|
247
|
+
Bucket: 'test-bucket'
|
|
248
|
+
}, expect.any(Object));
|
|
249
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket');
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
it('renders nothing while ISV bucket status is loading', ()=>{
|
|
198
253
|
mockUseGetBucketPolicy.mockReturnValue(createMockQueryResult({
|
|
199
254
|
status: 'success',
|
|
200
255
|
data: {
|
|
@@ -212,9 +267,10 @@ describe('BucketPolicyPage', ()=>{
|
|
|
212
267
|
bucketTagsStatus: 'pending'
|
|
213
268
|
});
|
|
214
269
|
renderBucketPolicyPage();
|
|
215
|
-
expect(screen.
|
|
270
|
+
expect(screen.queryByText('Loading policy...')).not.toBeInTheDocument();
|
|
271
|
+
expect(screen.queryByTestId('policy-editor')).not.toBeInTheDocument();
|
|
216
272
|
});
|
|
217
|
-
it('
|
|
273
|
+
it('redirects to bucket overview when bucket is ISV-managed', ()=>{
|
|
218
274
|
mockNoPolicyExists();
|
|
219
275
|
mockUseISVBucketStatus.mockReturnValue({
|
|
220
276
|
isVeeamBucket: true,
|
|
@@ -226,45 +282,8 @@ describe('BucketPolicyPage', ()=>{
|
|
|
226
282
|
bucketTagsStatus: 'success'
|
|
227
283
|
});
|
|
228
284
|
renderBucketPolicyPage();
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
});
|
|
232
|
-
expect(screen.getByText(/Warning:/i)).toBeInTheDocument();
|
|
233
|
-
expect(screen.getByText(/Veeam/i)).toBeInTheDocument();
|
|
234
|
-
});
|
|
235
|
-
it('displays warning banner when bucket is managed by Commvault', async ()=>{
|
|
236
|
-
mockNoPolicyExists();
|
|
237
|
-
mockUseISVBucketStatus.mockReturnValue({
|
|
238
|
-
isVeeamBucket: false,
|
|
239
|
-
isCommvaultBucket: true,
|
|
240
|
-
isKastenBucket: false,
|
|
241
|
-
isISVManaged: true,
|
|
242
|
-
isvApplication: 'Commvault',
|
|
243
|
-
isLoading: false,
|
|
244
|
-
bucketTagsStatus: 'success'
|
|
245
|
-
});
|
|
246
|
-
renderBucketPolicyPage();
|
|
247
|
-
await waitFor(()=>{
|
|
248
|
-
expect(screen.getByTestId('policy-editor')).toBeInTheDocument();
|
|
249
|
-
});
|
|
250
|
-
expect(screen.getByText(/Warning:/i)).toBeInTheDocument();
|
|
251
|
-
expect(screen.getByText(/Commvault/i)).toBeInTheDocument();
|
|
252
|
-
});
|
|
253
|
-
it('does not display warning banner when bucket is not ISV managed', async ()=>{
|
|
254
|
-
mockNoPolicyExists();
|
|
255
|
-
mockUseISVBucketStatus.mockReturnValue({
|
|
256
|
-
isVeeamBucket: false,
|
|
257
|
-
isCommvaultBucket: false,
|
|
258
|
-
isKastenBucket: false,
|
|
259
|
-
isISVManaged: false,
|
|
260
|
-
isvApplication: void 0,
|
|
261
|
-
isLoading: false,
|
|
262
|
-
bucketTagsStatus: 'success'
|
|
263
|
-
});
|
|
264
|
-
renderBucketPolicyPage();
|
|
265
|
-
await waitFor(()=>{
|
|
266
|
-
expect(screen.getByTestId('policy-editor')).toBeInTheDocument();
|
|
285
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket', {
|
|
286
|
+
replace: true
|
|
267
287
|
});
|
|
268
|
-
expect(screen.queryByText((_content, element)=>element?.textContent?.includes('This bucket is managed by') || false)).not.toBeInTheDocument();
|
|
269
288
|
});
|
|
270
289
|
});
|
|
@@ -56,6 +56,12 @@ const renderBucketReplicationFormPage = (bucketName = 'test-bucket', ruleId)=>{
|
|
|
56
56
|
})
|
|
57
57
|
}));
|
|
58
58
|
};
|
|
59
|
+
const findStatusToggle = ()=>{
|
|
60
|
+
const label = document.querySelector('[for="status"]');
|
|
61
|
+
let current = label?.parentElement;
|
|
62
|
+
while(current && !current.querySelector('input[type="checkbox"]'))current = current.parentElement;
|
|
63
|
+
return current?.querySelector('input[type="checkbox"]');
|
|
64
|
+
};
|
|
59
65
|
describe('BucketReplicationFormPage', ()=>{
|
|
60
66
|
const mockMutate = jest.fn();
|
|
61
67
|
const fillRequiredFields = async (options)=>{
|
|
@@ -185,7 +191,7 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
185
191
|
expect(screen.getByRole('textbox', {
|
|
186
192
|
name: /rule id/i
|
|
187
193
|
})).toBeInTheDocument();
|
|
188
|
-
expect(
|
|
194
|
+
expect(document.querySelector('[id="label-status"]')).toBeInTheDocument();
|
|
189
195
|
expect(screen.getByLabelText(/target bucket/i)).toBeInTheDocument();
|
|
190
196
|
});
|
|
191
197
|
it('shows Role ARN input when no existing rules', ()=>{
|
|
@@ -256,15 +262,15 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
256
262
|
name: /rule id/i
|
|
257
263
|
})).not.toBeInTheDocument();
|
|
258
264
|
});
|
|
259
|
-
it('renders Status
|
|
265
|
+
it('renders Status toggle field', async ()=>{
|
|
260
266
|
renderBucketReplicationFormPage();
|
|
261
267
|
await waitFor(()=>{
|
|
262
268
|
expect(screen.getByRole('textbox', {
|
|
263
269
|
name: /rule id/i
|
|
264
270
|
})).toBeInTheDocument();
|
|
265
271
|
});
|
|
266
|
-
const
|
|
267
|
-
expect(
|
|
272
|
+
const statusToggle = findStatusToggle();
|
|
273
|
+
expect(statusToggle).toBeInTheDocument();
|
|
268
274
|
});
|
|
269
275
|
it('renders Priority number input with auto-assigned placeholder', ()=>{
|
|
270
276
|
renderBucketReplicationFormPage();
|
|
@@ -892,11 +898,8 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
892
898
|
await waitFor(()=>{
|
|
893
899
|
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
894
900
|
});
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
await user_event.click(screen.getByRole('option', {
|
|
898
|
-
name: 'Disabled'
|
|
899
|
-
}));
|
|
901
|
+
const statusToggle = findStatusToggle();
|
|
902
|
+
fireEvent.click(statusToggle);
|
|
900
903
|
mockMutate.mockImplementation((_, options)=>{
|
|
901
904
|
options?.onSuccess?.();
|
|
902
905
|
});
|
|
@@ -942,11 +945,8 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
942
945
|
await waitFor(()=>{
|
|
943
946
|
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
944
947
|
});
|
|
945
|
-
const
|
|
946
|
-
|
|
947
|
-
await user_event.click(screen.getByRole('option', {
|
|
948
|
-
name: 'Disabled'
|
|
949
|
-
}));
|
|
948
|
+
const statusToggle = findStatusToggle();
|
|
949
|
+
fireEvent.click(statusToggle);
|
|
950
950
|
mockMutate.mockImplementation((_, options)=>{
|
|
951
951
|
options?.onSuccess?.();
|
|
952
952
|
});
|
|
@@ -987,11 +987,8 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
987
987
|
await waitFor(()=>{
|
|
988
988
|
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
989
989
|
});
|
|
990
|
-
const
|
|
991
|
-
|
|
992
|
-
await user_event.click(screen.getByRole('option', {
|
|
993
|
-
name: 'Disabled'
|
|
994
|
-
}));
|
|
990
|
+
const statusToggle = findStatusToggle();
|
|
991
|
+
fireEvent.click(statusToggle);
|
|
995
992
|
mockMutate.mockImplementation((_, options)=>{
|
|
996
993
|
options?.onSuccess?.();
|
|
997
994
|
});
|
|
@@ -1036,11 +1033,8 @@ describe('BucketReplicationFormPage', ()=>{
|
|
|
1036
1033
|
await waitFor(()=>{
|
|
1037
1034
|
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
1038
1035
|
});
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
await user_event.click(screen.getByRole('option', {
|
|
1042
|
-
name: 'Disabled'
|
|
1043
|
-
}));
|
|
1036
|
+
const statusToggle = findStatusToggle();
|
|
1037
|
+
fireEvent.click(statusToggle);
|
|
1044
1038
|
const error = new Error('Network Error');
|
|
1045
1039
|
mockMutate.mockImplementation((_, options)=>{
|
|
1046
1040
|
options?.onError?.(error);
|