From c4f3ada0ebf78be1c10ee8147ce9af63434f8759 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 6 Dec 2022 15:28:33 +0100 Subject: [PATCH] POC: integration tests (#2422) --- .../AddonMultiSelector.test.tsx | 4 + .../SelectProjectInput.test.tsx | 4 + .../changeRequest/ChangeRequest.test.tsx | 288 ++++++++++++++++++ .../ChangeRequest/ChangeRequest.tsx | 1 + .../UpdateEnabledMessage.tsx | 2 +- .../InstanceStatus/InstanceStatusBar.test.tsx | 7 + .../PermissionSwitch/PermissionSwitch.tsx | 1 + .../FeatureOverviewEnvSwitches.tsx | 2 +- .../user/ResetPassword/ResetPassword.test.tsx | 1 + .../useChangeRequestConfig.ts | 13 +- .../useEnterpriseSWR/useEnterpriseSWR.ts | 34 +++ .../usePendingChangeRequests.ts | 9 +- .../usePendingChangeRequestsForFeature.ts | 9 +- 13 files changed, 357 insertions(+), 18 deletions(-) create mode 100644 frontend/src/component/changeRequest/ChangeRequest.test.tsx create mode 100644 frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts diff --git a/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx b/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx index 186520ea37..98a2cee76a 100644 --- a/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx +++ b/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx @@ -7,6 +7,7 @@ import { IAddonMultiSelectorProps, AddonMultiSelector, } from './AddonMultiSelector'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; const onChange = vi.fn(); const onFocus = vi.fn(); @@ -24,10 +25,13 @@ const mockProps: IAddonMultiSelectorProps = { entityName: 'project', }; +const server = testServerSetup(); + describe('AddonMultiSelector', () => { beforeEach(() => { onChange.mockClear(); onFocus.mockClear(); + testServerRoute(server, '/api/admin/ui-config', {}); }); it('renders with default state', () => { diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx b/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx index 97ccfa906c..f013194bb5 100644 --- a/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx +++ b/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx @@ -7,6 +7,7 @@ import { ISelectProjectInputProps, SelectProjectInput, } from './SelectProjectInput'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; const onChange = vi.fn(); const onFocus = vi.fn(); @@ -22,10 +23,13 @@ const mockProps: ISelectProjectInputProps = { onFocus, }; +const server = testServerSetup(); + describe('SelectProjectInput', () => { beforeEach(() => { onChange.mockClear(); onFocus.mockClear(); + testServerRoute(server, '/api/admin/ui-config', {}); }); it('renders with default state', () => { diff --git a/frontend/src/component/changeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest.test.tsx new file mode 100644 index 0000000000..5d878a3205 --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest.test.tsx @@ -0,0 +1,288 @@ +import { + render, + screen, + waitFor, + within, + getAllByRole, + fireEvent, +} from '@testing-library/react'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { FeatureView } from '../feature/FeatureView/FeatureView'; +import { ThemeProvider } from 'themes/ThemeProvider'; +import { AccessProvider } from '../providers/AccessProvider/AccessProvider'; +import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider'; +import { testServerRoute, testServerSetup } from '../../utils/testServer'; +import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer'; +import { FC } from 'react'; + +const server = testServerSetup(); + +const pendingChangeRequest = (featureName: string) => + testServerRoute( + server, + 'api/admin/projects/default/change-requests/pending', + [ + { + id: 156, + environment: 'production', + state: 'Draft', + minApprovals: 1, + project: 'default', + createdBy: { + id: 1, + username: 'admin', + imageUrl: + 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro', + }, + createdAt: '2022-12-02T09:19:12.242Z', + features: [ + { + name: featureName, + changes: [ + { + id: 292, + action: 'addStrategy', + payload: { + name: 'default', + segments: [], + parameters: {}, + constraints: [], + }, + createdAt: '2022-12-02T09:19:12.245Z', + createdBy: { + id: 1, + username: 'admin', + imageUrl: + 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro', + }, + }, + ], + }, + ], + approvals: [], + comments: [], + }, + ] + ); + +const changeRequestsEnabledIn = (env: string) => + testServerRoute( + server, + '/api/admin/projects/default/change-requests/config', + [ + { + environment: 'development', + type: 'development', + changeRequestEnabled: env === 'development', + }, + { + environment: 'production', + type: 'production', + changeRequestEnabled: env === 'production', + }, + ] + ); + +const uiConfigForEnterprise = () => + testServerRoute(server, '/api/admin/ui-config', { + environment: 'Open Source', + flags: { + changeRequests: true, + }, + slogan: 'getunleash.io - All rights reserved', + name: 'Unleash enterprise', + links: [ + { + value: 'Documentation', + icon: 'library_books', + href: 'https://docs.getunleash.io/docs', + title: 'User documentation', + }, + { + value: 'GitHub', + icon: 'c_github', + href: 'https://github.com/Unleash/unleash', + title: 'Source code on GitHub', + }, + ], + version: '4.18.0-beta.5', + emailEnabled: false, + unleashUrl: 'http://localhost:4242', + baseUriPath: '', + authenticationType: 'enterprise', + segmentValuesLimit: 100, + strategySegmentsLimit: 5, + frontendApiOrigins: ['*'], + versionInfo: { + current: { oss: '4.18.0-beta.5', enterprise: '4.17.0-beta.1' }, + latest: {}, + isLatest: true, + instanceId: 'c7566052-15d7-4e09-9625-9c988e1f2be7', + }, + disablePasswordAuth: false, + }); + +const featureList = (featureName: string) => + testServerRoute(server, '/api/admin/projects/default', { + name: 'Default', + description: 'Default project', + health: 100, + updatedAt: '2022-11-14T10:15:59.228Z', + environments: ['development', 'production'], + features: [ + { + type: 'release', + name: featureName, + createdAt: '2022-11-14T08:16:33.338Z', + lastSeenAt: null, + stale: false, + environments: [ + { + name: 'development', + enabled: false, + type: 'development', + sortOrder: 100, + }, + { + name: 'production', + enabled: false, + type: 'production', + sortOrder: 200, + }, + ], + }, + ], + members: 0, + version: 1, + }); + +const feature = ({ name, enabled }: { name: string; enabled: boolean }) => + testServerRoute(server, `/api/admin/projects/default/features/${name}`, { + environments: [ + { + name: 'development', + enabled: false, + type: 'development', + sortOrder: 100, + strategies: [], + }, + { + name: 'production', + enabled, + type: 'production', + sortOrder: 200, + strategies: [], + }, + ], + name, + impressionData: false, + description: '', + project: 'default', + stale: false, + variants: [], + createdAt: '2022-11-14T08:16:33.338Z', + lastSeenAt: null, + type: 'release', + archived: false, + }); + +const otherRequests = (feature: string) => { + testServerRoute(server, `api/admin/client-metrics/features/${feature}`, { + version: 1, + maturity: 'stable', + featureName: feature, + lastHourUsage: [], + seenApplications: [], + }); + testServerRoute(server, `api/admin/features/${feature}/tags`, { + version: 1, + tags: [], + }); + testServerRoute(server, 'api/admin/user', { + user: { + isAPI: false, + id: 17, + name: 'Some User', + email: 'user@example.com', + imageUrl: + 'https://gravatar.com/avatar/8aa1132e102345f8c79322340e15340?size=42&default=retro', + seenAt: '2022-11-28T14:55:18.982Z', + loginAttempts: 0, + createdAt: '2022-11-23T13:31:17.061Z', + }, + permissions: [{ permission: 'ADMIN' }], + feedback: [], + splash: {}, + }); +}; + +const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({ + children, + path, + pathTemplate, +}) => ( + + + + + + + + + + + + + +); + +const setupHttpRoutes = ({ + featureName, + enabled, +}: { + featureName: string; + enabled: boolean; +}) => { + pendingChangeRequest(featureName); + changeRequestsEnabledIn('production'); + uiConfigForEnterprise(); + featureList(featureName); + feature({ name: featureName, enabled }); + otherRequests(featureName); +}; + +const verifyBannerForPendingChangeRequest = async () => { + return screen.findByText('Change request mode', {}, { timeout: 5000 }); +}; + +const changeToggle = async (environment: string) => { + const featureToggleStatusBox = screen.getByTestId('feature-toggle-status'); + await within(featureToggleStatusBox).findByText(environment); + const toggle = screen.getAllByRole('checkbox')[1]; + fireEvent.click(toggle); +}; + +const verifyChangeRequestDialog = async (bannerMainText: string) => { + await screen.findByText('Your suggestion:'); + const message = screen.getByTestId('update-enabled-message').textContent; + expect(message).toBe(bannerMainText); +}; + +test('add toggle change to pending change request', async () => { + setupHttpRoutes({ featureName: 'test', enabled: false }); + + render( + + + + ); + + await verifyBannerForPendingChangeRequest(); + + await changeToggle('production'); + + await verifyChangeRequestDialog('Enable feature toggle test in production'); +}); diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx index a571beb130..778ba31460 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx @@ -224,6 +224,7 @@ export const ChangeRequest: VFC = ({ > {feature.changes.map((change, index) => ( ( - + {enabled ? 'Enable' : 'Disable'} feature toggle{' '} {featureName} in {environment} diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx index 8791740e68..d78c26248d 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx @@ -5,6 +5,13 @@ import { screen } from '@testing-library/react'; import { addDays, subDays } from 'date-fns'; import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds'; import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; + +const server = testServerSetup(); + +beforeEach(() => { + testServerRoute(server, '/api/admin/ui-config', {}); +}); test('InstanceStatusBar should be hidden by default', async () => { render(); diff --git a/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx b/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx index 3d99a7af94..d0c91e65ee 100644 --- a/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx +++ b/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx @@ -44,6 +44,7 @@ const PermissionSwitch = React.forwardRef< { }; return ( - + Feature toggle status { + testServerRoute(server, '/api/admin/ui-config', {}); testServerRoute(server, '/api/admin/user', {}); testServerRoute(server, '/auth/reset/validate', { name: INVALID_TOKEN_ERROR, diff --git a/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts b/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts index 83f8d1c147..6b351aa1db 100644 --- a/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts +++ b/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts @@ -1,20 +1,21 @@ -import useSWR from 'swr'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import { IChangeRequestEnvironmentConfig } from 'component/changeRequest/changeRequest.types'; -import useUiConfig from '../useUiConfig/useUiConfig'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; export const useChangeRequestConfig = (projectId: string) => { - const { isOss } = useUiConfig(); - const { data, error, mutate } = useSWR( + const { data, error, mutate } = useEnterpriseSWR< + IChangeRequestEnvironmentConfig[] + >( formatApiPath(`api/admin/projects/${projectId}/change-requests/config`), - (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path)) + fetcher, + [] ); return { data: data || [], loading: !error && !data, - refetchChangeRequestConfig: () => mutate(), + refetchChangeRequestConfig: mutate, error, }; }; diff --git a/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts b/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts new file mode 100644 index 0000000000..c391bab7f5 --- /dev/null +++ b/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts @@ -0,0 +1,34 @@ +import useSWR, { BareFetcher, Key, SWRResponse } from 'swr'; +import { useEffect } from 'react'; +import useUiConfig from '../useUiConfig/useUiConfig'; + +export const useConditionalSWR = ( + key: Key, + fetcher: BareFetcher, + condition: T +): SWRResponse => { + const result = useSWR(key, fetcher); + + useEffect(() => { + result.mutate(); + }, [condition]); + + return result; +}; + +export const useEnterpriseSWR = ( + key: Key, + fetcher: BareFetcher, + fallback: Data +) => { + const { isEnterprise } = useUiConfig(); + + const result = useConditionalSWR( + key, + (path: string) => + isEnterprise() ? fetcher(path) : Promise.resolve(fallback), + isEnterprise() + ); + + return result; +}; diff --git a/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts b/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts index 1a73d1653f..9f8e939062 100644 --- a/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts +++ b/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts @@ -1,8 +1,7 @@ -import useSWR from 'swr'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import { IChangeRequest } from 'component/changeRequest/changeRequest.types'; -import useUiConfig from '../useUiConfig/useUiConfig'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; const fetcher = (path: string) => { return fetch(path) @@ -11,10 +10,10 @@ const fetcher = (path: string) => { }; export const usePendingChangeRequests = (project: string) => { - const { isOss } = useUiConfig(); - const { data, error, mutate } = useSWR( + const { data, error, mutate } = useEnterpriseSWR( formatApiPath(`api/admin/projects/${project}/change-requests/pending`), - (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path)) + fetcher, + [] ); return { diff --git a/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts b/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts index 9f780c0181..5a196f1f70 100644 --- a/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts +++ b/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts @@ -1,8 +1,7 @@ -import useSWR from 'swr'; import { formatApiPath } from 'utils/formatPath'; import handleErrorResponses from '../httpErrorResponseHandler'; import { IChangeRequest } from 'component/changeRequest/changeRequest.types'; -import useUiConfig from '../useUiConfig/useUiConfig'; +import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR'; const fetcher = (path: string) => { return fetch(path) @@ -14,12 +13,12 @@ export const usePendingChangeRequestsForFeature = ( project: string, featureName: string ) => { - const { isOss } = useUiConfig(); - const { data, error, mutate } = useSWR( + const { data, error, mutate } = useEnterpriseSWR( formatApiPath( `api/admin/projects/${project}/change-requests/pending/${featureName}` ), - (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path)) + fetcher, + [] ); return {