diff --git a/client/src/components/Auth/LoginForm.tsx b/client/src/components/Auth/LoginForm.tsx index c2d4ba8d3..9e23e77a9 100644 --- a/client/src/components/Auth/LoginForm.tsx +++ b/client/src/components/Auth/LoginForm.tsx @@ -4,8 +4,13 @@ import React, { useEffect, useState } from 'react'; import { FloatingLabel, Form } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { setCredentials, useAppDispatch, useAppSelector, useLoginMutation } from 'store'; -import { selectAuthenticated } from 'store/userSlice/user.slice'; +import { + selectAuthenticated, + setCredentials, + useAppDispatch, + useAppSelector, + useLoginMutation, +} from 'store'; import { z } from 'zod'; const loginSchema = z.object({ diff --git a/client/src/components/Layout/PrivateRoute.tsx b/client/src/components/Layout/PrivateRoute.tsx index 9eb23e117..06e388012 100644 --- a/client/src/components/Layout/PrivateRoute.tsx +++ b/client/src/components/Layout/PrivateRoute.tsx @@ -1,7 +1,6 @@ import React, { ReactElement } from 'react'; import { Navigate } from 'react-router-dom'; -import { useAppSelector } from 'store'; -import { selectAuthenticated } from 'store/userSlice/user.slice'; +import { selectAuthenticated, useAppSelector } from 'store'; interface Props { children: any; diff --git a/client/src/components/Layout/TopNav/TopNav.tsx b/client/src/components/Layout/TopNav/TopNav.tsx index c2a84195f..4dc7cf30b 100644 --- a/client/src/components/Layout/TopNav/TopNav.tsx +++ b/client/src/components/Layout/TopNav/TopNav.tsx @@ -6,7 +6,7 @@ import React, { useContext } from 'react'; import { Button, Dropdown } from 'react-bootstrap'; import { useDispatch, useSelector } from 'react-redux'; import { Link, useNavigate } from 'react-router-dom'; -import { logout, selectAuthenticated } from 'store/userSlice/user.slice'; +import { removeCredentials, selectAuthenticated } from 'store'; export function TopNav() { const { showSidebar, setShowSidebar } = useContext(NavContext); @@ -17,7 +17,7 @@ export function TopNav() { if (!isAuthenticated) return null; const handleLogout = () => { - dispatch(logout()); + dispatch(removeCredentials()); navigation('/login'); }; diff --git a/client/src/components/Manifest/Buttons/ManifestCancelBtn.tsx b/client/src/components/Manifest/Actions/ManifestCancelBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestCancelBtn.tsx rename to client/src/components/Manifest/Actions/ManifestCancelBtn.tsx diff --git a/client/src/components/Manifest/Buttons/ManifestEditBtn.tsx b/client/src/components/Manifest/Actions/ManifestEditBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestEditBtn.tsx rename to client/src/components/Manifest/Actions/ManifestEditBtn.tsx diff --git a/client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx b/client/src/components/Manifest/Actions/ManifestFABs.spec.tsx similarity index 97% rename from client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx rename to client/src/components/Manifest/Actions/ManifestFABs.spec.tsx index 048a339a5..de67ff2bf 100644 --- a/client/src/components/Manifest/Buttons/ManifestFABs.spec.tsx +++ b/client/src/components/Manifest/Actions/ManifestFABs.spec.tsx @@ -1,10 +1,10 @@ import '@testing-library/jest-dom'; -import { ManifestFABs } from 'components/Manifest/Buttons/ManifestFABs'; +import { ManifestFABs } from 'components/Manifest/Actions/ManifestFABs'; +import { ManifestContext } from 'components/Manifest/ManifestForm'; +import { ManifestStatus } from 'components/Manifest/manifestSchema'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { afterEach, describe, expect, test } from 'vitest'; -import { ManifestContext } from 'components/Manifest/ManifestForm'; -import { ManifestStatus } from 'components/Manifest/manifestSchema'; const TestComponent = ({ status, diff --git a/client/src/components/Manifest/Buttons/ManifestFABs.tsx b/client/src/components/Manifest/Actions/ManifestFABs.tsx similarity index 88% rename from client/src/components/Manifest/Buttons/ManifestFABs.tsx rename to client/src/components/Manifest/Actions/ManifestFABs.tsx index 593f27d0e..58fcb1dab 100644 --- a/client/src/components/Manifest/Buttons/ManifestFABs.tsx +++ b/client/src/components/Manifest/Actions/ManifestFABs.tsx @@ -1,5 +1,5 @@ -import { ManifestEditBtn } from 'components/Manifest/Buttons/ManifestEditBtn'; -import { ManifestSaveBtn } from 'components/Manifest/Buttons/ManifestSaveBtn'; +import { ManifestEditBtn } from 'components/Manifest/Actions/ManifestEditBtn'; +import { ManifestSaveBtn } from 'components/Manifest/Actions/ManifestSaveBtn'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { QuickSignBtn } from 'components/Manifest/QuickerSign'; import { FloatingActionBtn } from 'components/UI'; diff --git a/client/src/components/Manifest/Buttons/ManifestSaveBtn.tsx b/client/src/components/Manifest/Actions/ManifestSaveBtn.tsx similarity index 100% rename from client/src/components/Manifest/Buttons/ManifestSaveBtn.tsx rename to client/src/components/Manifest/Actions/ManifestSaveBtn.tsx diff --git a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx index a26b7d658..b8967e5e9 100644 --- a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx +++ b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.spec.tsx @@ -1,36 +1,51 @@ +import userEvent from '@testing-library/user-event'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; import '@testing-library/jest-dom'; -import { HandlerSearchForm } from './HandlerSearchForm'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { createMockRcrainfoSite } from 'test-utils/fixtures'; -import { setupServer } from 'msw/node'; -import { http, HttpResponse } from 'msw'; -import { API_BASE_URL } from 'test-utils/mock/handlers'; -import userEvent from '@testing-library/user-event'; +import { userApiMocks } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; +import { HandlerSearchForm } from './HandlerSearchForm'; const mockRcraSite1Id = 'VATEST111111111'; const mockRcraSite2Id = 'VATEST222222222'; const mockRcrainfoSite1Id = 'VATEST333333333'; const mockRcrainfoSite2Id = 'VATEST444444444'; +const mockProfile: HaztrakProfileResponse = { + user: 'testuser1', + sites: [], + org: { + name: 'my org', + rcrainfoIntegrated: true, + id: '1234', + }, +}; + const mockRcrainfoSite1 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite1Id }); const mockRcrainfoSite2 = createMockRcrainfoSite({ epaSiteId: mockRcrainfoSite2Id }); const mockRcraSite1 = createMockRcrainfoSite({ epaSiteId: mockRcraSite1Id }); const mockRcraSite2 = createMockRcrainfoSite({ epaSiteId: mockRcraSite2Id }); -export const testURL = [ - http.get(`${API_BASE_URL}/api/site/search`, (info) => { +export const mockHandlerSearches = [ + http.get(`${API_BASE_URL}/api/site/search`, () => { return HttpResponse.json([mockRcraSite1, mockRcraSite2], { status: 200 }); }), - http.get(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { + http.get(`${API_BASE_URL}/api/rcra/handler/search`, () => { return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); }), - http.post(`${API_BASE_URL}/api/rcra/handler/search`, (info) => { + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json({ ...mockProfile }, { status: 200 }); + }), + http.post(`${API_BASE_URL}/api/rcra/handler/search`, () => { return HttpResponse.json([mockRcrainfoSite1, mockRcrainfoSite2], { status: 200 }); }), ]; -const server = setupServer(...testURL); +const server = setupServer(...userApiMocks, ...mockHandlerSearches); afterEach(() => { cleanup(); }); @@ -46,19 +61,7 @@ describe('HandlerSearchForm', () => { }); test('retrieves rcra sites from haztrak and RCRAInfo', async () => { renderWithProviders( - undefined} handlerType="generator" />, - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - name: 'my org', - rcrainfoIntegrated: true, - id: '1234', - }, - }, - }, - } + undefined} handlerType="generator" /> ); const epaId = screen.getByRole('combobox'); await userEvent.type(epaId, 'VATEST'); @@ -68,20 +71,19 @@ describe('HandlerSearchForm', () => { expect(await screen.findByText(new RegExp(mockRcrainfoSite2Id, 'i'))).toBeInTheDocument(); }); test('retrieves rcra sites from haztrak if org not rcrainfo integrated', async () => { - renderWithProviders( - undefined} handlerType="generator" />, - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - name: 'my org', - rcrainfoIntegrated: false, - id: '1234', - }, + server.use( + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json( + { + ...mockProfile, + org: { rcrainfoIntegrated: false }, }, - }, - } + { status: 200 } + ); + }) + ); + renderWithProviders( + undefined} handlerType="generator" /> ); const epaId = screen.getByRole('combobox'); await userEvent.type(epaId, 'VATEST'); diff --git a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx index 35b37dd7c..0f5381d52 100644 --- a/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx +++ b/client/src/components/Manifest/Handler/Search/HandlerSearchForm.tsx @@ -1,3 +1,4 @@ +import { RcrainfoSiteSearchBadge } from 'components/Manifest/Handler/Search/RcrainfoSiteSearchBadge'; import { ManifestContext, ManifestContextType } from 'components/Manifest/ManifestForm'; import { Manifest, SiteType, Transporter } from 'components/Manifest/manifestSchema'; import { RcraSite } from 'components/RcraSite'; @@ -12,13 +13,7 @@ import { useFormContext, } from 'react-hook-form'; import Select from 'react-select'; -import { - selectHaztrakProfile, - useAppSelector, - useSearchRcrainfoSitesQuery, - useSearchRcraSitesQuery, -} from 'store'; -import { RcrainfoSiteSearchBadge } from 'components/Manifest/Handler/Search/RcrainfoSiteSearchBadge'; +import { useGetProfileQuery, useSearchRcrainfoSitesQuery, useSearchRcraSitesQuery } from 'store'; interface Props { handleClose: () => void; @@ -42,9 +37,13 @@ export function HandlerSearchForm({ const manifestMethods = useFormContext(); const [inputValue, setInputValue] = useState(''); const [selectedHandler, setSelectedHandler] = useState(null); - const { org } = useAppSelector(selectHaztrakProfile); + const { org } = useGetProfileQuery(undefined, { + selectFromResult: ({ data }) => { + return { org: data?.org }; + }, + }); const [skip, setSkip] = useState(true); - const { data, error, isLoading } = useSearchRcraSitesQuery( + const { data } = useSearchRcraSitesQuery( { siteType: handlerType, siteId: inputValue, diff --git a/client/src/components/Manifest/ManifestForm.tsx b/client/src/components/Manifest/ManifestForm.tsx index 9861bbe71..6529f4fe5 100644 --- a/client/src/components/Manifest/ManifestForm.tsx +++ b/client/src/components/Manifest/ManifestForm.tsx @@ -1,24 +1,25 @@ import { ErrorMessage } from '@hookform/error-message'; import { zodResolver } from '@hookform/resolvers/zod'; +import { createSelector } from '@reduxjs/toolkit'; +import { ManifestCancelBtn } from 'components/Manifest/Actions/ManifestCancelBtn'; +import { ManifestEditBtn } from 'components/Manifest/Actions/ManifestEditBtn'; +import { ManifestFABs } from 'components/Manifest/Actions/ManifestFABs'; +import { ManifestSaveBtn } from 'components/Manifest/Actions/ManifestSaveBtn'; import { AdditionalInfoForm } from 'components/Manifest/AdditionalInfo'; -import { ManifestCancelBtn } from 'components/Manifest/Buttons/ManifestCancelBtn'; -import { ManifestEditBtn } from 'components/Manifest/Buttons/ManifestEditBtn'; -import { ManifestFABs } from 'components/Manifest/Buttons/ManifestFABs'; -import { ManifestSaveBtn } from 'components/Manifest/Buttons/ManifestSaveBtn'; import { UpdateRcra } from 'components/Manifest/UpdateRcra/UpdateRcra'; import { WasteLine } from 'components/Manifest/WasteLine/wasteLineSchema'; import { RcraSiteDetails } from 'components/RcraSite'; import { HtButton, HtCard, HtForm, InfoIconTooltip } from 'components/UI'; -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Col, Container, Form, Row, Stack } from 'react-bootstrap'; import { FormProvider, SubmitHandler, useFieldArray, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; import { toast } from 'react-toastify'; import { manifest } from 'services'; import { - selectHaztrakSiteEpaIds, - useAppSelector, + ProfileSlice, useCreateManifestMutation, + useGetProfileQuery, useSaveEManifestMutation, useUpdateManifestMutation, } from 'store'; @@ -239,8 +240,25 @@ export function ManifestForm({ manifestData?.status ); + const selectUserSiteIds = useMemo( + () => + createSelector( + (res) => res.data, + (data: ProfileSlice) => + !data ?? !data.sites + ? [] + : Object.values(data.sites).map((site) => site.handler.epaSiteId) + ), + [] + ); + const nextSigner = manifest.getNextSigner(manifestData); - const userSiteIds = useAppSelector(selectHaztrakSiteEpaIds); + const { userSiteIds } = useGetProfileQuery(undefined, { + selectFromResult: (result) => ({ + ...result, + userSiteIds: selectUserSiteIds(result), + }), + }); // Whether the user has permissions and manifest is in a state to be signed const signAble = userSiteIds.includes(nextSigner?.epaSiteId ?? ''); diff --git a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx index 532c57d50..c878b7f7a 100644 --- a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx +++ b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.spec.tsx @@ -2,12 +2,31 @@ import '@testing-library/jest-dom'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { Handler, RcraSiteType } from 'components/Manifest/manifestSchema'; import { QuickSignBtn } from 'components/Manifest/QuickerSign/index'; +import { setupServer } from 'msw/node'; import React from 'react'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; import { cleanup, renderWithProviders, screen } from 'test-utils'; import { createMockMTNHandler } from 'test-utils/fixtures'; -import { afterEach, describe, expect, test } from 'vitest'; +import { userApiMocks } from 'test-utils/mock'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; import { undefined } from 'zod'; +const mockProfile: HaztrakProfileResponse = { + user: 'testuser1', + sites: [], + org: { + name: 'my org', + rcrainfoIntegrated: true, + id: '1234', + }, +}; + +const server = setupServer(...userApiMocks); +afterEach(() => { + cleanup(); +}); +beforeAll(() => server.listen()); +afterAll(() => server.close()); // Disable API mocking after the tests are done. afterEach(() => { cleanup(); }); @@ -40,60 +59,12 @@ function TestComponent({ } describe('QuickSignBtn', () => { - test('renders', () => { - const handlerId = 'TXD987654321'; - const handler = createMockMTNHandler({ siteType: 'Generator', epaSiteId: handlerId }); - renderWithProviders( - , - { - preloadedState: { - profile: { - user: 'testuser1', - org: { - rcrainfoIntegrated: true, - id: '123', - name: 'Test Org', - }, - sites: { - TXD987654321: { - name: 'Test Site', - handler: handler, - permissions: { eManifest: 'signer' }, - }, - }, - }, - }, - } - ); - expect(screen.getByRole('button')).toBeInTheDocument(); - }); test('is not disabled when user org is rcrainfo integrated', () => { const unsigned_handler = createMockMTNHandler({ signed: false, electronicSignaturesInfo: [], }); - renderWithProviders(, { - // Redux store state with an API user is required for this button to be active - preloadedState: { - profile: { - org: { - name: 'Test Org', - id: '123', - rcrainfoIntegrated: true, - }, - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: true, - }, - }, - }, - }); + renderWithProviders(); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); test('is disabled when API user but already signed', () => { @@ -108,20 +79,7 @@ describe('QuickSignBtn', () => { siteType={'Tsdf'} signingSite={{ epaSiteId: 'other_site', siteType: 'transporter' }} handler={unsigned_handler} - />, - // Redux store state with an API user is required for this button to be active - { - preloadedState: { - profile: { - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - }, - }, - }, - } + /> ); expect(screen.queryByRole('button')).not.toBeInTheDocument(); }); diff --git a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx index 9a958ca08..3f582c5e0 100644 --- a/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx +++ b/client/src/components/Manifest/QuickerSign/SignBtn/QuickSignBtn.tsx @@ -1,11 +1,12 @@ import { faFeather } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { createSelector } from '@reduxjs/toolkit'; import { ManifestContext } from 'components/Manifest/ManifestForm'; import { Handler, RcraSiteType } from 'components/Manifest/manifestSchema'; import { RcraApiUserBtn } from 'components/Rcrainfo'; -import React, { useContext } from 'react'; +import React, { useContext, useMemo } from 'react'; import { ButtonProps } from 'react-bootstrap'; -import { siteByEpaIdSelector, useAppSelector } from 'store'; +import { ProfileSlice, useGetProfileQuery } from 'store'; interface QuickSignBtnProps extends ButtonProps { siteType?: RcraSiteType; @@ -25,8 +26,24 @@ export function QuickSignBtn({ ...props }: QuickSignBtnProps) { const { nextSigningSite } = useContext(ManifestContext); - // if next site to sign is not one of the user's sites, don't show the button - if (!useAppSelector(siteByEpaIdSelector(nextSigningSite?.epaSiteId))) return <>; + + const selectBySiteId = useMemo(() => { + return createSelector( + (res) => res.data, + (res, siteId) => siteId, + (data: ProfileSlice, siteId) => { + return data && data.sites ? data.sites[siteId] : undefined; + } + ); + }, []); + + const { userSite } = useGetProfileQuery(undefined, { + selectFromResult: (res) => ({ + ...res, + userSite: selectBySiteId(res, nextSigningSite?.epaSiteId), + }), + }); + if (!userSite) return <>; if (mtnHandler && mtnHandler?.epaSiteId !== nextSigningSite?.epaSiteId) return <>; diff --git a/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx b/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx index d433b84fa..38b05eb59 100644 --- a/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx +++ b/client/src/components/Manifest/SiteSelect/SiteSelect.spec.tsx @@ -3,8 +3,6 @@ import { SiteSelect } from 'components/Manifest/SiteSelect/SiteSelect'; import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; import { renderWithProviders } from 'test-utils'; -import { createMockSite } from 'test-utils/fixtures'; -import { createMockRcrainfoPermissions } from 'test-utils/fixtures/mockHandler'; import { describe, expect, test } from 'vitest'; function TestComponent() { @@ -16,29 +14,7 @@ function TestComponent() { describe('SiteSelect', () => { test('renders', () => { - const mySite = createMockSite(); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: 'VATESTGEN001', - permissions: createMockRcrainfoPermissions(), - }, - VATEST00001: { - epaSiteId: 'VATEST00001', - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); + renderWithProviders(); expect(screen.queryByTestId('siteSelect')).toBeDefined(); }); }); diff --git a/client/src/components/Manifest/SiteSelect/SiteSelect.tsx b/client/src/components/Manifest/SiteSelect/SiteSelect.tsx index 2b381f798..3fa1efb95 100644 --- a/client/src/components/Manifest/SiteSelect/SiteSelect.tsx +++ b/client/src/components/Manifest/SiteSelect/SiteSelect.tsx @@ -1,9 +1,10 @@ +import { createSelector } from '@reduxjs/toolkit'; import { RcraSite } from 'components/RcraSite'; import { HtForm } from 'components/UI'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Control, Controller } from 'react-hook-form'; import Select from 'react-select'; -import { selectHaztrakSites, useAppSelector } from 'store'; +import { ProfileSlice, useGetProfileQuery } from 'store'; interface SiteSelectProps { control: Control; @@ -16,8 +17,21 @@ export function SiteSelect({ value, handleChange, }: SiteSelectProps) { - const userSites = useAppSelector(selectHaztrakSites); - const siteOptions = userSites?.map((site) => site.handler); + const selectUserSites = useMemo(() => { + return createSelector( + (res) => res?.data, + (data: ProfileSlice) => + !data || !data.sites ? [] : Object.values(data.sites).map((site) => site.handler) + ); + }, []); + + const { siteOptions } = useGetProfileQuery(undefined, { + selectFromResult: (result) => ({ + ...result, + siteOptions: selectUserSites(result), + }), + }); + return ( <> Site diff --git a/client/src/components/Org/OrgSitesTable.tsx b/client/src/components/Org/OrgSitesTable.tsx index e11a6bb40..6a932e15b 100644 --- a/client/src/components/Org/OrgSitesTable.tsx +++ b/client/src/components/Org/OrgSitesTable.tsx @@ -1,8 +1,7 @@ import { HaztrakSite } from 'components/HaztrakSite'; import React from 'react'; import { Row, Table } from 'react-bootstrap'; -import { useGetOrgSitesQuery } from 'store'; -import { HaztrakProfileOrg } from 'store/profileSlice/profile.slice'; +import { HaztrakProfileOrg, useGetOrgSitesQuery } from 'store'; interface OrgSitesProps { org: HaztrakProfileOrg; diff --git a/client/src/components/RcraProfile/RcraProfile.tsx b/client/src/components/RcraProfile/RcraProfile.tsx index 7ddf6874b..abcc14703 100644 --- a/client/src/components/RcraProfile/RcraProfile.tsx +++ b/client/src/components/RcraProfile/RcraProfile.tsx @@ -6,7 +6,7 @@ import React, { useEffect, useState } from 'react'; import { Button, Col, Container, Form, Row, Table } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; import { RcrainfoProfileState, useAppDispatch, useUpdateRcrainfoProfileMutation } from 'store'; -import { authApi } from 'store/userSlice/user.slice'; +import { userApi } from 'store/userSlice/user.slice'; import { z } from 'zod'; interface ProfileViewProps { @@ -33,7 +33,7 @@ export function RcraProfile({ profile }: ProfileViewProps) { }); useEffect(() => { - dispatch(authApi.util?.invalidateTags(['rcrainfoProfile'])); + dispatch(userApi.util?.invalidateTags(['rcrainfoProfile'])); }, [inProgress]); const { diff --git a/client/src/components/Rcrainfo/buttons/RcraApiUserBtn/RcraApiUserBtn.spec.tsx b/client/src/components/Rcrainfo/buttons/RcraApiUserBtn/RcraApiUserBtn.spec.tsx index 36eecbb75..f1d9bd0a6 100644 --- a/client/src/components/Rcrainfo/buttons/RcraApiUserBtn/RcraApiUserBtn.spec.tsx +++ b/client/src/components/Rcrainfo/buttons/RcraApiUserBtn/RcraApiUserBtn.spec.tsx @@ -23,18 +23,7 @@ describe('RcraApiUserBtn', () => { expect(screen.getByRole('button')).toBeDisabled(); }); test('is disabled when apiUser=false', () => { - renderWithProviders(Click Me, { - preloadedState: { - profile: { - user: 'username', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - }, - }, - }, - }); + renderWithProviders(Click Me); expect(screen.getByRole('button')).toBeDisabled(); }); }); diff --git a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx index e280e3078..2fa54bd87 100644 --- a/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx +++ b/client/src/components/Rcrainfo/buttons/SyncManifestBtn/SyncManifestBtn.spec.tsx @@ -4,23 +4,23 @@ import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { API_BASE_URL } from 'test-utils/mock/handlers'; +import { userApiMocks } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; const testTaskID = 'testTaskId'; -const server = setupServer( - ...[ - http.post(`${API_BASE_URL}rcra/manifest/emanifest/sync`, () => { - // Mock Sync Site Manifests response - return HttpResponse.json( - { - task: testTaskID, - }, - { status: 200 } - ); - }), - ] +const server = setupServer(...userApiMocks); +server.use( + http.post(`${API_BASE_URL}rcra/manifest/emanifest/sync`, () => { + // Mock Sync Site Manifests response + return HttpResponse.json( + { + task: testTaskID, + }, + { status: 200 } + ); + }) ); beforeAll(() => server.listen({ onUnhandledRequest: 'error' })); // setup mock http server diff --git a/client/src/components/User/UserInfoForm.spec.tsx b/client/src/components/User/UserInfoForm.spec.tsx index e501814da..00abfb9e5 100644 --- a/client/src/components/User/UserInfoForm.spec.tsx +++ b/client/src/components/User/UserInfoForm.spec.tsx @@ -2,27 +2,15 @@ import '@testing-library/jest-dom'; import { cleanup } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { UserInfoForm } from 'components/User/UserInfoForm'; -import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; import React from 'react'; import { HaztrakUser, ProfileSlice } from 'store'; import { renderWithProviders, screen } from 'test-utils'; -import { API_BASE_URL } from 'test-utils/mock/handlers'; +import { createMockHaztrakUser } from 'test-utils/fixtures'; +import { userApiMocks } from 'test-utils/mock'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; -const DEFAULT_USER: HaztrakUser = { - username: 'test', - firstName: 'David', - lastName: 'smith', - email: 'test@mail.com', -}; - -const server = setupServer( - http.put(`${API_BASE_URL}/api/user`, (info) => { - const user: HaztrakUser = { ...DEFAULT_USER }; - return HttpResponse.json({ ...user, ...info.request.body }); - }) -); +const server = setupServer(...userApiMocks); // pre-/post-test hooks beforeAll(() => server.listen()); @@ -35,13 +23,10 @@ afterAll(() => server.close()); // Disable API mocking after the tests are done. describe('UserProfile', () => { test('renders', () => { - const user: HaztrakUser = { - ...DEFAULT_USER, - username: 'test', - firstName: 'David', - }; + const myUsername = 'myUsername'; + const user: HaztrakUser = createMockHaztrakUser({ username: myUsername, firstName: 'David' }); const profile: ProfileSlice = { - user: 'test', + user: myUsername, }; renderWithProviders(, {}); expect(screen.getByRole('textbox', { name: 'First Name' })).toHaveValue(user.firstName); @@ -50,9 +35,7 @@ describe('UserProfile', () => { test('update profile fields', async () => { // Arrange const newEmail = 'newMockEmail@mail.com'; - const user: HaztrakUser = { - ...DEFAULT_USER, - }; + const user: HaztrakUser = createMockHaztrakUser({ email: 'oldEmail@gmail.com' }); const profile: ProfileSlice = { user: 'test', }; diff --git a/client/src/features/Dashboard/Dashboard.spec.tsx b/client/src/features/Dashboard/Dashboard.spec.tsx index 5f81fbaf5..aeaf61ec2 100644 --- a/client/src/features/Dashboard/Dashboard.spec.tsx +++ b/client/src/features/Dashboard/Dashboard.spec.tsx @@ -1,33 +1,26 @@ import '@testing-library/jest-dom'; import { Dashboard } from 'features/Dashboard/Dashboard'; -import { http, HttpResponse } from 'msw'; import { setupServer } from 'msw/node'; -import React from 'react'; +import React, { createElement } from 'react'; import { cleanup, renderWithProviders, screen } from 'test-utils'; -import { API_BASE_URL, handlers } from 'test-utils/mock/handlers'; +import { userApiMocks } from 'test-utils/mock'; +import { htApiMocks } from 'test-utils/mock/htApiMocks'; import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; const USERNAME = 'testuser1'; -const myAPIHandlers = [ - http.get(`${API_BASE_URL}/api/user/rcrainfo-profile/${USERNAME}`, (info) => { - return HttpResponse.json( - { - user: USERNAME, - rcraAPIID: 'mockRcraAPIID', - rcraUsername: undefined, - epaSites: [], - phoneNumber: undefined, - apiUser: true, - }, - { status: 200 } - ); - }), -]; - -const server = setupServer(...handlers, ...myAPIHandlers); +const server = setupServer(...htApiMocks, ...userApiMocks); -beforeAll(() => server.listen()); // setup mock http server +beforeAll(() => { + vi.mock('recharts', async (importOriginal) => { + const originalModule = (await importOriginal()) as Record; + return { + ...originalModule, + ResponsiveContainer: () => createElement('div'), + }; + }); + server.listen(); +}); // setup mock http server afterEach(() => { server.resetHandlers(); cleanup(); diff --git a/client/src/features/MmanifestDetails/ManifestDetails.tsx b/client/src/features/MmanifestDetails/ManifestDetails.tsx index e2be40c90..87c1650dc 100644 --- a/client/src/features/MmanifestDetails/ManifestDetails.tsx +++ b/client/src/features/MmanifestDetails/ManifestDetails.tsx @@ -1,14 +1,14 @@ import { ManifestForm } from 'components/Manifest'; -import { Manifest } from 'components/Manifest/manifestSchema'; import { HtSpinner } from 'components/UI'; -import { useHtApi, useTitle } from 'hooks'; +import { useTitle } from 'hooks'; import React from 'react'; import { useParams } from 'react-router-dom'; +import { useGetManifestQuery } from 'store'; export function ManifestDetails() { const { mtn, action, siteId } = useParams(); useTitle(`${mtn}`); - const [manifestData, loading, error] = useHtApi(`rcra/manifest/${mtn}`); + const { data, error, isLoading } = useGetManifestQuery(mtn!, { skip: !mtn }); let readOnly = true; if (action === 'edit') { @@ -16,7 +16,6 @@ export function ManifestDetails() { } if (error) { - // TODO: add global error handling via redux return (

Something went wrong

@@ -25,15 +24,10 @@ export function ManifestDetails() { ); } - return loading ? ( + return isLoading ? ( - ) : manifestData ? ( - + ) : data ? ( + ) : (

Something went wrong

diff --git a/client/src/features/NewManifest/NewManifest.spec.tsx b/client/src/features/NewManifest/NewManifest.spec.tsx index db68e7dbe..b74e56df8 100644 --- a/client/src/features/NewManifest/NewManifest.spec.tsx +++ b/client/src/features/NewManifest/NewManifest.spec.tsx @@ -1,86 +1,58 @@ import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import { NewManifest } from 'features/NewManifest/NewManifest'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; import React from 'react'; -import { renderWithProviders, screen } from 'test-utils'; -import { createMockRcrainfoPermissions, createMockSite } from 'test-utils/fixtures'; -import { describe, expect, test } from 'vitest'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; +import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { createMockSite } from 'test-utils/fixtures'; +import { userApiMocks } from 'test-utils/mock'; +import { API_BASE_URL } from 'test-utils/mock/htApiMocks'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; + +const mySiteId = 'VATESTGEN001'; +const mySiteName = 'My Site'; +const mySite = createMockSite({ + name: mySiteName, + // @ts-ignore + handler: { epaSiteId: mySiteId, siteType: 'Tsdf' }, +}); + +const mockProfile: HaztrakProfileResponse = { + user: 'testuser1', + sites: [{ site: mySite, eManifest: 'viewer' }], + org: { + name: 'my org', + rcrainfoIntegrated: true, + id: '1234', + }, +}; + +const server = setupServer(...userApiMocks); +server.use( + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json({ ...mockProfile }, { status: 200 }); + }) +); +afterEach(() => { + cleanup(); +}); +beforeAll(() => server.listen()); +afterAll(() => server.close()); // Disable API mocking after the tests are done. describe('NewManifest', () => { - test('renders', () => { - const mySiteId = 'VATESTGEN001'; - const mySiteName = 'My Site'; - const mySite = createMockSite({ - name: mySiteName, - // @ts-ignore - handler: { epaSiteId: mySiteId, siteType: 'Tsdf' }, - }); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - sites: { VATESTGEN001: { ...mySite, permissions: { eManifest: 'viewer' } } }, - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); + test('renders', async () => { + renderWithProviders(); expect(screen.getByRole('combobox', { name: /site Role/i })).toBeInTheDocument(); }); test('site type is initially disabled', () => { - const mySiteId = 'VATESTGEN001'; - const mySiteName = 'My Site'; - const mySite = createMockSite({ - name: mySiteName, - // @ts-ignore - handler: { epaSiteId: mySiteId, siteType: 'Tsdf' }, - }); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); + renderWithProviders(); const siteRole = screen.getByRole('combobox', { name: /site Role/i }); expect(siteRole).toBeDisabled(); }); test('site type is not disabled after selecting a site', async () => { - const mySiteId = 'VATESTGEN001'; - const mySiteName = 'My Site'; - const mySite = createMockSite({ - name: mySiteName, - // @ts-ignore - handler: { epaSiteId: mySiteId, siteType: 'Tsdf' }, - }); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - sites: { VATESTGEN001: { ...mySite, permissions: { eManifest: 'viewer' } } }, - }, - }, - }); + renderWithProviders(); const siteSelection = screen.getByRole('combobox', { name: /site select/i }); await userEvent.click(siteSelection); await userEvent.keyboard('{enter}'); diff --git a/client/src/features/NewManifest/NewManifest.tsx b/client/src/features/NewManifest/NewManifest.tsx index ddfe74583..f283ca3c9 100644 --- a/client/src/features/NewManifest/NewManifest.tsx +++ b/client/src/features/NewManifest/NewManifest.tsx @@ -1,14 +1,15 @@ +import { createSelector } from '@reduxjs/toolkit'; import { Manifest, ManifestForm } from 'components/Manifest'; import { RcraSiteType } from 'components/Manifest/manifestSchema'; import { SiteSelect, SiteTypeSelect } from 'components/Manifest/SiteSelect'; import { RcraSite } from 'components/RcraSite'; -import { HtCard } from 'components/UI'; +import { HtCard, HtSpinner } from 'components/UI'; import { useTitle } from 'hooks'; -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import { Col, Container, Form } from 'react-bootstrap'; import { useForm } from 'react-hook-form'; import { useParams } from 'react-router-dom'; -import { siteByEpaIdSelector, useAppSelector } from 'store'; +import { useGetProfileQuery } from 'store'; /** * NewManifest component allows a user to create a new electronic manifest. @@ -21,7 +22,23 @@ export function NewManifest() { useTitle('New Manifest'); const { control } = useForm(); const { siteId } = useParams(); - const rcraSite = useAppSelector(siteByEpaIdSelector(siteId)); + + const selectBySiteId = useMemo(() => { + return createSelector( + (res) => res.data, + (res, siteId) => siteId, + (data, siteId) => { + return data?.sites[siteId]?.handler ?? undefined; + } + ); + }, []); + + const { rcraSite, isLoading } = useGetProfileQuery(undefined, { + selectFromResult: (result) => ({ + ...result, + rcraSite: selectBySiteId(result, siteId), + }), + }); const [manifestingSite, setManifestingSite] = useState(rcraSite); const [selectedSiteType, setSelectedSiteType] = useState( rcraSite?.siteType === 'Generator' ? rcraSite.siteType : undefined @@ -30,6 +47,8 @@ export function NewManifest() { rcraSite?.siteType ); + if (isLoading && siteId) return ; + const handleSiteChange = (site: any) => { setManifestingSite(site); setManifestingSiteType(site.siteType); diff --git a/client/src/features/SiteList/SiteList.spec.tsx b/client/src/features/SiteList/SiteList.spec.tsx index 54fb6d0db..028237f0d 100644 --- a/client/src/features/SiteList/SiteList.spec.tsx +++ b/client/src/features/SiteList/SiteList.spec.tsx @@ -3,7 +3,8 @@ import { setupServer } from 'msw/node'; import React from 'react'; import { renderWithProviders, screen } from 'test-utils'; import { createMockHandler, createMockSite } from 'test-utils/fixtures/mockHandler'; -import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; +import { htApiMocks, userApiMocks } from 'test-utils/mock'; +import { afterAll, beforeAll, describe, expect, test } from 'vitest'; import { SiteList } from './SiteList'; const mockHandler1 = createMockHandler({ epaSiteId: 'VAT987654321' }); @@ -12,11 +13,7 @@ const mockSites = [ createMockSite({ handler: mockHandler1 }), createMockSite({ handler: mockHandler2 }), ]; -const server = setupServer( - http.get(`${import.meta.env.VITE_HT_API_URL}/api/site`, (info) => { - return HttpResponse.json(mockSites, { status: 200 }); - }) -); +const server = setupServer(...userApiMocks, ...htApiMocks); // pre-/post-test hooks beforeAll(() => server.listen()); @@ -26,11 +23,14 @@ describe('SiteList component', () => { test('renders', () => { renderWithProviders(, {}); }); - test('fetches displays all sites a user has access to', async () => { - // Act + test('displays all sites a user has access to', async () => { + server.use( + http.get(`${import.meta.env.VITE_HT_API_URL}/api/site`, (info) => { + return HttpResponse.json(mockSites, { status: 200 }); + }) + ); renderWithProviders(); let numIds = await screen.findAllByRole('listitem'); - // Assert expect(numIds.length).toEqual(mockSites.length); }); }); diff --git a/client/src/hooks/index.ts b/client/src/hooks/index.ts index 60c17db15..2a9292a7c 100644 --- a/client/src/hooks/index.ts +++ b/client/src/hooks/index.ts @@ -1,6 +1,3 @@ -import { useHtApi } from './useHtAPI/useHtApi'; -import { usePagination } from './usePagination/usePagination'; -import { useProgressTracker } from './useProgressTracker/useProgressTracker'; -import { useTitle } from './useTitle/useTitle'; - -export { usePagination, useTitle, useHtApi, useProgressTracker }; +export { usePagination } from './usePagination/usePagination'; +export { useProgressTracker } from './useProgressTracker/useProgressTracker'; +export { useTitle } from './useTitle/useTitle'; diff --git a/client/src/hooks/useHtAPI/useHtApi.spec.tsx b/client/src/hooks/useHtAPI/useHtApi.spec.tsx deleted file mode 100644 index e62832880..000000000 --- a/client/src/hooks/useHtAPI/useHtApi.spec.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import '@testing-library/jest-dom'; -import { cleanup } from '@testing-library/react'; -import { useHtApi } from 'hooks'; -import { http, HttpResponse } from 'msw'; -import { setupServer } from 'msw/node'; -import React from 'react'; -import { render, renderWithProviders, screen } from 'test-utils'; -import { afterAll, afterEach, beforeAll, describe, expect, test, vi } from 'vitest'; - -interface exampleData { - foo: string; - bar: string; -} - -interface exampleProps { - url: string; -} - -function TestComponent({ url }: exampleProps) { - const [data, loading, error] = useHtApi(url); - if (error) { - return ( - <> -

{error.message}

- - ); - } else { - return <>{loading ?

loading

:

{data?.foo}

}; - } -} - -/** - * mock Rest API - */ -const API_BASE_URL = import.meta.env.VITE_HT_API_URL; -export const testURL = [ - http.get(`${API_BASE_URL}/api/test/url`, (info) => { - return HttpResponse.json( - { - foo: 'foo', - bar: 'bar', - }, - { status: 200 } - ); - }), - http.get(`${API_BASE_URL}/api/bad/url`, (info) => { - return HttpResponse.json( - { - error: { - message: 'resource not found', - }, - }, - { status: 404 } - ); - }), -]; - -const server = setupServer(...testURL); - -beforeAll(() => server.listen()); // setup mock http server -afterEach(() => { - server.resetHandlers(); - cleanup(); - vi.resetAllMocks(); -}); -afterAll(() => server.close()); // Disable API mocking after the tests are done. - -afterEach(() => { - cleanup(); - vi.resetAllMocks(); -}); - -describe('useHtAPI', () => { - test('initially sets loading to true', () => { - render(); - expect(screen.getByText(/loading/i)).toBeInTheDocument(); - }); - test('sets data to the response body', async () => { - renderWithProviders(); - expect(await screen.findByText(/foo/i)).toBeInTheDocument(); - }); - test('sets the error for bad requests', async () => { - renderWithProviders(); - expect(await screen.findByText(/404/i)).toBeInTheDocument(); - }); -}); diff --git a/client/src/hooks/useHtAPI/useHtApi.tsx b/client/src/hooks/useHtAPI/useHtApi.tsx deleted file mode 100644 index 728fa49e2..000000000 --- a/client/src/hooks/useHtAPI/useHtApi.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useEffect, useState } from 'react'; -import { htApi } from 'services'; - -/** - * Hook for retrieving data from the haztrak http server - * - * @description - * Easy abstraction for GET request from the haztrak http server. Currently, - * does not support other http methods since those situations tend to require - * more thought. - * - * @param url {string} - * @return array {[data, loading, error]} - * - * @example - * const [data, loading , error ] = useHtApi(`/api/resource/path/${id}`) - */ -export function useHtApi(url: string) { - const [data, setData] = useState(undefined); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(undefined); - - useEffect(() => { - if (!url) return; - htApi - .get(url) - .then((response) => setData(response.data)) - .then(() => setLoading(false)) - .catch(setError); - }, [url]); - - return [data, loading, error] as const; -} diff --git a/client/src/store/authSlice/auth.slice.spec.tsx b/client/src/store/authSlice/auth.slice.spec.tsx new file mode 100644 index 000000000..2ee8c01fe --- /dev/null +++ b/client/src/store/authSlice/auth.slice.spec.tsx @@ -0,0 +1,86 @@ +import '@testing-library/jest-dom'; +import userEvent from '@testing-library/user-event'; +import { + removeCredentials, + selectAuthenticated, + setCredentials, + useAppDispatch, + useAppSelector, +} from 'store'; +import { renderWithProviders, screen } from 'test-utils'; +import { afterEach, describe, expect, test, vi } from 'vitest'; + +const getItemSpy = vi.spyOn(Storage.prototype, 'getItem'); +const setItemSpy = vi.spyOn(Storage.prototype, 'setItem'); +const removeItemSpy = vi.spyOn(Storage.prototype, 'removeItem'); + +afterEach(() => { + getItemSpy.mockClear(); + setItemSpy.mockClear(); + removeItemSpy.mockClear(); +}); + +const TestComponent = () => { + const dispatch = useAppDispatch(); + const isAuthenticated = useAppSelector(selectAuthenticated); + return ( +
+

Test

+

{isAuthenticated ? 'authenticated' : 'unauthenticated'}

+
+ + +
+
+ ); +}; + +describe('auth slice', () => { + test('our localStorage mocks are working', () => { + const value = 'test'; + localStorage.setItem('token', value); + expect(setItemSpy).toHaveBeenCalled(); + const foo = localStorage.getItem('token'); + expect(setItemSpy).toHaveBeenCalled(); + expect(foo).toBe(value); + }); + test('initial state is unauthenticated', () => { + renderWithProviders(); + expect(screen.queryByText('unauthenticated')).toBeInTheDocument(); + expect(screen.queryByText('authenticated')).not.toBeInTheDocument(); + }); + test('setCredentials stores the token in local storage', async () => { + renderWithProviders(); + await userEvent.click(screen.getByText('Log in')); + expect(setItemSpy).toHaveBeenCalled(); + }); + test('selectAuthenticated is true after credentials are set', async () => { + renderWithProviders(); + await userEvent.click(screen.getByText('Log in')); + expect(screen.queryByText('authenticated')).toBeInTheDocument(); + }); + test('selectAuthenticated is false removeCredentials is dispatched', async () => { + renderWithProviders(, { preloadedState: { auth: { token: 'mockToken' } } }); + localStorage.setItem('token', 'mockToken'); + await userEvent.click(screen.getByText('Log out')); + expect(screen.queryByText('unauthenticated')).toBeInTheDocument(); + }); + test('tokens are removed from localStorage on logout', async () => { + renderWithProviders(, { preloadedState: { auth: { token: 'mockToken' } } }); + localStorage.setItem('token', 'mockToken'); + await userEvent.click(screen.getByText('Log out')); + expect(removeItemSpy).toHaveBeenCalled(); + }); +}); diff --git a/client/src/store/authSlice/auth.slice.ts b/client/src/store/authSlice/auth.slice.ts new file mode 100644 index 000000000..7dc0ddd9f --- /dev/null +++ b/client/src/store/authSlice/auth.slice.ts @@ -0,0 +1,54 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; + +export interface HaztrakUser { + username: string; + email?: string; + firstName?: string; + lastName?: string; + isLoading?: boolean; + error?: string; +} + +/** + * The Redux stored information on the current haztrak user + */ +export interface AuthSlice { + user?: HaztrakUser; + token?: string; + loading?: boolean; + error?: string; +} + +const initialState: AuthSlice = { + user: { username: JSON.parse(localStorage.getItem('user') || 'null') || null, isLoading: false }, + token: JSON.parse(localStorage.getItem('token') || 'null') || null, + loading: false, + error: undefined, +}; +export const authSlice = createSlice({ + name: 'auth', + initialState, + selectors: { + selectAuthenticated: (state: AuthSlice): boolean => !!state.token, + selectUser: (state: AuthSlice): HaztrakUser | undefined => state.user, + selectUserName: (state: AuthSlice): string | undefined => state.user?.username, + }, + reducers: { + setCredentials(state: AuthSlice, action: PayloadAction<{ token: string }>) { + const { token } = action.payload; + localStorage.setItem('token', JSON.stringify(token)); + return { + ...state, + token, + } as AuthSlice; + }, + removeCredentials(): AuthSlice { + localStorage.removeItem('token'); + return { ...initialState, user: undefined, token: undefined }; + }, + }, +}); + +export default authSlice.reducer; +export const { removeCredentials, setCredentials } = authSlice.actions; +export const { selectAuthenticated, selectUser, selectUserName } = authSlice.selectors; diff --git a/client/src/store/haztrakApiSlice.ts b/client/src/store/htApi.slice.ts similarity index 86% rename from client/src/store/haztrakApiSlice.ts rename to client/src/store/htApi.slice.ts index e105eef6a..f2a8dac6c 100644 --- a/client/src/store/haztrakApiSlice.ts +++ b/client/src/store/htApi.slice.ts @@ -19,10 +19,8 @@ export interface HtApiQueryArgs { params?: AxiosRequestConfig['params']; } -export interface HtApiError { - status?: number; +export interface HtApiError extends AxiosError { data?: AxiosResponse['data']; - code?: string; statusText?: string; } @@ -50,10 +48,10 @@ export const htApiBaseQuery = return { data: response.data }; } catch (axiosError) { let err = axiosError as AxiosError; + console.log(err); return { error: { - code: err.code, - status: err.response?.status, + ...err, statusText: err.response?.statusText, data: err.response?.data || err.message, } as HtApiError, @@ -76,7 +74,7 @@ interface RcrainfoSiteSearch { } export const haztrakApi = createApi({ - tagTypes: ['user'], + tagTypes: ['user', 'auth', 'profile', 'rcrainfoProfile', 'site', 'code', 'manifest'], reducerPath: 'haztrakApi', baseQuery: htApiBaseQuery({ baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/`, @@ -102,24 +100,35 @@ export const haztrakApi = createApi({ }), getFedWasteCodes: build.query, void>({ query: () => ({ url: 'rcra/waste/code/federal', method: 'get' }), + providesTags: ['code'], }), getStateWasteCodes: build.query, string>({ query: (state) => ({ url: `rcra/waste/code/state/${state}`, method: 'get' }), + providesTags: ['code'], }), getDotIdNumbers: build.query, string>({ query: (id) => ({ url: 'rcra/waste/dot/id', method: 'get', params: { q: id } }), + providesTags: ['code'], }), getOrgSites: build.query, string>({ query: (id) => ({ url: `org/${id}/site`, method: 'get' }), + providesTags: ['site'], }), getUserHaztrakSites: build.query, void>({ query: () => ({ url: 'site', method: 'get' }), + providesTags: ['site'], }), getUserHaztrakSite: build.query({ query: (epaId) => ({ url: `site/${epaId}`, method: 'get' }), + providesTags: ['site'], }), getMTN: build.query, string | undefined>({ query: (siteId) => ({ url: siteId ? `rcra/mtn/${siteId}` : 'rcra/mtn', method: 'get' }), + providesTags: ['manifest'], + }), + getManifest: build.query({ + query: (mtn) => ({ url: `rcra/manifest/${mtn}`, method: 'get' }), + providesTags: ['manifest'], }), createManifest: build.mutation({ query: (data) => ({ @@ -127,6 +136,7 @@ export const haztrakApi = createApi({ method: 'POST', data, }), + invalidatesTags: ['manifest'], }), updateManifest: build.mutation({ query: ({ mtn, manifest }) => ({ @@ -134,6 +144,7 @@ export const haztrakApi = createApi({ method: 'PUT', data: manifest, }), + invalidatesTags: ['manifest'], }), saveEManifest: build.mutation({ query: (data) => ({ @@ -141,6 +152,7 @@ export const haztrakApi = createApi({ method: 'POST', data, }), + invalidatesTags: ['manifest'], }), syncEManifest: build.mutation({ query: (siteId) => ({ @@ -148,6 +160,7 @@ export const haztrakApi = createApi({ method: 'POST', data: { siteId: siteId }, }), + invalidatesTags: ['manifest'], }), signEManifest: build.mutation({ query: (signature) => ({ @@ -155,6 +168,7 @@ export const haztrakApi = createApi({ method: 'POST', data: signature, }), + invalidatesTags: ['manifest'], }), }), }); diff --git a/client/src/store/index.ts b/client/src/store/index.ts index 2e28f106e..aab921280 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -1,6 +1,6 @@ -import { authApi } from 'store/userSlice/user.slice'; // Haztrak API - RTK Query -import { haztrakApi } from './haztrakApiSlice'; +import { haztrakApi } from 'store/htApi.slice'; +import { userApi } from 'store/userSlice/user.slice'; import type { AppDispatch, AppStore, RootState } from './rootStore'; // Root Store @@ -23,6 +23,7 @@ export const { useSyncEManifestMutation, useSignEManifestMutation, useUpdateManifestMutation, + useGetManifestQuery, } = haztrakApi; export const { @@ -33,26 +34,17 @@ export const { useUpdateUserMutation, useUpdateRcrainfoProfileMutation, useSyncRcrainfoProfileMutation, -} = authApi; +} = userApi; // Authentication Slice export { selectUser, selectUserName, - updateUserProfile, setCredentials, -} from 'store/userSlice/user.slice'; - -// Profile Slice -export { - selectRcrainfoSites, - selectRcraProfile, - siteByEpaIdSelector, - updateProfile, - selectHaztrakSites, - selectHaztrakSiteEpaIds, - selectHaztrakProfile, -} from './profileSlice/profile.slice'; + selectAuthenticated, + removeCredentials, +} from 'store/authSlice/auth.slice'; +export type { HaztrakUser } from 'store/authSlice/auth.slice'; // Notification Slice export { @@ -71,9 +63,8 @@ export { export { addError, selectAllErrors } from './errorSlice/error.slice'; // Types -export type { HaztrakUser } from 'store/userSlice/user.slice'; export type { HaztrakError } from './errorSlice/error.slice'; -export type { TaskStatus } from './haztrakApiSlice'; +export type { TaskStatus } from 'store/htApi.slice'; export type { LongRunningTask, HaztrakAlert } from './notificationSlice/notification.slice'; export type { ProfileSlice, @@ -82,6 +73,7 @@ export type { HaztrakSitePermissions, RcrainfoSitePermissions, HaztrakModulePermissions, + HaztrakProfileOrg, RcrainfoProfile, RcrainfoProfileSite, -} from './profileSlice/profile.slice'; +} from './userSlice/user.slice'; diff --git a/client/src/store/profileSlice/profile.slice.spec.tsx b/client/src/store/profileSlice/profile.slice.spec.tsx deleted file mode 100644 index cb3d5bcc8..000000000 --- a/client/src/store/profileSlice/profile.slice.spec.tsx +++ /dev/null @@ -1,91 +0,0 @@ -/** - * RcrainfoProfile tests - */ -import '@testing-library/jest-dom'; -import { screen } from '@testing-library/react'; -import React from 'react'; -import { useAppSelector } from 'store'; -import { renderWithProviders } from 'test-utils'; -import { createMockSite } from 'test-utils/fixtures'; -import { createMockRcrainfoPermissions } from 'test-utils/fixtures/mockHandler'; -import { describe, expect, test } from 'vitest'; -import { selectRcrainfoSites, siteByEpaIdSelector } from './profile.slice'; - -interface TestComponentProps { - siteId: string; -} - -function TestComponent({ siteId }: TestComponentProps) { - const rcraSite = useAppSelector(siteByEpaIdSelector(siteId)); - return ( - <> -

{rcraSite?.epaSiteId}

- - ); -} - -describe('RcraProfileSlice selectors', () => { - test('Retrieve RcraProfileSite by EPA ID', () => { - const mySite = createMockSite(); - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - sites: { VATESTGEN001: { ...mySite, permissions: { eManifest: 'viewer' } } }, - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); - expect(screen.getByText(mySite.handler.epaSiteId)).toBeInTheDocument(); - }); - test('retrieve all RcraProfileSites', () => { - const mySite = createMockSite(); - const TestComp = () => { - const myRcraSite = useAppSelector(selectRcrainfoSites); - return ( - <> -

{myRcraSite ? Object.keys(myRcraSite).length : 'not defined'}

- {myRcraSite - ? myRcraSite.map((site, index) => ( -

{site.epaSiteId}

- )) - : 'not defined'} - - ); - }; - renderWithProviders(, { - preloadedState: { - profile: { - user: 'testuser1', - rcrainfoProfile: { - user: 'username', - phoneNumber: '1231231234', - apiUser: false, - rcraSites: { - VATESTGEN001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - VATEST00001: { - epaSiteId: mySite.handler.epaSiteId, - permissions: createMockRcrainfoPermissions(), - }, - }, - }, - }, - }, - }); - expect(screen.getByText('2')).toBeInTheDocument(); - expect(screen.queryByText(/Not Defined/i)).not.toBeInTheDocument(); - }); -}); diff --git a/client/src/store/profileSlice/profile.slice.ts b/client/src/store/profileSlice/profile.slice.ts deleted file mode 100644 index f7629eb5c..000000000 --- a/client/src/store/profileSlice/profile.slice.ts +++ /dev/null @@ -1,149 +0,0 @@ -/** - * A user's RcrainfoProfile slice encapsulates our logic related what actions and data a user - * has access to for each EPA site ID. - */ -import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { HaztrakSite } from 'components/HaztrakSite'; -import { RcraSite } from 'components/RcraSite'; -import { RootState } from 'store'; - -/**The user's RCRAInfo account data stored in the Redux store*/ -export interface ProfileSlice { - user: string | undefined; - rcrainfoProfile?: RcrainfoProfile>; - sites?: Record; - org?: HaztrakProfileOrg | null; - loading?: boolean; - error?: string; -} - -export interface HaztrakProfileOrg { - id: string; - name: string; - rcrainfoIntegrated: boolean; -} - -/** A site a user has access to in RCRAInfo and their module permissions */ -export interface RcrainfoProfileSite { - epaSiteId: string; - permissions: RcrainfoSitePermissions; -} - -export interface HaztrakProfileSite extends HaztrakSite { - permissions: HaztrakSitePermissions; -} - -export type HaztrakModulePermissions = 'viewer' | 'editor' | 'signer'; - -export interface HaztrakSitePermissions { - eManifest: HaztrakModulePermissions; -} - -export interface RcrainfoProfileState - extends RcrainfoProfile> {} - -export interface RcrainfoProfile { - user: string; - rcraAPIID?: string; - rcraUsername?: string; - rcraAPIKey?: string; - apiUser?: boolean; - rcraSites?: T; - phoneNumber?: string; - isLoading?: boolean; - error?: string; -} - -export interface RcrainfoSitePermissions { - siteManagement: boolean; - annualReport: string; - biennialReport: string; - eManifest: string; - WIETS: string; - myRCRAid: string; -} - -/**initial, state of a user's RcrainfoProfile.*/ -const initialState: ProfileSlice = { - user: undefined, - rcrainfoProfile: undefined, - sites: undefined, - loading: false, - error: undefined, -}; - -const profileSlice = createSlice({ - name: 'profile', - initialState, - reducers: { - updateProfile: (state: ProfileSlice, action: PayloadAction) => { - return { - ...state, - ...action.payload, - }; - }, - }, -}); - -/** Retrieve a Haztrak site from the users Profile by the site's EPA ID number */ -export const siteByEpaIdSelector = ( - epaId: string | undefined -): ((state: RootState) => RcraSite | undefined) => - createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return undefined; - - const siteId = Object.keys(sites).find((key) => sites[key]?.handler.epaSiteId === epaId); - if (!siteId) return undefined; - - const sitePermissions = sites[siteId]; - if (!sitePermissions) return undefined; - - return sitePermissions.handler; - } - ); - -/** Get all sites a user has access to their Haztrak Profile*/ -export const selectHaztrakSites = createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return undefined; - - return Object.values(sites).map((site) => site); - } -); - -/** Get all sites a user has access to their Haztrak Profile*/ -export const selectHaztrakSiteEpaIds = createSelector( - (state: { profile: ProfileSlice }) => state.profile.sites, - (sites: Record | undefined) => { - if (!sites) return []; - return Object.values(sites).map((site) => site.handler.epaSiteId); - } -); - -/** select all RCRAInfo sites a user has access to from their RCRAInfo Profile if they're updated it*/ -export const selectRcrainfoSites = createSelector( - (state: { profile: ProfileSlice }) => state.profile.rcrainfoProfile?.rcraSites, - (rcraSites: Record | undefined) => { - if (!rcraSites) return undefined; - - return Object.values(rcraSites).map((site) => site); - } -); - -/** Retrieve a user's RcrainfoProfile from the Redux store. */ -export const selectRcraProfile = createSelector( - (state: RootState) => state, - (state: RootState) => state.profile.rcrainfoProfile -); - -/** Retrieve a user's HaztrakProfile from the Redux store. */ -export const selectHaztrakProfile = createSelector( - (state: RootState) => state, - (state: RootState) => state.profile -); - -export default profileSlice.reducer; -export const { updateProfile } = profileSlice.actions; diff --git a/client/src/store/rootStore.ts b/client/src/store/rootStore.ts index f9077ac99..8a80e7aeb 100644 --- a/client/src/store/rootStore.ts +++ b/client/src/store/rootStore.ts @@ -1,26 +1,22 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'; import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; -import userReducers, { authApi } from 'store/userSlice/user.slice'; +import authReducers from 'store/authSlice/auth.slice'; +import { haztrakApi } from 'store/htApi.slice'; import errorReducers from './errorSlice/error.slice'; -import { haztrakApi } from './haztrakApiSlice'; import notificationReducers from './notificationSlice/notification.slice'; -import profileReducers from './profileSlice/profile.slice'; const rootReducer = combineReducers({ - auth: userReducers, - profile: profileReducers, + auth: authReducers, error: errorReducers, notifications: notificationReducers, [haztrakApi.reducerPath]: haztrakApi.reducer, - [authApi.reducerPath]: authApi.reducer, }); /**A utility function to initialize the store with preloaded state used for testing*/ const setupStore = (preloadedState?: Partial) => { return configureStore({ reducer: rootReducer, - middleware: (getDefaultMiddleware) => - getDefaultMiddleware().concat(haztrakApi.middleware, authApi.middleware), + middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(haztrakApi.middleware), preloadedState, }); }; diff --git a/client/src/store/userSlice/user.slice.spec.tsx b/client/src/store/userSlice/user.slice.spec.tsx new file mode 100644 index 000000000..40fd90705 --- /dev/null +++ b/client/src/store/userSlice/user.slice.spec.tsx @@ -0,0 +1,60 @@ +import '@testing-library/jest-dom'; +import { waitFor } from '@testing-library/react'; +import { setupServer } from 'msw/node'; +import { useEffect, useState } from 'react'; +import { useGetUserQuery, useUpdateUserMutation } from 'store'; +import { cleanup, renderWithProviders, screen } from 'test-utils'; +import { userApiMocks } from 'test-utils/mock'; +import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; + +const server = setupServer(...userApiMocks); +afterEach(() => { + cleanup(); +}); +beforeAll(() => server.listen()); +afterAll(() => server.close()); + +const UserQueryComponent = () => { + const [fetchCount, setFetchCount] = useState(0); + const { data, error, isLoading, isFetching } = useGetUserQuery(); + const [updateUser, results] = useUpdateUserMutation(); + + useEffect(() => { + if (isFetching) setFetchCount(fetchCount + 1); + }, [isFetching]); + + if (isLoading || isFetching) { + return
Loading
; + } + if (error) { + return
Error
; + } + if (data) { + return ( +
+

Data

+

username: {data.username}

+

email: {data.email}

+

fetchCount: {fetchCount}

+
+ +
+
+ ); + } +}; + +describe('userApi', () => { + test('get user query', async () => { + renderWithProviders(); + await waitFor(() => screen.getByText('Data')); + expect(screen.queryByText(/username:/i)).toBeInTheDocument(); + }); +}); diff --git a/client/src/store/userSlice/user.slice.ts b/client/src/store/userSlice/user.slice.ts index 72976a720..3d84ebd4a 100644 --- a/client/src/store/userSlice/user.slice.ts +++ b/client/src/store/userSlice/user.slice.ts @@ -1,81 +1,69 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { createApi } from '@reduxjs/toolkit/query/react'; import { HaztrakSite } from 'components/HaztrakSite'; -import { htApiBaseQuery, TaskResponse } from 'store/haztrakApiSlice'; -import { - HaztrakModulePermissions, - ProfileSlice, - RcrainfoProfile, - RcrainfoProfileSite, - RcrainfoProfileState, -} from 'store/profileSlice/profile.slice'; +import { HaztrakUser } from 'store/authSlice/auth.slice'; +import { haztrakApi, TaskResponse } from 'store/htApi.slice'; -export interface HaztrakUser { - username: string; - email?: string; - firstName?: string; - lastName?: string; - isLoading?: boolean; +/**The user's RCRAInfo account data stored in the Redux store*/ +export interface ProfileSlice { + user: string | undefined; + rcrainfoProfile?: RcrainfoProfile>; + sites?: Record; + org?: HaztrakProfileOrg | null; + loading?: boolean; error?: string; } -/** - * The Redux stored information on the current haztrak user - */ -export interface UserSlice { - user?: HaztrakUser; - token?: string; - loading?: boolean; - error?: string; +export interface HaztrakProfileOrg { + id: string; + name: string; + rcrainfoIntegrated: boolean; } -const initialState: UserSlice = { - user: { username: JSON.parse(localStorage.getItem('user') || 'null') || null, isLoading: false }, - token: JSON.parse(localStorage.getItem('token') || 'null') || null, - loading: false, - error: undefined, -}; +/** A site a user has access to in RCRAInfo and their module permissions */ +export interface RcrainfoProfileSite { + epaSiteId: string; + permissions: RcrainfoSitePermissions; +} -const authSlice = createSlice({ - name: 'auth', - initialState, - selectors: { - selectAuthenticated: (state: UserSlice): boolean => !!state.token, - selectUser: (state: UserSlice): HaztrakUser | undefined => state.user, - selectUserName: (state: UserSlice): string | undefined => state.user?.username, - }, - reducers: { - setCredentials(state: UserSlice, action: PayloadAction<{ token: string }>) { - const token = action.payload.token; - localStorage.setItem('token', JSON.stringify(token)); - return { - ...state, - token, - } as UserSlice; - }, - logout(state: UserSlice): UserSlice { - localStorage.removeItem('token'); - return { ...initialState, user: undefined, token: undefined }; - }, - updateUserProfile(state: UserSlice, action: PayloadAction) { - return { - ...state, - user: action.payload, - }; - }, - }, -}); +export interface HaztrakProfileSite extends HaztrakSite { + permissions: HaztrakSitePermissions; +} -export default authSlice.reducer; -export const { updateUserProfile, logout, setCredentials } = authSlice.actions; -export const { selectAuthenticated, selectUser, selectUserName } = authSlice.selectors; +export type HaztrakModulePermissions = 'viewer' | 'editor' | 'signer'; -interface LoginRequest { +export interface HaztrakSitePermissions { + eManifest: HaztrakModulePermissions; +} + +export interface RcrainfoProfileState + extends RcrainfoProfile> {} + +export interface RcrainfoProfile { + user: string; + rcraAPIID?: string; + rcraUsername?: string; + rcraAPIKey?: string; + apiUser?: boolean; + rcraSites?: T; + phoneNumber?: string; + isLoading?: boolean; + error?: string; +} + +export interface RcrainfoSitePermissions { + siteManagement: boolean; + annualReport: string; + biennialReport: string; + eManifest: string; + WIETS: string; + myRCRAid: string; +} + +export interface LoginRequest { username: string; password: string; } -interface LoginResponse { +export interface LoginResponse { key: string; } @@ -85,7 +73,7 @@ interface HaztrakOrgResponse { rcrainfoIntegrated: boolean; } -interface HaztrakProfileResponse { +export interface HaztrakProfileResponse { user: string; sites: Array<{ site: HaztrakSite; @@ -96,17 +84,12 @@ interface HaztrakProfileResponse { interface RcrainfoProfileResponse extends RcrainfoProfile> {} -export const authApi = createApi({ - tagTypes: ['user', 'auth', 'profile', 'rcrainfoProfile'], - reducerPath: 'authApi', - baseQuery: htApiBaseQuery({ - baseUrl: `${import.meta.env.VITE_HT_API_URL}/api/user`, - }), +export const userApi = haztrakApi.injectEndpoints({ endpoints: (build) => ({ // Note: build.query login: build.mutation({ query: (data) => ({ - url: '/login', + url: 'login', method: 'POST', data: data, }), @@ -114,14 +97,14 @@ export const authApi = createApi({ }), getUser: build.query({ query: () => ({ - url: '', + url: 'user', method: 'GET', }), providesTags: ['user'], }), updateUser: build.mutation({ query: (data) => ({ - url: '', + url: 'user', method: 'PUT', data: data, }), @@ -129,7 +112,7 @@ export const authApi = createApi({ }), getProfile: build.query({ query: () => ({ - url: '/profile', + url: 'user/profile', method: 'GET', }), providesTags: ['profile'], @@ -152,7 +135,7 @@ export const authApi = createApi({ }), getRcrainfoProfile: build.query({ query: (username) => ({ - url: `/rcrainfo-profile/${username}`, + url: `user/rcrainfo-profile/${username}`, method: 'GET', }), providesTags: ['rcrainfoProfile'], @@ -171,7 +154,7 @@ export const authApi = createApi({ }), updateRcrainfoProfile: build.mutation({ query: (data) => ({ - url: `/rcrainfo-profile/${data.username}`, + url: `user/rcrainfo-profile/${data.username}`, method: 'PUT', data: data.data, }), @@ -179,7 +162,7 @@ export const authApi = createApi({ }), syncRcrainfoProfile: build.mutation({ query: () => ({ - url: `/rcrainfo-profile/sync`, + url: `user/rcrainfo-profile/sync`, method: 'POST', }), invalidatesTags: ['rcrainfoProfile'], diff --git a/client/src/test-utils/fixtures/index.ts b/client/src/test-utils/fixtures/index.ts index 687b20c00..3938bc145 100644 --- a/client/src/test-utils/fixtures/index.ts +++ b/client/src/test-utils/fixtures/index.ts @@ -9,3 +9,4 @@ export { createMockRcrainfoPermissions, } from './mockHandler'; export { createMockManifest } from './mockManifest'; +export { createMockHaztrakUser } from './mockUser'; diff --git a/client/src/test-utils/fixtures/mockUser.ts b/client/src/test-utils/fixtures/mockUser.ts new file mode 100644 index 000000000..ad145e19e --- /dev/null +++ b/client/src/test-utils/fixtures/mockUser.ts @@ -0,0 +1,57 @@ +import { HaztrakProfileOrg, HaztrakUser, RcrainfoProfile, RcrainfoProfileSite } from 'store'; +import { HaztrakProfileResponse } from 'store/userSlice/user.slice'; +import { createMockSite } from 'test-utils/fixtures/mockHandler'; + +export const DEFAULT_HAZTRAK_USER: HaztrakUser = { + username: 'testuser1', + firstName: 'john', + lastName: 'smith', + email: 'test@mail.com', +}; + +export function createMockHaztrakUser(overWrites?: Partial): HaztrakUser { + return { + ...DEFAULT_HAZTRAK_USER, + ...overWrites, + }; +} + +interface RcrainfoProfileResponse extends RcrainfoProfile> {} + +const DEFAULT_RCRAINFO_PROFILE_RESPONSE: RcrainfoProfileResponse = { + user: 'testuser1', + rcraAPIID: 'mockRcraAPIID', + rcraUsername: undefined, + rcraSites: [], + phoneNumber: undefined, + apiUser: true, +}; + +export function createMockRcrainfoProfileResponse( + overWrites?: Partial +): RcrainfoProfileResponse { + return { + ...DEFAULT_RCRAINFO_PROFILE_RESPONSE, + ...overWrites, + }; +} + +export function createMockOrg(overWrites?: Partial): HaztrakProfileOrg { + return { + name: 'mockOrg', + id: 'mockOrgId', + rcrainfoIntegrated: true, + ...overWrites, + }; +} + +export function createMockProfileResponse( + overWrites?: Partial +): HaztrakProfileResponse { + return { + user: DEFAULT_HAZTRAK_USER.username, + org: createMockOrg(), + sites: [{ site: createMockSite(), eManifest: 'signer' }], + ...overWrites, + }; +} diff --git a/client/src/test-utils/index.tsx b/client/src/test-utils/index.ts similarity index 100% rename from client/src/test-utils/index.tsx rename to client/src/test-utils/index.ts diff --git a/client/src/test-utils/mock/browser.js b/client/src/test-utils/mock/browser.js deleted file mode 100644 index b3e2bd2f8..000000000 --- a/client/src/test-utils/mock/browser.js +++ /dev/null @@ -1,4 +0,0 @@ -// src/test/browser.js - -// This configures a Service Worker with the given request handlers. -export const worker = setupWorker(...handlers); diff --git a/client/src/test-utils/mock/handlers.ts b/client/src/test-utils/mock/htApiMocks.ts similarity index 57% rename from client/src/test-utils/mock/handlers.ts rename to client/src/test-utils/mock/htApiMocks.ts index 828706099..a17dcfe9b 100644 --- a/client/src/test-utils/mock/handlers.ts +++ b/client/src/test-utils/mock/htApiMocks.ts @@ -4,39 +4,9 @@ import { createMockHandler, createMockManifest, createMockSite } from '../fixtur export const API_BASE_URL = import.meta.env.VITE_HT_API_URL; const mockMTN = createMockManifest().manifestTrackingNumber; const mockEpaId = createMockHandler().epaSiteId; -const mockUsername = 'testuser1'; const mockSites = [createMockSite(), createMockSite()]; -export const handlers = [ - /** Login endpoint*/ - http.post(`${API_BASE_URL}/api/user/login`, (info) => { - // Persist user's authentication in the session - sessionStorage.setItem('token', 'this_is_a_fake_token'); - sessionStorage.setItem('user', mockUsername); - - // Mock response from haztrak API - return HttpResponse.json( - { - token: 'fake_token', - user: mockUsername, - }, - { status: 200 } - ); - }), - /** User RcrainfoProfile data*/ - http.get(`${API_BASE_URL}/api/user/rcrainfo-profile/${mockUsername}`, (info) => { - return HttpResponse.json( - { - user: mockUsername, - rcraAPIID: 'mockRcraAPIID', - rcraUsername: undefined, - epaSites: [], - phoneNumber: undefined, - apiUser: true, - }, - { status: 200 } - ); - }), +export const htApiMocks = [ /** List user sites*/ http.get(`${API_BASE_URL}/api/site`, (info) => { return HttpResponse.json(mockSites, { status: 200 }); diff --git a/client/src/test-utils/mock/index.ts b/client/src/test-utils/mock/index.ts new file mode 100644 index 000000000..720f3980a --- /dev/null +++ b/client/src/test-utils/mock/index.ts @@ -0,0 +1,2 @@ +export { userApiMocks } from './userApiMocks'; +export { htApiMocks } from './htApiMocks'; diff --git a/client/src/test-utils/mock/userApiMocks.ts b/client/src/test-utils/mock/userApiMocks.ts new file mode 100644 index 000000000..eaf852d3e --- /dev/null +++ b/client/src/test-utils/mock/userApiMocks.ts @@ -0,0 +1,48 @@ +import { http, HttpResponse } from 'msw'; +import { HaztrakUser } from 'store/authSlice/auth.slice'; +import { LoginResponse } from 'store/userSlice/user.slice'; +import { createMockHaztrakUser } from 'test-utils/fixtures'; +import { + createMockProfileResponse, + createMockRcrainfoProfileResponse, +} from 'test-utils/fixtures/mockUser'; + +/** mock Rest API*/ +const API_BASE_URL = import.meta.env.VITE_HT_API_URL; +export const userApiMocks = [ + /** GET User */ + http.get(`${API_BASE_URL}/api/user`, () => { + return HttpResponse.json({ ...createMockHaztrakUser() }, { status: 200 }); + }), + /** Update User */ + http.put(`${API_BASE_URL}/api/user`, (info) => { + const user: HaztrakUser = { ...createMockHaztrakUser() }; + return HttpResponse.json({ ...user, ...info.request.body }, { status: 200 }); + }), + /** GET Profile */ + http.get(`${API_BASE_URL}/api/user/profile`, () => { + return HttpResponse.json({ ...createMockProfileResponse() }, { status: 200 }); + }), + /** Login */ + http.post(`${API_BASE_URL}/api/user/login`, (info) => { + const body: LoginResponse = { key: 'mockToken' }; + return HttpResponse.json( + { + ...body, + }, + { status: 200 } + ); + }), + /** GET RCRAInfo profile */ + http.get(`${API_BASE_URL}/api/user/rcrainfo-profile/:username`, (info) => { + const { username } = info.params; + // @ts-ignore + const rcrainfoProfile = createMockRcrainfoProfileResponse({ user: username ?? '' }); + return HttpResponse.json( + { + ...rcrainfoProfile, + }, + { status: 200 } + ); + }), +]; diff --git a/server/apps/core/tests/test_rcra_profile_views.py b/server/apps/core/tests/test_rcrainfo_profile_views.py similarity index 100% rename from server/apps/core/tests/test_rcra_profile_views.py rename to server/apps/core/tests/test_rcrainfo_profile_views.py