diff --git a/Makefile b/Makefile index 3cb08b67..0db44cb9 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ test_unit: install terraform -chdir=terraform/envs/qa init -reconfigure -backend=false -upgrade terraform -chdir=terraform/envs/qa fmt -check terraform -chdir=terraform/envs/qa validate - terraform -chdir=terraform/envs/prod init -reconfigure -backend=false + terraform -chdir=terraform/envs/prod init -reconfigure -backend=false -upgrade terraform -chdir=terraform/envs/prod fmt -check terraform -chdir=terraform/envs/prod validate yarn prettier @@ -96,3 +96,7 @@ prod_health_check: lock_terraform: terraform -chdir=terraform/envs/qa providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 -platform=linux_arm64 terraform -chdir=terraform/envs/prod providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=darwin_arm64 -platform=linux_amd64 -platform=linux_arm64 + +upgrade_terraform: + terraform -chdir=terraform/envs/qa init -reconfigure -backend=false -upgrade + terraform -chdir=terraform/envs/prod init -reconfigure -backend=false -upgrade diff --git a/src/ui/components/AuthGuard/index.tsx b/src/ui/components/AuthGuard/index.tsx index 412bc154..8904f521 100644 --- a/src/ui/components/AuthGuard/index.tsx +++ b/src/ui/components/AuthGuard/index.tsx @@ -5,10 +5,10 @@ import { AcmAppShell, AcmAppShellProps } from "@ui/components/AppShell"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { getRunEnvironmentConfig, ValidService } from "@ui/config"; import { useApi } from "@ui/util/api"; -import { AppRoles } from "@common/roles"; +import { AppRoles, OrgRoleDefinition } from "@common/roles"; export const CACHE_KEY_PREFIX = "auth_response_cache_"; -const CACHE_DURATION = 2 * 60 * 60 * 1000; // 2 hours in milliseconds +const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds type CacheData = { data: any; // Just the JSON response data @@ -87,7 +87,6 @@ export const clearAuthCache = () => { /** * Retrieves the user's roles from the session cache for a specific service. * @param service The service to check the cache for. - * @param route The authentication check route. * @returns A promise that resolves to an array of roles, or null if not found in cache. */ export const getUserRoles = async ( @@ -105,6 +104,25 @@ export const getUserRoles = async ( return null; }; +/** + * Retrieves the user's org roles from the session cache for Core API. + * @returns A promise that resolves to an array of roles, or null if not found in cache. + */ +export const getCoreOrgRoles = async (): Promise< + OrgRoleDefinition[] | null +> => { + const { authCheckRoute } = + getRunEnvironmentConfig().ServiceConfiguration.core; + if (!authCheckRoute) { + throw new Error("no auth check route"); + } + const cachedData = await getCachedResponse("core", authCheckRoute); + if (cachedData?.data?.orgRoles && Array.isArray(cachedData.data.orgRoles)) { + return cachedData.data.orgRoles; + } + return null; +}; + export const AuthGuard: React.FC< { resourceDef: ResourceDefinition; diff --git a/src/ui/pages/organization/OrgInfo.page.tsx b/src/ui/pages/organization/OrgInfo.page.tsx index 8b0a15c7..7f295d0f 100644 --- a/src/ui/pages/organization/OrgInfo.page.tsx +++ b/src/ui/pages/organization/OrgInfo.page.tsx @@ -1,13 +1,16 @@ import { useState, useEffect } from "react"; import { Title, Stack, Container, Select } from "@mantine/core"; -import { AuthGuard, getUserRoles } from "@ui/components/AuthGuard"; +import { + AuthGuard, + getUserRoles, + getCoreOrgRoles, +} from "@ui/components/AuthGuard"; import { useApi } from "@ui/util/api"; import { AppRoles } from "@common/roles"; import { notifications } from "@mantine/notifications"; import { IconAlertCircle } from "@tabler/icons-react"; import FullScreenLoader from "@ui/components/AuthContext/LoadingScreen"; import { AllOrganizationNameList, OrganizationName } from "@acm-uiuc/js-shared"; -import { useAuth } from "@ui/components/AuthContext"; import { ManageOrganizationForm } from "./ManageOrganizationForm"; import { LeadEntry, @@ -21,7 +24,6 @@ type OrganizationData = z.infer; export const OrgInfoPage = () => { const api = useApi("core"); - const { orgRoles } = useAuth(); const [searchParams, setSearchParams] = useSearchParams(); const [manageableOrgs, setManagableOrgs] = useState< OrganizationName[] | null @@ -112,7 +114,11 @@ export const OrgInfoPage = () => { useEffect(() => { (async () => { const appRoles = await getUserRoles("core"); - if (appRoles?.includes(AppRoles.ALL_ORG_MANAGER)) { + const orgRoles = await getCoreOrgRoles(); + if (appRoles === null || orgRoles === null) { + return; + } + if (appRoles.includes(AppRoles.ALL_ORG_MANAGER)) { setManagableOrgs(AllOrganizationNameList); return; } @@ -120,7 +126,7 @@ export const OrgInfoPage = () => { orgRoles.filter((x) => x.role === "LEAD").map((x) => x.org), ); })(); - }, [orgRoles]); + }, []); // Update URL when selected org changes const handleOrgChange = (org: OrganizationName | null) => { diff --git a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx index 01688ddb..83d0dbe2 100644 --- a/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx +++ b/src/ui/pages/roomRequest/RoomRequestLanding.page.tsx @@ -14,13 +14,21 @@ import { type RoomRequestStatus, } from "@common/types/roomRequest"; import { OrganizationName } from "@acm-uiuc/js-shared"; +import { useSearchParams } from "react-router-dom"; export const ManageRoomRequestsPage: React.FC = () => { const api = useApi("core"); - const [semester, setSemester] = useState(null); // TODO: Create a selector for this + const [semester, setSemesterState] = useState(null); const [isLoading, setIsLoading] = useState(false); const nextSemesters = getSemesters(); const semesterOptions = [...getPreviousSemesters(), ...nextSemesters]; + const [searchParams, setSearchParams] = useSearchParams(); + const setSemester = (semester: string | null) => { + setSemesterState(semester); + if (semester) { + setSearchParams({ semester }); + } + }; const createRoomRequest = async ( payload: RoomRequestFormValues, ): Promise => { @@ -45,8 +53,16 @@ export const ManageRoomRequestsPage: React.FC = () => { }; useEffect(() => { - setSemester(nextSemesters[0].value); - }, []); + const semeseterFromUrl = searchParams.get("semester") as string | null; + if ( + semeseterFromUrl && + semesterOptions.map((x) => x.value).includes(semeseterFromUrl) + ) { + setSemester(semeseterFromUrl); + } else { + setSemester(nextSemesters[0].value); + } + }, [searchParams, semesterOptions, nextSemesters]); return ( {} + export const getSecretValue = async ( secretId: string, ): Promise | null> => { @@ -71,12 +74,12 @@ export async function getUpcomingEvents() { const data = await fetch( "https://core.aws.qa.acmuiuc.org/api/v1/events?upcomingOnly=true", ); - return (await data.json()) as Record[]; + return (await data.json()) as RecursiveRecord[]; } export async function getAllEvents() { const data = await fetch("https://core.aws.qa.acmuiuc.org/api/v1/events"); - return (await data.json()) as Record[]; + return (await data.json()) as RecursiveRecord[]; } export const test = base.extend<{ becomeUser: (page: Page) => Promise }>({ diff --git a/tests/e2e/orgInfo.spec.ts b/tests/e2e/orgInfo.spec.ts new file mode 100644 index 00000000..55114a15 --- /dev/null +++ b/tests/e2e/orgInfo.spec.ts @@ -0,0 +1,72 @@ +import { expect } from "@playwright/test"; +import { RecursiveRecord, test } from "./base.js"; +import { describe } from "node:test"; + +describe("Organization Info Tests", () => { + test("A user can update org metadata", async ({ page, becomeUser }) => { + const date = new Date().toISOString(); + await becomeUser(page); + await expect( + page.locator("a").filter({ hasText: "Management Portal DEV ENV" }), + ).toBeVisible(); + await expect( + page.locator("a").filter({ hasText: "Organization Info" }), + ).toBeVisible(); + await page.locator("a").filter({ hasText: "Organization Info" }).click(); + await expect(page.getByRole("heading")).toContainText( + "Manage Organization Info", + ); + await page.getByRole("textbox", { name: "Select an organization" }).click(); + await page.getByText("Infrastructure Committee").click(); + await page.getByRole("textbox", { name: "Description" }).click(); + await page + .getByRole("textbox", { name: "Description" }) + .fill(`Populated by E2E tests on ${date}`); + await page + .getByRole("textbox", { name: "Website" }) + .fill(`https://infra.acm.illinois.edu?date=${date}`); + + const existingOtherLink = page.locator("text=Other").first(); + const hasExistingOther = await existingOtherLink + .isVisible() + .catch(() => false); + + if (!hasExistingOther) { + await page.getByRole("button", { name: "Add Link" }).click(); + await page.getByRole("textbox", { name: "Type" }).click(); + await page.getByRole("option", { name: "Other" }).click(); + } + + await page.getByRole("textbox", { name: "URL" }).click(); + await page + .getByRole("textbox", { name: "URL" }) + .fill(`https://infra.acm.illinois.edu/e2e?date=${date}`); + await page + .locator("form") + .getByRole("button", { name: "Save Changes" }) + .click(); + await expect( + page.getByText("Infrastructure Committee updated"), + ).toBeVisible(); + + const data = await fetch( + `https://core.aws.qa.acmuiuc.org/api/v1/organizations?date=${date}`, + ); + const json = (await data.json()) as RecursiveRecord[]; + const infraEntry = json.find((x) => x.id === "Infrastructure Committee"); + + expect(infraEntry).toBeDefined(); + expect(infraEntry?.description).toBe(`Populated by E2E tests on ${date}`); + expect(infraEntry?.website).toBe( + `https://infra.acm.illinois.edu?date=${date}`, + ); + + const links = infraEntry?.links as RecursiveRecord[]; + expect(links).toBeDefined(); + const otherLink = links.find((link) => link.type === "OTHER"); + expect(otherLink).toBeDefined(); + expect(otherLink?.url).toBe( + `https://infra.acm.illinois.edu/e2e?date=${date}`, + ); + }); +});