@scality/data-browser-library 1.0.0-preview.8 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/DataBrowserUI.d.ts +12 -0
- package/dist/components/DataBrowserUI.js +99 -0
- package/dist/components/Editor.d.ts +1 -1
- package/dist/components/Editor.js +3 -3
- package/dist/components/__tests__/BucketAccessor.test.js +214 -0
- package/dist/components/__tests__/BucketCorsPage.test.d.ts +1 -0
- package/dist/components/__tests__/BucketCorsPage.test.js +263 -0
- package/dist/components/__tests__/BucketCreate.test.d.ts +1 -0
- package/dist/components/__tests__/BucketCreate.test.js +574 -0
- package/dist/components/__tests__/BucketDetails.test.d.ts +1 -0
- package/dist/components/__tests__/BucketDetails.test.js +421 -0
- package/dist/components/__tests__/BucketLifecycleFormPage.test.d.ts +14 -0
- package/dist/components/__tests__/BucketLifecycleFormPage.test.js +618 -0
- package/dist/components/__tests__/BucketLifecycleList.test.d.ts +1 -0
- package/dist/components/__tests__/BucketLifecycleList.test.js +325 -0
- package/dist/components/__tests__/BucketList.test.js +495 -81
- package/dist/components/__tests__/BucketNotificationFormPage.test.d.ts +1 -0
- package/dist/components/__tests__/BucketNotificationFormPage.test.js +348 -0
- package/dist/components/__tests__/BucketNotificationList.test.d.ts +1 -0
- package/dist/components/__tests__/BucketNotificationList.test.js +379 -0
- package/dist/components/__tests__/BucketOverview.test.js +484 -179
- package/dist/components/__tests__/BucketPolicyPage.test.js +151 -99
- package/dist/components/__tests__/BucketReplicationFormPage.test.d.ts +16 -0
- package/dist/components/__tests__/BucketReplicationFormPage.test.js +1757 -0
- package/dist/components/__tests__/BucketReplicationList.test.d.ts +1 -0
- package/dist/components/__tests__/BucketReplicationList.test.js +344 -0
- package/dist/components/__tests__/CreateFolderButton.test.js +56 -56
- package/dist/components/__tests__/DeleteBucketButton.test.js +64 -64
- package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.d.ts +1 -0
- package/dist/components/__tests__/DeleteBucketConfigRuleButton.test.js +196 -0
- package/dist/components/__tests__/DeleteObjectButton.test.js +64 -64
- package/dist/components/__tests__/EmptyBucketButton.test.d.ts +1 -0
- package/dist/components/__tests__/EmptyBucketButton.test.js +302 -0
- package/dist/components/__tests__/MetadataSearch.test.js +65 -65
- package/dist/components/__tests__/ObjectList.test.js +741 -240
- package/dist/components/__tests__/UploadButton.test.js +45 -45
- package/dist/components/breadcrumb/Breadcrumb.d.ts +6 -0
- package/dist/components/breadcrumb/Breadcrumb.js +37 -0
- package/dist/components/breadcrumb/DataBrowserBreadcrumb.d.ts +1 -0
- package/dist/components/breadcrumb/DataBrowserBreadcrumb.js +10 -0
- package/dist/components/breadcrumb/__tests__/Breadcrumb.test.d.ts +1 -0
- package/dist/components/breadcrumb/__tests__/Breadcrumb.test.js +196 -0
- package/dist/components/breadcrumb/__tests__/DataBrowserBreadcrumb.test.d.ts +1 -0
- package/dist/components/breadcrumb/__tests__/DataBrowserBreadcrumb.test.js +153 -0
- package/dist/components/breadcrumb/__tests__/useBreadcrumbPaths.test.d.ts +1 -0
- package/dist/components/breadcrumb/__tests__/useBreadcrumbPaths.test.js +134 -0
- package/dist/components/breadcrumb/index.d.ts +8 -0
- package/dist/components/breadcrumb/index.js +4 -0
- package/dist/components/breadcrumb/useBreadcrumbPaths.d.ts +2 -0
- package/dist/components/breadcrumb/useBreadcrumbPaths.js +82 -0
- package/dist/components/buckets/BucketAccessor.d.ts +2 -0
- package/dist/components/buckets/BucketAccessor.js +125 -0
- package/dist/components/buckets/BucketConfigEditButton.d.ts +8 -0
- package/dist/components/buckets/{BucketPolicyButton.js → BucketConfigEditButton.js} +9 -5
- package/dist/components/buckets/BucketCorsPage.d.ts +1 -0
- package/dist/components/buckets/BucketCorsPage.js +234 -0
- package/dist/components/buckets/BucketCreate.d.ts +50 -0
- package/dist/components/buckets/BucketCreate.js +279 -0
- package/dist/components/buckets/BucketDetails.d.ts +42 -0
- package/dist/components/buckets/BucketDetails.js +256 -40
- package/dist/components/buckets/BucketLifecycleFormPage.d.ts +15 -0
- package/dist/components/buckets/BucketLifecycleFormPage.js +1086 -0
- package/dist/components/buckets/BucketLifecycleList.d.ts +10 -0
- package/dist/components/buckets/BucketLifecycleList.js +270 -0
- package/dist/components/buckets/BucketList.d.ts +6 -4
- package/dist/components/buckets/BucketList.js +161 -94
- package/dist/components/buckets/BucketLocation.js +4 -4
- package/dist/components/buckets/BucketOverview.d.ts +86 -5
- package/dist/components/buckets/BucketOverview.js +481 -192
- package/dist/components/buckets/BucketPage.js +44 -22
- package/dist/components/buckets/BucketPolicyPage.js +155 -127
- package/dist/components/buckets/BucketReplicationFormPage.d.ts +1 -0
- package/dist/components/buckets/BucketReplicationFormPage.js +835 -0
- package/dist/components/buckets/BucketReplicationList.d.ts +11 -0
- package/dist/components/buckets/BucketReplicationList.js +189 -0
- package/dist/components/buckets/BucketVersioning.d.ts +4 -0
- package/dist/components/buckets/BucketVersioning.js +76 -0
- package/dist/components/buckets/DeleteBucketButton.js +8 -8
- package/dist/components/buckets/DeleteBucketConfigRuleButton.d.ts +18 -0
- package/dist/components/buckets/DeleteBucketConfigRuleButton.js +53 -0
- package/dist/components/buckets/EmptyBucketButton.d.ts +5 -0
- package/dist/components/buckets/EmptyBucketButton.js +232 -0
- package/dist/components/buckets/EmptyBucketSummary.d.ts +9 -0
- package/dist/components/buckets/EmptyBucketSummary.js +60 -0
- package/dist/components/buckets/EmptyBucketSummaryList.d.ts +13 -0
- package/dist/components/buckets/EmptyBucketSummaryList.js +140 -0
- package/dist/components/buckets/__tests__/BucketVersioning.test.d.ts +1 -0
- package/dist/components/buckets/__tests__/BucketVersioning.test.js +163 -0
- package/dist/components/buckets/notifications/BucketNotificationFormPage.d.ts +1 -0
- package/dist/components/buckets/notifications/BucketNotificationFormPage.js +316 -0
- package/dist/components/buckets/notifications/BucketNotificationList.d.ts +10 -0
- package/dist/components/buckets/notifications/BucketNotificationList.js +267 -0
- package/dist/components/buckets/notifications/EventsSection.js +145 -29
- package/dist/components/buckets/notifications/__tests__/events.test.d.ts +1 -0
- package/dist/components/buckets/notifications/__tests__/events.test.js +56 -0
- package/dist/components/buckets/notifications/events.d.ts +71 -7
- package/dist/components/buckets/notifications/events.js +98 -16
- package/dist/components/index.d.ts +27 -13
- package/dist/components/index.js +20 -6
- package/dist/components/layouts/ArrowNavigation.d.ts +3 -0
- package/dist/components/layouts/ArrowNavigation.js +28 -0
- package/dist/components/layouts/BrowserPageLayout.d.ts +5 -1
- package/dist/components/layouts/BrowserPageLayout.js +10 -5
- package/dist/components/objects/CreateFolderButton.d.ts +2 -2
- package/dist/components/objects/CreateFolderButton.js +12 -12
- package/dist/components/objects/DeleteObjectButton.d.ts +1 -1
- package/dist/components/objects/DeleteObjectButton.js +19 -21
- package/dist/components/objects/GetPresignedUrlButton.d.ts +7 -0
- package/dist/components/objects/GetPresignedUrlButton.js +255 -0
- package/dist/components/objects/ObjectDetails/ObjectMetadata.d.ts +2 -2
- package/dist/components/objects/ObjectDetails/ObjectMetadata.js +263 -230
- package/dist/components/objects/ObjectDetails/ObjectSummary.d.ts +2 -2
- package/dist/components/objects/ObjectDetails/ObjectSummary.js +540 -138
- package/dist/components/objects/ObjectDetails/ObjectTags.d.ts +2 -2
- package/dist/components/objects/ObjectDetails/ObjectTags.js +95 -123
- package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.d.ts +1 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectDetails.test.js +516 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.d.ts +1 -0
- package/dist/components/objects/ObjectDetails/__tests__/ObjectSummary.test.js +1064 -0
- package/dist/components/objects/ObjectDetails/index.d.ts +18 -2
- package/dist/components/objects/ObjectDetails/index.js +152 -40
- package/dist/components/objects/ObjectList.d.ts +12 -10
- package/dist/components/objects/ObjectList.js +590 -263
- package/dist/components/objects/ObjectLock/EditRetentionButton.d.ts +4 -0
- package/dist/components/objects/ObjectLock/EditRetentionButton.js +32 -0
- package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.d.ts +3 -0
- package/dist/components/objects/ObjectLock/ObjectLockRetentionSettings.js +211 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettings.d.ts +9 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettings.js +159 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.d.ts +8 -0
- package/dist/components/objects/ObjectLock/ObjectLockSettingsUtils.js +39 -0
- package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.d.ts +1 -0
- package/dist/components/objects/ObjectLock/__tests__/EditRetentionButton.test.js +204 -0
- package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.d.ts +1 -0
- package/dist/components/objects/ObjectLock/__tests__/ObjectLockSettings.test.js +374 -0
- package/dist/components/objects/ObjectPage.js +12 -8
- package/dist/components/objects/UploadButton.d.ts +3 -3
- package/dist/components/objects/UploadButton.js +10 -10
- package/dist/components/objects/__tests__/GetPresignedUrlButton.test.d.ts +1 -0
- package/dist/components/objects/__tests__/GetPresignedUrlButton.test.js +531 -0
- package/dist/components/providers/DataBrowserProvider.d.ts +23 -12
- package/dist/components/providers/DataBrowserProvider.js +60 -38
- package/dist/components/providers/QueryProvider.d.ts +9 -0
- package/dist/components/providers/QueryProvider.js +21 -0
- package/dist/components/search/MetadataSearch.js +29 -28
- package/dist/components/search/SearchHints.js +1 -1
- package/dist/components/ui/ArrayFieldActions.d.ts +36 -0
- package/dist/components/ui/ArrayFieldActions.js +43 -0
- package/dist/components/ui/ConfirmDeleteRuleModal.d.ts +16 -0
- package/dist/components/ui/ConfirmDeleteRuleModal.js +48 -0
- package/dist/components/ui/DeleteObjectModalContent.d.ts +1 -1
- package/dist/components/ui/DeleteObjectModalContent.js +12 -12
- package/dist/components/ui/FilterFormSection.d.ts +44 -0
- package/dist/components/ui/FilterFormSection.js +159 -0
- package/dist/components/ui/Search.elements.d.ts +2 -2
- package/dist/components/ui/Search.elements.js +7 -7
- package/dist/components/ui/Table.elements.d.ts +2 -1
- package/dist/components/ui/Table.elements.js +18 -12
- package/dist/config/__tests__/factory.test.d.ts +1 -0
- package/dist/config/__tests__/factory.test.js +311 -0
- package/dist/config/factory.d.ts +14 -49
- package/dist/config/factory.js +23 -68
- package/dist/config/types.d.ts +212 -34
- package/dist/contexts/DataBrowserUICustomizationContext.d.ts +27 -0
- package/dist/contexts/DataBrowserUICustomizationContext.js +13 -0
- package/dist/hooks/__tests__/useAccessibleBuckets.test.d.ts +1 -0
- package/dist/hooks/__tests__/useAccessibleBuckets.test.js +145 -0
- package/dist/hooks/__tests__/useISVBucketDetection.test.d.ts +1 -0
- package/dist/hooks/__tests__/useISVBucketDetection.test.js +188 -0
- package/dist/hooks/__tests__/useIsBucketEmpty.test.js +27 -27
- package/dist/hooks/__tests__/useLoginMutation.test.d.ts +1 -0
- package/dist/hooks/__tests__/useLoginMutation.test.js +194 -0
- package/dist/hooks/bucketConfiguration.d.ts +8 -1
- package/dist/hooks/bucketConfiguration.js +52 -51
- package/dist/hooks/bucketOperations.d.ts +10 -1
- package/dist/hooks/bucketOperations.js +10 -9
- package/dist/hooks/factories/__tests__/useCreateS3FunctionMutationHook.test.js +80 -80
- package/dist/hooks/factories/__tests__/useCreateS3InfiniteQueryHook.test.js +80 -80
- package/dist/hooks/factories/__tests__/useCreateS3LoginHook.test.js +44 -44
- package/dist/hooks/factories/__tests__/useCreateS3MutationHook.test.js +63 -63
- package/dist/hooks/factories/__tests__/useCreateS3QueryHook.test.js +95 -52
- package/dist/hooks/factories/index.d.ts +4 -4
- package/dist/hooks/factories/index.js +2 -2
- package/dist/hooks/factories/useCreateS3InfiniteQueryHook.d.ts +2 -2
- package/dist/hooks/factories/useCreateS3InfiniteQueryHook.js +16 -13
- package/dist/hooks/factories/useCreateS3LoginHook.d.ts +2 -2
- package/dist/hooks/factories/useCreateS3LoginHook.js +1 -1
- package/dist/hooks/factories/useCreateS3MutationHook.d.ts +3 -3
- package/dist/hooks/factories/useCreateS3MutationHook.js +7 -2
- package/dist/hooks/factories/useCreateS3QueryHook.d.ts +2 -2
- package/dist/hooks/factories/useCreateS3QueryHook.js +29 -3
- package/dist/hooks/index.d.ts +19 -8
- package/dist/hooks/index.js +16 -5
- package/dist/hooks/loginOperations.d.ts +1 -1
- package/dist/hooks/loginOperations.js +1 -1
- package/dist/hooks/objectOperations.d.ts +2 -2
- package/dist/hooks/objectOperations.js +50 -49
- package/dist/hooks/presignedOperations.d.ts +4 -4
- package/dist/hooks/presignedOperations.js +5 -5
- package/dist/hooks/useAccessibleBuckets.d.ts +11 -0
- package/dist/hooks/useAccessibleBuckets.js +115 -0
- package/dist/hooks/useBatchObjectLegalHold.d.ts +11 -0
- package/dist/hooks/useBatchObjectLegalHold.js +48 -0
- package/dist/hooks/useBucketConfigEditor.d.ts +31 -0
- package/dist/hooks/useBucketConfigEditor.js +82 -0
- package/dist/hooks/useDataBrowserNavigate.d.ts +28 -0
- package/dist/hooks/useDataBrowserNavigate.js +24 -0
- package/dist/hooks/useDeleteBucketConfigRule.d.ts +26 -0
- package/dist/hooks/useDeleteBucketConfigRule.js +46 -0
- package/dist/hooks/useEmptyBucket.d.ts +27 -0
- package/dist/hooks/useEmptyBucket.js +116 -0
- package/dist/hooks/useFeatures.d.ts +7 -0
- package/dist/hooks/useFeatures.js +8 -0
- package/dist/hooks/useISVBucketDetection.d.ts +15 -0
- package/dist/hooks/useISVBucketDetection.js +27 -0
- package/dist/hooks/useIsBucketEmpty.js +4 -4
- package/dist/hooks/useLimitedAccessFlow.d.ts +48 -0
- package/dist/hooks/useLimitedAccessFlow.js +23 -0
- package/dist/hooks/useS3Client.d.ts +6 -0
- package/dist/hooks/useS3Client.js +3 -2
- package/dist/hooks/useS3ConfigSwitch.d.ts +11 -0
- package/dist/hooks/useS3ConfigSwitch.js +37 -0
- package/dist/hooks/useSupportedNotificationEvents.d.ts +6 -0
- package/dist/hooks/useSupportedNotificationEvents.js +8 -0
- package/dist/hooks/useTableRowSelection.d.ts +9 -0
- package/dist/hooks/useTableRowSelection.js +45 -0
- package/dist/index.d.ts +6 -6
- package/dist/index.js +2 -2
- package/dist/schemas/bucketPolicySchema.json +3 -13
- package/dist/test/msw/handlers/deleteBucket.d.ts +1 -1
- package/dist/test/msw/handlers/deleteBucket.js +20 -10
- package/dist/test/msw/handlers/getBucketAcl.d.ts +1 -1
- package/dist/test/msw/handlers/getBucketAcl.js +29 -17
- package/dist/test/msw/handlers/getBucketLocation.d.ts +1 -1
- package/dist/test/msw/handlers/getBucketLocation.js +29 -15
- package/dist/test/msw/handlers/getBucketPolicy.d.ts +1 -1
- package/dist/test/msw/handlers/getBucketPolicy.js +52 -32
- package/dist/test/msw/handlers/headObject.d.ts +1 -1
- package/dist/test/msw/handlers/headObject.js +31 -13
- package/dist/test/msw/handlers/listBuckets.d.ts +1 -1
- package/dist/test/msw/handlers/listBuckets.js +5 -3
- package/dist/test/msw/handlers/listObjectVersions.d.ts +1 -1
- package/dist/test/msw/handlers/listObjectVersions.js +38 -26
- package/dist/test/msw/handlers/listObjects.d.ts +1 -1
- package/dist/test/msw/handlers/listObjects.js +35 -23
- package/dist/test/msw/handlers/objectLegalHold.d.ts +1 -1
- package/dist/test/msw/handlers/objectLegalHold.js +32 -17
- package/dist/test/msw/handlers/objectRetention.d.ts +1 -1
- package/dist/test/msw/handlers/objectRetention.js +31 -17
- package/dist/test/msw/handlers/putBucketAcl.d.ts +1 -1
- package/dist/test/msw/handlers/putBucketAcl.js +29 -14
- package/dist/test/msw/handlers/putObject.d.ts +1 -1
- package/dist/test/msw/handlers/putObject.js +27 -12
- package/dist/test/msw/handlers.d.ts +3 -3
- package/dist/test/msw/handlers.js +77 -54
- package/dist/test/msw/index.d.ts +2 -2
- package/dist/test/msw/index.js +1 -1
- package/dist/test/msw/server.d.ts +1 -1
- package/dist/test/msw/server.js +1 -1
- package/dist/test/msw/utils.js +2 -2
- package/dist/test/setup.d.ts +1 -1
- package/dist/test/setup.js +13 -30
- package/dist/test/testUtils.d.ts +170 -36
- package/dist/test/testUtils.js +229 -116
- package/dist/test/utils/errorHandling.test.js +146 -108
- package/dist/types/index.d.ts +49 -36
- package/dist/types/monaco.d.ts +13 -0
- package/dist/types/monaco.js +0 -0
- package/dist/utils/__tests__/proxyMiddleware.test.d.ts +1 -0
- package/dist/utils/__tests__/proxyMiddleware.test.js +579 -0
- package/dist/utils/__tests__/s3Client.test.d.ts +1 -0
- package/dist/utils/__tests__/s3Client.test.js +340 -0
- package/dist/utils/__tests__/s3ConfigIdentifier.test.d.ts +1 -0
- package/dist/utils/__tests__/s3ConfigIdentifier.test.js +437 -0
- package/dist/utils/constants.d.ts +22 -0
- package/dist/utils/constants.js +19 -0
- package/dist/utils/deletion/index.d.ts +2 -2
- package/dist/utils/deletion/index.js +1 -1
- package/dist/utils/deletion/messages.d.ts +1 -1
- package/dist/utils/deletion/messages.js +4 -4
- package/dist/utils/errorHandling.d.ts +12 -3
- package/dist/utils/errorHandling.js +12 -7
- package/dist/utils/hooks.js +8 -8
- package/dist/utils/index.d.ts +5 -2
- package/dist/utils/index.js +5 -1
- package/dist/utils/proxyMiddleware.d.ts +32 -13
- package/dist/utils/proxyMiddleware.js +90 -36
- package/dist/utils/s3Client.d.ts +14 -4
- package/dist/utils/s3Client.js +5 -26
- package/dist/utils/s3ConfigIdentifier.d.ts +79 -0
- package/dist/utils/s3ConfigIdentifier.js +57 -0
- package/dist/utils/s3RuleUtils.d.ts +53 -0
- package/dist/utils/s3RuleUtils.js +101 -0
- package/package.json +10 -8
- package/dist/components/__tests__/BucketNotificationCreatePage.test.js +0 -316
- package/dist/components/buckets/BucketPolicyButton.d.ts +0 -7
- package/dist/components/buckets/notifications/BucketNotificationCreatePage.d.ts +0 -1
- package/dist/components/buckets/notifications/BucketNotificationCreatePage.js +0 -234
- package/dist/hooks/useLoginMutation.d.ts +0 -21
- package/dist/hooks/useLoginMutation.js +0 -9
- package/dist/utils/useFeatures.d.ts +0 -1
- package/dist/utils/useFeatures.js +0 -7
- /package/dist/components/__tests__/{BucketNotificationCreatePage.test.d.ts → BucketAccessor.test.d.ts} +0 -0
|
@@ -0,0 +1,1757 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
3
|
+
import user_event from "@testing-library/user-event";
|
|
4
|
+
import { MemoryRouter, Route, Routes } from "react-router";
|
|
5
|
+
import { useGetBucketReplication, useSetBucketReplication } from "../../hooks/bucketConfiguration.js";
|
|
6
|
+
import { useBuckets } from "../../hooks/bucketOperations.js";
|
|
7
|
+
import { createTestWrapper, findToggleByLabel, mockErrorSubmit, mockOffsetSize, mockSuccessSubmit, submitForm } from "../../test/testUtils.js";
|
|
8
|
+
import { BucketReplicationFormPage } from "../buckets/BucketReplicationFormPage.js";
|
|
9
|
+
jest.mock('../../hooks/bucketConfiguration', ()=>({
|
|
10
|
+
useGetBucketReplication: jest.fn(),
|
|
11
|
+
useSetBucketReplication: jest.fn()
|
|
12
|
+
}));
|
|
13
|
+
jest.mock('../../hooks/bucketOperations', ()=>({
|
|
14
|
+
useBuckets: jest.fn()
|
|
15
|
+
}));
|
|
16
|
+
const mockUseGetBucketReplication = jest.mocked(useGetBucketReplication);
|
|
17
|
+
const mockUseSetBucketReplication = jest.mocked(useSetBucketReplication);
|
|
18
|
+
const mockUseBuckets = jest.mocked(useBuckets);
|
|
19
|
+
const mockNavigate = jest.fn();
|
|
20
|
+
const mockShowToast = jest.fn();
|
|
21
|
+
jest.mock('react-router', ()=>({
|
|
22
|
+
...jest.requireActual('react-router'),
|
|
23
|
+
useNavigate: ()=>mockNavigate
|
|
24
|
+
}));
|
|
25
|
+
jest.mock('@scality/core-ui', ()=>({
|
|
26
|
+
...jest.requireActual('@scality/core-ui'),
|
|
27
|
+
useToast: ()=>({
|
|
28
|
+
showToast: mockShowToast
|
|
29
|
+
})
|
|
30
|
+
}));
|
|
31
|
+
const renderBucketReplicationFormPage = (bucketName = 'test-bucket', ruleId)=>{
|
|
32
|
+
const Wrapper = createTestWrapper();
|
|
33
|
+
const path = ruleId ? `/buckets/${bucketName}/replication/${ruleId}/edit` : `/buckets/${bucketName}/replication/create`;
|
|
34
|
+
return render(/*#__PURE__*/ jsx(MemoryRouter, {
|
|
35
|
+
initialEntries: [
|
|
36
|
+
path
|
|
37
|
+
],
|
|
38
|
+
children: /*#__PURE__*/ jsx(Wrapper, {
|
|
39
|
+
children: /*#__PURE__*/ jsxs(Routes, {
|
|
40
|
+
children: [
|
|
41
|
+
/*#__PURE__*/ jsx(Route, {
|
|
42
|
+
path: "/buckets/:bucketName/replication/create",
|
|
43
|
+
element: /*#__PURE__*/ jsx(BucketReplicationFormPage, {})
|
|
44
|
+
}),
|
|
45
|
+
/*#__PURE__*/ jsx(Route, {
|
|
46
|
+
path: "/buckets/:bucketName/replication/:ruleId/edit",
|
|
47
|
+
element: /*#__PURE__*/ jsx(BucketReplicationFormPage, {})
|
|
48
|
+
})
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
})
|
|
52
|
+
}));
|
|
53
|
+
};
|
|
54
|
+
describe('BucketReplicationFormPage', ()=>{
|
|
55
|
+
const mockMutate = jest.fn();
|
|
56
|
+
const fillRequiredFields = async (options)=>{
|
|
57
|
+
const { roleArn = 'arn:aws:iam::123456789012:role/replication-role', ruleId, targetBucket = 'destination-bucket' } = options;
|
|
58
|
+
await user_event.type(screen.getByLabelText(/role arn/i), roleArn);
|
|
59
|
+
await user_event.type(screen.getByLabelText(/rule id/i), ruleId);
|
|
60
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
61
|
+
await user_event.click(targetBucketSelect);
|
|
62
|
+
await user_event.click(screen.getByRole('option', {
|
|
63
|
+
name: targetBucket
|
|
64
|
+
}));
|
|
65
|
+
};
|
|
66
|
+
beforeEach(()=>{
|
|
67
|
+
jest.clearAllMocks();
|
|
68
|
+
mockNavigate.mockClear();
|
|
69
|
+
mockShowToast.mockClear();
|
|
70
|
+
mockOffsetSize(800, 600);
|
|
71
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
72
|
+
data: void 0,
|
|
73
|
+
status: 'success'
|
|
74
|
+
});
|
|
75
|
+
mockUseSetBucketReplication.mockReturnValue({
|
|
76
|
+
mutate: mockMutate,
|
|
77
|
+
isPending: false
|
|
78
|
+
});
|
|
79
|
+
mockUseBuckets.mockReturnValue({
|
|
80
|
+
data: {
|
|
81
|
+
Buckets: [
|
|
82
|
+
{
|
|
83
|
+
Name: 'test-bucket'
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
Name: 'destination-bucket'
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
Name: 'backup-bucket'
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
status: 'success'
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('Page Rendering', ()=>{
|
|
97
|
+
it('renders create mode with correct title', ()=>{
|
|
98
|
+
renderBucketReplicationFormPage();
|
|
99
|
+
expect(screen.getByText('Create Replication Rule')).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
it('renders edit mode with correct title when rule exists', async ()=>{
|
|
102
|
+
const existingRule = {
|
|
103
|
+
ID: 'test-rule',
|
|
104
|
+
Status: 'Enabled',
|
|
105
|
+
Priority: 1,
|
|
106
|
+
Destination: {
|
|
107
|
+
Bucket: 'arn:aws:s3:::destination-bucket'
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
111
|
+
data: {
|
|
112
|
+
ReplicationConfiguration: {
|
|
113
|
+
Role: 'arn:aws:iam::123456789012:role/replication-role',
|
|
114
|
+
Rules: [
|
|
115
|
+
existingRule
|
|
116
|
+
]
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
status: 'success'
|
|
120
|
+
});
|
|
121
|
+
renderBucketReplicationFormPage('test-bucket', 'test-rule');
|
|
122
|
+
await waitFor(()=>{
|
|
123
|
+
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('shows loading state when fetching replication data', ()=>{
|
|
127
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
128
|
+
data: void 0,
|
|
129
|
+
status: 'pending'
|
|
130
|
+
});
|
|
131
|
+
renderBucketReplicationFormPage();
|
|
132
|
+
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
it('shows error when rule not found in edit mode', ()=>{
|
|
135
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
136
|
+
data: {
|
|
137
|
+
ReplicationConfiguration: {
|
|
138
|
+
Role: 'arn:aws:iam::123456789012:role/replication-role',
|
|
139
|
+
Rules: []
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
status: 'success'
|
|
143
|
+
});
|
|
144
|
+
renderBucketReplicationFormPage('test-bucket', 'non-existent');
|
|
145
|
+
expect(screen.getByText('Rule not found')).toBeInTheDocument();
|
|
146
|
+
});
|
|
147
|
+
it('displays all form sections', async ()=>{
|
|
148
|
+
renderBucketReplicationFormPage();
|
|
149
|
+
await waitFor(()=>{
|
|
150
|
+
expect(screen.getByLabelText(/role arn/i)).toBeInTheDocument();
|
|
151
|
+
expect(screen.getByLabelText(/rule id/i)).toBeInTheDocument();
|
|
152
|
+
expect(screen.getByText('Replicate encrypted objects')).toBeInTheDocument();
|
|
153
|
+
expect(screen.getByText('Delete marker replication')).toBeInTheDocument();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
describe('Form Fields - Initial State', ()=>{
|
|
158
|
+
it('renders required form fields in create mode', ()=>{
|
|
159
|
+
renderBucketReplicationFormPage();
|
|
160
|
+
expect(screen.getByLabelText(/role arn/i)).toBeInTheDocument();
|
|
161
|
+
expect(screen.getByLabelText(/rule id/i)).toBeInTheDocument();
|
|
162
|
+
expect(screen.getByLabelText(/status/i)).toBeInTheDocument();
|
|
163
|
+
expect(screen.getByLabelText(/target bucket/i)).toBeInTheDocument();
|
|
164
|
+
});
|
|
165
|
+
it('shows Role ARN input when no existing rules', ()=>{
|
|
166
|
+
renderBucketReplicationFormPage();
|
|
167
|
+
const roleInput = screen.getByLabelText(/role arn/i);
|
|
168
|
+
expect(roleInput).toBeInTheDocument();
|
|
169
|
+
expect(roleInput.tagName).toBe('INPUT');
|
|
170
|
+
});
|
|
171
|
+
it('displays existing Role as read-only text when rules exist', ()=>{
|
|
172
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
173
|
+
data: {
|
|
174
|
+
ReplicationConfiguration: {
|
|
175
|
+
Role: 'arn:aws:iam::123456789012:role/existing-role',
|
|
176
|
+
Rules: [
|
|
177
|
+
{
|
|
178
|
+
ID: 'existing-rule',
|
|
179
|
+
Status: 'Enabled',
|
|
180
|
+
Priority: 1,
|
|
181
|
+
Destination: {
|
|
182
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
]
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
status: 'success'
|
|
189
|
+
});
|
|
190
|
+
renderBucketReplicationFormPage();
|
|
191
|
+
expect(screen.getByText('arn:aws:iam::123456789012:role/existing-role')).toBeInTheDocument();
|
|
192
|
+
expect(screen.getByText(/to change the role, edit it through bucket/i)).toBeInTheDocument();
|
|
193
|
+
expect(screen.queryByPlaceholderText(/arn:aws:iam/)).not.toBeInTheDocument();
|
|
194
|
+
});
|
|
195
|
+
it('shows Rule ID input in create mode', ()=>{
|
|
196
|
+
renderBucketReplicationFormPage();
|
|
197
|
+
const ruleIdInput = screen.getByLabelText(/rule id/i);
|
|
198
|
+
expect(ruleIdInput).toBeInTheDocument();
|
|
199
|
+
expect(ruleIdInput.tagName).toBe('INPUT');
|
|
200
|
+
});
|
|
201
|
+
it('displays Rule ID as read-only text in edit mode', async ()=>{
|
|
202
|
+
const existingRule = {
|
|
203
|
+
ID: 'readonly-rule-id',
|
|
204
|
+
Status: 'Enabled',
|
|
205
|
+
Priority: 1,
|
|
206
|
+
Destination: {
|
|
207
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
211
|
+
data: {
|
|
212
|
+
ReplicationConfiguration: {
|
|
213
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
214
|
+
Rules: [
|
|
215
|
+
existingRule
|
|
216
|
+
]
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
status: 'success'
|
|
220
|
+
});
|
|
221
|
+
renderBucketReplicationFormPage('test-bucket', 'readonly-rule-id');
|
|
222
|
+
await waitFor(()=>{
|
|
223
|
+
expect(screen.getByText('readonly-rule-id')).toBeInTheDocument();
|
|
224
|
+
});
|
|
225
|
+
expect(screen.queryByRole('textbox', {
|
|
226
|
+
name: /rule id/i
|
|
227
|
+
})).not.toBeInTheDocument();
|
|
228
|
+
});
|
|
229
|
+
it('renders Status select field', async ()=>{
|
|
230
|
+
renderBucketReplicationFormPage();
|
|
231
|
+
await waitFor(()=>{
|
|
232
|
+
expect(screen.getByLabelText(/rule id/i)).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
const statusElement = document.querySelector('[id="status"]');
|
|
235
|
+
expect(statusElement).toBeInTheDocument();
|
|
236
|
+
});
|
|
237
|
+
it('renders Priority number input with auto-assigned placeholder', ()=>{
|
|
238
|
+
renderBucketReplicationFormPage();
|
|
239
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
240
|
+
expect(priorityInput).toBeInTheDocument();
|
|
241
|
+
expect(priorityInput).toHaveAttribute('placeholder', 'Example: Auto-assigned: 0');
|
|
242
|
+
});
|
|
243
|
+
it('renders all toggle fields', ()=>{
|
|
244
|
+
renderBucketReplicationFormPage();
|
|
245
|
+
expect(screen.getByText('Replicate encrypted objects')).toBeInTheDocument();
|
|
246
|
+
expect(screen.getByText('Encrypt replicated objects')).toBeInTheDocument();
|
|
247
|
+
expect(screen.getByText('Replication Time Control (RTC)')).toBeInTheDocument();
|
|
248
|
+
expect(screen.getByText('RTC metrics and notifications')).toBeInTheDocument();
|
|
249
|
+
expect(screen.getByText('Replica modification sync')).toBeInTheDocument();
|
|
250
|
+
expect(screen.getByText('Delete marker replication')).toBeInTheDocument();
|
|
251
|
+
});
|
|
252
|
+
it('renders target bucket as Select for same account', ()=>{
|
|
253
|
+
renderBucketReplicationFormPage();
|
|
254
|
+
expect(screen.getAllByText(/target bucket/i)[0]).toBeInTheDocument();
|
|
255
|
+
});
|
|
256
|
+
it('renders storage class select with all options', async ()=>{
|
|
257
|
+
renderBucketReplicationFormPage();
|
|
258
|
+
const storageClassSelect = screen.getByLabelText(/storage class/i);
|
|
259
|
+
await user_event.click(storageClassSelect);
|
|
260
|
+
expect(screen.getByRole('option', {
|
|
261
|
+
name: 'Same as source'
|
|
262
|
+
})).toBeInTheDocument();
|
|
263
|
+
expect(screen.getByRole('option', {
|
|
264
|
+
name: 'Glacier'
|
|
265
|
+
})).toBeInTheDocument();
|
|
266
|
+
expect(screen.getByRole('option', {
|
|
267
|
+
name: 'Glacier Deep Archive'
|
|
268
|
+
})).toBeInTheDocument();
|
|
269
|
+
expect(screen.getByRole('option', {
|
|
270
|
+
name: 'Standard-IA'
|
|
271
|
+
})).toBeInTheDocument();
|
|
272
|
+
expect(screen.getByRole('option', {
|
|
273
|
+
name: 'One Zone-IA'
|
|
274
|
+
})).toBeInTheDocument();
|
|
275
|
+
expect(screen.getByRole('option', {
|
|
276
|
+
name: 'Intelligent-Tiering'
|
|
277
|
+
})).toBeInTheDocument();
|
|
278
|
+
expect(screen.getByRole('option', {
|
|
279
|
+
name: 'Glacier Instant Retrieval'
|
|
280
|
+
})).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
describe('Form Validation', ()=>{
|
|
284
|
+
it('validates Role ARN is required for first replication rule', async ()=>{
|
|
285
|
+
renderBucketReplicationFormPage();
|
|
286
|
+
const roleInput = screen.getByLabelText(/role arn/i);
|
|
287
|
+
await user_event.type(roleInput, 'test');
|
|
288
|
+
await user_event.clear(roleInput);
|
|
289
|
+
await waitFor(()=>{
|
|
290
|
+
expect(screen.getByText(/role arn is required for first replication rule/i)).toBeInTheDocument();
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
it('validates Role ARN is optional when existing rules present', async ()=>{
|
|
294
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
295
|
+
data: {
|
|
296
|
+
ReplicationConfiguration: {
|
|
297
|
+
Role: 'arn:aws:iam::123456789012:role/existing-role',
|
|
298
|
+
Rules: [
|
|
299
|
+
{
|
|
300
|
+
ID: 'existing-rule',
|
|
301
|
+
Status: 'Enabled',
|
|
302
|
+
Priority: 1,
|
|
303
|
+
Destination: {
|
|
304
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
status: 'success'
|
|
311
|
+
});
|
|
312
|
+
renderBucketReplicationFormPage();
|
|
313
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'new-rule');
|
|
314
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
315
|
+
await user_event.click(targetBucketSelect);
|
|
316
|
+
await user_event.click(screen.getByRole('option', {
|
|
317
|
+
name: 'destination-bucket'
|
|
318
|
+
}));
|
|
319
|
+
await waitFor(()=>{
|
|
320
|
+
const createButton = screen.getByRole('button', {
|
|
321
|
+
name: /create/i
|
|
322
|
+
});
|
|
323
|
+
expect(createButton).toBeEnabled();
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
it('validates Rule ID is required', async ()=>{
|
|
327
|
+
renderBucketReplicationFormPage();
|
|
328
|
+
const ruleIdInput = screen.getByLabelText(/rule id/i);
|
|
329
|
+
await user_event.type(ruleIdInput, 'test');
|
|
330
|
+
await user_event.clear(ruleIdInput);
|
|
331
|
+
await waitFor(()=>{
|
|
332
|
+
expect(screen.getByText(/rule id is required/i)).toBeInTheDocument();
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
it('validates Rule ID uniqueness against existing rules', async ()=>{
|
|
336
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
337
|
+
data: {
|
|
338
|
+
ReplicationConfiguration: {
|
|
339
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
340
|
+
Rules: [
|
|
341
|
+
{
|
|
342
|
+
ID: 'existing-rule',
|
|
343
|
+
Status: 'Enabled',
|
|
344
|
+
Priority: 1,
|
|
345
|
+
Destination: {
|
|
346
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
status: 'success'
|
|
353
|
+
});
|
|
354
|
+
renderBucketReplicationFormPage();
|
|
355
|
+
const ruleIdInput = screen.getByLabelText(/rule id/i);
|
|
356
|
+
await user_event.type(ruleIdInput, 'existing-rule');
|
|
357
|
+
await waitFor(()=>{
|
|
358
|
+
expect(screen.getByText(/a rule with this id already exists/i)).toBeInTheDocument();
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
it('validates Priority must be >= 0', async ()=>{
|
|
362
|
+
renderBucketReplicationFormPage();
|
|
363
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'test-rule');
|
|
364
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
365
|
+
await user_event.clear(priorityInput);
|
|
366
|
+
await user_event.type(priorityInput, '-1');
|
|
367
|
+
await user_event.tab();
|
|
368
|
+
await waitFor(()=>{
|
|
369
|
+
expect(screen.getByText(/priority must be at least 0/i)).toBeInTheDocument();
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
it('disables submit button when Target Bucket is not selected', async ()=>{
|
|
373
|
+
renderBucketReplicationFormPage();
|
|
374
|
+
await user_event.type(screen.getByLabelText(/role arn/i), 'arn:aws:iam::123456789012:role/role');
|
|
375
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'test-rule');
|
|
376
|
+
await waitFor(()=>{
|
|
377
|
+
const createButton = screen.getByRole('button', {
|
|
378
|
+
name: /create/i
|
|
379
|
+
});
|
|
380
|
+
expect(createButton).toBeDisabled();
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
it('shows Target Account ID field when sameAccount is false', async ()=>{
|
|
384
|
+
renderBucketReplicationFormPage();
|
|
385
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
386
|
+
await user_event.click(sameAccountToggle);
|
|
387
|
+
await waitFor(()=>{
|
|
388
|
+
expect(screen.getByLabelText(/target account id/i)).toBeInTheDocument();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
it('shows Replica KMS Key ID field when encryptReplicatedObjects is true', async ()=>{
|
|
392
|
+
renderBucketReplicationFormPage();
|
|
393
|
+
const encryptToggle = findToggleByLabel('Encrypt replicated objects');
|
|
394
|
+
await user_event.click(encryptToggle);
|
|
395
|
+
await waitFor(()=>{
|
|
396
|
+
expect(screen.getByLabelText(/replica kms key id/i)).toBeInTheDocument();
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
it('shows prefix field when filterType is prefix', async ()=>{
|
|
400
|
+
renderBucketReplicationFormPage();
|
|
401
|
+
const filterSelect = document.querySelector('[id="filterType"]');
|
|
402
|
+
await user_event.click(filterSelect);
|
|
403
|
+
await waitFor(()=>{
|
|
404
|
+
expect(screen.getByRole('option', {
|
|
405
|
+
name: 'Prefix filter'
|
|
406
|
+
})).toBeInTheDocument();
|
|
407
|
+
});
|
|
408
|
+
await user_event.click(screen.getByRole('option', {
|
|
409
|
+
name: 'Prefix filter'
|
|
410
|
+
}));
|
|
411
|
+
await waitFor(()=>{
|
|
412
|
+
expect(screen.getByLabelText(/prefix/i)).toBeInTheDocument();
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
describe('Automatic Field Behaviors - Encryption', ()=>{
|
|
417
|
+
it('shows encryption requirement when includeEncryptedObjects is enabled', async ()=>{
|
|
418
|
+
renderBucketReplicationFormPage();
|
|
419
|
+
const includeEncryptedToggle = findToggleByLabel('Replicate encrypted objects');
|
|
420
|
+
expect(includeEncryptedToggle).toBeInTheDocument();
|
|
421
|
+
await user_event.click(includeEncryptedToggle);
|
|
422
|
+
await waitFor(()=>{
|
|
423
|
+
expect(screen.getByText(/encryption is required when replicating encrypted objects/i)).toBeInTheDocument();
|
|
424
|
+
});
|
|
425
|
+
});
|
|
426
|
+
});
|
|
427
|
+
describe('Automatic Field Behaviors - RTC', ()=>{
|
|
428
|
+
it('shows metrics requirement when enforceRTC is enabled', async ()=>{
|
|
429
|
+
renderBucketReplicationFormPage();
|
|
430
|
+
const enforceRTCToggle = findToggleByLabel('Replication Time Control (RTC)');
|
|
431
|
+
await user_event.click(enforceRTCToggle);
|
|
432
|
+
await waitFor(()=>{
|
|
433
|
+
expect(screen.getByText(/metrics must be enabled when rtc is enabled/i)).toBeInTheDocument();
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
describe('Automatic Field Behaviors - Delete Marker', ()=>{
|
|
438
|
+
it('disables deleteMarkerReplication when filterType is tags', async ()=>{
|
|
439
|
+
renderBucketReplicationFormPage();
|
|
440
|
+
const filterSelect = screen.getByLabelText(/^filter$/i);
|
|
441
|
+
await user_event.click(filterSelect);
|
|
442
|
+
await user_event.click(screen.getByRole('option', {
|
|
443
|
+
name: 'Tags filter'
|
|
444
|
+
}));
|
|
445
|
+
await waitFor(()=>{
|
|
446
|
+
const deleteMarkerToggle = findToggleByLabel('Delete marker replication');
|
|
447
|
+
expect(deleteMarkerToggle).toBeDisabled();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
it('disables deleteMarkerReplication when filterType is and', async ()=>{
|
|
451
|
+
renderBucketReplicationFormPage();
|
|
452
|
+
const filterSelect = screen.getByLabelText(/^filter$/i);
|
|
453
|
+
await user_event.click(filterSelect);
|
|
454
|
+
await user_event.click(screen.getByRole('option', {
|
|
455
|
+
name: 'Prefix and tags filter'
|
|
456
|
+
}));
|
|
457
|
+
await waitFor(()=>{
|
|
458
|
+
const deleteMarkerToggle = findToggleByLabel('Delete marker replication');
|
|
459
|
+
expect(deleteMarkerToggle).toBeDisabled();
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
it('auto-disables deleteMarkerReplication when switching to tag-based filters', async ()=>{
|
|
463
|
+
renderBucketReplicationFormPage();
|
|
464
|
+
const deleteMarkerToggle = findToggleByLabel('Delete marker replication');
|
|
465
|
+
await user_event.click(deleteMarkerToggle);
|
|
466
|
+
await waitFor(()=>{
|
|
467
|
+
expect(deleteMarkerToggle).toBeChecked();
|
|
468
|
+
});
|
|
469
|
+
const filterSelect = screen.getByLabelText(/^filter$/i);
|
|
470
|
+
await user_event.click(filterSelect);
|
|
471
|
+
await user_event.click(screen.getByRole('option', {
|
|
472
|
+
name: 'Tags filter'
|
|
473
|
+
}));
|
|
474
|
+
await waitFor(()=>{
|
|
475
|
+
const updatedToggle = findToggleByLabel('Delete marker replication');
|
|
476
|
+
expect(updatedToggle).not.toBeChecked();
|
|
477
|
+
expect(updatedToggle).toBeDisabled();
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
it('shows helper text explaining tag filter restriction', async ()=>{
|
|
481
|
+
renderBucketReplicationFormPage();
|
|
482
|
+
const filterSelect = screen.getByLabelText(/^filter$/i);
|
|
483
|
+
await user_event.click(filterSelect);
|
|
484
|
+
await user_event.click(screen.getByRole('option', {
|
|
485
|
+
name: 'Tags filter'
|
|
486
|
+
}));
|
|
487
|
+
await waitFor(()=>{
|
|
488
|
+
expect(screen.getByText(/delete marker replication is not supported for tag-based filters/i)).toBeInTheDocument();
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
describe('Automatic Field Behaviors - Conditional Fields', ()=>{
|
|
493
|
+
it('shows switchObjectOwnership toggle only when sameAccount is false', async ()=>{
|
|
494
|
+
renderBucketReplicationFormPage();
|
|
495
|
+
expect(screen.queryByText('Switch Object ownership')).not.toBeInTheDocument();
|
|
496
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
497
|
+
await user_event.click(sameAccountToggle);
|
|
498
|
+
await waitFor(()=>{
|
|
499
|
+
expect(screen.getByText('Switch Object ownership')).toBeInTheDocument();
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
it('shows Target Account ID input when sameAccount is false', async ()=>{
|
|
503
|
+
renderBucketReplicationFormPage();
|
|
504
|
+
expect(screen.queryByLabelText(/target account id/i)).not.toBeInTheDocument();
|
|
505
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
506
|
+
await user_event.click(sameAccountToggle);
|
|
507
|
+
await waitFor(()=>{
|
|
508
|
+
expect(screen.getByLabelText(/target account id/i)).toBeInTheDocument();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
it('renders target bucket as Input for cross-account replication', async ()=>{
|
|
512
|
+
renderBucketReplicationFormPage();
|
|
513
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
514
|
+
await user_event.click(sameAccountToggle);
|
|
515
|
+
await waitFor(()=>{
|
|
516
|
+
const targetBucketInput = screen.getByLabelText(/target bucket/i);
|
|
517
|
+
expect(targetBucketInput.tagName).toBe('INPUT');
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
it('shows Replica KMS Key ID input when encryptReplicatedObjects is true', async ()=>{
|
|
521
|
+
renderBucketReplicationFormPage();
|
|
522
|
+
expect(screen.queryByLabelText(/replica kms key id/i)).not.toBeInTheDocument();
|
|
523
|
+
const encryptToggle = findToggleByLabel('Encrypt replicated objects');
|
|
524
|
+
await user_event.click(encryptToggle);
|
|
525
|
+
await waitFor(()=>{
|
|
526
|
+
expect(screen.getByLabelText(/replica kms key id/i)).toBeInTheDocument();
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
describe('Form Submission - Create Mode', ()=>{
|
|
531
|
+
it('submits minimal replication rule with required fields only', async ()=>{
|
|
532
|
+
renderBucketReplicationFormPage();
|
|
533
|
+
await fillRequiredFields({
|
|
534
|
+
ruleId: 'minimal-rule'
|
|
535
|
+
});
|
|
536
|
+
mockSuccessSubmit(mockMutate);
|
|
537
|
+
await submitForm('create');
|
|
538
|
+
await waitFor(()=>{
|
|
539
|
+
expect(mockMutate).toHaveBeenCalledWith(expect.objectContaining({
|
|
540
|
+
Bucket: 'test-bucket',
|
|
541
|
+
ReplicationConfiguration: {
|
|
542
|
+
Role: 'arn:aws:iam::123456789012:role/replication-role',
|
|
543
|
+
Rules: expect.arrayContaining([
|
|
544
|
+
expect.objectContaining({
|
|
545
|
+
ID: 'minimal-rule',
|
|
546
|
+
Status: 'Enabled',
|
|
547
|
+
Priority: 0,
|
|
548
|
+
Destination: expect.objectContaining({
|
|
549
|
+
Bucket: 'arn:aws:s3:::destination-bucket'
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
])
|
|
553
|
+
}
|
|
554
|
+
}), expect.any(Object));
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
it('submits complete replication rule with all optional fields', async ()=>{
|
|
558
|
+
renderBucketReplicationFormPage();
|
|
559
|
+
await user_event.type(screen.getByLabelText(/role arn/i), 'arn:aws:iam::123456789012:role/replication-role');
|
|
560
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'complete-rule');
|
|
561
|
+
await user_event.type(screen.getByLabelText(/rule priority/i), '5');
|
|
562
|
+
const filterSelect = document.querySelector('[id="filterType"]');
|
|
563
|
+
await user_event.click(filterSelect);
|
|
564
|
+
await waitFor(()=>{
|
|
565
|
+
expect(screen.getByRole('option', {
|
|
566
|
+
name: 'Prefix filter'
|
|
567
|
+
})).toBeInTheDocument();
|
|
568
|
+
});
|
|
569
|
+
await user_event.click(screen.getByRole('option', {
|
|
570
|
+
name: 'Prefix filter'
|
|
571
|
+
}));
|
|
572
|
+
await waitFor(()=>{
|
|
573
|
+
expect(screen.getByLabelText(/prefix/i)).toBeInTheDocument();
|
|
574
|
+
});
|
|
575
|
+
await user_event.type(screen.getByLabelText(/prefix/i), 'logs/');
|
|
576
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
577
|
+
await user_event.click(sameAccountToggle);
|
|
578
|
+
await waitFor(()=>{
|
|
579
|
+
expect(screen.getByLabelText(/target account id/i)).toBeInTheDocument();
|
|
580
|
+
});
|
|
581
|
+
await user_event.type(screen.getByLabelText(/target account id/i), '987654321098');
|
|
582
|
+
await user_event.type(screen.getByLabelText(/target bucket/i), 'cross-account-bucket');
|
|
583
|
+
const storageClassSelect = document.querySelector('[id="storageClass"]');
|
|
584
|
+
await user_event.click(storageClassSelect);
|
|
585
|
+
await waitFor(()=>{
|
|
586
|
+
expect(screen.getByRole('option', {
|
|
587
|
+
name: 'Glacier'
|
|
588
|
+
})).toBeInTheDocument();
|
|
589
|
+
});
|
|
590
|
+
await user_event.click(screen.getByRole('option', {
|
|
591
|
+
name: 'Glacier'
|
|
592
|
+
}));
|
|
593
|
+
const switchOwnershipToggle = findToggleByLabel('Switch Object ownership');
|
|
594
|
+
await user_event.click(switchOwnershipToggle);
|
|
595
|
+
const includeEncryptedToggle = findToggleByLabel('Replicate encrypted objects');
|
|
596
|
+
await user_event.click(includeEncryptedToggle);
|
|
597
|
+
await waitFor(()=>{
|
|
598
|
+
expect(screen.getByLabelText(/replica kms key id/i)).toBeInTheDocument();
|
|
599
|
+
});
|
|
600
|
+
await user_event.type(screen.getByLabelText(/replica kms key id/i), 'arn:aws:kms:us-east-1:987654321098:key/12345678-1234-1234-1234-123456789012');
|
|
601
|
+
const enforceRTCToggle = findToggleByLabel('Replication Time Control (RTC)');
|
|
602
|
+
await user_event.click(enforceRTCToggle);
|
|
603
|
+
const replicaModToggle = findToggleByLabel('Replica modification sync');
|
|
604
|
+
await user_event.click(replicaModToggle);
|
|
605
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
606
|
+
options?.onSuccess?.();
|
|
607
|
+
});
|
|
608
|
+
await waitFor(()=>{
|
|
609
|
+
const createButton = screen.getByRole('button', {
|
|
610
|
+
name: /create/i
|
|
611
|
+
});
|
|
612
|
+
expect(createButton).toBeEnabled();
|
|
613
|
+
});
|
|
614
|
+
fireEvent.click(screen.getByRole('button', {
|
|
615
|
+
name: /create/i
|
|
616
|
+
}));
|
|
617
|
+
await waitFor(()=>{
|
|
618
|
+
const call = mockMutate.mock.calls[0][0];
|
|
619
|
+
expect(call.ReplicationConfiguration.Rules[0]).toMatchObject({
|
|
620
|
+
ID: 'complete-rule',
|
|
621
|
+
Status: 'Enabled',
|
|
622
|
+
Priority: 5,
|
|
623
|
+
Filter: {
|
|
624
|
+
Prefix: 'logs/'
|
|
625
|
+
},
|
|
626
|
+
Destination: expect.objectContaining({
|
|
627
|
+
Bucket: 'arn:aws:s3:::cross-account-bucket',
|
|
628
|
+
Account: '987654321098',
|
|
629
|
+
StorageClass: 'GLACIER',
|
|
630
|
+
AccessControlTranslation: {
|
|
631
|
+
Owner: 'Destination'
|
|
632
|
+
},
|
|
633
|
+
EncryptionConfiguration: {
|
|
634
|
+
ReplicaKmsKeyID: 'arn:aws:kms:us-east-1:987654321098:key/12345678-1234-1234-1234-123456789012'
|
|
635
|
+
},
|
|
636
|
+
ReplicationTime: {
|
|
637
|
+
Status: 'Enabled',
|
|
638
|
+
Time: {
|
|
639
|
+
Minutes: 15
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
Metrics: expect.objectContaining({
|
|
643
|
+
Status: 'Enabled'
|
|
644
|
+
})
|
|
645
|
+
}),
|
|
646
|
+
SourceSelectionCriteria: expect.objectContaining({
|
|
647
|
+
SseKmsEncryptedObjects: {
|
|
648
|
+
Status: 'Enabled'
|
|
649
|
+
},
|
|
650
|
+
ReplicaModifications: {
|
|
651
|
+
Status: 'Enabled'
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
});
|
|
655
|
+
});
|
|
656
|
+
});
|
|
657
|
+
it('appends new rule to existing rules array', async ()=>{
|
|
658
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
659
|
+
data: {
|
|
660
|
+
ReplicationConfiguration: {
|
|
661
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
662
|
+
Rules: [
|
|
663
|
+
{
|
|
664
|
+
ID: 'existing-rule',
|
|
665
|
+
Status: 'Enabled',
|
|
666
|
+
Priority: 1,
|
|
667
|
+
Destination: {
|
|
668
|
+
Bucket: 'arn:aws:s3:::existing-bucket'
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
]
|
|
672
|
+
}
|
|
673
|
+
},
|
|
674
|
+
status: 'success'
|
|
675
|
+
});
|
|
676
|
+
renderBucketReplicationFormPage();
|
|
677
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'new-rule');
|
|
678
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
679
|
+
await user_event.click(targetBucketSelect);
|
|
680
|
+
await user_event.click(screen.getByRole('option', {
|
|
681
|
+
name: 'destination-bucket'
|
|
682
|
+
}));
|
|
683
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
684
|
+
options?.onSuccess?.();
|
|
685
|
+
});
|
|
686
|
+
await waitFor(()=>{
|
|
687
|
+
const createButton = screen.getByRole('button', {
|
|
688
|
+
name: /create/i
|
|
689
|
+
});
|
|
690
|
+
expect(createButton).toBeEnabled();
|
|
691
|
+
});
|
|
692
|
+
fireEvent.click(screen.getByRole('button', {
|
|
693
|
+
name: /create/i
|
|
694
|
+
}));
|
|
695
|
+
await waitFor(()=>{
|
|
696
|
+
const call = mockMutate.mock.calls[0][0];
|
|
697
|
+
expect(call.ReplicationConfiguration.Rules).toHaveLength(2);
|
|
698
|
+
expect(call.ReplicationConfiguration.Rules[0].ID).toBe('existing-rule');
|
|
699
|
+
expect(call.ReplicationConfiguration.Rules[1].ID).toBe('new-rule');
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
it('navigates to bucket replication tab on success', async ()=>{
|
|
703
|
+
renderBucketReplicationFormPage();
|
|
704
|
+
await fillRequiredFields({
|
|
705
|
+
roleArn: 'arn:aws:iam::123456789012:role/role',
|
|
706
|
+
ruleId: 'success-rule'
|
|
707
|
+
});
|
|
708
|
+
mockSuccessSubmit(mockMutate);
|
|
709
|
+
await submitForm('create');
|
|
710
|
+
await waitFor(()=>{
|
|
711
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket?tab=replication');
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
it('displays success toast after rule creation', async ()=>{
|
|
715
|
+
renderBucketReplicationFormPage();
|
|
716
|
+
await fillRequiredFields({
|
|
717
|
+
roleArn: 'arn:aws:iam::123456789012:role/role',
|
|
718
|
+
ruleId: 'toast-rule'
|
|
719
|
+
});
|
|
720
|
+
mockSuccessSubmit(mockMutate);
|
|
721
|
+
await submitForm('create');
|
|
722
|
+
await waitFor(()=>{
|
|
723
|
+
expect(mockShowToast).toHaveBeenCalledWith({
|
|
724
|
+
open: true,
|
|
725
|
+
message: 'Replication rule created successfully',
|
|
726
|
+
status: 'success'
|
|
727
|
+
});
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
it('displays error toast when creation fails', async ()=>{
|
|
731
|
+
renderBucketReplicationFormPage();
|
|
732
|
+
await fillRequiredFields({
|
|
733
|
+
roleArn: 'arn:aws:iam::123456789012:role/role',
|
|
734
|
+
ruleId: 'error-rule'
|
|
735
|
+
});
|
|
736
|
+
mockErrorSubmit(mockMutate, 'Access Denied');
|
|
737
|
+
await submitForm('create');
|
|
738
|
+
await waitFor(()=>{
|
|
739
|
+
expect(mockShowToast).toHaveBeenCalledWith({
|
|
740
|
+
open: true,
|
|
741
|
+
message: 'Access Denied',
|
|
742
|
+
status: 'error'
|
|
743
|
+
});
|
|
744
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
745
|
+
});
|
|
746
|
+
});
|
|
747
|
+
it('disables submit button when form is pristine', ()=>{
|
|
748
|
+
renderBucketReplicationFormPage();
|
|
749
|
+
const createButton = screen.getByRole('button', {
|
|
750
|
+
name: /create/i
|
|
751
|
+
});
|
|
752
|
+
expect(createButton).toBeDisabled();
|
|
753
|
+
});
|
|
754
|
+
it('disables submit button when form is invalid', async ()=>{
|
|
755
|
+
renderBucketReplicationFormPage();
|
|
756
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'test');
|
|
757
|
+
await waitFor(()=>{
|
|
758
|
+
const createButton = screen.getByRole('button', {
|
|
759
|
+
name: /create/i
|
|
760
|
+
});
|
|
761
|
+
expect(createButton).toBeDisabled();
|
|
762
|
+
});
|
|
763
|
+
});
|
|
764
|
+
it('enables submit button when form is valid and dirty', async ()=>{
|
|
765
|
+
renderBucketReplicationFormPage();
|
|
766
|
+
await fillRequiredFields({
|
|
767
|
+
roleArn: 'arn:aws:iam::123456789012:role/role',
|
|
768
|
+
ruleId: 'valid-rule'
|
|
769
|
+
});
|
|
770
|
+
await waitFor(()=>{
|
|
771
|
+
const createButton = screen.getByRole('button', {
|
|
772
|
+
name: /create/i
|
|
773
|
+
});
|
|
774
|
+
expect(createButton).toBeEnabled();
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
});
|
|
778
|
+
describe('Form Submission - Edit Mode', ()=>{
|
|
779
|
+
it('updates existing rule while preserving other rules', async ()=>{
|
|
780
|
+
const existingRules = [
|
|
781
|
+
{
|
|
782
|
+
ID: 'rule-1',
|
|
783
|
+
Status: 'Enabled',
|
|
784
|
+
Priority: 1,
|
|
785
|
+
Destination: {
|
|
786
|
+
Bucket: 'arn:aws:s3:::bucket-1'
|
|
787
|
+
}
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
ID: 'rule-2',
|
|
791
|
+
Status: 'Enabled',
|
|
792
|
+
Priority: 2,
|
|
793
|
+
Destination: {
|
|
794
|
+
Bucket: 'arn:aws:s3:::bucket-2'
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
];
|
|
798
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
799
|
+
data: {
|
|
800
|
+
ReplicationConfiguration: {
|
|
801
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
802
|
+
Rules: existingRules
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
status: 'success'
|
|
806
|
+
});
|
|
807
|
+
renderBucketReplicationFormPage('test-bucket', 'rule-1');
|
|
808
|
+
await waitFor(()=>{
|
|
809
|
+
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
810
|
+
});
|
|
811
|
+
const statusSelect = screen.getByLabelText(/status/i);
|
|
812
|
+
await user_event.click(statusSelect);
|
|
813
|
+
await user_event.click(screen.getByRole('option', {
|
|
814
|
+
name: 'Disabled'
|
|
815
|
+
}));
|
|
816
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
817
|
+
options?.onSuccess?.();
|
|
818
|
+
});
|
|
819
|
+
await waitFor(()=>{
|
|
820
|
+
const saveButton = screen.getByRole('button', {
|
|
821
|
+
name: /save/i
|
|
822
|
+
});
|
|
823
|
+
expect(saveButton).toBeEnabled();
|
|
824
|
+
});
|
|
825
|
+
fireEvent.click(screen.getByRole('button', {
|
|
826
|
+
name: /save/i
|
|
827
|
+
}));
|
|
828
|
+
await waitFor(()=>{
|
|
829
|
+
const call = mockMutate.mock.calls[0][0];
|
|
830
|
+
expect(call.ReplicationConfiguration.Rules).toHaveLength(2);
|
|
831
|
+
expect(call.ReplicationConfiguration.Rules[0].ID).toBe('rule-1');
|
|
832
|
+
expect(call.ReplicationConfiguration.Rules[0].Status).toBe('Disabled');
|
|
833
|
+
expect(call.ReplicationConfiguration.Rules[1].ID).toBe('rule-2');
|
|
834
|
+
expect(call.ReplicationConfiguration.Rules[1].Status).toBe('Enabled');
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
it('navigates to bucket replication tab on success', async ()=>{
|
|
838
|
+
const existingRule = {
|
|
839
|
+
ID: 'edit-rule',
|
|
840
|
+
Status: 'Enabled',
|
|
841
|
+
Priority: 1,
|
|
842
|
+
Destination: {
|
|
843
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
847
|
+
data: {
|
|
848
|
+
ReplicationConfiguration: {
|
|
849
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
850
|
+
Rules: [
|
|
851
|
+
existingRule
|
|
852
|
+
]
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
status: 'success'
|
|
856
|
+
});
|
|
857
|
+
renderBucketReplicationFormPage('test-bucket', 'edit-rule');
|
|
858
|
+
await waitFor(()=>{
|
|
859
|
+
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
860
|
+
});
|
|
861
|
+
const statusSelect = screen.getByLabelText(/status/i);
|
|
862
|
+
await user_event.click(statusSelect);
|
|
863
|
+
await user_event.click(screen.getByRole('option', {
|
|
864
|
+
name: 'Disabled'
|
|
865
|
+
}));
|
|
866
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
867
|
+
options?.onSuccess?.();
|
|
868
|
+
});
|
|
869
|
+
await waitFor(()=>{
|
|
870
|
+
const saveButton = screen.getByRole('button', {
|
|
871
|
+
name: /save/i
|
|
872
|
+
});
|
|
873
|
+
expect(saveButton).toBeEnabled();
|
|
874
|
+
});
|
|
875
|
+
fireEvent.click(screen.getByRole('button', {
|
|
876
|
+
name: /save/i
|
|
877
|
+
}));
|
|
878
|
+
await waitFor(()=>{
|
|
879
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket?tab=replication');
|
|
880
|
+
});
|
|
881
|
+
});
|
|
882
|
+
it('displays update success toast in edit mode', async ()=>{
|
|
883
|
+
const existingRule = {
|
|
884
|
+
ID: 'toast-rule',
|
|
885
|
+
Status: 'Enabled',
|
|
886
|
+
Priority: 1,
|
|
887
|
+
Destination: {
|
|
888
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
892
|
+
data: {
|
|
893
|
+
ReplicationConfiguration: {
|
|
894
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
895
|
+
Rules: [
|
|
896
|
+
existingRule
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
},
|
|
900
|
+
status: 'success'
|
|
901
|
+
});
|
|
902
|
+
renderBucketReplicationFormPage('test-bucket', 'toast-rule');
|
|
903
|
+
await waitFor(()=>{
|
|
904
|
+
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
905
|
+
});
|
|
906
|
+
const statusSelect = screen.getByLabelText(/status/i);
|
|
907
|
+
await user_event.click(statusSelect);
|
|
908
|
+
await user_event.click(screen.getByRole('option', {
|
|
909
|
+
name: 'Disabled'
|
|
910
|
+
}));
|
|
911
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
912
|
+
options?.onSuccess?.();
|
|
913
|
+
});
|
|
914
|
+
await waitFor(()=>{
|
|
915
|
+
const saveButton = screen.getByRole('button', {
|
|
916
|
+
name: /save/i
|
|
917
|
+
});
|
|
918
|
+
expect(saveButton).toBeEnabled();
|
|
919
|
+
});
|
|
920
|
+
fireEvent.click(screen.getByRole('button', {
|
|
921
|
+
name: /save/i
|
|
922
|
+
}));
|
|
923
|
+
await waitFor(()=>{
|
|
924
|
+
expect(mockShowToast).toHaveBeenCalledWith({
|
|
925
|
+
open: true,
|
|
926
|
+
message: 'Replication rule updated successfully',
|
|
927
|
+
status: 'success'
|
|
928
|
+
});
|
|
929
|
+
});
|
|
930
|
+
});
|
|
931
|
+
it('displays error toast with error message on failure', async ()=>{
|
|
932
|
+
const existingRule = {
|
|
933
|
+
ID: 'error-rule',
|
|
934
|
+
Status: 'Enabled',
|
|
935
|
+
Priority: 1,
|
|
936
|
+
Destination: {
|
|
937
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
941
|
+
data: {
|
|
942
|
+
ReplicationConfiguration: {
|
|
943
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
944
|
+
Rules: [
|
|
945
|
+
existingRule
|
|
946
|
+
]
|
|
947
|
+
}
|
|
948
|
+
},
|
|
949
|
+
status: 'success'
|
|
950
|
+
});
|
|
951
|
+
renderBucketReplicationFormPage('test-bucket', 'error-rule');
|
|
952
|
+
await waitFor(()=>{
|
|
953
|
+
expect(screen.getByText('Edit Replication Rule')).toBeInTheDocument();
|
|
954
|
+
});
|
|
955
|
+
const statusSelect = screen.getByLabelText(/status/i);
|
|
956
|
+
await user_event.click(statusSelect);
|
|
957
|
+
await user_event.click(screen.getByRole('option', {
|
|
958
|
+
name: 'Disabled'
|
|
959
|
+
}));
|
|
960
|
+
const error = new Error('Network Error');
|
|
961
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
962
|
+
options?.onError?.(error);
|
|
963
|
+
});
|
|
964
|
+
await waitFor(()=>{
|
|
965
|
+
const saveButton = screen.getByRole('button', {
|
|
966
|
+
name: /save/i
|
|
967
|
+
});
|
|
968
|
+
expect(saveButton).toBeEnabled();
|
|
969
|
+
});
|
|
970
|
+
fireEvent.click(screen.getByRole('button', {
|
|
971
|
+
name: /save/i
|
|
972
|
+
}));
|
|
973
|
+
await waitFor(()=>{
|
|
974
|
+
expect(mockShowToast).toHaveBeenCalledWith({
|
|
975
|
+
open: true,
|
|
976
|
+
message: 'Network Error',
|
|
977
|
+
status: 'error'
|
|
978
|
+
});
|
|
979
|
+
expect(mockNavigate).not.toHaveBeenCalled();
|
|
980
|
+
});
|
|
981
|
+
});
|
|
982
|
+
});
|
|
983
|
+
describe('Rule Data Loading in Edit Mode', ()=>{
|
|
984
|
+
it('loads rule with no filter correctly', async ()=>{
|
|
985
|
+
const rule = {
|
|
986
|
+
ID: 'no-filter-rule',
|
|
987
|
+
Status: 'Enabled',
|
|
988
|
+
Priority: 1,
|
|
989
|
+
Destination: {
|
|
990
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
994
|
+
data: {
|
|
995
|
+
ReplicationConfiguration: {
|
|
996
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
997
|
+
Rules: [
|
|
998
|
+
rule
|
|
999
|
+
]
|
|
1000
|
+
}
|
|
1001
|
+
},
|
|
1002
|
+
status: 'success'
|
|
1003
|
+
});
|
|
1004
|
+
renderBucketReplicationFormPage('test-bucket', 'no-filter-rule');
|
|
1005
|
+
await waitFor(()=>{
|
|
1006
|
+
const filterSelect = screen.getByLabelText(/^filter$/i);
|
|
1007
|
+
expect(filterSelect).toBeInTheDocument();
|
|
1008
|
+
expect(screen.queryByLabelText(/prefix/i)).not.toBeInTheDocument();
|
|
1009
|
+
});
|
|
1010
|
+
});
|
|
1011
|
+
it('loads rule with prefix filter correctly', async ()=>{
|
|
1012
|
+
const rule = {
|
|
1013
|
+
ID: 'prefix-rule',
|
|
1014
|
+
Status: 'Enabled',
|
|
1015
|
+
Priority: 1,
|
|
1016
|
+
Filter: {
|
|
1017
|
+
Prefix: 'documents/'
|
|
1018
|
+
},
|
|
1019
|
+
Destination: {
|
|
1020
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1024
|
+
data: {
|
|
1025
|
+
ReplicationConfiguration: {
|
|
1026
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1027
|
+
Rules: [
|
|
1028
|
+
rule
|
|
1029
|
+
]
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
status: 'success'
|
|
1033
|
+
});
|
|
1034
|
+
renderBucketReplicationFormPage('test-bucket', 'prefix-rule');
|
|
1035
|
+
await waitFor(()=>{
|
|
1036
|
+
const prefixInput = screen.getByLabelText(/prefix/i);
|
|
1037
|
+
expect(prefixInput).toHaveValue('documents/');
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
it('loads rule with single tag filter correctly', async ()=>{
|
|
1041
|
+
const rule = {
|
|
1042
|
+
ID: 'tag-rule',
|
|
1043
|
+
Status: 'Enabled',
|
|
1044
|
+
Priority: 1,
|
|
1045
|
+
Filter: {
|
|
1046
|
+
Tag: {
|
|
1047
|
+
Key: 'env',
|
|
1048
|
+
Value: 'prod'
|
|
1049
|
+
}
|
|
1050
|
+
},
|
|
1051
|
+
Destination: {
|
|
1052
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1056
|
+
data: {
|
|
1057
|
+
ReplicationConfiguration: {
|
|
1058
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1059
|
+
Rules: [
|
|
1060
|
+
rule
|
|
1061
|
+
]
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
status: 'success'
|
|
1065
|
+
});
|
|
1066
|
+
renderBucketReplicationFormPage('test-bucket', 'tag-rule');
|
|
1067
|
+
await waitFor(()=>{
|
|
1068
|
+
expect(screen.getByDisplayValue('env')).toBeInTheDocument();
|
|
1069
|
+
expect(screen.getByDisplayValue('prod')).toBeInTheDocument();
|
|
1070
|
+
});
|
|
1071
|
+
});
|
|
1072
|
+
it('loads rule with prefix and tags filter correctly', async ()=>{
|
|
1073
|
+
const rule = {
|
|
1074
|
+
ID: 'and-rule',
|
|
1075
|
+
Status: 'Enabled',
|
|
1076
|
+
Priority: 1,
|
|
1077
|
+
Filter: {
|
|
1078
|
+
And: {
|
|
1079
|
+
Prefix: 'logs/',
|
|
1080
|
+
Tags: [
|
|
1081
|
+
{
|
|
1082
|
+
Key: 'type',
|
|
1083
|
+
Value: 'audit'
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
Key: 'year',
|
|
1087
|
+
Value: '2024'
|
|
1088
|
+
}
|
|
1089
|
+
]
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
Destination: {
|
|
1093
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1094
|
+
}
|
|
1095
|
+
};
|
|
1096
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1097
|
+
data: {
|
|
1098
|
+
ReplicationConfiguration: {
|
|
1099
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1100
|
+
Rules: [
|
|
1101
|
+
rule
|
|
1102
|
+
]
|
|
1103
|
+
}
|
|
1104
|
+
},
|
|
1105
|
+
status: 'success'
|
|
1106
|
+
});
|
|
1107
|
+
renderBucketReplicationFormPage('test-bucket', 'and-rule');
|
|
1108
|
+
await waitFor(()=>{
|
|
1109
|
+
const prefixInput = screen.getByLabelText(/prefix/i);
|
|
1110
|
+
expect(prefixInput).toHaveValue('logs/');
|
|
1111
|
+
expect(screen.getByDisplayValue('type')).toBeInTheDocument();
|
|
1112
|
+
expect(screen.getByDisplayValue('audit')).toBeInTheDocument();
|
|
1113
|
+
expect(screen.getByDisplayValue('year')).toBeInTheDocument();
|
|
1114
|
+
expect(screen.getByDisplayValue('2024')).toBeInTheDocument();
|
|
1115
|
+
});
|
|
1116
|
+
});
|
|
1117
|
+
it('loads rule with includeEncryptedObjects enabled', async ()=>{
|
|
1118
|
+
const rule = {
|
|
1119
|
+
ID: 'encrypted-rule',
|
|
1120
|
+
Status: 'Enabled',
|
|
1121
|
+
Priority: 1,
|
|
1122
|
+
Destination: {
|
|
1123
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1124
|
+
},
|
|
1125
|
+
SourceSelectionCriteria: {
|
|
1126
|
+
SseKmsEncryptedObjects: {
|
|
1127
|
+
Status: 'Enabled'
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1132
|
+
data: {
|
|
1133
|
+
ReplicationConfiguration: {
|
|
1134
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1135
|
+
Rules: [
|
|
1136
|
+
rule
|
|
1137
|
+
]
|
|
1138
|
+
}
|
|
1139
|
+
},
|
|
1140
|
+
status: 'success'
|
|
1141
|
+
});
|
|
1142
|
+
renderBucketReplicationFormPage('test-bucket', 'encrypted-rule');
|
|
1143
|
+
await waitFor(()=>{
|
|
1144
|
+
const includeEncryptedToggle = findToggleByLabel('Replicate encrypted objects');
|
|
1145
|
+
expect(includeEncryptedToggle).toBeChecked();
|
|
1146
|
+
});
|
|
1147
|
+
});
|
|
1148
|
+
it('loads rule with encryptReplicatedObjects and KMS Key ID', async ()=>{
|
|
1149
|
+
const rule = {
|
|
1150
|
+
ID: 'encrypt-replica-rule',
|
|
1151
|
+
Status: 'Enabled',
|
|
1152
|
+
Priority: 1,
|
|
1153
|
+
Destination: {
|
|
1154
|
+
Bucket: 'arn:aws:s3:::bucket',
|
|
1155
|
+
EncryptionConfiguration: {
|
|
1156
|
+
ReplicaKmsKeyID: 'arn:aws:kms:us-east-1:123456789012:key/12345'
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
};
|
|
1160
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1161
|
+
data: {
|
|
1162
|
+
ReplicationConfiguration: {
|
|
1163
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1164
|
+
Rules: [
|
|
1165
|
+
rule
|
|
1166
|
+
]
|
|
1167
|
+
}
|
|
1168
|
+
},
|
|
1169
|
+
status: 'success'
|
|
1170
|
+
});
|
|
1171
|
+
renderBucketReplicationFormPage('test-bucket', 'encrypt-replica-rule');
|
|
1172
|
+
await waitFor(()=>{
|
|
1173
|
+
const encryptReplicatedToggle = findToggleByLabel('Encrypt replicated objects');
|
|
1174
|
+
expect(encryptReplicatedToggle).toBeChecked();
|
|
1175
|
+
const kmsKeyInput = screen.getByLabelText(/replica kms key id/i);
|
|
1176
|
+
expect(kmsKeyInput).toHaveValue('arn:aws:kms:us-east-1:123456789012:key/12345');
|
|
1177
|
+
});
|
|
1178
|
+
});
|
|
1179
|
+
it('loads rule with enforceRTC and enableRTCNotification', async ()=>{
|
|
1180
|
+
const rule = {
|
|
1181
|
+
ID: 'rtc-rule',
|
|
1182
|
+
Status: 'Enabled',
|
|
1183
|
+
Priority: 1,
|
|
1184
|
+
Destination: {
|
|
1185
|
+
Bucket: 'arn:aws:s3:::bucket',
|
|
1186
|
+
ReplicationTime: {
|
|
1187
|
+
Status: 'Enabled',
|
|
1188
|
+
Time: {
|
|
1189
|
+
Minutes: 15
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
Metrics: {
|
|
1193
|
+
Status: 'Enabled',
|
|
1194
|
+
EventThreshold: {
|
|
1195
|
+
Minutes: 15
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
};
|
|
1200
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1201
|
+
data: {
|
|
1202
|
+
ReplicationConfiguration: {
|
|
1203
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1204
|
+
Rules: [
|
|
1205
|
+
rule
|
|
1206
|
+
]
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
status: 'success'
|
|
1210
|
+
});
|
|
1211
|
+
renderBucketReplicationFormPage('test-bucket', 'rtc-rule');
|
|
1212
|
+
await waitFor(()=>{
|
|
1213
|
+
expect(screen.getByText('Replication Time Control (RTC)')).toBeInTheDocument();
|
|
1214
|
+
});
|
|
1215
|
+
const enforceRTCToggle = findToggleByLabel('Replication Time Control (RTC)');
|
|
1216
|
+
expect(enforceRTCToggle).toBeChecked();
|
|
1217
|
+
});
|
|
1218
|
+
it('loads rule with replicaModifications enabled', async ()=>{
|
|
1219
|
+
const rule = {
|
|
1220
|
+
ID: 'replica-mod-rule',
|
|
1221
|
+
Status: 'Enabled',
|
|
1222
|
+
Priority: 1,
|
|
1223
|
+
Destination: {
|
|
1224
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1225
|
+
},
|
|
1226
|
+
SourceSelectionCriteria: {
|
|
1227
|
+
ReplicaModifications: {
|
|
1228
|
+
Status: 'Enabled'
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
};
|
|
1232
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1233
|
+
data: {
|
|
1234
|
+
ReplicationConfiguration: {
|
|
1235
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1236
|
+
Rules: [
|
|
1237
|
+
rule
|
|
1238
|
+
]
|
|
1239
|
+
}
|
|
1240
|
+
},
|
|
1241
|
+
status: 'success'
|
|
1242
|
+
});
|
|
1243
|
+
renderBucketReplicationFormPage('test-bucket', 'replica-mod-rule');
|
|
1244
|
+
await waitFor(()=>{
|
|
1245
|
+
const replicaModToggle = findToggleByLabel('Replica modification sync');
|
|
1246
|
+
expect(replicaModToggle).toBeChecked();
|
|
1247
|
+
});
|
|
1248
|
+
});
|
|
1249
|
+
it('loads rule with deleteMarkerReplication enabled', async ()=>{
|
|
1250
|
+
const rule = {
|
|
1251
|
+
ID: 'delete-marker-rule',
|
|
1252
|
+
Status: 'Enabled',
|
|
1253
|
+
Priority: 1,
|
|
1254
|
+
Destination: {
|
|
1255
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1256
|
+
},
|
|
1257
|
+
DeleteMarkerReplication: {
|
|
1258
|
+
Status: 'Enabled'
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1261
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1262
|
+
data: {
|
|
1263
|
+
ReplicationConfiguration: {
|
|
1264
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1265
|
+
Rules: [
|
|
1266
|
+
rule
|
|
1267
|
+
]
|
|
1268
|
+
}
|
|
1269
|
+
},
|
|
1270
|
+
status: 'success'
|
|
1271
|
+
});
|
|
1272
|
+
renderBucketReplicationFormPage('test-bucket', 'delete-marker-rule');
|
|
1273
|
+
await waitFor(()=>{
|
|
1274
|
+
const deleteMarkerToggle = findToggleByLabel('Delete marker replication');
|
|
1275
|
+
expect(deleteMarkerToggle).toBeChecked();
|
|
1276
|
+
});
|
|
1277
|
+
});
|
|
1278
|
+
it('loads rule with cross-account destination', async ()=>{
|
|
1279
|
+
const rule = {
|
|
1280
|
+
ID: 'cross-account-rule',
|
|
1281
|
+
Status: 'Enabled',
|
|
1282
|
+
Priority: 1,
|
|
1283
|
+
Destination: {
|
|
1284
|
+
Bucket: 'arn:aws:s3:::cross-account-bucket',
|
|
1285
|
+
Account: '987654321098'
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1289
|
+
data: {
|
|
1290
|
+
ReplicationConfiguration: {
|
|
1291
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1292
|
+
Rules: [
|
|
1293
|
+
rule
|
|
1294
|
+
]
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
status: 'success'
|
|
1298
|
+
});
|
|
1299
|
+
renderBucketReplicationFormPage('test-bucket', 'cross-account-rule');
|
|
1300
|
+
await waitFor(()=>{
|
|
1301
|
+
const sameAccountToggle = screen.getByText(/not same account destination/i);
|
|
1302
|
+
expect(sameAccountToggle).toBeInTheDocument();
|
|
1303
|
+
const accountIdInput = screen.getByLabelText(/target account id/i);
|
|
1304
|
+
expect(accountIdInput).toHaveValue('987654321098');
|
|
1305
|
+
const targetBucketInput = screen.getByLabelText(/target bucket/i);
|
|
1306
|
+
expect(targetBucketInput).toHaveValue('cross-account-bucket');
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
it('loads rule with switchObjectOwnership enabled', async ()=>{
|
|
1310
|
+
const rule = {
|
|
1311
|
+
ID: 'ownership-rule',
|
|
1312
|
+
Status: 'Enabled',
|
|
1313
|
+
Priority: 1,
|
|
1314
|
+
Destination: {
|
|
1315
|
+
Bucket: 'arn:aws:s3:::bucket',
|
|
1316
|
+
Account: '987654321098',
|
|
1317
|
+
AccessControlTranslation: {
|
|
1318
|
+
Owner: 'Destination'
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1323
|
+
data: {
|
|
1324
|
+
ReplicationConfiguration: {
|
|
1325
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1326
|
+
Rules: [
|
|
1327
|
+
rule
|
|
1328
|
+
]
|
|
1329
|
+
}
|
|
1330
|
+
},
|
|
1331
|
+
status: 'success'
|
|
1332
|
+
});
|
|
1333
|
+
renderBucketReplicationFormPage('test-bucket', 'ownership-rule');
|
|
1334
|
+
await waitFor(()=>{
|
|
1335
|
+
const ownershipToggle = findToggleByLabel('Switch Object ownership');
|
|
1336
|
+
expect(ownershipToggle).toBeChecked();
|
|
1337
|
+
});
|
|
1338
|
+
});
|
|
1339
|
+
it('loads rule with custom StorageClass', async ()=>{
|
|
1340
|
+
const rule = {
|
|
1341
|
+
ID: 'storage-class-rule',
|
|
1342
|
+
Status: 'Enabled',
|
|
1343
|
+
Priority: 1,
|
|
1344
|
+
Destination: {
|
|
1345
|
+
Bucket: 'arn:aws:s3:::bucket',
|
|
1346
|
+
StorageClass: 'GLACIER'
|
|
1347
|
+
}
|
|
1348
|
+
};
|
|
1349
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1350
|
+
data: {
|
|
1351
|
+
ReplicationConfiguration: {
|
|
1352
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1353
|
+
Rules: [
|
|
1354
|
+
rule
|
|
1355
|
+
]
|
|
1356
|
+
}
|
|
1357
|
+
},
|
|
1358
|
+
status: 'success'
|
|
1359
|
+
});
|
|
1360
|
+
renderBucketReplicationFormPage('test-bucket', 'storage-class-rule');
|
|
1361
|
+
await waitFor(()=>{
|
|
1362
|
+
const storageClassSelect = screen.getByLabelText(/storage class/i);
|
|
1363
|
+
expect(storageClassSelect).toBeInTheDocument();
|
|
1364
|
+
});
|
|
1365
|
+
});
|
|
1366
|
+
it('loads rule with custom Priority', async ()=>{
|
|
1367
|
+
const rule = {
|
|
1368
|
+
ID: 'priority-rule',
|
|
1369
|
+
Status: 'Enabled',
|
|
1370
|
+
Priority: 99,
|
|
1371
|
+
Destination: {
|
|
1372
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1373
|
+
}
|
|
1374
|
+
};
|
|
1375
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1376
|
+
data: {
|
|
1377
|
+
ReplicationConfiguration: {
|
|
1378
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1379
|
+
Rules: [
|
|
1380
|
+
rule
|
|
1381
|
+
]
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
status: 'success'
|
|
1385
|
+
});
|
|
1386
|
+
renderBucketReplicationFormPage('test-bucket', 'priority-rule');
|
|
1387
|
+
await waitFor(()=>{
|
|
1388
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1389
|
+
expect(priorityInput).toHaveValue(99);
|
|
1390
|
+
});
|
|
1391
|
+
});
|
|
1392
|
+
});
|
|
1393
|
+
describe('Priority Auto-assignment', ()=>{
|
|
1394
|
+
it('auto-assigns priority 0 when no existing rules', ()=>{
|
|
1395
|
+
renderBucketReplicationFormPage();
|
|
1396
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1397
|
+
expect(priorityInput).toHaveAttribute('placeholder', 'Example: Auto-assigned: 0');
|
|
1398
|
+
});
|
|
1399
|
+
it('auto-assigns next available priority when rules exist', ()=>{
|
|
1400
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1401
|
+
data: {
|
|
1402
|
+
ReplicationConfiguration: {
|
|
1403
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1404
|
+
Rules: [
|
|
1405
|
+
{
|
|
1406
|
+
ID: 'rule-1',
|
|
1407
|
+
Status: 'Enabled',
|
|
1408
|
+
Priority: 5,
|
|
1409
|
+
Destination: {
|
|
1410
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1411
|
+
}
|
|
1412
|
+
},
|
|
1413
|
+
{
|
|
1414
|
+
ID: 'rule-2',
|
|
1415
|
+
Status: 'Enabled',
|
|
1416
|
+
Priority: 10,
|
|
1417
|
+
Destination: {
|
|
1418
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
]
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
status: 'success'
|
|
1425
|
+
});
|
|
1426
|
+
renderBucketReplicationFormPage();
|
|
1427
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1428
|
+
expect(priorityInput).toHaveAttribute('placeholder', 'Example: Auto-assigned: 11');
|
|
1429
|
+
});
|
|
1430
|
+
it('allows manual priority override', async ()=>{
|
|
1431
|
+
renderBucketReplicationFormPage();
|
|
1432
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1433
|
+
await user_event.type(priorityInput, '25');
|
|
1434
|
+
expect(priorityInput).toHaveValue(25);
|
|
1435
|
+
});
|
|
1436
|
+
it('accepts empty priority and defaults to 0 on submit', async ()=>{
|
|
1437
|
+
renderBucketReplicationFormPage();
|
|
1438
|
+
await user_event.type(screen.getByLabelText(/role arn/i), 'arn:aws:iam::123456789012:role/role');
|
|
1439
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'no-priority-rule');
|
|
1440
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
1441
|
+
await user_event.click(targetBucketSelect);
|
|
1442
|
+
await user_event.click(screen.getByRole('option', {
|
|
1443
|
+
name: 'destination-bucket'
|
|
1444
|
+
}));
|
|
1445
|
+
mockMutate.mockImplementation((_, options)=>{
|
|
1446
|
+
options?.onSuccess?.();
|
|
1447
|
+
});
|
|
1448
|
+
await waitFor(()=>{
|
|
1449
|
+
const createButton = screen.getByRole('button', {
|
|
1450
|
+
name: /create/i
|
|
1451
|
+
});
|
|
1452
|
+
expect(createButton).toBeEnabled();
|
|
1453
|
+
});
|
|
1454
|
+
fireEvent.click(screen.getByRole('button', {
|
|
1455
|
+
name: /create/i
|
|
1456
|
+
}));
|
|
1457
|
+
await waitFor(()=>{
|
|
1458
|
+
const call = mockMutate.mock.calls[0][0];
|
|
1459
|
+
expect(call.ReplicationConfiguration.Rules[0].Priority).toBe(0);
|
|
1460
|
+
});
|
|
1461
|
+
});
|
|
1462
|
+
});
|
|
1463
|
+
describe('Target Bucket Selection', ()=>{
|
|
1464
|
+
it('renders target bucket field for same-account replication', ()=>{
|
|
1465
|
+
renderBucketReplicationFormPage();
|
|
1466
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
1467
|
+
expect(targetBucketSelect).toBeInTheDocument();
|
|
1468
|
+
});
|
|
1469
|
+
it('filters out current bucket from bucket list', async ()=>{
|
|
1470
|
+
renderBucketReplicationFormPage();
|
|
1471
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
1472
|
+
await user_event.click(targetBucketSelect);
|
|
1473
|
+
expect(screen.getByRole('option', {
|
|
1474
|
+
name: 'destination-bucket'
|
|
1475
|
+
})).toBeInTheDocument();
|
|
1476
|
+
expect(screen.getByRole('option', {
|
|
1477
|
+
name: 'backup-bucket'
|
|
1478
|
+
})).toBeInTheDocument();
|
|
1479
|
+
expect(screen.queryByRole('option', {
|
|
1480
|
+
name: 'test-bucket'
|
|
1481
|
+
})).not.toBeInTheDocument();
|
|
1482
|
+
});
|
|
1483
|
+
it('shows text Input for cross-account replication', async ()=>{
|
|
1484
|
+
renderBucketReplicationFormPage();
|
|
1485
|
+
const sameAccountToggle = screen.getByText(/same account destination/i);
|
|
1486
|
+
await user_event.click(sameAccountToggle);
|
|
1487
|
+
await waitFor(()=>{
|
|
1488
|
+
const targetBucketInput = screen.getByLabelText(/target bucket/i);
|
|
1489
|
+
expect(targetBucketInput.tagName).toBe('INPUT');
|
|
1490
|
+
});
|
|
1491
|
+
});
|
|
1492
|
+
it('loads available buckets using useBuckets hook', ()=>{
|
|
1493
|
+
renderBucketReplicationFormPage();
|
|
1494
|
+
expect(mockUseBuckets).toHaveBeenCalled();
|
|
1495
|
+
});
|
|
1496
|
+
});
|
|
1497
|
+
describe('Navigation & Button States', ()=>{
|
|
1498
|
+
it('navigates back to bucket replication tab on Cancel', ()=>{
|
|
1499
|
+
renderBucketReplicationFormPage();
|
|
1500
|
+
const cancelButton = screen.getByRole('button', {
|
|
1501
|
+
name: /cancel/i
|
|
1502
|
+
});
|
|
1503
|
+
fireEvent.click(cancelButton);
|
|
1504
|
+
expect(mockNavigate).toHaveBeenCalledWith('/buckets/test-bucket?tab=replication');
|
|
1505
|
+
});
|
|
1506
|
+
it('disables Cancel button when mutation is pending', ()=>{
|
|
1507
|
+
mockUseSetBucketReplication.mockReturnValue({
|
|
1508
|
+
mutate: mockMutate,
|
|
1509
|
+
isPending: true
|
|
1510
|
+
});
|
|
1511
|
+
renderBucketReplicationFormPage();
|
|
1512
|
+
const cancelButton = screen.getByRole('button', {
|
|
1513
|
+
name: /cancel/i
|
|
1514
|
+
});
|
|
1515
|
+
expect(cancelButton).toBeDisabled();
|
|
1516
|
+
});
|
|
1517
|
+
it('disables Submit button when mutation is pending', async ()=>{
|
|
1518
|
+
mockUseSetBucketReplication.mockReturnValue({
|
|
1519
|
+
mutate: mockMutate,
|
|
1520
|
+
isPending: true
|
|
1521
|
+
});
|
|
1522
|
+
renderBucketReplicationFormPage();
|
|
1523
|
+
await user_event.type(screen.getByLabelText(/role arn/i), 'arn:aws:iam::123456789012:role/role');
|
|
1524
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'test-rule');
|
|
1525
|
+
const targetBucketSelect = screen.getByLabelText(/target bucket/i);
|
|
1526
|
+
await user_event.click(targetBucketSelect);
|
|
1527
|
+
await user_event.click(screen.getByRole('option', {
|
|
1528
|
+
name: 'destination-bucket'
|
|
1529
|
+
}));
|
|
1530
|
+
await waitFor(()=>{
|
|
1531
|
+
const createButton = screen.getByRole('button', {
|
|
1532
|
+
name: /create/i
|
|
1533
|
+
});
|
|
1534
|
+
expect(createButton).toBeDisabled();
|
|
1535
|
+
});
|
|
1536
|
+
});
|
|
1537
|
+
it('shows Create button with no icon in create mode', ()=>{
|
|
1538
|
+
renderBucketReplicationFormPage();
|
|
1539
|
+
const createButton = screen.getByRole('button', {
|
|
1540
|
+
name: /create/i
|
|
1541
|
+
});
|
|
1542
|
+
expect(createButton).toBeInTheDocument();
|
|
1543
|
+
expect(createButton.querySelector('svg')).not.toBeInTheDocument();
|
|
1544
|
+
});
|
|
1545
|
+
it('shows Save button with Save icon in edit mode', async ()=>{
|
|
1546
|
+
const existingRule = {
|
|
1547
|
+
ID: 'edit-rule',
|
|
1548
|
+
Status: 'Enabled',
|
|
1549
|
+
Priority: 1,
|
|
1550
|
+
Destination: {
|
|
1551
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1552
|
+
}
|
|
1553
|
+
};
|
|
1554
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1555
|
+
data: {
|
|
1556
|
+
ReplicationConfiguration: {
|
|
1557
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1558
|
+
Rules: [
|
|
1559
|
+
existingRule
|
|
1560
|
+
]
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
status: 'success'
|
|
1564
|
+
});
|
|
1565
|
+
renderBucketReplicationFormPage('test-bucket', 'edit-rule');
|
|
1566
|
+
await waitFor(()=>{
|
|
1567
|
+
const saveButton = screen.getByRole('button', {
|
|
1568
|
+
name: /save/i
|
|
1569
|
+
});
|
|
1570
|
+
expect(saveButton).toBeInTheDocument();
|
|
1571
|
+
expect(saveButton.querySelector('svg')).toBeInTheDocument();
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
});
|
|
1575
|
+
describe('Edge Cases', ()=>{
|
|
1576
|
+
it('handles URL-encoded rule ID in edit mode', async ()=>{
|
|
1577
|
+
const existingRule = {
|
|
1578
|
+
ID: 'rule with spaces',
|
|
1579
|
+
Status: 'Enabled',
|
|
1580
|
+
Priority: 1,
|
|
1581
|
+
Destination: {
|
|
1582
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1586
|
+
data: {
|
|
1587
|
+
ReplicationConfiguration: {
|
|
1588
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1589
|
+
Rules: [
|
|
1590
|
+
existingRule
|
|
1591
|
+
]
|
|
1592
|
+
}
|
|
1593
|
+
},
|
|
1594
|
+
status: 'success'
|
|
1595
|
+
});
|
|
1596
|
+
renderBucketReplicationFormPage('test-bucket', encodeURIComponent('rule with spaces'));
|
|
1597
|
+
await waitFor(()=>{
|
|
1598
|
+
expect(screen.getByText('rule with spaces')).toBeInTheDocument();
|
|
1599
|
+
});
|
|
1600
|
+
});
|
|
1601
|
+
it('handles rules without Priority field', async ()=>{
|
|
1602
|
+
const rule = {
|
|
1603
|
+
ID: 'no-priority',
|
|
1604
|
+
Status: 'Enabled',
|
|
1605
|
+
Destination: {
|
|
1606
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1610
|
+
data: {
|
|
1611
|
+
ReplicationConfiguration: {
|
|
1612
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1613
|
+
Rules: [
|
|
1614
|
+
rule
|
|
1615
|
+
]
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
status: 'success'
|
|
1619
|
+
});
|
|
1620
|
+
renderBucketReplicationFormPage('test-bucket', 'no-priority');
|
|
1621
|
+
await waitFor(()=>{
|
|
1622
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1623
|
+
expect(priorityInput).toHaveValue(null);
|
|
1624
|
+
});
|
|
1625
|
+
});
|
|
1626
|
+
it('handles rules without StorageClass', async ()=>{
|
|
1627
|
+
const rule = {
|
|
1628
|
+
ID: 'no-storage-class',
|
|
1629
|
+
Status: 'Enabled',
|
|
1630
|
+
Priority: 1,
|
|
1631
|
+
Destination: {
|
|
1632
|
+
Bucket: 'arn:aws:s3:::bucket'
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
mockUseGetBucketReplication.mockReturnValue({
|
|
1636
|
+
data: {
|
|
1637
|
+
ReplicationConfiguration: {
|
|
1638
|
+
Role: 'arn:aws:iam::123456789012:role/role',
|
|
1639
|
+
Rules: [
|
|
1640
|
+
rule
|
|
1641
|
+
]
|
|
1642
|
+
}
|
|
1643
|
+
},
|
|
1644
|
+
status: 'success'
|
|
1645
|
+
});
|
|
1646
|
+
renderBucketReplicationFormPage('test-bucket', 'no-storage-class');
|
|
1647
|
+
await waitFor(()=>{
|
|
1648
|
+
const storageClassSelect = screen.getByLabelText(/storage class/i);
|
|
1649
|
+
expect(storageClassSelect).toBeInTheDocument();
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
});
|
|
1653
|
+
describe('Boundary Conditions', ()=>{
|
|
1654
|
+
it('accepts Rule ID with reasonable length', async ()=>{
|
|
1655
|
+
renderBucketReplicationFormPage();
|
|
1656
|
+
const longRuleId = 'rule-name-with-many-segments-' + 'segment-'.repeat(10);
|
|
1657
|
+
const ruleIdInput = screen.getByLabelText(/rule id/i);
|
|
1658
|
+
await user_event.type(ruleIdInput, longRuleId);
|
|
1659
|
+
await waitFor(()=>{
|
|
1660
|
+
expect(ruleIdInput).toHaveValue(longRuleId);
|
|
1661
|
+
});
|
|
1662
|
+
});
|
|
1663
|
+
it('handles special characters in Rule ID', async ()=>{
|
|
1664
|
+
renderBucketReplicationFormPage();
|
|
1665
|
+
const validSpecialChars = 'rule-name_with.special-chars_123';
|
|
1666
|
+
const ruleIdInput = screen.getByLabelText(/rule id/i);
|
|
1667
|
+
await user_event.type(ruleIdInput, validSpecialChars);
|
|
1668
|
+
await user_event.tab();
|
|
1669
|
+
await waitFor(()=>{
|
|
1670
|
+
expect(ruleIdInput).toHaveValue(validSpecialChars);
|
|
1671
|
+
});
|
|
1672
|
+
});
|
|
1673
|
+
it('validates Priority with boundary values', async ()=>{
|
|
1674
|
+
renderBucketReplicationFormPage();
|
|
1675
|
+
await user_event.type(screen.getByLabelText(/rule id/i), 'boundary-test');
|
|
1676
|
+
const priorityInput = screen.getByLabelText(/rule priority/i);
|
|
1677
|
+
await user_event.clear(priorityInput);
|
|
1678
|
+
await user_event.type(priorityInput, '0');
|
|
1679
|
+
await user_event.tab();
|
|
1680
|
+
await waitFor(()=>{
|
|
1681
|
+
expect(screen.queryByText(/priority must be at least 0/i)).not.toBeInTheDocument();
|
|
1682
|
+
});
|
|
1683
|
+
await user_event.clear(priorityInput);
|
|
1684
|
+
await user_event.type(priorityInput, '2147483647');
|
|
1685
|
+
await user_event.tab();
|
|
1686
|
+
await waitFor(()=>{
|
|
1687
|
+
expect(priorityInput).toHaveValue(2147483647);
|
|
1688
|
+
});
|
|
1689
|
+
});
|
|
1690
|
+
it('handles very long prefix values', async ()=>{
|
|
1691
|
+
renderBucketReplicationFormPage();
|
|
1692
|
+
const filterSelect = document.querySelector('[id="filterType"]');
|
|
1693
|
+
await user_event.click(filterSelect);
|
|
1694
|
+
await waitFor(()=>{
|
|
1695
|
+
expect(screen.getByRole('option', {
|
|
1696
|
+
name: 'Prefix filter'
|
|
1697
|
+
})).toBeInTheDocument();
|
|
1698
|
+
});
|
|
1699
|
+
await user_event.click(screen.getByRole('option', {
|
|
1700
|
+
name: 'Prefix filter'
|
|
1701
|
+
}));
|
|
1702
|
+
await waitFor(()=>{
|
|
1703
|
+
expect(screen.getByLabelText(/prefix/i)).toBeInTheDocument();
|
|
1704
|
+
});
|
|
1705
|
+
const longPrefix = 'logs/' + 'a'.repeat(100);
|
|
1706
|
+
const prefixInput = screen.getByLabelText(/prefix/i);
|
|
1707
|
+
await user_event.type(prefixInput, longPrefix);
|
|
1708
|
+
await waitFor(()=>{
|
|
1709
|
+
expect(prefixInput).toHaveValue(longPrefix);
|
|
1710
|
+
});
|
|
1711
|
+
});
|
|
1712
|
+
it('accepts valid Role ARN format', async ()=>{
|
|
1713
|
+
renderBucketReplicationFormPage();
|
|
1714
|
+
const roleArnInput = screen.getByLabelText(/role arn/i);
|
|
1715
|
+
await user_event.type(roleArnInput, 'arn:aws:iam::123456789012:role/replication-role');
|
|
1716
|
+
await user_event.tab();
|
|
1717
|
+
await waitFor(()=>{
|
|
1718
|
+
expect(roleArnInput).toHaveValue('arn:aws:iam::123456789012:role/replication-role');
|
|
1719
|
+
});
|
|
1720
|
+
});
|
|
1721
|
+
it('accepts valid Target Account ID', async ()=>{
|
|
1722
|
+
renderBucketReplicationFormPage();
|
|
1723
|
+
const sameAccountToggle = findToggleByLabel('Same account destination');
|
|
1724
|
+
await user_event.click(sameAccountToggle);
|
|
1725
|
+
await waitFor(()=>{
|
|
1726
|
+
expect(screen.getByLabelText(/target account id/i)).toBeInTheDocument();
|
|
1727
|
+
});
|
|
1728
|
+
const accountIdInput = screen.getByLabelText(/target account id/i);
|
|
1729
|
+
await user_event.type(accountIdInput, '123456789012');
|
|
1730
|
+
await user_event.tab();
|
|
1731
|
+
await waitFor(()=>{
|
|
1732
|
+
expect(accountIdInput).toHaveValue('123456789012');
|
|
1733
|
+
});
|
|
1734
|
+
});
|
|
1735
|
+
it('handles empty form submission attempt', async ()=>{
|
|
1736
|
+
renderBucketReplicationFormPage();
|
|
1737
|
+
const createButton = screen.getByRole('button', {
|
|
1738
|
+
name: /create/i
|
|
1739
|
+
});
|
|
1740
|
+
expect(createButton).toBeDisabled();
|
|
1741
|
+
});
|
|
1742
|
+
it('validates KMS Key ID format', async ()=>{
|
|
1743
|
+
renderBucketReplicationFormPage();
|
|
1744
|
+
const encryptToggle = findToggleByLabel('Encrypt replicated objects');
|
|
1745
|
+
await user_event.click(encryptToggle);
|
|
1746
|
+
await waitFor(()=>{
|
|
1747
|
+
expect(screen.getByLabelText(/replica kms key id/i)).toBeInTheDocument();
|
|
1748
|
+
});
|
|
1749
|
+
const kmsKeyInput = screen.getByLabelText(/replica kms key id/i);
|
|
1750
|
+
const validKmsArn = 'arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012';
|
|
1751
|
+
await user_event.type(kmsKeyInput, validKmsArn);
|
|
1752
|
+
await waitFor(()=>{
|
|
1753
|
+
expect(kmsKeyInput).toHaveValue(validKmsArn);
|
|
1754
|
+
});
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
});
|