From a2ea0f3456e7416b279c95b029f3a3f6e6d61f6c Mon Sep 17 00:00:00 2001 From: Alejandro Sanchez Date: Fri, 24 Feb 2023 08:52:04 -0800 Subject: [PATCH] PSP-5500 Limit project regions to those the user has access to (#2897) --- .../project/add/AddProjectContainer.test.tsx | 25 ++++++ .../map/project/add/AddProjectContainer.tsx | 5 +- .../map/project/add/AddProjectForm.test.tsx | 52 ++++++++++-- .../map/project/add/AddProjectForm.tsx | 12 +-- .../AddProjectContainer.test.tsx.snap | 16 +--- .../AddProjectForm.test.tsx.snap | 80 ++++++++----------- .../update/UpdateProjectContainer.test.tsx | 26 ++++-- .../project/update/UpdateProjectContainer.tsx | 9 +-- source/frontend/src/mocks/mockLookups.ts | 2 +- source/frontend/src/mocks/userMock.ts | 4 +- source/frontend/src/models/api/RegionUser.ts | 4 +- source/frontend/src/models/api/Role.ts | 6 +- source/frontend/src/models/api/User.ts | 5 +- source/frontend/src/models/api/UserRole.ts | 5 +- 14 files changed, 150 insertions(+), 101 deletions(-) diff --git a/source/frontend/src/features/properties/map/project/add/AddProjectContainer.test.tsx b/source/frontend/src/features/properties/map/project/add/AddProjectContainer.test.tsx index e6e09f7e5c..55053f0121 100644 --- a/source/frontend/src/features/properties/map/project/add/AddProjectContainer.test.tsx +++ b/source/frontend/src/features/properties/map/project/add/AddProjectContainer.test.tsx @@ -3,8 +3,10 @@ import MockAdapter from 'axios-mock-adapter'; import { MapStateContextProvider } from 'components/maps/providers/MapStateContext'; import { Feature, GeoJsonProperties, Geometry } from 'geojson'; import { createMemoryHistory } from 'history'; +import { useUserInfoRepository } from 'hooks/repositories/useUserInfoRepository'; import { mockLookups } from 'mocks/mockLookups'; import { mockProjectPostResponse } from 'mocks/mockProjects'; +import { getUserMock } from 'mocks/userMock'; import { Api_Project } from 'models/api/Project'; import { lookupCodesSlice } from 'store/slices/lookupCodes'; import { act, render, RenderOptions, userEvent, waitFor } from 'utils/test-utils'; @@ -33,6 +35,29 @@ jest.mock('react-visibility-sensor', () => { }); }); +jest.mock('hooks/repositories/useUserInfoRepository'); +(useUserInfoRepository as jest.MockedFunction).mockReturnValue({ + retrieveUserInfo: jest.fn(), + retrieveUserInfoLoading: true, + retrieveUserInfoResponse: { + ...getUserMock(), + userRegions: [ + { + id: 1, + userId: 5, + regionCode: 1, + region: { id: 1 }, + }, + { + id: 2, + userId: 5, + regionCode: 2, + region: { id: 2 }, + }, + ], + }, +}); + describe('AddProjectContainer component', () => { // render component under test const setup = ( diff --git a/source/frontend/src/features/properties/map/project/add/AddProjectContainer.tsx b/source/frontend/src/features/properties/map/project/add/AddProjectContainer.tsx index 51d3fc35e1..152468d386 100644 --- a/source/frontend/src/features/properties/map/project/add/AddProjectContainer.tsx +++ b/source/frontend/src/features/properties/map/project/add/AddProjectContainer.tsx @@ -7,7 +7,6 @@ import { Api_Project } from 'models/api/Project'; import { useCallback, useRef } from 'react'; import { FaBriefcase } from 'react-icons/fa'; import { useHistory } from 'react-router-dom'; -import { mapLookupCode } from 'utils'; import SidebarFooter from '../../shared/SidebarFooter'; import { useAddProjectForm } from '../hooks/useAddProjectFormManagement'; @@ -23,9 +22,8 @@ const AddProjectContainer: React.FC mapLookupCode(c)); const formikRef = useRef>(null); @@ -55,7 +53,6 @@ const AddProjectContainer: React.FC; jest.mock('@react-keycloak/web'); -let mockRegionOptions: SelectOption[] = GetMockLookUpsByType(API.REGION_TYPES); -let mockProjectStatuses: SelectOption[] = GetMockLookUpsByType(API.PROJECT_STATUS_TYPES); +jest.mock('hooks/repositories/useUserInfoRepository'); +(useUserInfoRepository as jest.MockedFunction).mockReturnValue({ + retrieveUserInfo: jest.fn(), + retrieveUserInfoLoading: true, + retrieveUserInfoResponse: { + ...getUserMock(), + userRegions: [ + { + id: 1, + userId: 5, + regionCode: 1, + region: { id: 1 }, + }, + { + id: 2, + userId: 5, + regionCode: 2, + region: { id: 2 }, + }, + ], + }, +}); + +const mockStatusOptions: SelectOption[] = getMockLookUpsByType(API.PROJECT_STATUS_TYPES); describe('AddProjectForm component', () => { // render component under test @@ -27,11 +51,11 @@ describe('AddProjectForm component', () => { const ref = createRef>(); const utils = render( , { ...renderOptions, @@ -104,7 +128,7 @@ describe('AddProjectForm component', () => { expect(status.tagName).toBe('SELECT'); }); - it.only('should validate character limits', async () => { + it('should validate character limits', async () => { const { container, getFormikRef, findByText } = setup({ initialValues, }); @@ -124,13 +148,27 @@ describe('AddProjectForm component', () => { expect(await findByText(/Project summary must be at most 2000 characters/i)).toBeVisible(); }); + it('should call onSubmit and save form data as expected', async () => { + const { getFormikRef, getNameTextbox, getRegionDropdown } = setup({ + initialValues, + }); + + await act(() => userEvent.selectOptions(getRegionDropdown(), '1')); + await act(() => userEvent.paste(getNameTextbox(), `TRANS-CANADA HWY - 10`)); + + // submit form to trigger validation check + await act(() => getFormikRef().current?.submitForm()); + + expect(onSubmit).toHaveBeenCalled(); + }); + it('should add a product', async () => { const { getByText, getProductCodeTextBox } = setup({ initialValues, }); const addProductButton = getByText('+ Add another product'); - act(() => { + await act(() => { userEvent.click(addProductButton); }); diff --git a/source/frontend/src/features/properties/map/project/add/AddProjectForm.tsx b/source/frontend/src/features/properties/map/project/add/AddProjectForm.tsx index be420b268d..292356d7c4 100644 --- a/source/frontend/src/features/properties/map/project/add/AddProjectForm.tsx +++ b/source/frontend/src/features/properties/map/project/add/AddProjectForm.tsx @@ -1,4 +1,5 @@ import { Form, Input, Select, SelectOption, TextArea } from 'components/common/form'; +import { UserRegionSelectContainer } from 'components/common/form/UserRegionSelect/UserRegionSelectContainer'; import { Section } from 'features/mapSideBar/tabs/Section'; import { SectionField } from 'features/mapSideBar/tabs/SectionField'; import { Formik, FormikHelpers, FormikProps } from 'formik'; @@ -15,7 +16,6 @@ export interface IAddProjectFormProps { /** Initial values of the form */ initialValues: ProjectForm; projectStatusOptions: SelectOption[]; - projectRegionOptions: SelectOption[]; /** A Yup Schema or a function that returns a Yup schema */ validationSchema?: any | (() => any); /** Submission handler */ @@ -24,13 +24,7 @@ export interface IAddProjectFormProps { const AddProjectForm = React.forwardRef, IAddProjectFormProps>( (props, formikRef) => { - const { - initialValues, - projectStatusOptions, - projectRegionOptions, - validationSchema, - onSubmit, - } = props; + const { initialValues, projectStatusOptions, validationSchema, onSubmit } = props; const handleSubmit = async (values: ProjectForm, formikHelpers: FormikHelpers) => { await onSubmit(values, formikHelpers); @@ -73,7 +67,7 @@ const AddProjectForm = React.forwardRef, IAddProjectFor /> - diff --git a/source/frontend/src/features/properties/map/project/add/__snapshots__/AddProjectForm.test.tsx.snap b/source/frontend/src/features/properties/map/project/add/__snapshots__/AddProjectForm.test.tsx.snap index a743381778..2bc8c23194 100644 --- a/source/frontend/src/features/properties/map/project/add/__snapshots__/AddProjectForm.test.tsx.snap +++ b/source/frontend/src/features/properties/map/project/add/__snapshots__/AddProjectForm.test.tsx.snap @@ -273,31 +273,45 @@ exports[`AddProjectForm component renders as expected 1`] = ` + + @@ -329,49 +343,21 @@ exports[`AddProjectForm component renders as expected 1`] = ` - - - - diff --git a/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.test.tsx b/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.test.tsx index 626995dbf0..d2a0f394dd 100644 --- a/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.test.tsx +++ b/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.test.tsx @@ -1,5 +1,5 @@ import { AxiosError } from 'axios'; -import { FormikProps } from 'formik'; +import { FormikHelpers, FormikProps } from 'formik'; import { mockProjectGetResponse, mockProjectPostResponse } from 'mocks/mockProjects'; import React from 'react'; import { act, render, RenderOptions, screen, waitFor } from 'utils/test-utils'; @@ -53,7 +53,6 @@ describe('UpdateProjectContainer', () => { beforeEach(() => { viewProps = { initialValues: formValues, - projectRegionOptions: [], projectStatusOptions: [], onSubmit: jest.fn(), }; @@ -77,7 +76,11 @@ describe('UpdateProjectContainer', () => { it('makes request to update the Project', async () => { setup(); - const onSubmitForm = jest.fn(); + const formikHelpers: Partial> = { + setSubmitting: jest.fn(), + resetForm: jest.fn(), + }; + mockApi.execute.mockResolvedValue( mockProjectPostResponse( 1, @@ -91,23 +94,34 @@ describe('UpdateProjectContainer', () => { ); await act(async () => { - return viewProps?.onSubmit(viewProps.initialValues, onSubmitForm()); + return viewProps?.onSubmit( + viewProps.initialValues, + formikHelpers as FormikHelpers, + ); }); expect(mockApi.execute).toHaveBeenCalled(); expect(onSuccess).toHaveBeenCalled(); + expect(formikHelpers.setSubmitting).toHaveBeenCalled(); + expect(formikHelpers.resetForm).toHaveBeenCalled(); }); it('displays expected error toast when update fails', async () => { setup(); - const onSubmitForm = jest.fn(); + const formikHelpers: Partial> = { + setSubmitting: jest.fn(), + resetForm: jest.fn(), + }; mockApi.execute.mockRejectedValue({ isAxiosError: true, response: { status: 409, data: 'expected error' }, } as AxiosError); await act(async () => { - return viewProps?.onSubmit(viewProps.initialValues, onSubmitForm()); + return viewProps?.onSubmit( + viewProps.initialValues, + formikHelpers as FormikHelpers, + ); }); expect(mockApi.execute).toHaveBeenCalled(); diff --git a/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.tsx b/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.tsx index eaf0a6ce15..39ad0e968b 100644 --- a/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.tsx +++ b/source/frontend/src/features/properties/map/project/update/UpdateProjectContainer.tsx @@ -6,7 +6,6 @@ import useLookupCodeHelpers from 'hooks/useLookupCodeHelpers'; import { Api_Project } from 'models/api/Project'; import React from 'react'; import { toast } from 'react-toastify'; -import { mapLookupCode } from 'utils/mapLookupCode'; import { AddProjectYupSchema } from '../add/AddProjectFileYupSchema'; import { IAddProjectFormProps } from '../add/AddProjectForm'; @@ -30,11 +29,10 @@ const UpdateProjectContainer = React.forwardRef< updateProject: { execute: updateProject }, } = useProjectProvider(); - const { getOptionsByType, getByType } = useLookupCodeHelpers(); + const { getOptionsByType } = useLookupCodeHelpers(); - const intialValues = ProjectForm.fromApi(project); + const initialValues = ProjectForm.fromApi(project); const projectStatusTypeCodes = getOptionsByType(API.PROJECT_STATUS_TYPES); - const regionTypeCodes = getByType(API.REGION_TYPES).map(c => mapLookupCode(c)); const handleSubmit = async (values: ProjectForm, formikHelpers: FormikHelpers) => { try { @@ -63,9 +61,8 @@ const UpdateProjectContainer = React.forwardRef< ); diff --git a/source/frontend/src/mocks/mockLookups.ts b/source/frontend/src/mocks/mockLookups.ts index bac8559c23..c1f29c2ddd 100644 --- a/source/frontend/src/mocks/mockLookups.ts +++ b/source/frontend/src/mocks/mockLookups.ts @@ -3502,7 +3502,7 @@ export const mockLookups: Partial[] = [ }, ]; -export const GetMockLookUpsByType = (codeType: string): SelectOption[] => { +export const getMockLookUpsByType = (codeType: string): SelectOption[] => { let codes = mockLookups.reduce(function (filtered: SelectOption[], reg) { if (reg.type === codeType) { let option = mapLookupCode(reg as ILookupCode); diff --git a/source/frontend/src/mocks/userMock.ts b/source/frontend/src/mocks/userMock.ts index fcc3684e5a..36e5f238c7 100644 --- a/source/frontend/src/mocks/userMock.ts +++ b/source/frontend/src/mocks/userMock.ts @@ -1,4 +1,6 @@ -export const getUserMock = () => ({ +import { Api_User } from 'models/api/User'; + +export const getUserMock = (): Api_User => ({ id: 30, guidIdentifierValue: 'e81274eb-a007-4f2e-ada3-2817efcdb0a6', businessIdentifierValue: 'desmith@idir', diff --git a/source/frontend/src/models/api/RegionUser.ts b/source/frontend/src/models/api/RegionUser.ts index 4eed5af15b..ac303778ae 100644 --- a/source/frontend/src/models/api/RegionUser.ts +++ b/source/frontend/src/models/api/RegionUser.ts @@ -1,8 +1,10 @@ +import { Api_AuditFields } from 'models/api/AuditFields'; import { Api_ConcurrentVersion } from 'models/api/ConcurrentVersion'; import Api_TypeCode from 'models/api/TypeCode'; import { Api_User } from 'models/api/User'; -export interface Api_RegionUser extends Api_ConcurrentVersion { +export interface Api_RegionUser extends Api_ConcurrentVersion, Api_AuditFields { + id?: number; user?: Api_User; userId?: number; region: Api_TypeCode; diff --git a/source/frontend/src/models/api/Role.ts b/source/frontend/src/models/api/Role.ts index ac3b362832..53c300ada4 100644 --- a/source/frontend/src/models/api/Role.ts +++ b/source/frontend/src/models/api/Role.ts @@ -1,5 +1,7 @@ +import { Api_AuditFields } from 'models/api/AuditFields'; import { Api_ConcurrentVersion } from 'models/api/ConcurrentVersion'; -export interface Api_Role extends Api_ConcurrentVersion { + +export interface Api_Role extends Api_ConcurrentVersion, Api_AuditFields { id?: number; roleUid?: string; keycloakGroupId?: string; @@ -7,4 +9,6 @@ export interface Api_Role extends Api_ConcurrentVersion { description?: string; isPublic: boolean; isDisabled: boolean; + displayOrder?: number; + roleClaims?: any[]; } diff --git a/source/frontend/src/models/api/User.ts b/source/frontend/src/models/api/User.ts index 8155fa09cd..7bbdfb6629 100644 --- a/source/frontend/src/models/api/User.ts +++ b/source/frontend/src/models/api/User.ts @@ -1,9 +1,11 @@ import { Api_ConcurrentVersion } from 'models/api/ConcurrentVersion'; import { Api_Person } from 'models/api/Person'; +import { Api_AuditFields } from './AuditFields'; import { Api_RegionUser } from './RegionUser'; import { Api_UserRole } from './UserRole'; -export interface Api_User extends Api_ConcurrentVersion { + +export interface Api_User extends Api_ConcurrentVersion, Api_AuditFields { id?: number; businessIdentifierValue?: string; guidIdentifierValue?: string; @@ -13,7 +15,6 @@ export interface Api_User extends Api_ConcurrentVersion { isDisabled?: boolean; issueDate?: string; lastLogin?: string; - appCreateTimestamp?: string; userRoles: Api_UserRole[]; userRegions: Api_RegionUser[]; person?: Api_Person; diff --git a/source/frontend/src/models/api/UserRole.ts b/source/frontend/src/models/api/UserRole.ts index ca173e9e2f..13aa7f326d 100644 --- a/source/frontend/src/models/api/UserRole.ts +++ b/source/frontend/src/models/api/UserRole.ts @@ -1,8 +1,11 @@ import { Api_ConcurrentVersion } from 'models/api/ConcurrentVersion'; +import { Api_AuditFields } from './AuditFields'; import { Api_Role } from './Role'; import { Api_User } from './User'; -export interface Api_UserRole extends Api_ConcurrentVersion { + +export interface Api_UserRole extends Api_ConcurrentVersion, Api_AuditFields { + id?: number; roleId?: number; userId?: number; role?: Api_Role;