From 0f76baf8b1b9f5c97484e6b8e3121bd502dd5373 Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Wed, 25 Sep 2024 20:29:14 -0400 Subject: [PATCH 1/9] fix: use `custom:cms-roles` to determine whether user can submit --- react-app/src/features/dashboard/index.tsx | 37 ++++++++++------------ 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/react-app/src/features/dashboard/index.tsx b/react-app/src/features/dashboard/index.tsx index aa5ff391e..1e8949e85 100644 --- a/react-app/src/features/dashboard/index.tsx +++ b/react-app/src/features/dashboard/index.tsx @@ -1,7 +1,6 @@ -import { useMemo } from "react"; import { QueryClient } from "@tanstack/react-query"; import { Plus as PlusIcon } from "lucide-react"; -import { getUser } from "@/api"; +import { getUser, useGetUser } from "@/api"; import { WaiversList } from "./Lists/waivers"; import { SpasList } from "./Lists/spas"; import { @@ -9,7 +8,6 @@ import { type OsTab, useOsData, FilterDrawerProvider, - useUserContext, Tabs, TabsContent, TabsList, @@ -43,18 +41,17 @@ const loader = (queryClient: QueryClient) => { export const dashboardLoader = loader; export const Dashboard = () => { - const userContext = useUserContext(); + const { data: userObj } = useGetUser(); const osData = useOsData(); useScrollToTop(); - const role = useMemo(() => { - return userContext?.user?.["custom:cms-roles"] ? true : false; - }, []); - - if (!role) { + if (userObj === undefined) { return ; } + const canUserSubmit = + userObj.user["custom:cms-roles"] === "onemac-micro-statesubmitter"; + return ( { >
- {/* Header */} -
+

Dashboard

- {!userContext?.isCms && ( + {canUserSubmit && ( { )}
- {/* Tabs */}
{ } >
- - -

SPAs

-
- -

Waivers

-
-
+ + +

SPAs

+
+ +

Waivers

+
+
From 79413ada6c551853aecfd3fe14a55fe509a99c8c Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Thu, 26 Sep 2024 16:21:02 -0400 Subject: [PATCH 2/9] chore: move county logic in `useGetCounties` and delete user context --- react-app/src/api/useGetCounties.ts | 65 ++++++++++++------- react-app/src/components/Context/index.ts | 1 - .../src/components/Context/userContext.tsx | 62 ------------------ react-app/src/components/Layout/index.tsx | 14 ++-- react-app/src/components/RHF/SlotField.tsx | 12 ++-- react-app/src/components/index.tsx | 1 - react-app/src/main.tsx | 14 ++-- 7 files changed, 56 insertions(+), 113 deletions(-) delete mode 100644 react-app/src/components/Context/index.ts delete mode 100644 react-app/src/components/Context/userContext.tsx diff --git a/react-app/src/api/useGetCounties.ts b/react-app/src/api/useGetCounties.ts index b4b9c6124..b8736005a 100644 --- a/react-app/src/api/useGetCounties.ts +++ b/react-app/src/api/useGetCounties.ts @@ -1,29 +1,18 @@ import { useQuery } from "@tanstack/react-query"; +import { useGetUser } from "./useGetUser"; +import { getUserStateCodes } from "@/utils"; +import { FULL_CENSUS_STATES } from "shared-types"; +import { useMemo } from "react"; -const fetchPopulationData = async (stateString: string) => { - try { - if (stateString) { - const response = await fetch( - `https://api.census.gov/data/2019/pep/population?get=NAME&for=county:*&in=state:${stateString}`, - ); - if (!response.ok) { - throw new Error("Failed to fetch county data"); - } - - const data = await response.json(); - return data.slice(1).map((item) => item[0]); - } - return []; - } catch (error) { - console.error("Error fetching county data:", error); - throw error; - } -}; - -export const usePopulationData = (stateString: string) => { +const usePopulationData = (stateString: string) => { return useQuery( ["populationData", stateString], - () => fetchPopulationData(stateString), + () => + fetch( + `https://api.census.gov/data/2019/pep/population?get=NAME&for=county:*&in=state:${stateString}`, + ) + .then((response) => response.json()) + .then((population) => population.slice(1).map((item) => item[0])), { refetchOnWindowFocus: false, refetchOnReconnect: false, @@ -32,4 +21,34 @@ export const usePopulationData = (stateString: string) => { ); }; -export default usePopulationData; +export const useGetCounties = (): { label: string; value: string }[] => { + const { data: userData } = useGetUser(); + + const stateCodes = useMemo( + () => getUserStateCodes(userData?.user), + [userData], + ); + + const stateNumericCodesString = useMemo( + () => + stateCodes + .map( + (code) => + FULL_CENSUS_STATES.find((state) => state.value === code)?.code, + ) + .filter((code): code is string => code !== undefined && code !== "00") + .join(","), + [stateCodes], + ); + + const { data: populationData = [] } = usePopulationData( + stateNumericCodesString, + ); + + return ( + populationData.map((county) => { + const [label] = county.split(","); + return { label, value: county }; + }) ?? [] + ); +}; diff --git a/react-app/src/components/Context/index.ts b/react-app/src/components/Context/index.ts deleted file mode 100644 index bba969cc9..000000000 --- a/react-app/src/components/Context/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./userContext"; diff --git a/react-app/src/components/Context/userContext.tsx b/react-app/src/components/Context/userContext.tsx deleted file mode 100644 index 396f589ae..000000000 --- a/react-app/src/components/Context/userContext.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { OneMacUser, useGetUser } from "@/api"; -import { PropsWithChildren, createContext, useContext, useMemo } from "react"; -import { getUserStateCodes } from "@/utils"; -import { usePopulationData } from "@/api"; -import { FULL_CENSUS_STATES } from "shared-types"; - -const initialState = { user: null, counties: [] }; - -export const UserContext = createContext(initialState); - -export const UserContextProvider = ({ children }: PropsWithChildren) => { - const { data: userData, error: userError } = useGetUser(); - - const stateCodes = useMemo( - () => getUserStateCodes(userData?.user), - [userData], - ); - - const stateNumericCodesString = useMemo( - () => - stateCodes - .map( - (code) => - FULL_CENSUS_STATES.find((state) => state.value === code)?.code, - ) - .filter((code): code is string => code !== undefined && code !== "00") - .join(","), - [stateCodes], - ); - - const { data: populationData, error: populationError } = usePopulationData( - stateNumericCodesString, - ); - - const counties = useMemo( - () => - populationData?.map((county) => { - const [label] = county.split(","); - return { label, value: county }; - }) ?? [], - [populationData], - ); - - const contextValue = useMemo( - () => ({ - user: userData?.user ?? null, - counties, - }), - [userData, counties], - ); - - if (userError || populationError) { - console.error("Error fetching data:", userError || populationError); - return null; - } - - return ( - {children} - ); -}; - -export const useUserContext = () => useContext(UserContext); diff --git a/react-app/src/components/Layout/index.tsx b/react-app/src/components/Layout/index.tsx index 88bc66fd3..c987ec5e9 100644 --- a/react-app/src/components/Layout/index.tsx +++ b/react-app/src/components/Layout/index.tsx @@ -2,13 +2,12 @@ import { NavLink, NavLinkProps, Outlet, Link } from "react-router-dom"; import oneMacLogo from "@/assets/onemac_logo.svg"; import { useMediaQuery } from "@/hooks"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; -import { useMemo, useState } from "react"; +import { useState } from "react"; import { useGetUser } from "@/api"; import { Auth } from "aws-amplify"; import { AwsCognitoOAuthOpts } from "@aws-amplify/auth/lib-esm/types"; import { Footer } from "../Footer"; import { UsaBanner } from "../UsaBanner"; -import { useUserContext } from "../Context"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import config from "@/config"; import { useNavigate } from "../Routing"; @@ -16,12 +15,7 @@ import { SimplePageContainer, UserPrompt, Banner } from "@/components"; import { isFaqPage, isProd } from "@/utils"; const useGetLinks = () => { - const { isLoading, data } = useGetUser(); - const userContext = useUserContext(); - - const role = useMemo(() => { - return userContext?.user?.["custom:cms-roles"] ? true : false; - }, []); + const { isLoading, data: userObj } = useGetUser(); const links = isLoading || isFaqPage @@ -35,7 +29,7 @@ const useGetLinks = () => { { name: "Dashboard", link: "/dashboard", - condition: !!data?.user && role, + condition: userObj.user && userObj.user["custom:cms-roles"], }, { name: "FAQ", @@ -45,7 +39,7 @@ const useGetLinks = () => { { name: "Webforms", link: "/webforms", - condition: !!data?.user && !isProd, + condition: userObj.user && !isProd, }, ].filter((l) => l.condition); diff --git a/react-app/src/components/RHF/SlotField.tsx b/react-app/src/components/RHF/SlotField.tsx index 114f18fb7..9330f8015 100644 --- a/react-app/src/components/RHF/SlotField.tsx +++ b/react-app/src/components/RHF/SlotField.tsx @@ -7,12 +7,7 @@ import { import { CalendarIcon } from "lucide-react"; import { format } from "date-fns"; import { cn } from "@/utils"; -import { - Popover, - PopoverContent, - PopoverTrigger, - useUserContext, -} from "@/components"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components"; import { DependencyWrapper, RHFFieldArray, @@ -43,6 +38,7 @@ import { Textarea, Upload, } from "@/components/Inputs"; +import { useGetCounties } from "@/api"; type SlotFieldProps = RHFSlotProps & { control: any; field: any }; type SelectedSubsetProps = RHFOption & { @@ -62,7 +58,7 @@ export const SlotField = ({ horizontalLayout, index, }: SlotFieldProps) => { - const userContext = useUserContext(); + const counties = useGetCounties(); switch (rhf) { case "Input": @@ -109,7 +105,7 @@ export const SlotField = ({ case "countySelect": opts = - userContext?.counties?.sort((a, b) => + counties.sort((a, b) => props.customSort ? sortFunctions[props.customSort](a.label, b.label) : stringCompare(a, b), diff --git a/react-app/src/components/index.tsx b/react-app/src/components/index.tsx index 00f4e87fc..0e657af66 100644 --- a/react-app/src/components/index.tsx +++ b/react-app/src/components/index.tsx @@ -3,7 +3,6 @@ export * from "./BreadCrumb"; export * from "./Cards"; export * from "./Chip"; export * from "./Container"; -export * from "./Context"; export * from "./DetailsSection"; export * from "./Dialog"; export * from "./ErrorAlert"; diff --git a/react-app/src/main.tsx b/react-app/src/main.tsx index 4e37464f1..8cb6356fa 100644 --- a/react-app/src/main.tsx +++ b/react-app/src/main.tsx @@ -2,11 +2,11 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { RouterProvider } from "react-router-dom"; import "@fontsource/open-sans"; -import "./index.css"; // this one second +import "./index.css"; import { queryClient, router } from "./router"; import { QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; -import { UserContextProvider, TimeoutModal } from "@/components"; +import { TimeoutModal } from "@/components"; import config from "@/config"; import { asyncWithLDProvider } from "launchdarkly-react-client-sdk"; @@ -31,12 +31,10 @@ const initializeLaunchDarkly = async () => { ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - - + + + + , From 206f85eac2efab24d5e6d7166ab1a94e55ba132d Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Thu, 26 Sep 2024 16:40:38 -0400 Subject: [PATCH 3/9] chore: mock `useGetCounties` in tests --- .../src/components/RHF/tests/FieldArray.test.tsx | 12 +++++++++++- react-app/src/components/RHF/tests/NameGen.test.tsx | 12 +++++++++++- react-app/src/components/RHF/tests/Section.test.tsx | 12 +++++++++++- react-app/src/components/RHF/tests/Slot.test.tsx | 12 +++++++++++- .../src/components/RHF/tests/SlotField.test.tsx | 12 +++++++++++- 5 files changed, 55 insertions(+), 5 deletions(-) diff --git a/react-app/src/components/RHF/tests/FieldArray.test.tsx b/react-app/src/components/RHF/tests/FieldArray.test.tsx index e0e59507f..c6ebbd85c 100644 --- a/react-app/src/components/RHF/tests/FieldArray.test.tsx +++ b/react-app/src/components/RHF/tests/FieldArray.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; import { fireEvent, render } from "@testing-library/react"; import { RHFSlot } from ".."; import { Form, FormField } from "../../Inputs"; @@ -121,6 +121,16 @@ const testWrapperDependency: RHFSlotProps = { ], }; +vi.mock("@/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useGetCounties: vi.fn(() => { + return { data: [], isLoading: false, error: null }; + }), + }; +}); + describe("Field Tests", () => { test("renders FieldArray", () => { const rend = render(); diff --git a/react-app/src/components/RHF/tests/NameGen.test.tsx b/react-app/src/components/RHF/tests/NameGen.test.tsx index 970c53d28..a05121701 100644 --- a/react-app/src/components/RHF/tests/NameGen.test.tsx +++ b/react-app/src/components/RHF/tests/NameGen.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; import { render } from "@testing-library/react"; import { RHFDocument } from "../."; import { Form } from "../../Inputs"; @@ -69,6 +69,16 @@ const TestWrapper = (props: { onSubmit: (d: any) => void }) => { ); }; +vi.mock("@/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useGetCounties: vi.fn(() => { + return { data: [], isLoading: false, error: null }; + }), + }; +}); + describe("Test Name Generation", () => { test("Generate Structure Correctly", () => { const rend = render( diff --git a/react-app/src/components/RHF/tests/Section.test.tsx b/react-app/src/components/RHF/tests/Section.test.tsx index 400263a79..1cee48ecb 100644 --- a/react-app/src/components/RHF/tests/Section.test.tsx +++ b/react-app/src/components/RHF/tests/Section.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; import { render } from "@testing-library/react"; import { RHFDocument, documentInitializer } from ".."; import { Form } from "../../Inputs"; @@ -49,6 +49,16 @@ const testDocData: FormSchema = { ], }; +vi.mock("@/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useGetCounties: vi.fn(() => { + return { data: [], isLoading: false, error: null }; + }), + }; +}); + describe("Section Tests", () => { test("renders, subsections distinct", () => { const rend = render(); diff --git a/react-app/src/components/RHF/tests/Slot.test.tsx b/react-app/src/components/RHF/tests/Slot.test.tsx index 8e370bf1d..3401503a1 100644 --- a/react-app/src/components/RHF/tests/Slot.test.tsx +++ b/react-app/src/components/RHF/tests/Slot.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; import { render } from "@testing-library/react"; import { RHFSlot } from "../."; import { Form, FormField } from "../../Inputs"; @@ -35,6 +35,16 @@ const testValues: RHFSlotProps = { formItemClassName: "py-4", }; +vi.mock("@/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useGetCounties: vi.fn(() => { + return { data: [], isLoading: false, error: null }; + }), + }; +}); + describe("RHFSlot tests", () => { test("render label, desc, and comp", () => { const rend = render(); diff --git a/react-app/src/components/RHF/tests/SlotField.test.tsx b/react-app/src/components/RHF/tests/SlotField.test.tsx index 364de9502..e4c847b6d 100644 --- a/react-app/src/components/RHF/tests/SlotField.test.tsx +++ b/react-app/src/components/RHF/tests/SlotField.test.tsx @@ -1,4 +1,4 @@ -import { describe, test, expect } from "vitest"; +import { describe, test, expect, vi } from "vitest"; import { render, fireEvent } from "@testing-library/react"; import { RHFSlot } from "../."; import { Form, FormField } from "../../Inputs"; @@ -35,6 +35,16 @@ const testValues: RHFSlotProps = { formItemClassName: "py-4", }; +vi.mock("@/api", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...(actual as object), + useGetCounties: vi.fn(() => { + return { data: [], isLoading: false, error: null }; + }), + }; +}); + describe("Slot Input Field Tests", () => { test("renders Input", () => { const rend = render(); From dddce0e3e1560197bcf20e3e9c6e5da1af3a6690 Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 10:13:23 -0400 Subject: [PATCH 4/9] fix: prevent non-state users from accessing submission menus --- react-app/src/features/dashboard/index.tsx | 6 ++---- react-app/src/features/selection-flow/plan-types.tsx | 10 +++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/react-app/src/features/dashboard/index.tsx b/react-app/src/features/dashboard/index.tsx index 1e8949e85..76b692848 100644 --- a/react-app/src/features/dashboard/index.tsx +++ b/react-app/src/features/dashboard/index.tsx @@ -17,6 +17,7 @@ import { redirect, } from "@/components"; import { useScrollToTop } from "@/hooks"; +import { isStateUser } from "shared-utils"; const loader = (queryClient: QueryClient) => { return async () => { @@ -49,9 +50,6 @@ export const Dashboard = () => { return ; } - const canUserSubmit = - userObj.user["custom:cms-roles"] === "onemac-micro-statesubmitter"; - return ( {

Dashboard

- {canUserSubmit && ( + {isStateUser(userObj.user) && ( ( @@ -34,6 +36,12 @@ type OptionsPageProps = { /** A page for rendering an array of {@link OptionData} */ const OptionsPage = ({ options, title, fieldsetLegend }: OptionsPageProps) => { const location = useLocation(); + const { data: userObj } = useGetUser(); + + if (userObj && isStateUser(userObj.user) === false) { + return ; + } + return ( From 07a8152b94430508e7a0c44b25ffa7c40aa86cbe Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 10:18:46 -0400 Subject: [PATCH 5/9] chore: delete useless test --- react-app/src/api/index.test.ts | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 react-app/src/api/index.test.ts diff --git a/react-app/src/api/index.test.ts b/react-app/src/api/index.test.ts deleted file mode 100644 index 46b1cc472..000000000 --- a/react-app/src/api/index.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { it, describe, expect } from "vitest"; -describe("checkEnvVars", () => { - it("is the greatest test ever written", () => { - expect(1 + 1).toEqual(2); - }); -}); From 29b2054ab17417ac7b858308e17e79157e49c5f5 Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 10:39:39 -0400 Subject: [PATCH 6/9] fix: add prop to control user access of forms --- .../components/ActionForm/ActionForm.test.tsx | 75 ++++++++++++------- react-app/src/components/ActionForm/index.tsx | 24 +++++- react-app/testing/setup.ts | 10 ++- 3 files changed, 78 insertions(+), 31 deletions(-) diff --git a/react-app/src/components/ActionForm/ActionForm.test.tsx b/react-app/src/components/ActionForm/ActionForm.test.tsx index cfa9fb316..ece0df2c8 100644 --- a/react-app/src/components/ActionForm/ActionForm.test.tsx +++ b/react-app/src/components/ActionForm/ActionForm.test.tsx @@ -1,6 +1,4 @@ -import { ReactElement } from "react"; -import { MemoryRouter } from "react-router-dom"; -import { render, waitFor } from "@testing-library/react"; +import { waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, test, expect, vi } from "vitest"; import { ActionForm } from "./index"; @@ -9,6 +7,8 @@ import { attachmentArraySchemaOptional } from "shared-types"; import * as userPrompt from "@/components/ConfirmationDialog/userPrompt"; import * as banner from "@/components/Banner/banner"; import * as documentPoller from "@/utils/Poller/documentPoller"; +import { renderForm } from "@/utils/test-helpers/renderForm"; +import { isCmsReadonlyUser } from "shared-utils"; vi.mock("aws-amplify", async (importOriginal) => { const actual = await importOriginal(); @@ -28,17 +28,12 @@ vi.mock("../../utils/Poller/DataPoller", () => { }; }); -const renderWithMemoryRouter = (ActionFormArg: ReactElement) => - render(ActionFormArg, { - wrapper: ({ children }) => {children}, - }); - const PROGRESS_REMINDER = /If you leave this page, you will lose your progress on this form./; describe("ActionForm", () => { test("renders `title`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders `attachments.faqLink`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { ); }); + test("doesn't render form if user access is denied", () => { + const { queryByText } = renderForm( + null} + documentPollerArgs={{ + property: () => "id", + documentChecker: () => true, + }} + attachments={{ faqLink: "hello-world-link" }} + tab={"waivers"} + conditionsDeterminingUserAccess={[isCmsReadonlyUser]} + />, + ); + + expect(queryByText("Action Form Title")).not.toBeInTheDocument(); + }); + test("renders `defaultValues` in appropriate input", () => { - const { queryByDisplayValue } = renderWithMemoryRouter( + const { queryByDisplayValue } = renderForm( { }); test("renders `attachments.specialInstructions`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders custom `promptOnLeavingForm` when clicking Cancel", async () => { - const { container } = renderWithMemoryRouter( + const { container } = renderForm( { }); test("renders custom `promptPreSubmission` when clicking Submit", async () => { - const { container } = renderWithMemoryRouter( + const { container } = renderForm( { test("calls `documentPoller` with `documentPollerArgs`", async () => { const documentCheckerFunc = vi.fn(); - const { container } = renderWithMemoryRouter( + const { container } = renderForm( { test("calls `banner` with `bannerPostSubmission`", async () => { const documentCheckerFunc = vi.fn(); - const { container } = renderWithMemoryRouter( + const { container } = renderForm( { }); test("renders all attachment properties within `attachments`", async () => { - const { queryAllByText } = renderWithMemoryRouter( + const { queryAllByText } = renderForm( { }); test("renders Additional Information if `additionalInformation` is defined in schema", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("doesn't render Additional Information if `additionalInformation` is undefined in schema", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders Attachments if `attachments` is defined in schema", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("doesn't render Attachments if `attachments` is undefined in schema", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders ProgressReminder if schema has `attachments` property", () => { - const { queryAllByText } = renderWithMemoryRouter( + const { queryAllByText } = renderForm( { }); test("renders ProgressReminder if `fields` property is defined", () => { - const { queryAllByText } = renderWithMemoryRouter( + const { queryAllByText } = renderForm( { }); test("doesn't render ProgressReminder `fields` is undefined and `attachments` isn't defined in schema", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders default wrapper if `fieldsLayout` is undefined", () => { - const { queryAllByText } = renderWithMemoryRouter( + const { queryAllByText } = renderForm( { }); test("renders `fieldsLayout`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { }); test("renders `fieldsLayout` with correct `title`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( = z.ZodObject< Shape & { @@ -81,6 +88,9 @@ type ActionFormProps = { | ((values: z.TypeOf) => string); documentChecker: CheckDocumentFunction; }; + conditionsDeterminingUserAccess?: (( + user: CognitoUserAttributes | null, + ) => boolean)[]; tab: "spas" | "waivers"; }; @@ -106,10 +116,12 @@ export const ActionForm = ({ documentPollerArgs, attachments, tab, + conditionsDeterminingUserAccess = [isStateUser, isCmsSuperUser], }: ActionFormProps) => { const { id, authority } = useParams<{ id: string; authority: Authority }>(); const location = useLocation(); const navigate = useNavigate(); + const { data: userObj } = useGetUser(); const form = useForm>({ resolver: zodResolver(schema), @@ -164,6 +176,14 @@ export const ActionForm = ({ [attachmentsFromSchema, Fields, form], ); + const doesUserHaveAccessToForm = conditionsDeterminingUserAccess.some( + (condition) => condition(userObj.user), + ); + + if (doesUserHaveAccessToForm === false) { + return ; + } + return ( diff --git a/react-app/testing/setup.ts b/react-app/testing/setup.ts index af2d9db3b..ee21a1827 100644 --- a/react-app/testing/setup.ts +++ b/react-app/testing/setup.ts @@ -66,6 +66,14 @@ beforeAll(() => { return idsThatExist.includes(id); }), + useGetUser: () => ({ + data: { + user: { + "custom:cms-roles": + "onemac-micro-statesubmitter,onemac-micro-super", + }, + }, + }), })); vi.mock("@/utils/user", () => ({ isAuthorizedState: vi.fn(async (id: string) => { @@ -74,8 +82,6 @@ beforeAll(() => { return validStates.includes(id.substring(0, 2)); }), })); - // mock the api calls that the frontend uses here - // vi.mock("@/api/stuff") } }); From 900a94c5a2ad3901c753be82e81b4377d191f672 Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 10:48:08 -0400 Subject: [PATCH 7/9] chore: remove super user from default user permissions --- react-app/src/components/ActionForm/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/react-app/src/components/ActionForm/index.tsx b/react-app/src/components/ActionForm/index.tsx index c001573d1..bfa15f849 100644 --- a/react-app/src/components/ActionForm/index.tsx +++ b/react-app/src/components/ActionForm/index.tsx @@ -43,7 +43,7 @@ import { getAttachments, getAdditionalInformation, } from "./actionForm.utilities"; -import { isCmsSuperUser, isStateUser } from "shared-utils"; +import { isStateUser } from "shared-utils"; import { useGetUser } from "@/api"; type EnforceSchemaProps = z.ZodObject< @@ -116,7 +116,7 @@ export const ActionForm = ({ documentPollerArgs, attachments, tab, - conditionsDeterminingUserAccess = [isStateUser, isCmsSuperUser], + conditionsDeterminingUserAccess = [isStateUser], }: ActionFormProps) => { const { id, authority } = useParams<{ id: string; authority: Authority }>(); const location = useLocation(); From 1edc51f9f5b4f5c495a01b263f8235281f085e6b Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 11:38:10 -0400 Subject: [PATCH 8/9] fix: change from `/dashboard` to `/` --- react-app/src/components/ActionForm/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/react-app/src/components/ActionForm/index.tsx b/react-app/src/components/ActionForm/index.tsx index bfa15f849..6c97d9261 100644 --- a/react-app/src/components/ActionForm/index.tsx +++ b/react-app/src/components/ActionForm/index.tsx @@ -181,7 +181,7 @@ export const ActionForm = ({ ); if (doesUserHaveAccessToForm === false) { - return ; + return ; } return ( From c8afd46b4b0b31b5c42ed3ed585a8b1fb667afc0 Mon Sep 17 00:00:00 2001 From: Asharon Baltazar Date: Fri, 27 Sep 2024 14:17:28 -0400 Subject: [PATCH 9/9] chore: fix tests --- react-app/src/components/ActionForm/ActionForm.test.tsx | 4 ++-- react-app/testing/setup.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/react-app/src/components/ActionForm/ActionForm.test.tsx b/react-app/src/components/ActionForm/ActionForm.test.tsx index 69534e754..606f6b152 100644 --- a/react-app/src/components/ActionForm/ActionForm.test.tsx +++ b/react-app/src/components/ActionForm/ActionForm.test.tsx @@ -33,7 +33,7 @@ const PROGRESS_REMINDER = describe("ActionForm", () => { test("renders `breadcrumbText`", () => { - const { queryByText } = renderWithMemoryRouter( + const { queryByText } = renderForm( { documentChecker: () => true, }} attachments={{ faqLink: "hello-world-link" }} - tab={"waivers"} conditionsDeterminingUserAccess={[isCmsReadonlyUser]} + breadcrumbText="Example Breadcrumb" />, ); diff --git a/react-app/testing/setup.ts b/react-app/testing/setup.ts index ee21a1827..1992144bb 100644 --- a/react-app/testing/setup.ts +++ b/react-app/testing/setup.ts @@ -1,6 +1,6 @@ import { expect, afterEach, beforeAll, afterAll, vi } from "vitest"; import { cleanup } from "@testing-library/react"; -import matchers from "@testing-library/jest-dom/matchers"; +import * as matchers from "@testing-library/jest-dom/matchers"; global.ResizeObserver = vi.fn().mockImplementation(() => ({ observe: vi.fn(),