@scality/data-browser-library 1.0.8 → 1.1.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/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 +51 -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.d.ts +1 -0
- package/dist/components/buckets/BucketReplicationFormPage.js +165 -129
- 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/index.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +3 -3
- package/dist/components/objects/ObjectList.js +22 -25
- package/dist/components/objects/ObjectLock/EditRetentionButton.js +2 -2
- package/dist/config/types.d.ts +11 -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/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
|
});
|