@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.
Files changed (44) hide show
  1. package/dist/components/__tests__/BucketCorsPage.test.js +67 -9
  2. package/dist/components/__tests__/BucketDetails.test.js +1 -0
  3. package/dist/components/__tests__/BucketLifecycleFormPage.test.js +16 -11
  4. package/dist/components/__tests__/BucketNotificationFormPage.test.js +45 -0
  5. package/dist/components/__tests__/BucketOverview.test.js +92 -2
  6. package/dist/components/__tests__/BucketPolicyPage.test.js +70 -51
  7. package/dist/components/__tests__/BucketReplicationFormPage.test.js +18 -24
  8. package/dist/components/__tests__/ObjectList.test.js +43 -2
  9. package/dist/components/buckets/BucketConfigEditButton.d.ts +2 -0
  10. package/dist/components/buckets/BucketConfigEditButton.js +9 -3
  11. package/dist/components/buckets/BucketCorsPage.js +57 -20
  12. package/dist/components/buckets/BucketDetails.js +27 -2
  13. package/dist/components/buckets/BucketLifecycleFormPage.js +310 -270
  14. package/dist/components/buckets/BucketOverview.js +21 -18
  15. package/dist/components/buckets/BucketPolicyPage.js +119 -83
  16. package/dist/components/buckets/BucketReplicationFormPage.js +39 -29
  17. package/dist/components/buckets/BucketVersioning.js +16 -10
  18. package/dist/components/buckets/__tests__/BucketVersioning.test.js +76 -23
  19. package/dist/components/buckets/notifications/BucketNotificationFormPage.js +13 -5
  20. package/dist/components/objects/ObjectList.js +22 -25
  21. package/dist/components/objects/ObjectLock/EditRetentionButton.js +2 -2
  22. package/dist/components/objects/UploadButton.js +25 -15
  23. package/dist/config/__tests__/resolveBrandingTheme.test.d.ts +1 -0
  24. package/dist/config/__tests__/resolveBrandingTheme.test.js +96 -0
  25. package/dist/config/resolveBrandingTheme.d.ts +16 -0
  26. package/dist/config/resolveBrandingTheme.js +23 -0
  27. package/dist/config/types.d.ts +36 -0
  28. package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +2 -0
  29. package/dist/hooks/index.d.ts +1 -1
  30. package/dist/hooks/objectOperations.d.ts +3 -3
  31. package/dist/hooks/objectOperations.js +3 -3
  32. package/dist/hooks/useBucketConfigEditor.d.ts +4 -4
  33. package/dist/hooks/useBucketConfigEditor.js +16 -31
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +1 -0
  36. package/dist/test/mocks/esmOnlyModules.js +4 -0
  37. package/dist/types/index.d.ts +0 -1
  38. package/dist/utils/__tests__/proxyMiddleware.test.js +34 -0
  39. package/dist/utils/proxyMiddleware.js +2 -0
  40. package/package.json +4 -4
  41. package/dist/components/Editor.d.ts +0 -12
  42. package/dist/components/Editor.js +0 -28
  43. package/dist/types/monaco.d.ts +0 -13
  44. 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('../Editor', ()=>({
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 default template when no CORS exists', async ()=>{
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).toContain('AllowedHeaders');
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(screen.getByLabelText(/status/i)).toBeInTheDocument();
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
- const statusSelect = screen.getByLabelText(/status/i);
362
- await user_event.click(statusSelect);
363
- await user_event.click(screen.getByRole('option', {
364
- name: 'Disabled'
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 statusSelect = screen.getByLabelText(/status/i);
397
- await user_event.click(statusSelect);
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('../Editor', ()=>({
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 default policy template when no policy exists', async ()=>{
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).toContain('ExampleStatement');
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('shows loading state when ISV bucket status is loading', ()=>{
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.getByText('Loading policy...')).toBeInTheDocument();
270
+ expect(screen.queryByText('Loading policy...')).not.toBeInTheDocument();
271
+ expect(screen.queryByTestId('policy-editor')).not.toBeInTheDocument();
216
272
  });
217
- it('displays warning banner when bucket is managed by Veeam', async ()=>{
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
- await waitFor(()=>{
230
- expect(screen.getByTestId('policy-editor')).toBeInTheDocument();
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(screen.getByLabelText(/status/i)).toBeInTheDocument();
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 select field', async ()=>{
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 statusElement = document.querySelector('[id="status"]');
267
- expect(statusElement).toBeInTheDocument();
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 statusSelect = screen.getByLabelText(/status/i);
896
- await user_event.click(statusSelect);
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 statusSelect = screen.getByLabelText(/status/i);
946
- await user_event.click(statusSelect);
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 statusSelect = screen.getByLabelText(/status/i);
991
- await user_event.click(statusSelect);
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 statusSelect = screen.getByLabelText(/status/i);
1040
- await user_event.click(statusSelect);
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);