diff --git a/app/__tests__/api/update-realm.test.ts b/app/__tests__/api/update-realm.test.ts index 64caa53..32b3501 100644 --- a/app/__tests__/api/update-realm.test.ts +++ b/app/__tests__/api/update-realm.test.ts @@ -143,4 +143,16 @@ describe('Profile Validations', () => { const updatedFields = Object.keys(profileUpdate.mock.calls[0][0].data); adminAllowedFields.forEach((field) => expect(updatedFields.includes(field)).toBeTruthy()); }); + + it('does not allow to update rejected realms', async () => { + (prisma.roster.findUnique as jest.Mock).mockImplementation(() => { + return Promise.resolve({ ...CustomRealmProfiles[0], approved: false }); + }); + const { req, res } = createMocks({ + method: 'PUT', + query: { id: 1 }, + }); + await handler(req, res); + expect(res.statusCode).toBe(400); + }); }); diff --git a/app/__tests__/custom-realm-dashboard.test.tsx b/app/__tests__/custom-realm-dashboard.test.tsx index ac0d2d2..bb296b5 100644 --- a/app/__tests__/custom-realm-dashboard.test.tsx +++ b/app/__tests__/custom-realm-dashboard.test.tsx @@ -7,12 +7,12 @@ import { getRealmEvents } from 'services/events'; import { CustomRealmFormData } from 'types/realm-profile'; import Router from 'next/router'; import { CustomRealms } from './fixtures'; -import { debug } from 'jest-preview'; jest.mock('services/realm', () => { return { deleteRealmRequest: jest.fn((realmInfo: CustomRealmFormData) => Promise.resolve([true, null])), updateRealmProfile: jest.fn((id: number, status: string) => Promise.resolve([true, null])), + getRealmProfiles: jest.fn((excludeArchived: boolean) => Promise.resolve([CustomRealms, null])), }; }); @@ -107,10 +107,10 @@ jest.mock('../pages/api/auth/[...nextauth]', () => { }); describe('Table', () => { - it('Loads in table data from serverside props', () => { - render(); - expect(screen.getByText('realm 1')); - expect(screen.getByText('realm 2')); + it('Loads in table data from serverside props', async () => { + render(); + await waitFor(() => screen.getByText('realm 1')); + await waitFor(() => screen.getByText('realm 2')); }); }); @@ -127,6 +127,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); fireEvent.click(screen.getByText('realm 1')); screen.getByText('Access Request').click(); await waitFor(() => screen.getByText('Approve Custom Realm', { selector: 'button' }).click()); @@ -141,6 +142,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); fireEvent.click(screen.getByText('realm 1')); screen.getByText('Access Request').click(); await waitFor(() => screen.getByText('Decline Custom Realm', { selector: 'button' }).click()); @@ -155,6 +157,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); fireEvent.click(screen.getByText('realm 1')); screen.getByText('Access Request').click(); await waitFor(() => screen.getByText('Approve Custom Realm', { selector: 'button' }).click()); @@ -173,6 +176,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); fireEvent.click(screen.getByText('realm 1')); screen.getByText('Access Request').click(); await waitFor(() => screen.getByText('Decline Custom Realm', { selector: 'button' }).click()); @@ -191,6 +195,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); (updateRealmProfile as jest.MockedFunction).mockImplementationOnce(() => Promise.resolve([null, { message: 'failure' }]), ); @@ -227,6 +232,7 @@ describe('Status update', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); (updateRealmProfile as jest.MockedFunction).mockImplementationOnce(() => Promise.resolve([null, { message: 'failure' }]), ); @@ -263,7 +269,8 @@ describe('Events table', () => { }); it('fetches correct events when selected row changes', async () => { - render(); + render(); + await waitFor(() => screen.getByText('realm 1')); const row1 = screen.getByText('realm 1'); fireEvent.click(row1); expect(getRealmEvents).toHaveBeenCalledTimes(1); @@ -279,8 +286,8 @@ describe('Events table', () => { }); it('displays events for the selected realm and updates when changing rows', async () => { - render(); - + render(); + await waitFor(() => screen.getByText('realm 1')); const firstRealmRow = screen.getByText('realm 1'); fireEvent.click(firstRealmRow); const eventTab = screen.getByText('Events'); @@ -306,6 +313,7 @@ describe('Events table', () => { router={Router as any} />, ); + await waitFor(() => screen.getByText('realm 1')); fireEvent.click(screen.getByText('realm 1')); await waitFor(() => screen.getByText('Network error when fetching realm events.')); }); diff --git a/app/__tests__/fixtures.ts b/app/__tests__/fixtures.ts index 902b646..224e2bf 100644 --- a/app/__tests__/fixtures.ts +++ b/app/__tests__/fixtures.ts @@ -23,6 +23,8 @@ export const CustomRealms: CustomRealmFormData[] = [ rcChannelOwnedBy: '', materialToSend: '', status: 'pending', + idps: [], + protocol: [], }, { id: 2, @@ -43,6 +45,30 @@ export const CustomRealms: CustomRealmFormData[] = [ division: 'division', approved: null, status: 'pending', + idps: [], + protocol: [], + }, + { + id: 3, + realm: 'realm 3', + productName: 'name', + purpose: 'purpose', + primaryEndUsers: ['livingInBC', 'businessInBC', 'govEmployees', 'details'], + environments: ['dev', 'test', 'prod'], + preferredAdminLoginMethod: 'idir', + productOwnerEmail: 'a@b.com', + productOwnerIdirUserId: 'po', + technicalContactEmail: 'b@c.com', + technicalContactIdirUserId: 'd@e.com', + secondTechnicalContactIdirUserId: 'dmsd', + secondTechnicalContactEmail: 'a@b.com', + ministry: 'ministry', + branch: 'branch', + division: 'division', + approved: false, + status: 'pending', + idps: [], + protocol: [], }, ]; diff --git a/app/__tests__/my-dashboard.test.tsx b/app/__tests__/my-dashboard.test.tsx new file mode 100644 index 0000000..9cd91e7 --- /dev/null +++ b/app/__tests__/my-dashboard.test.tsx @@ -0,0 +1,85 @@ +import React from 'react'; +import { render, screen, within, waitFor, fireEvent } from '@testing-library/react'; +import App from 'pages/_app'; +import CustomRealmDashboard from 'pages/custom-realm-dashboard'; +import { updateRealmProfile } from 'services/realm'; +import { getRealmEvents } from 'services/events'; +import { CustomRealmFormData } from 'types/realm-profile'; +import Router from 'next/router'; +import { CustomRealms } from './fixtures'; +import RealmLeftPanel from 'page-partials/my-dashboard/RealmLeftPanel'; +import noop from 'lodash.noop'; +import { debug } from 'jest-preview'; + +const editFunction = jest.fn(); + +jest.mock('next/router', () => ({ + useRouter() { + return { + route: '/', + pathname: '', + query: '', + asPath: '', + push: jest.fn(() => Promise.resolve(true)), + events: { + on: jest.fn(), + off: jest.fn(), + }, + beforePopState: jest.fn(() => null), + prefetch: jest.fn(() => null), + }; + }, +})); + +// Mock authentication +const mockSession = { + expires: new Date(Date.now() + 2 * 86400).toISOString(), + user: { username: 'admin' }, +}; +jest.mock('next-auth/react', () => { + const originalModule = jest.requireActual('next-auth/react'); + return { + __esModule: true, + ...originalModule, + useSession: jest.fn(() => { + return { data: mockSession, status: 'authenticated' }; // return type is [] in v3 but changed to {} in v4 + }), + }; +}); + +jest.mock('next-auth/next', () => { + return { + __esModule: true, + getServerSession: jest.fn(() => { + return { data: mockSession, status: 'authenticated' }; + }), + }; +}); + +jest.mock('../pages/api/realms', () => { + return { + __esModule: true, + getAllRealms: jest.fn(() => Promise.resolve([CustomRealms, null])), + authOptions: {}, + }; +}); + +describe('realm table', () => { + it('loads all realms', () => { + render(); + }); + + it('edit button disabled if realm is not approved', async () => { + render( + , + ); + const table = screen.getByRole('table'); + const thirdRow = table.querySelector('tbody tr:nth-child(3)') as HTMLTableRowElement; + expect(thirdRow).toBeInTheDocument(); + const actionCell = thirdRow.querySelector('td:nth-child(10)') as HTMLTableCellElement; + expect(actionCell).toBeInTheDocument(); + const editButton = within(actionCell).getByRole('img', { name: 'Edit' }); + fireEvent.click(editButton); + expect(editFunction).toHaveBeenCalledTimes(0); + }); +}); diff --git a/app/package.json b/app/package.json index a86558c..6b6302a 100644 --- a/app/package.json +++ b/app/package.json @@ -68,12 +68,12 @@ }, "devDependencies": { "@octokit/types": "^12.1.1", - "@testing-library/jest-dom": "^6.1.4", + "@testing-library/jest-dom": "^6.4.8", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^14.5.1", "@types/deep-diff": "^1.0.5", "@types/easy-soap-request": "^4.1.1", - "@types/jest": "^29.5.7", + "@types/jest": "^29.5.12", "@types/jsonwebtoken": "^8.5.8", "@types/jwk-to-pem": "^2.0.1", "@types/jws": "^3.2.4", diff --git a/app/page-partials/my-dashboard/RealmTable.tsx b/app/page-partials/my-dashboard/RealmTable.tsx index 06f61f9..4c3fedc 100644 --- a/app/page-partials/my-dashboard/RealmTable.tsx +++ b/app/page-partials/my-dashboard/RealmTable.tsx @@ -94,20 +94,19 @@ function RealmTable({ realms, onEditClick, onViewClick }: Props) { title="Edit" icon={faEdit} onClick={() => { + if (props.row.original.approved === false) return; onEditClick(String(props.row.getValue('id'))); }} + disabled={props.row.original.approved === false} /> ), }, ]; - const getStatus = (status?: string) => { - switch (status) { - case StatusEnum.APPLIED: - return 'Ready'; - default: - return 'In Progress'; - } + const getStatus = (status?: string, approved?: boolean) => { + if (status === StatusEnum.APPLIED) return 'Ready'; + else if (approved === false) return 'Rejected'; + else return 'In Progress'; }; return ( @@ -127,9 +126,10 @@ function RealmTable({ realms, onEditClick, onViewClick }: Props) { secondTechnicalContact: r.secondTechnicalContactEmail ? `${r.secondTechnicalContactEmail} (${r.secondTechnicalContactIdirUserId})` : '', - status: getStatus(r.status), + status: getStatus(r.status, r.approved), rcChannel: r.rcChannel, rcChannelOwnedBy: r.rcChannelOwnedBy, + approved: r.approved, }; })} columns={columns} diff --git a/app/pages/api/realms/[id].ts b/app/pages/api/realms/[id].ts index d556511..4401e57 100644 --- a/app/pages/api/realms/[id].ts +++ b/app/pages/api/realms/[id].ts @@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< }, }); - if (!currentRequest) { + if (!currentRequest || currentRequest.approved === false) { return res.status(400).json({ success: false, error: 'Invalid request' }); } diff --git a/app/pages/custom-realm-dashboard.tsx b/app/pages/custom-realm-dashboard.tsx index 793a84d..d2225ba 100644 --- a/app/pages/custom-realm-dashboard.tsx +++ b/app/pages/custom-realm-dashboard.tsx @@ -57,12 +57,16 @@ interface SelectOption { label: string; } -function CustomRealmDashboard({ defaultRealmRequests, alert }: Props) { - const [realmRequests, setRealmRequests] = useState(defaultRealmRequests || []); +function CustomRealmDashboard({ alert }: Props) { + const [realmRequests, setRealmRequests] = useState([]); const [selectedRow, setSelectedRow] = useState(); const [lastUpdateTime, setLastUpdateTime] = useState(new Date()); const { setModalConfig } = useContext(ModalContext); + useEffect(() => { + fetchRealms(); + }, []); + const handleDeleteRequest = (id: number) => { const handleConfirm = async () => { const [, err] = await deleteRealmRequest(id); @@ -118,11 +122,11 @@ function CustomRealmDashboard({ defaultRealmRequests, alert }: Props) { content: `Realm request for ${realm?.realm} ${approval}.`, }); const updatedRealms = realmRequests.map((realm) => { - if (realm.id === realmId) return { ...realm, approved: approving } as CustomRealmFormData; + if (realm.id === realmId) return { ...realm, approved: approving } as RealmProfile; return realm; }); setRealmRequests(updatedRealms); - setSelectedRow({ ...selectedRow, approved: approving } as CustomRealmFormData); + setSelectedRow({ ...selectedRow, approved: approving } as RealmProfile); }; const statusVerb = approval === 'approved' ? 'Approve' : 'Decline'; setModalConfig({ @@ -268,42 +272,3 @@ function CustomRealmDashboard({ defaultRealmRequests, alert }: Props) { } export default withBottomAlert(CustomRealmDashboard); - -interface ExtendedForm extends CustomRealmFormData { - createdAt: object; - updatedAt: object; -} - -/**Fetch realm data with first page load */ -export const getServerSideProps = async (context: GetServerSidePropsContext) => { - const session = await getServerSession(context.req, context.res, authOptions); - if (!session) - return { - props: { defaultRealmRequests: [] }, - }; - - const username = session?.user?.idir_username || ''; - const isAdmin = checkAdminRole(session?.user); - - try { - const realms = await getAllRealms(username, isAdmin); - // Strip non-serializable dates - const formattedRealms = realms.map((realm: ExtendedForm) => { - const { createdAt, updatedAt, ...rest } = realm; - return rest; - }); - - return { - props: { - defaultRealmRequests: formattedRealms, - }, - }; - } catch (err) { - console.error(err); - return { - props: { - defaltRealmRequests: [], - }, - }; - } -}; diff --git a/app/tsconfig.json b/app/tsconfig.json index b9019a9..7893438 100644 --- a/app/tsconfig.json +++ b/app/tsconfig.json @@ -14,7 +14,8 @@ "isolatedModules": true, "jsx": "preserve", "baseUrl": ".", - "incremental": true + "incremental": true, + "types": ["node", "jest", "@testing-library/jest-dom"] }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "pages/**/*", "src/**/*"], "exclude": ["node_modules"] diff --git a/app/yarn.lock b/app/yarn.lock index 8ae4545..ef6ae4d 100644 --- a/app/yarn.lock +++ b/app/yarn.lock @@ -7,10 +7,10 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== -"@adobe/css-tools@^4.3.1": - version "4.3.1" - resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.3.1.tgz#abfccb8ca78075a2b6187345c26243c1a0842f28" - integrity sha512-/62yikz7NLScCGAAST5SHdnjaDJQBDq0M2muyRTpf2VQhw6StBg2ALiu73zSJQ4fMVLA+0uBhBHAle7Wg+2kSg== +"@adobe/css-tools@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.0.tgz#728c484f4e10df03d5a3acd0d8adcbbebff8ad63" + integrity sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ== "@ampproject/remapping@^2.2.0": version "2.2.1" @@ -1808,18 +1808,18 @@ lz-string "^1.5.0" pretty-format "^27.0.2" -"@testing-library/jest-dom@^6.1.4": - version "6.1.4" - resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.1.4.tgz#cf0835c33bc5ef00befb9e672b1e3e6a710e30e3" - integrity sha512-wpoYrCYwSZ5/AxcrjLxJmCU6I5QAJXslEeSiMQqaWmP2Kzpd1LvF/qxmAIW2qposULGWq2gw30GgVNFLSc2Jnw== +"@testing-library/jest-dom@^6.4.8": + version "6.4.8" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz#9c435742b20c6183d4e7034f2b329d562c079daa" + integrity sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw== dependencies: - "@adobe/css-tools" "^4.3.1" + "@adobe/css-tools" "^4.4.0" "@babel/runtime" "^7.9.2" aria-query "^5.0.0" chalk "^3.0.0" css.escape "^1.5.1" - dom-accessibility-api "^0.5.6" - lodash "^4.17.15" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" redent "^3.0.0" "@testing-library/react@^14.0.0": @@ -1947,10 +1947,10 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.7": - version "29.5.7" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.7.tgz#2c0dafe2715dd958a455bc10e2ec3e1ec47b5036" - integrity sha512-HLyetab6KVPSiF+7pFcUyMeLsx25LDNDemw9mGsJBkai/oouwrjTycocSDYopMEwFhN2Y4s9oPyOCZNofgSt2g== +"@types/jest@^29.5.12": + version "29.5.12" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544" + integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw== dependencies: expect "^29.0.0" pretty-format "^29.0.0" @@ -3367,11 +3367,16 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.5.6, dom-accessibility-api@^0.5.9: +dom-accessibility-api@^0.5.9: version "0.5.16" resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-align@^1.7.0: version "1.12.4" resolved "https://registry.yarnpkg.com/dom-align/-/dom-align-1.12.4.tgz#3503992eb2a7cfcb2ed3b2a6d21e0b9c00d54511"