From 79770f85f0f0e80af3446ae656b63612e6be8135 Mon Sep 17 00:00:00 2001 From: RachelHowellNHS <127406911+RachelHowellNHS@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:42:23 +0000 Subject: [PATCH] PRMDR-700 add feature flag check to upload LG workflow --- ...upload_lloyd_george_is_bsol_gp_admin.cy.js | 7 ++ .../LloydGeorgeDownloadAllStage.test.tsx | 11 +-- .../LloydGeorgeRecordError.test.tsx | 70 +++++++++++++++---- .../LloydGeorgeRecordError.tsx | 38 ++++++---- .../LloydGeorgeRecordStage.test.tsx | 11 ++- app/src/helpers/hooks/useConfig.test.tsx | 8 +-- app/src/helpers/requests/getFeatureFlags.ts | 8 +-- app/src/helpers/test/testBuilders.ts | 25 +++++++ app/src/helpers/utils/errorCodes.ts | 3 + .../AuthCallbackPage.test.tsx | 12 ++-- .../LloydGeorgeRecordPage.test.tsx | 4 ++ .../configProvider/ConfigProvider.test.tsx | 11 +-- .../configProvider/ConfigProvider.tsx | 3 +- app/src/types/generic/featureFlags.ts | 12 +++- lambdas/enums/lambda_error.py | 5 ++ .../create_document_reference_handler.py | 21 +++++- .../test_create_document_reference_handler.py | 54 ++++++++++++-- 17 files changed, 239 insertions(+), 64 deletions(-) diff --git a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/upload_lloyd_george_is_bsol_gp_admin.cy.js b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/upload_lloyd_george_is_bsol_gp_admin.cy.js index 424de4316..e1bb15ee3 100644 --- a/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/upload_lloyd_george_is_bsol_gp_admin.cy.js +++ b/app/cypress/e2e/0-ndr-core-tests/gp_user_workflows/upload_lloyd_george_is_bsol_gp_admin.cy.js @@ -78,6 +78,13 @@ const stubbedResponseMulti = { }; describe('GP Workflow: Upload Lloyd George record when user is GP admin BSOL and patient has no record', () => { const beforeEachConfiguration = () => { + cy.intercept('GET', '/FeatureFlags*', { + statusCode: 200, + body: { + uploadLloydGeorgeWorkflowEnabled: true, + uploadLambdaEnabled: true, + }, + }); cy.login(Roles.GP_ADMIN); cy.visit(searchPatientUrl); diff --git a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx index 883ced258..0f767237c 100644 --- a/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeDownloadAllStage/LloydGeorgeDownloadAllStage.test.tsx @@ -1,6 +1,10 @@ import { render, screen, waitFor } from '@testing-library/react'; import LgDownloadAllStage, { Props } from './LloydGeorgeDownloadAllStage'; -import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/testBuilders'; +import { + buildConfig, + buildLgSearchResult, + buildPatientDetails, +} from '../../../helpers/test/testBuilders'; import axios from 'axios'; import { act } from 'react-dom/test-utils'; import userEvent from '@testing-library/user-event'; @@ -8,9 +12,7 @@ import usePatient from '../../../helpers/hooks/usePatient'; import { LinkProps } from 'react-router-dom'; import { routes } from '../../../types/generic/routes'; import useConfig from '../../../helpers/hooks/useConfig'; -import { defaultFeatureFlags } from '../../../helpers/requests/getFeatureFlags'; -jest.mock('../../../helpers/hooks/useConfig'); const mockedUseNavigate = jest.fn(); const mockedAxios = axios as jest.Mocked; const mockedUsePatient = usePatient as jest.Mock; @@ -30,12 +32,13 @@ jest.mock('moment', () => { jest.mock('axios'); jest.mock('../../../helpers/hooks/useBaseAPIHeaders'); jest.mock('../../../helpers/hooks/usePatient'); +jest.mock('../../../helpers/hooks/useConfig'); describe('LloydGeorgeDownloadAllStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; mockedUsePatient.mockReturnValue(mockPatient); - mockUseConfig.mockReturnValue({ featureFlags: defaultFeatureFlags, mockLocal: {} }); + mockUseConfig.mockReturnValue(buildConfig()); }); afterEach(() => { jest.clearAllMocks(); diff --git a/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.test.tsx b/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.test.tsx index 3a2a3a5eb..bfee7a15f 100644 --- a/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.test.tsx @@ -7,29 +7,32 @@ import useRole from '../../../helpers/hooks/useRole'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { routes } from '../../../types/generic/routes'; import useIsBSOL from '../../../helpers/hooks/useIsBSOL'; +import useConfig from '../../../helpers/hooks/useConfig'; +import { buildConfig } from '../../../helpers/test/testBuilders'; -const mockSetStage = jest.fn(); -const mockNavigate = jest.fn(); jest.mock('../../../helpers/hooks/useIsBSOL'); - +jest.mock('../../../helpers/hooks/useRole'); +jest.mock('../../../helpers/hooks/useConfig'); jest.mock('react-router-dom', () => ({ __esModule: true, Link: (props: LinkProps) => , useNavigate: () => mockNavigate, })); - jest.mock('react-router', () => ({ useNavigate: () => mockNavigate, })); -jest.mock('../../../helpers/hooks/useRole'); const mockUseRole = useRole as jest.Mock; -const mockedIsBSOL = useIsBSOL as jest.Mock; +const mockIsBSOL = useIsBSOL as jest.Mock; +const mockUseConfig = useConfig as jest.Mock; +const mockSetStage = jest.fn(); +const mockNavigate = jest.fn(); describe('LloydGeorgeRecordError', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + mockUseConfig.mockReturnValue(buildConfig()); }); afterEach(() => { @@ -48,6 +51,7 @@ describe('LloydGeorgeRecordError', () => { ).toBeInTheDocument(); expect(screen.getByText(/please download instead/i)).toBeInTheDocument(); }); + it("renders an error when the document download status is 'Failed'", () => { const timeoutStatus = DOWNLOAD_STAGE.FAILED; render( @@ -61,9 +65,10 @@ describe('LloydGeorgeRecordError', () => { screen.getByText(/An error has occurred when creating the Lloyd George preview/i), ).toBeInTheDocument(); }); + it("renders a message when the document download status is 'No records' and user is non BSOL", () => { const timeoutStatus = DOWNLOAD_STAGE.NO_RECORDS; - mockedIsBSOL.mockReturnValue(false); + mockIsBSOL.mockReturnValue(false); render( , @@ -74,10 +79,20 @@ describe('LloydGeorgeRecordError', () => { screen.queryByRole('button', { name: 'Upload patient record' }), ).not.toBeInTheDocument(); }); - it("renders a message and upload button when the document download status is 'No records' and user is admin BSOL", () => { + + it("renders a message and upload button when the document download status is 'No records', user is admin BSOL and upload flags are enabled", () => { const noRecordsStatus = DOWNLOAD_STAGE.NO_RECORDS; - mockedIsBSOL.mockReturnValue(true); + mockIsBSOL.mockReturnValue(true); mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + mockUseConfig.mockReturnValue( + buildConfig( + {}, + { + uploadLloydGeorgeWorkflowEnabled: true, + uploadLambdaEnabled: true, + }, + ), + ); render( , @@ -88,11 +103,28 @@ describe('LloydGeorgeRecordError', () => { screen.getByRole('button', { name: 'Upload patient record' }), ).toBeInTheDocument(); }); + + it("renders a message but no upload button when the document download status is 'No records', user is admin BSOL and upload flags are not enabled", () => { + const noRecordsStatus = DOWNLOAD_STAGE.NO_RECORDS; + + mockIsBSOL.mockReturnValue(true); + mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + + render( + , + ); + + expect(screen.getByText('No records available for this patient')).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Upload patient record' }), + ).not.toBeInTheDocument(); + }); }); describe('Navigation', () => { it("renders a link that can navigate to the download all stage, when download status is 'Timeout'", () => { const timeoutStatus = DOWNLOAD_STAGE.TIMEOUT; + render( , ); @@ -110,6 +142,7 @@ describe('LloydGeorgeRecordError', () => { it("navigates to the download all stage, when download status is 'Timeout' and the link is clicked: GP_ADMIN", () => { const timeoutStatus = DOWNLOAD_STAGE.TIMEOUT; + render( , ); @@ -131,9 +164,10 @@ describe('LloydGeorgeRecordError', () => { expect(mockSetStage).toHaveBeenCalledWith(LG_RECORD_STAGE.DOWNLOAD_ALL); }); - it("navigates to unauthorised, when download status is 'Timeout' and the link is clicked: GP_CLINCIAL", () => { + it("navigates to unauthorised, when download status is 'Timeout' and the link is clicked: GP_CLINICAL", () => { mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_CLINICAL); const timeoutStatus = DOWNLOAD_STAGE.TIMEOUT; + render( , ); @@ -154,17 +188,29 @@ describe('LloydGeorgeRecordError', () => { expect(mockNavigate).toBeCalledWith(routes.UNAUTHORISED); }); - it("navigates to upload page, when the document download status is 'No records' and user is admin BSOL", () => { + + it("navigates to upload page, when the document download status is 'No records', user is admin BSOL and upload flags are enabled", () => { const noRecordsStatus = DOWNLOAD_STAGE.NO_RECORDS; - mockedIsBSOL.mockReturnValue(true); + mockIsBSOL.mockReturnValue(true); mockUseRole.mockReturnValue(REPOSITORY_ROLE.GP_ADMIN); + mockUseConfig.mockReturnValue( + buildConfig( + {}, + { + uploadLloydGeorgeWorkflowEnabled: true, + uploadLambdaEnabled: true, + }, + ), + ); render( , ); + const uploadButton = screen.getByRole('button', { name: 'Upload patient record' }); expect(screen.getByText('No records available for this patient')).toBeInTheDocument(); expect(uploadButton).toBeInTheDocument(); + act(() => { uploadButton.click(); }); diff --git a/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.tsx b/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.tsx index e041eda41..ef1e28732 100644 --- a/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordError/LloydGeorgeRecordError.tsx @@ -8,6 +8,7 @@ import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; import { routes } from '../../../types/generic/routes'; import useIsBSOL from '../../../helpers/hooks/useIsBSOL'; import { ButtonLink } from 'nhsuk-react-components'; +import useConfig from '../../../helpers/hooks/useConfig'; type Props = { downloadStage: DOWNLOAD_STAGE; @@ -18,6 +19,7 @@ function LloydGeorgeRecordError({ downloadStage, setStage }: Props) { const role = useRole(); const navigate = useNavigate(); const isBSOL = useIsBSOL(); + const { featureFlags } = useConfig(); if (downloadStage === DOWNLOAD_STAGE.TIMEOUT) { return ( @@ -48,21 +50,27 @@ function LloydGeorgeRecordError({ downloadStage, setStage }: Props) { return (

No records available for this patient

-

- You can upload full or part of a patient record. You can upload supporting files - once the record is uploaded. -

-
- { - navigate(routes.LLOYD_GEORGE_UPLOAD); - }} - > - Upload patient record - -
+ {featureFlags.uploadLloydGeorgeWorkflowEnabled && + featureFlags.uploadLambdaEnabled && ( + <> +

+ You can upload full or part of a patient record. You can upload + supporting files once the record is uploaded. +

+ +
+ { + navigate(routes.LLOYD_GEORGE_UPLOAD); + }} + > + Upload patient record + +
+ + )}
); } else if (downloadStage === DOWNLOAD_STAGE.NO_RECORDS) { diff --git a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx index 347096914..b70f2a0a8 100644 --- a/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx +++ b/app/src/components/blocks/lloydGeorgeRecordStage/LloydGeorgeRecordStage.test.tsx @@ -1,10 +1,13 @@ import { render, screen, waitFor } from '@testing-library/react'; -import { buildLgSearchResult, buildPatientDetails } from '../../../helpers/test/testBuilders'; +import { + buildConfig, + buildLgSearchResult, + buildPatientDetails, +} from '../../../helpers/test/testBuilders'; import userEvent from '@testing-library/user-event'; import LgRecordStage, { Props } from './LloydGeorgeRecordStage'; import { getFormattedDate } from '../../../helpers/utils/formatDate'; import { DOWNLOAD_STAGE } from '../../../types/generic/downloadStage'; -import { useState } from 'react'; import formatFileSize from '../../../helpers/utils/formatFileSize'; import { act } from 'react-dom/test-utils'; import { LG_RECORD_STAGE } from '../../../types/blocks/lloydGeorgeStages'; @@ -12,6 +15,7 @@ import usePatient from '../../../helpers/hooks/usePatient'; import useRole from '../../../helpers/hooks/useRole'; import useIsBSOL from '../../../helpers/hooks/useIsBSOL'; import { REPOSITORY_ROLE } from '../../../types/generic/authRole'; +import useConfig from '../../../helpers/hooks/useConfig'; const mockPdf = buildLgSearchResult(); const mockPatientDetails = buildPatientDetails(); @@ -19,11 +23,13 @@ const mockPatientDetails = buildPatientDetails(); jest.mock('../../../helpers/hooks/useRole'); jest.mock('../../../helpers/hooks/usePatient'); jest.mock('../../../helpers/hooks/useIsBSOL'); +jest.mock('../../../helpers/hooks/useConfig'); const mockedUsePatient = usePatient as jest.Mock; const mockNavigate = jest.fn(); const mockedUseRole = useRole as jest.Mock; const mockedIsBSOL = useIsBSOL as jest.Mock; const mockSetStage = jest.fn(); +const mockUseConfig = useConfig as jest.Mock; jest.mock('react-router', () => ({ useNavigate: () => mockNavigate, @@ -33,6 +39,7 @@ describe('LloydGeorgeRecordStage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; mockedUsePatient.mockReturnValue(mockPatientDetails); + mockUseConfig.mockReturnValue(buildConfig()); }); afterEach(() => { jest.clearAllMocks(); diff --git a/app/src/helpers/hooks/useConfig.test.tsx b/app/src/helpers/hooks/useConfig.test.tsx index 6b4e544f0..980bae44e 100644 --- a/app/src/helpers/hooks/useConfig.test.tsx +++ b/app/src/helpers/hooks/useConfig.test.tsx @@ -1,7 +1,7 @@ import { render, screen } from '@testing-library/react'; import useConfig from './useConfig'; import ConfigProvider, { GlobalConfig } from '../../providers/configProvider/ConfigProvider'; -import { defaultFeatureFlags } from '../requests/getFeatureFlags'; +import { defaultFeatureFlags } from '../../types/generic/featureFlags'; describe('useConfig', () => { beforeEach(() => { @@ -14,7 +14,7 @@ describe('useConfig', () => { it('returns true when feature flag in context', () => { const config: GlobalConfig = { - featureFlags: { ...defaultFeatureFlags, testFeature1: true }, + featureFlags: { ...defaultFeatureFlags, uploadLloydGeorgeWorkflowEnabled: true }, mockLocal: {}, }; renderHook(config); @@ -23,7 +23,7 @@ describe('useConfig', () => { it('returns false when there is no feature flag in context', () => { const config: GlobalConfig = { - featureFlags: { ...defaultFeatureFlags, testFeature1: false }, + featureFlags: { ...defaultFeatureFlags, uploadLloydGeorgeWorkflowEnabled: false }, mockLocal: {}, }; renderHook(config); @@ -33,7 +33,7 @@ describe('useConfig', () => { const TestApp = () => { const config = useConfig(); - return
{`FLAG: ${!!config.featureFlags.testFeature1}`.normalize()}
; + return
{`FLAG: ${config.featureFlags.uploadLloydGeorgeWorkflowEnabled}`.normalize()}
; }; const renderHook = (config?: GlobalConfig) => { diff --git a/app/src/helpers/requests/getFeatureFlags.ts b/app/src/helpers/requests/getFeatureFlags.ts index 3a45ffd78..497a7d560 100644 --- a/app/src/helpers/requests/getFeatureFlags.ts +++ b/app/src/helpers/requests/getFeatureFlags.ts @@ -2,7 +2,7 @@ import { AuthHeaders } from '../../types/blocks/authHeaders'; import { endpoints } from '../../types/generic/endpoints'; import axios from 'axios'; -import { FeatureFlags } from '../../types/generic/featureFlags'; +import { defaultFeatureFlags, FeatureFlags } from '../../types/generic/featureFlags'; type Args = { baseUrl: string; @@ -13,12 +13,6 @@ type GetFeatureFlagsResponse = { data: FeatureFlags; }; -export const defaultFeatureFlags = { - testFeature1: false, - testFeature2: false, - testFeature3: false, -}; - const getFeatureFlags = async ({ baseUrl, baseHeaders }: Args) => { const gatewayUrl = baseUrl + endpoints.FEATURE_FLAGS; try { diff --git a/app/src/helpers/test/testBuilders.ts b/app/src/helpers/test/testBuilders.ts index 53a837d59..e9a6fb6c4 100644 --- a/app/src/helpers/test/testBuilders.ts +++ b/app/src/helpers/test/testBuilders.ts @@ -11,6 +11,8 @@ import { LloydGeorgeStitchResult } from '../requests/getLloydGeorgeRecord'; import { REPOSITORY_ROLE } from '../../types/generic/authRole'; import { v4 as uuidv4 } from 'uuid'; import moment from 'moment'; +import { GlobalConfig, LocalFlags } from '../../providers/configProvider/ConfigProvider'; +import { FeatureFlags } from '../../types/generic/featureFlags'; const buildUserAuth = (userAuthOverride?: Partial) => { const auth: UserAuth = { @@ -110,6 +112,28 @@ const buildLgSearchResult = () => { return result; }; +const buildConfig = ( + localFlagsOverride?: Partial, + featureFlagsOverride?: Partial, +) => { + const globalConfig: GlobalConfig = { + mockLocal: { + isBsol: true, + recordUploaded: true, + userRole: REPOSITORY_ROLE.GP_ADMIN, + ...localFlagsOverride, + }, + featureFlags: { + uploadLloydGeorgeWorkflowEnabled: false, + uploadLambdaEnabled: false, + uploadArfWorkflowEnabled: false, + ...featureFlagsOverride, + }, + }; + + return globalConfig; +}; + export { buildPatientDetails, buildTextFile, @@ -118,4 +142,5 @@ export { buildLgSearchResult, buildUserAuth, buildLgFile, + buildConfig, }; diff --git a/app/src/helpers/utils/errorCodes.ts b/app/src/helpers/utils/errorCodes.ts index ab88559a4..eb241ea2b 100644 --- a/app/src/helpers/utils/errorCodes.ts +++ b/app/src/helpers/utils/errorCodes.ts @@ -27,6 +27,9 @@ const errorCodes: { [key: string]: string } = { GWY_5001: 'Failed to utilise AWS client/resource', SFB_5001: 'Error occur when sending email by SES', SFB_5002: 'Failed to fetch parameters for sending email from SSM param store', + FFL_5001: 'Failed to parse feature flag/s from AppConfig response', + FFL_5002: 'Failed to retrieve feature flag/s from AppConfig profile', + FFL_5003: 'Feature is not enabled', }; export default errorCodes; diff --git a/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx b/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx index 315adb5ee..fbc391409 100644 --- a/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx +++ b/app/src/pages/authCallbackPage/AuthCallbackPage.test.tsx @@ -9,7 +9,7 @@ import { routes } from '../../types/generic/routes'; import ConfigProvider, { useConfigContext } from '../../providers/configProvider/ConfigProvider'; import { act } from 'react-dom/test-utils'; import { endpoints } from '../../types/generic/endpoints'; -import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; +import { defaultFeatureFlags } from '../../types/generic/featureFlags'; jest.mock('../../helpers/hooks/useConfig'); const mockedUseNavigate = jest.fn(); @@ -129,7 +129,7 @@ describe('AuthCallbackPage', () => { return Promise.resolve({ data: buildUserAuth() }); } else { return Promise.resolve({ - data: { ...defaultFeatureFlags, testFeature1: true }, + data: { ...defaultFeatureFlags, uploadLloydGeorgeWorkflowEnabled: true }, }); } }); @@ -148,8 +148,12 @@ const TestApp = () => { return (
-
{`FLAG: ${JSON.stringify(config.featureFlags.testFeature1)}`.normalize()}
; -
{`LOGGEDIN: ${!!session.auth?.role}`.normalize()}
; +
+ {`FLAG: ${JSON.stringify( + config.featureFlags.uploadLloydGeorgeWorkflowEnabled, + )}`.normalize()} +
+ ;
{`LOGGEDIN: ${!!session.auth?.role}`.normalize()}
;
); }; diff --git a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx index 9eb97acd9..c9061f508 100644 --- a/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx +++ b/app/src/pages/lloydGeorgeRecordPage/LloydGeorgeRecordPage.test.tsx @@ -4,6 +4,7 @@ import { buildPatientDetails, buildLgSearchResult, buildSearchResult, + buildConfig, } from '../../helpers/test/testBuilders'; import { getFormattedDate } from '../../helpers/utils/formatDate'; import axios from 'axios'; @@ -11,6 +12,7 @@ import formatFileSize from '../../helpers/utils/formatFileSize'; import usePatient from '../../helpers/hooks/usePatient'; import { act } from 'react-dom/test-utils'; import { routes } from '../../types/generic/routes'; +import useConfig from '../../helpers/hooks/useConfig'; jest.mock('../../helpers/hooks/useConfig'); jest.mock('axios'); @@ -23,6 +25,7 @@ const mockAxios = axios as jest.Mocked; const mockPatientDetails = buildPatientDetails(); const mockedUsePatient = usePatient as jest.Mock; const mockNavigate = jest.fn(); +const mockUseConfig = useConfig as jest.Mock; jest.mock('react-router', () => ({ useNavigate: () => mockNavigate, @@ -35,6 +38,7 @@ describe('LloydGeorgeRecordPage', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; mockedUsePatient.mockReturnValue(mockPatientDetails); + mockUseConfig.mockReturnValue(buildConfig()); }); afterEach(() => { diff --git a/app/src/providers/configProvider/ConfigProvider.test.tsx b/app/src/providers/configProvider/ConfigProvider.test.tsx index 375114bc0..8eef11112 100644 --- a/app/src/providers/configProvider/ConfigProvider.test.tsx +++ b/app/src/providers/configProvider/ConfigProvider.test.tsx @@ -1,7 +1,8 @@ import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ConfigProvider, { GlobalConfig, useConfigContext } from './ConfigProvider'; -import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; +import { defaultFeatureFlags } from '../../types/generic/featureFlags'; + describe('SessionProvider', () => { beforeEach(() => { process.env.REACT_APP_ENVIRONMENT = 'jest'; @@ -37,14 +38,14 @@ const TestApp = () => { ...config, featureFlags: { ...defaultFeatureFlags, - testFeature1: true, + uploadLloydGeorgeWorkflowEnabled: true, }, }; const flagOff: GlobalConfig = { ...config, featureFlags: { ...defaultFeatureFlags, - testFeature1: false, + uploadLloydGeorgeWorkflowEnabled: false, }, }; return ( @@ -56,7 +57,9 @@ const TestApp = () => {

Flags

- testFeature - {`${!!config.featureFlags.testFeature1}`} + + testFeature - {`${config.featureFlags.uploadLloydGeorgeWorkflowEnabled}`} +
); diff --git a/app/src/providers/configProvider/ConfigProvider.tsx b/app/src/providers/configProvider/ConfigProvider.tsx index aeb1d43e4..a6557d7ea 100644 --- a/app/src/providers/configProvider/ConfigProvider.tsx +++ b/app/src/providers/configProvider/ConfigProvider.tsx @@ -1,8 +1,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from 'react'; import type { Dispatch, ReactNode, SetStateAction } from 'react'; import { REPOSITORY_ROLE } from '../../types/generic/authRole'; -import { FeatureFlags } from '../../types/generic/featureFlags'; -import { defaultFeatureFlags } from '../../helpers/requests/getFeatureFlags'; +import { FeatureFlags, defaultFeatureFlags } from '../../types/generic/featureFlags'; import { isLocal } from '../../helpers/utils/isLocal'; type SetConfigOverride = (config: GlobalConfig) => void; diff --git a/app/src/types/generic/featureFlags.ts b/app/src/types/generic/featureFlags.ts index 275ba9eb7..f8fcd93dd 100644 --- a/app/src/types/generic/featureFlags.ts +++ b/app/src/types/generic/featureFlags.ts @@ -1,5 +1,11 @@ export type FeatureFlags = { - testFeature1: boolean; - testFeature2: boolean; - testFeature3: boolean; + uploadLloydGeorgeWorkflowEnabled: boolean; + uploadLambdaEnabled: boolean; + uploadArfWorkflowEnabled: boolean; +}; + +export const defaultFeatureFlags = { + uploadLloydGeorgeWorkflowEnabled: false, + uploadLambdaEnabled: false, + uploadArfWorkflowEnabled: false, }; diff --git a/lambdas/enums/lambda_error.py b/lambdas/enums/lambda_error.py index 9ef3be73c..8134a630b 100644 --- a/lambdas/enums/lambda_error.py +++ b/lambdas/enums/lambda_error.py @@ -254,6 +254,11 @@ def to_str(self) -> str: "message": "Failed to retrieve feature flag/s from AppConfig profile", } + FeatureFlagDisabled = { + "err_code": "FFL_5003", + "message": "Feature is not enabled", + } + """ Errors with no exception """ diff --git a/lambdas/handlers/create_document_reference_handler.py b/lambdas/handlers/create_document_reference_handler.py index 94522bb44..fb01764e0 100644 --- a/lambdas/handlers/create_document_reference_handler.py +++ b/lambdas/handlers/create_document_reference_handler.py @@ -3,15 +3,17 @@ import sys from json import JSONDecodeError +from enums.feature_flags import FeatureFlags from enums.lambda_error import LambdaError from enums.logging_app_interaction import LoggingAppInteraction from services.create_document_reference_service import CreateDocumentReferenceService +from services.feature_flags_service import FeatureFlagService from utils.audit_logging_setup import LoggingService from utils.decorators.ensure_env_var import ensure_environment_variables from utils.decorators.handle_lambda_exceptions import handle_lambda_exceptions from utils.decorators.override_error_check import override_error_check from utils.decorators.set_audit_arg import set_request_context_for_logging -from utils.lambda_exceptions import CreateDocumentRefException +from utils.lambda_exceptions import CreateDocumentRefException, FeatureFlagsException from utils.lambda_response import ApiGatewayResponse from utils.request_context import request_context @@ -23,10 +25,13 @@ @set_request_context_for_logging @ensure_environment_variables( names=[ - "LLOYD_GEORGE_BUCKET_NAME", - "LLOYD_GEORGE_DYNAMODB_NAME", + "APPCONFIG_APPLICATION", + "APPCONFIG_CONFIGURATION", + "APPCONFIG_ENVIRONMENT", "DOCUMENT_STORE_BUCKET_NAME", "DOCUMENT_STORE_DYNAMODB_NAME", + "LLOYD_GEORGE_BUCKET_NAME", + "LLOYD_GEORGE_DYNAMODB_NAME", ] ) @override_error_check @@ -34,6 +39,16 @@ def lambda_handler(event, context): request_context.app_interaction = LoggingAppInteraction.UPLOAD_RECORD.value + feature_flag_service = FeatureFlagService() + upload_flag_name = FeatureFlags.UPLOAD_LAMBDA_ENABLED.value + upload_lambda_enabled_flag_object = feature_flag_service.get_feature_flags_by_flag( + upload_flag_name + ) + + if not upload_lambda_enabled_flag_object[upload_flag_name]: + logger.info("Feature flag not enabled, event will not be processed") + raise FeatureFlagsException(500, LambdaError.FeatureFlagDisabled) + logger.info("Starting document reference creation process") nhs_number, doc_list = processing_event_details(event) diff --git a/lambdas/tests/unit/handlers/test_create_document_reference_handler.py b/lambdas/tests/unit/handlers/test_create_document_reference_handler.py index 311d1f111..954df07a1 100644 --- a/lambdas/tests/unit/handlers/test_create_document_reference_handler.py +++ b/lambdas/tests/unit/handlers/test_create_document_reference_handler.py @@ -6,6 +6,7 @@ lambda_handler, processing_event_details, ) +from services.feature_flags_service import FeatureFlagService from tests.unit.conftest import ( MOCK_ARF_BUCKET, MOCK_LG_BUCKET, @@ -58,8 +59,26 @@ def mock_processing_event_details(mocker): ) +@pytest.fixture +def mock_upload_lambda_enabled(mocker): + mock_function = mocker.patch.object(FeatureFlagService, "get_feature_flags_by_flag") + mock_upload_lambda_feature_flag = mock_function.return_value = { + "uploadLambdaEnabled": True + } + yield mock_upload_lambda_feature_flag + + +@pytest.fixture +def mock_upload_lambda_disabled(mocker): + mock_function = mocker.patch.object(FeatureFlagService, "get_feature_flags_by_flag") + mock_upload_lambda_feature_flag = mock_function.return_value = { + "uploadLambdaEnabled": False + } + yield mock_upload_lambda_feature_flag + + def test_create_document_reference_valid_both_lg_and_arf_type_returns_200( - set_env, both_type_event, context, mocker + set_env, both_type_event, context, mocker, mock_upload_lambda_enabled ): mock_service = mocker.patch( "handlers.create_document_reference_handler.CreateDocumentReferenceService", @@ -186,7 +205,12 @@ def test_processing_event_details_get_nhs_number_and_doc_list(arf_type_event): def test_lambda_handler_processing_event_details_raise_error( - mocker, arf_type_event, context, set_env, mock_processing_event_details + mocker, + arf_type_event, + context, + set_env, + mock_processing_event_details, + mock_upload_lambda_enabled, ): mock_processing_event_details.side_effect = CreateDocumentRefException( 400, MockError.Error @@ -202,7 +226,12 @@ def test_lambda_handler_processing_event_details_raise_error( def test_lambda_handler_service_raise_error( - mocker, arf_type_event, context, set_env, mock_processing_event_details + mocker, + arf_type_event, + context, + set_env, + mock_processing_event_details, + mock_upload_lambda_enabled, ): mock_processing_event_details.return_value = (TEST_NHS_NUMBER, ARF_FILE_LIST) @@ -222,7 +251,12 @@ def test_lambda_handler_service_raise_error( def test_lambda_handler_valid( - mocker, arf_type_event, context, set_env, mock_processing_event_details + mocker, + arf_type_event, + context, + set_env, + mock_processing_event_details, + mock_upload_lambda_enabled, ): mock_processing_event_details.return_value = (TEST_NHS_NUMBER, ARF_FILE_LIST) @@ -239,3 +273,15 @@ def test_lambda_handler_valid( actual = lambda_handler(arf_type_event, context) assert expected == actual mock_processing_event_details.assert_called_with(arf_type_event) + + +def test_no_event_processing_when_upload_lambda_flag_not_enabled( + set_env, + both_type_event, + context, + mock_processing_event_details, + mock_upload_lambda_disabled, +): + lambda_handler(both_type_event, context) + + mock_processing_event_details.assert_not_called()