From b018caa40e045e3f989d7fec88ec76fcbf0306a6 Mon Sep 17 00:00:00 2001 From: gitcarrot Date: Fri, 20 Dec 2024 10:58:10 -1000 Subject: [PATCH 1/8] Using OOTB in react project --- ui/.env.development | 3 + ui/ootb.active.user.profiles.json | 283 ++++++++++++++++++ .../components/layout/navbar/login-button.tsx | 38 +-- ui/src/lib/access/user.ts | 45 ++- ui/src/lib/actions-ootb.ts | 49 +++ ui/src/lib/types.ts | 23 ++ 6 files changed, 422 insertions(+), 19 deletions(-) create mode 100644 ui/.env.development create mode 100644 ui/ootb.active.user.profiles.json create mode 100644 ui/src/lib/actions-ootb.ts diff --git a/ui/.env.development b/ui/.env.development new file mode 100644 index 00000000..389a33f5 --- /dev/null +++ b/ui/.env.development @@ -0,0 +1,3 @@ +NEXT_PUBLIC_OOTB_MODE=false +# Profiles: user, owner, admin +NEXT_PUBLIC_OOTB_PROFILE=admin \ No newline at end of file diff --git a/ui/ootb.active.user.profiles.json b/ui/ootb.active.user.profiles.json new file mode 100644 index 00000000..037aa9dd --- /dev/null +++ b/ui/ootb.active.user.profiles.json @@ -0,0 +1,283 @@ +[ + { + "uid": "member0123", + "uhUuid": "11111111", + "authorities": ["ROLE_UH"], + "attributes": { + "cn": "MEMBER", + "mail": "member@hawaii.edu", + "givenName": "user" + }, + "groupings": [ + { + "name": "admin-include:exclude", + "displayName": "admin-include:exclude", + "extension": "exclude", + "displayExtension": "exclude", + "description": "Admin owned include group", + "members": [ + { + "name": "MemberUser", + "uid": "member0123", + "uhUuid": "11111111" + } + ] + }, + { + "name": "member-group:owners", + "displayName": "member-group:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "Member owned group", + "members": [] + }, + { + "name": "member-group:include", + "displayName": "member-group:include", + "extension": "include", + "displayExtension": "include", + "description": "Member owned group", + "members": [ + { + "name": "complex-member7", + "uhUuid": "19283746", + "uid": "cmember7" + }, + { + "name": "complex-member10", + "uhUuid": "01234567", + "uid": "cmember10" + }, + { + "name": "AdminUser", + "uid": "admin0123", + "uhUuid": "33333333" + } + ] + } + ] + }, + { + "uid": "owner0123", + "uhUuid": "22222222", + "authorities": ["ROLE_UH", "ROLE_OWNER"], + "attributes": { + "cn": "OWNER", + "mail": "owner@hawaii.edu", + "givenName": "owner" + }, + "groupings": [ + { + "name": "shared-group-in-each-profile:owners", + "displayName": "shared-group-in-each-profile:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "This is a shared group in each profile", + "members": [ + { + "name": "shared-owner-2", + "uhUuid": "29325231", + "uid": "sowner2" + } + ] + }, + { + "name": "admin-include:include", + "displayName": "admin-include:include", + "extension": "include", + "displayExtension": "include", + "description": "Admin owned include group", + "members": [ + { + "name": "OwnerUser", + "uid": "owner0123", + "uhUuid": "22222222" + }, + { + "name": "AdminUser", + "uid": "admin0123", + "uhUuid": "33333333" + } + ] + }, + { + "name": "owner-group:exclude", + "displayName": "owner-group:exclude", + "extension": "exclude", + "displayExtension": "exclude", + "description": "Owner owned group", + "members": [ + { + "name": "complex-member3", + "uhUuid": "56473829", + "uid": "cmember3" + }, + { + "name": "complex-member4", + "uhUuid": "45261378", + "uid": "cmember4" + }, + { + "name": "OwnerUser", + "uid": "owner0123", + "uhUuid": "22222222" + } + ] + } + ] + }, + { + "uid": "admin0123", + "uhUuid": "33333333", + "authorities": ["ROLE_ADMIN", "ROLE_UH", "ROLE_OWNER"], + "attributes": { + "cn": "ADMIN", + "mail": "admin@hawaii.edu", + "givenName": "admin" + }, + "groupings": [ + { + "name": "shared-group-in-each-profile:basis", + "displayName": "shared-group-in-each-profile:basis", + "extension": "basis", + "displayExtension": "basis", + "description": "This is a shared group in each profile", + "members": [ + { + "name": "shared-owner-3", + "uhUuid": "29382734", + "uid": "sowner3" + }, + { + "name": "MemberUser", + "uid": "member0123", + "uhUuid": "11111111" + } + ] + }, + { + "name": "shared-group-in-groupings:owners", + "displayName": "shared-group-in-groupings:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "This is a shared group in admin user groupings", + "members": [ + { + "name": "AdminUser", + "uid": "admin0123", + "uhUuid": "33333333" + }, + { + "name": "OwnerUser", + "uid": "owner0123", + "uhUuid": "22222222" + } + ] + }, + { + "name": "shared-group-in-each-profile:owners", + "displayName": "shared-group-in-each-profile:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "This is a shared group in each profile", + "members": [ + { + "name": "shared-owner-3", + "uhUuid": "29382734", + "uid": "sowner3" + } + ] + }, + { + "name": "admin-include:owners", + "displayName": "admin-include:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "Admin owned include group", + "members": [] + }, + { + "name": "admin-include:include", + "displayName": "admin-include:include", + "extension": "include", + "displayExtension": "include", + "description": "Admin owned include group", + "members": [ + { + "name": "AdminUser", + "uid": "admin0123", + "uhUuid": "33333333" + } + ] + }, + { + "name": "admin-group:owners", + "displayName": "admin-group:owners", + "extension": "owners", + "displayExtension": "owners", + "description": "Admin owned group", + "members": [] + }, + { + "name": "owner-complex:owners", + "displayName": "Owner-Complex: Owners", + "extension": "owners", + "displayExtension": "Owners", + "description": "Owner's owned complex group", + "members": [ + { + "name": "complex-member1", + "uhUuid": "32532314", + "uid": "cmember1" + }, + { + "name": "complex-member2", + "uhUuid": "87453218", + "uid": "cmember2" + }, + { + "name": "complex-member3", + "uhUuid": "56473829", + "uid": "cmember3" + }, + { + "name": "complex-member4", + "uhUuid": "45261378", + "uid": "cmember4" + }, + { + "name": "complex-member5", + "uhUuid": "98765432", + "uid": "cmember5" + }, + { + "name": "complex-member6", + "uhUuid": "12345678", + "uid": "cmember6" + }, + { + "name": "complex-member7", + "uhUuid": "19283746", + "uid": "cmember7" + }, + { + "name": "complex-member8", + "uhUuid": "72635489", + "uid": "cmember8" + }, + { + "name": "complex-member9", + "uhUuid": "65432109", + "uid": "cmember9" + }, + { + "name": "complex-member10", + "uhUuid": "01234567", + "uid": "cmember10" + } + ] + } + ] + } +] + diff --git a/ui/src/components/layout/navbar/login-button.tsx b/ui/src/components/layout/navbar/login-button.tsx index 526caea1..749da862 100644 --- a/ui/src/components/layout/navbar/login-button.tsx +++ b/ui/src/components/layout/navbar/login-button.tsx @@ -2,25 +2,29 @@ import { Button } from '@/components/ui/button'; import Role from '@/lib/access/role'; -import User from '@/lib/access/user'; import { login, logout } from 'next-cas-client'; +import User from '@/lib/access/user'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faSignInAlt, faSignOutAlt } from '@fortawesome/free-solid-svg-icons'; -const LoginButton = ({ currentUser }: { currentUser: User }) => ( - <> - {!currentUser.roles.includes(Role.UH) ? ( - - ) : ( - - )} - -); +const LoginButton = ({ currentUser }: { currentUser: User }) => { + const isOotbMode = process.env.NEXT_PUBLIC_OOTB_MODE === 'true'; + + return ( + <> + {!currentUser?.roles.includes(Role.UH) && !isOotbMode ? ( + + ) : ( + + )} + + ); +}; -export default LoginButton; +export default LoginButton; \ No newline at end of file diff --git a/ui/src/lib/access/user.ts b/ui/src/lib/access/user.ts index 7e07a682..968e3c07 100644 --- a/ui/src/lib/access/user.ts +++ b/ui/src/lib/access/user.ts @@ -3,7 +3,8 @@ import Role from './role'; import { CasUser } from 'next-cas-client'; import { setRoles } from './authorization'; import { getCurrentUser } from 'next-cas-client/app'; - +import { getOotbCurrentUser, matchProfile, updateActiveDefaultUser } from '@/lib/actions-ootb'; +import { OotbActiveProfile } from '../types'; type User = { roles: Role[]; } & MemberResult; @@ -17,6 +18,35 @@ export const AnonymousUser: User = { roles: [Role.ANONYMOUS] as const }; +export const loadOotbUser = async (profile: OotbActiveProfile): Promise => { + console.log('Loading OOTB user:', profile); + + const roles: Role[] = [Role.ANONYMOUS]; + + if (Array.isArray(profile.authorities)) { + const mappedRoles = profile.authorities + .map((authority) => { + const roleName = authority.replace(/^ROLE_/, ''); + return roleName.toUpperCase(); + }) + .filter((roleName) => Object.values(Role).includes(roleName as Role)) + .map((roleName) => roleName as Role); + roles.push(...mappedRoles); + } + + const user = { + name: profile.attributes.cn, + firstName: profile.attributes.givenName, + lastName: profile.attributes.sn, + uid: profile.uid, + uhUuid: profile.uhUuid, + roles: roles + } as User; + + console.log('OOTB user after setRoles:', user); + return user; +}; + export const loadUser = async (casUser: CasUser): Promise => { const user = { name: casUser.attributes.cn, @@ -28,9 +58,20 @@ export const loadUser = async (casUser: CasUser): Promise => { } as User; await setRoles(user); + console.log('CAS user after setRoles:', user); return user; }; -export const getUser = async (): Promise => (await getCurrentUser()) ?? AnonymousUser; +export const getUser = async (): Promise => { + if (process.env.NEXT_PUBLIC_OOTB_MODE === 'true') { + const givenName = process.env.NEXT_PUBLIC_OOTB_PROFILE; + await updateActiveDefaultUser(givenName); + const profile = await matchProfile(givenName); + return profile ? await loadOotbUser(profile) : AnonymousUser; + } + const user = (await getCurrentUser()) ?? AnonymousUser; + return user; +}; export default User; + \ No newline at end of file diff --git a/ui/src/lib/actions-ootb.ts b/ui/src/lib/actions-ootb.ts new file mode 100644 index 00000000..ff5b7116 --- /dev/null +++ b/ui/src/lib/actions-ootb.ts @@ -0,0 +1,49 @@ +'use server'; + +import { + OotbActiveProfile +} from './types'; +import { + postRequest, + getRequest +} from './http-client'; +import ootbProfiles from '../../ootb.active.user.profiles.json' assert { type: 'json' }; + +const profiles = ootbProfiles as OotbActiveProfile[]; +const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; + +/** + * Fetches the current default user. + * @returns The current user as a string. + */ +export const getOotbCurrentUser = async (): Promise => { + const endpoint = `${baseUrl}/currentUser/ootb`; + return getRequest(endpoint); +}; + +/** + * Updates the active default user based on the given name. + * @param givenName - The given name to match with attributes.givenName. + * @returns The matched OotbActiveProfile. + * @throws Error if no matching profile is found. + */ +export const updateActiveDefaultUser = async (givenName: string | undefined): Promise => { + const matchedProfile = await matchProfile(givenName); + const endpoint = `${baseUrl}/activeProfile/ootb`; + return postRequest(endpoint, matchedProfile.uid, matchedProfile); +}; + +/** + * Matches a profile by the given name. + * @param givenName - The given name to search for. + * @returns The matched profile as OotbActiveProfile. + * @throws Error if no profile matches the given name. + */ +export const matchProfile = async (givenName: string | undefined): Promise => { + const matchedProfile = profiles.find(profile => profile.attributes.givenName === givenName) as OotbActiveProfile; + if (!matchedProfile) { + throw new Error(`No profile found for givenName: ${givenName}`); + } + console.log("Matched Profile:", matchedProfile); + return matchedProfile; +}; \ No newline at end of file diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 3dd843ff..4a4c354e 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -165,3 +165,26 @@ export type EmailResult = { subject: string; text: string; }; + +export type OotbActiveProfile = { + uid: string; + uhUuid: string; + authorities: string[]; + attributes: Record; + groupings: OotbGrouping[]; +} + +export type OotbGrouping = { + name: string; + displayName: string; + extension: string; + displayExtension: string; + description: string; + members: OotbMember[]; +} + +export type OotbMember = { + uid: string; + uhUuid: string; + name: string; +} \ No newline at end of file From 554f5caf4c99ece08747cf7eb4cfa52b45282e7f Mon Sep 17 00:00:00 2001 From: Michelle Ho <89873670+michho8@users.noreply.github.com> Date: Thu, 12 Dec 2024 11:54:43 -1000 Subject: [PATCH 2/8] Create the departmental account icon (#112) --- .../layout/navbar/dept-account-icon.tsx | 66 +++++++++++++++++++ .../components/layout/navbar/navbar-menu.tsx | 2 +- ui/src/components/layout/navbar/navbar.tsx | 3 + ui/src/components/modal/dynamic-modal.tsx | 21 ++---- ui/src/lib/access/authorization.ts | 7 ++ ui/src/lib/access/role.ts | 3 +- .../layout/navbar/dept-account-icon.test.tsx | 31 +++++++++ .../components/layout/navbar/navbar.test.tsx | 19 ++++++ .../components/modal/dynamic-modal.test.tsx | 27 ++++++-- 9 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 ui/src/components/layout/navbar/dept-account-icon.tsx create mode 100644 ui/tests/components/layout/navbar/dept-account-icon.test.tsx diff --git a/ui/src/components/layout/navbar/dept-account-icon.tsx b/ui/src/components/layout/navbar/dept-account-icon.tsx new file mode 100644 index 00000000..7fbdc5f6 --- /dev/null +++ b/ui/src/components/layout/navbar/dept-account-icon.tsx @@ -0,0 +1,66 @@ +'use client'; + +import { useState } from 'react'; + +import DynamicModal from '@/components/modal/dynamic-modal'; +import { faUser, faSchool } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import User from '@/lib/access/user'; +import Role from '@/lib/access/role'; + +const DeptAccountIcon = ({ currentUser }: { currentUser: User }) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const openDepartmentalModal = () => { + setIsModalOpen(true); + }; + + const closeDepartmentalModal = () => { + setIsModalOpen(false); + }; + + return ( + <> + {currentUser.roles.includes(Role.DEPARTMENTAL) && ( +
+ + + + +
+ +
+
+ +

You are not in your personal account

+
+
+
+
+ )} + {isModalOpen && ( + + )} + + ); +}; + +export default DeptAccountIcon; diff --git a/ui/src/components/layout/navbar/navbar-menu.tsx b/ui/src/components/layout/navbar/navbar-menu.tsx index 483e605b..d7d4ec50 100644 --- a/ui/src/components/layout/navbar/navbar-menu.tsx +++ b/ui/src/components/layout/navbar/navbar-menu.tsx @@ -1,7 +1,7 @@ 'use client'; import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'; -import { User } from 'next-cas-client'; +import User from '@/lib/access/user'; import Link from 'next/link'; import { NavbarLinks } from './navbar-links'; import { useState } from 'react'; diff --git a/ui/src/components/layout/navbar/navbar.tsx b/ui/src/components/layout/navbar/navbar.tsx index 8382e4bc..e5a0e11d 100644 --- a/ui/src/components/layout/navbar/navbar.tsx +++ b/ui/src/components/layout/navbar/navbar.tsx @@ -6,9 +6,11 @@ import NavbarMenu from './navbar-menu'; import TimeoutModal from '@/components/modal/timeout-modal'; import { getUser } from '@/lib/access/user'; import Role from '@/lib/access/role'; +import DeptAccountIcon from '@/components/layout/navbar/dept-account-icon'; const Navbar = async () => { const currentUser = await getUser(); + return ( <> @@ -34,6 +36,7 @@ const Navbar = async () => { /> +
{NavbarLinks.filter( (navbarLink) => diff --git a/ui/src/components/modal/dynamic-modal.tsx b/ui/src/components/modal/dynamic-modal.tsx index d8933183..9db92515 100644 --- a/ui/src/components/modal/dynamic-modal.tsx +++ b/ui/src/components/modal/dynamic-modal.tsx @@ -9,41 +9,34 @@ import { AlertDialogFooter, AlertDialogCancel } from '@/components/ui/alert-dialog'; -import { ReactNode, useState } from 'react'; +import { ReactNode } from 'react'; import { Button } from '@/components/ui/button'; const DynamicModal = ({ open, title, body, - buttons + buttons, + onClose }: { open: boolean; title: string; body: string; buttons?: ReactNode[]; + onClose: () => void; }) => { - const [openDynamicModal, setOpenDynamicModal] = useState(open); - - /** - * Closes the modal. - */ - const close = () => { - setOpenDynamicModal(false); - }; - return ( - + {title} {body} - close()}>OK + OK {/*Any buttons that should lead the user to a different page.*/} {buttons?.map((button, index) => ( - ))} diff --git a/ui/src/lib/access/authorization.ts b/ui/src/lib/access/authorization.ts index 532bcf16..593893c2 100644 --- a/ui/src/lib/access/authorization.ts +++ b/ui/src/lib/access/authorization.ts @@ -21,6 +21,9 @@ export const setRoles = async (user: User): Promise => { if (await isAdmin(user.uhUuid)) { user.roles.push(Role.ADMIN); } + if (isDepartmental(user)) { + user.roles.push(Role.DEPARTMENTAL); + } }; /** @@ -58,3 +61,7 @@ const isValidUhUuid = (uhUuid: string): boolean => { const uhUuidPattern = new RegExp(/^[0-9]{8}$/); return uhUuidPattern.test(uhUuid); }; + +const isDepartmental = (user: User): boolean => { + return user.uid === user.uhUuid || !user.uhUuid; +}; diff --git a/ui/src/lib/access/role.ts b/ui/src/lib/access/role.ts index 58c32d8f..b58b8c53 100644 --- a/ui/src/lib/access/role.ts +++ b/ui/src/lib/access/role.ts @@ -2,7 +2,8 @@ enum Role { ADMIN = 'ADMIN', ANONYMOUS = 'ANONYMOUS', OWNER = 'OWNER', - UH = 'UH' + UH = 'UH', + DEPARTMENTAL = 'DEPARTMENTAL' } export default Role; diff --git a/ui/tests/components/layout/navbar/dept-account-icon.test.tsx b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx new file mode 100644 index 00000000..53610fb7 --- /dev/null +++ b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx @@ -0,0 +1,31 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import DeptAccountIcon from '@/components/layout/navbar/dept-account-icon'; +import User, { AnonymousUser } from '@/lib/access/user'; +import * as NextCasClient from 'next-cas-client/app'; +import Role from '@/lib/access/role'; + +const testUser: User = JSON.parse(process.env.TEST_USER_A as string); +jest.mock('next-cas-client/app'); + +describe('Dept Account Icon', () => { + it('should render the Departmental Account icon and open warning modal when clicked on', () => { + testUser.roles = [Role.DEPARTMENTAL]; + render(); + + fireEvent.focus(document); + expect(screen.getByLabelText('Departmental Account Icon')).toBeInTheDocument(); + + fireEvent.click(screen.getByLabelText('Departmental Account Icon')); + expect(screen.getByRole('alertdialog', { name: 'Warning' })).toBeInTheDocument(); + expect(screen.getByRole('alertdialog')).toHaveTextContent('You are not in your personal account'); + }); + + it('should not render the Departmental Account icon for other roles', () => { + jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); + testUser.roles = [Role.ANONYMOUS, Role.ADMIN, Role.UH, Role.OWNER]; + render(); + + fireEvent.focus(document); + expect(screen.queryByRole('button', { name: 'Departmental Account Icon' })).not.toBeInTheDocument(); + }); +}); diff --git a/ui/tests/components/layout/navbar/navbar.test.tsx b/ui/tests/components/layout/navbar/navbar.test.tsx index 5fab47d7..2b49161c 100644 --- a/ui/tests/components/layout/navbar/navbar.test.tsx +++ b/ui/tests/components/layout/navbar/navbar.test.tsx @@ -90,5 +90,24 @@ describe('Navbar', () => { expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); expect(screen.getByRole('button', { name: `Logout (${testUser.uid})` })).toBeInTheDocument(); }); + + it('should render the departmental icon for a Departmental Account without Admin or Groupings links', async () => { + testUser.roles.push(Role.DEPARTMENTAL, Role.UH); + jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + render(await Navbar()); + + expect(screen.getByRole('navigation')).toBeInTheDocument(); + expect(screen.getAllByRole('img', { name: 'UH Groupings Logo' })[0]).toHaveAttribute( + 'src', + '/uhgroupings/uh-groupings-logo.svg' + ); + + expect(screen.getAllByRole('link', { name: 'UH Groupings Logo' })[0]).toHaveAttribute('href', '/'); + expect(screen.getByLabelText('Departmental Account Icon')).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Memberships' })).toHaveAttribute('href', '/memberships'); + expect(screen.getByRole('link', { name: 'About' })).toHaveAttribute('href', '/about'); + expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); + expect(screen.getByRole('button', { name: `Logout (${testUser.uid})` })).toBeInTheDocument(); + }); }); }); diff --git a/ui/tests/components/modal/dynamic-modal.test.tsx b/ui/tests/components/modal/dynamic-modal.test.tsx index f52cef0b..2df95ced 100644 --- a/ui/tests/components/modal/dynamic-modal.test.tsx +++ b/ui/tests/components/modal/dynamic-modal.test.tsx @@ -1,10 +1,14 @@ import { fireEvent, render, screen } from '@testing-library/react'; import DynamicModal from '@/components/modal/dynamic-modal'; import Link from 'next/link'; +import { useState } from 'react'; describe('DynamicModal', () => { it('should open an informational modal with test contents and no extra buttons', () => { - render(); + const onClose = jest.fn(); + render( + + ); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument(); @@ -14,11 +18,13 @@ describe('DynamicModal', () => { }); it('should open an informational modal with test contents and extra buttons', () => { + const onClose = jest.fn(); render( Button1, <>Button2]} /> ); @@ -33,7 +39,16 @@ describe('DynamicModal', () => { }); it('should close the modal upon clicking the OK button', () => { - render(); + const onClose = jest.fn(); + render( + + ); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'A Dynamic Title' })).toBeInTheDocument(); @@ -43,15 +58,18 @@ describe('DynamicModal', () => { expect(screen.getByRole('button', { name: 'OK' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'OK' })); - expect(screen.queryByRole('alertdialog', { name: 'A Dynamic Title' })).not.toBeInTheDocument(); + // Cannot use useState in the test environment so expect the onClose function to be called once. + expect(onClose).toHaveBeenCalledTimes(1); // Assumes the onClose function toggles a useState variable. }); it('should close the modal and route to the provided link (Feedback)', () => { + const onClose = jest.fn(); render( Feedback @@ -69,6 +87,7 @@ describe('DynamicModal', () => { expect(screen.getByRole('link', { name: 'Feedback' })).toHaveAttribute('href', '/feedback'); fireEvent.click(screen.getByRole('button', { name: 'Feedback' })); - expect(screen.queryByRole('alertdialog', { name: 'A Modal to the Feedback Page' })).not.toBeInTheDocument(); + // Cannot use useState in the test environment so expect the onClose function to be called once. + expect(onClose).toHaveBeenCalledTimes(1); // Assumes the onClose function toggles a useState variable. }); }); From 307b5f5bb093151c2b7d431b5afc587394e876c3 Mon Sep 17 00:00:00 2001 From: Michelle Ho <89873670+michho8@users.noreply.github.com> Date: Thu, 12 Dec 2024 12:27:01 -1000 Subject: [PATCH 3/8] Change icon size (#115) --- ui/src/components/layout/navbar/dept-account-icon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/layout/navbar/dept-account-icon.tsx b/ui/src/components/layout/navbar/dept-account-icon.tsx index 7fbdc5f6..a1608fdf 100644 --- a/ui/src/components/layout/navbar/dept-account-icon.tsx +++ b/ui/src/components/layout/navbar/dept-account-icon.tsx @@ -33,7 +33,7 @@ const DeptAccountIcon = ({ currentUser }: { currentUser: User }) => {
Date: Fri, 13 Dec 2024 15:28:26 -1000 Subject: [PATCH 4/8] Create Admin Table component (#105) --- ui/package.json | 2 +- ui/src/app/admin/page.tsx | 77 +++++++----- .../_components/description-form.tsx | 14 ++- ui/src/app/groupings/page.tsx | 2 +- .../components/modal/remove-member-modal.tsx | 109 +++++++++++++++++ .../admin-table/admin-table-skeleton.tsx | 48 ++++++++ .../table/admin-table/admin-table.tsx | 100 +++++++++++++++ .../admin-table/table-element/add-admin.tsx | 50 ++++++++ .../table-element/add-admins-dialog.tsx | 112 +++++++++++++++++ .../table-element/admin-table-columns.tsx | 35 ++++++ .../groupings-table-skeleton.tsx | 2 +- .../{ => groupings-table}/groupings-table.tsx | 8 +- .../grouping-description-cell.tsx} | 0 .../table-element/grouping-name-cell.tsx} | 0 .../table-element/grouping-path-cell.tsx | 0 .../table-element/groupings-table-columns.tsx | 6 +- .../table/table-element/global-filter.tsx | 12 +- ui/src/components/ui/dialog.tsx | 114 ++++++++++++++++++ ui/src/components/ui/pagination.tsx | 1 + ui/src/lib/fetchers.ts | 11 +- ui/tailwind.config.ts | 2 +- ui/tests/app/admin/page.test.tsx | 87 ++++++++++--- .../table/groupings-table-skeleton.test.tsx | 2 +- .../groupings-table.test.tsx | 2 +- .../grouping-description-cell.test.tsx | 2 +- .../table-element/grouping-name-cell.test.tsx | 2 +- .../table-element/grouping-path-cell.test.tsx | 2 +- .../table-element/global-filter.test.tsx | 4 +- ui/tests/lib/fetchers.test.ts | 4 +- 29 files changed, 734 insertions(+), 76 deletions(-) create mode 100644 ui/src/components/modal/remove-member-modal.tsx create mode 100644 ui/src/components/table/admin-table/admin-table-skeleton.tsx create mode 100644 ui/src/components/table/admin-table/admin-table.tsx create mode 100644 ui/src/components/table/admin-table/table-element/add-admin.tsx create mode 100644 ui/src/components/table/admin-table/table-element/add-admins-dialog.tsx create mode 100644 ui/src/components/table/admin-table/table-element/admin-table-columns.tsx rename ui/src/components/table/{ => groupings-table}/groupings-table-skeleton.tsx (95%) rename ui/src/components/table/{ => groupings-table}/groupings-table.tsx (93%) rename ui/src/components/table/{table-element/ grouping-description-cell.tsx => groupings-table/table-element/grouping-description-cell.tsx} (100%) rename ui/src/components/table/{table-element/ grouping-name-cell.tsx => groupings-table/table-element/grouping-name-cell.tsx} (100%) rename ui/src/components/table/{ => groupings-table}/table-element/grouping-path-cell.tsx (100%) rename ui/src/components/table/{ => groupings-table}/table-element/groupings-table-columns.tsx (73%) create mode 100644 ui/src/components/ui/dialog.tsx rename ui/tests/components/table/{ => groupings-table}/groupings-table.test.tsx (99%) rename ui/tests/components/table/{ => groupings-table}/table-element/grouping-description-cell.test.tsx (90%) rename ui/tests/components/table/{ => groupings-table}/table-element/grouping-name-cell.test.tsx (84%) rename ui/tests/components/table/{ => groupings-table}/table-element/grouping-path-cell.test.tsx (96%) diff --git a/ui/package.json b/ui/package.json index 9a06510b..8f36ac89 100644 --- a/ui/package.json +++ b/ui/package.json @@ -18,7 +18,7 @@ "@fortawesome/react-fontawesome": "^0.2.2", "@hookform/resolvers": "^3.3.4", "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", diff --git a/ui/src/app/admin/page.tsx b/ui/src/app/admin/page.tsx index 5e7a9677..d37d674b 100644 --- a/ui/src/app/admin/page.tsx +++ b/ui/src/app/admin/page.tsx @@ -1,37 +1,54 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; +//TODO: import { getAllGroupings, groupingAdmins } from '@/lib/fetchers'; +import { groupingAdmins, ownerGroupings } from '@/lib/fetchers'; +import GroupingsTable from '@/components/table/groupings-table/groupings-table'; +import AdminTable from '@/components/table/admin-table/admin-table'; + +const Admin = async () => { + //TODO: const { groupingPaths } = await getAllGroupings(); + const { groupingPaths } = await ownerGroupings(); + const { members } = await groupingAdmins(); -const Admin = () => { return ( - -
- - - Manage Groupings - - - Manage Admins - - - Manage Person - - +
+
+ +
+ + + Manage Groupings + + + Manage Admins + + + Manage Person + + +
+ +
+
+ +
+
+
+ +
+
+ + {/**/} +
+
+
+ +
+
{/* PersonTable goes here */}
+
+
+
- -
-
{/* GroupingsTable goes here */}
-
-
- -
-
{/* AdminTable goes here */}
-
-
- -
-
{/* PersonTable goes here */}
-
-
- +
); }; diff --git a/ui/src/app/groupings/[groupingPath]/_components/description-form.tsx b/ui/src/app/groupings/[groupingPath]/_components/description-form.tsx index c9b01974..e46ecdde 100644 --- a/ui/src/app/groupings/[groupingPath]/_components/description-form.tsx +++ b/ui/src/app/groupings/[groupingPath]/_components/description-form.tsx @@ -88,7 +88,10 @@ const DescriptionForm = ({ groupDescription, groupPath }: { groupDescription: st
+ close()}>Cancel + + + + ); +}; + +export default RemoveMemberModal; diff --git a/ui/src/components/table/admin-table/admin-table-skeleton.tsx b/ui/src/components/table/admin-table/admin-table-skeleton.tsx new file mode 100644 index 00000000..69deb1f0 --- /dev/null +++ b/ui/src/components/table/admin-table/admin-table-skeleton.tsx @@ -0,0 +1,48 @@ +import { Skeleton } from '@/components/ui/skeleton'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import AdminTableColumns from '@/components/table/admin-table/table-element/admin-table-columns'; + +const AdminTableSkeleton = () => { + const pageSize = 7; // Average number of rows + + return ( +
+
+

Manage Admins

+
+ +
+ +
+
+
+ + + + {AdminTableColumns.map((column) => ( + + + + ))} + + + + {Array.from(Array(pageSize), (_, index) => ( + + {AdminTableColumns.map((column) => ( + + + + ))} + + ))} + +
+
+ +
+
+ ); +}; + +export default AdminTableSkeleton; diff --git a/ui/src/components/table/admin-table/admin-table.tsx b/ui/src/components/table/admin-table/admin-table.tsx new file mode 100644 index 00000000..81372700 --- /dev/null +++ b/ui/src/components/table/admin-table/admin-table.tsx @@ -0,0 +1,100 @@ +'use client'; + +import { + useReactTable, + flexRender, + getCoreRowModel, + getPaginationRowModel, + getFilteredRowModel, + getSortedRowModel +} from '@tanstack/react-table'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import AdminTableColumns from '@/components/table/admin-table/table-element/admin-table-columns'; +import PaginationBar from '@/components/table/table-element/pagination-bar'; +import GlobalFilter from '@/components/table/table-element/global-filter'; +import SortArrow from '@/components/table/table-element/sort-arrow'; +import { useState } from 'react'; +import { MemberResult } from '@/lib/types'; +import dynamic from 'next/dynamic'; +import AdminTableSkeleton from '@/components/table/admin-table/admin-table-skeleton'; +const pageSize = parseInt(process.env.NEXT_PUBLIC_PAGE_SIZE as string); + +const AdminTable = ({ members }: { members: MemberResult[] }) => { + const [globalFilter, setGlobalFilter] = useState(''); + const [sorting, setSorting] = useState([]); + + const table = useReactTable({ + columns: AdminTableColumns, + data: members, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + state: { globalFilter, sorting }, + initialState: { pagination: { pageSize } }, + onGlobalFilterChange: setGlobalFilter, + onSortingChange: setSorting, + enableMultiSort: true + }); + + return ( + <> +
+

Manage Admins

+
+ +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + +
+ {flexRender(header.column.columnDef.header, header.getContext())} + +
+
+ ))} +
+ ))} +
+ + {table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + 0 ? 'hidden sm:table-cell' : ''}`} + > +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+
+ ))} +
+ ))} +
+
+
+
+ {/*TODO: + TODO: */} +
+
+ +
+
+ + ); +}; + +export default dynamic(() => Promise.resolve(AdminTable), { + ssr: false, // Disable SSR for localStorage + loading: () => +}); diff --git a/ui/src/components/table/admin-table/table-element/add-admin.tsx b/ui/src/components/table/admin-table/table-element/add-admin.tsx new file mode 100644 index 00000000..d6e522cf --- /dev/null +++ b/ui/src/components/table/admin-table/table-element/add-admin.tsx @@ -0,0 +1,50 @@ +import {Input} from '@/components/ui/input' +import {Dispatch, SetStateAction} from 'react'; +import {Button} from '@/components/ui/button'; +import {addAdmin} from '@/actions/groupings-api'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from '@/components/ui/tooltip'; + +interface InputProps { + input: string; + setInput: Dispatch>; +} + +const handleClick = (input: string) => { + //TODO: create a condition where the admin list is checked for the uhid/uhUuid the user entered in. + //TODO: if it does not exist in the admin list, add the new admin to the UH Groupings admin list. + addAdmin(input); +}; +const AddAdmin = ({input, setInput}: InputProps) => ( + //Add tooltip +
+ setInput(e.target.value)} + /> + + + + + + +

Add to admins

+
+
+
+
+); + +export default AddAdmin; diff --git a/ui/src/components/table/admin-table/table-element/add-admins-dialog.tsx b/ui/src/components/table/admin-table/table-element/add-admins-dialog.tsx new file mode 100644 index 00000000..de7a038b --- /dev/null +++ b/ui/src/components/table/admin-table/table-element/add-admins-dialog.tsx @@ -0,0 +1,112 @@ +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger +} from '@/components/ui/alert-dialog'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Dispatch, SetStateAction } from 'react'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Label } from '@/components/ui/label'; +import { MemberResult } from '@/lib/types'; +import { addAdmin } from '@/lib/actions'; + +interface InputProps { + input: string; + setInput: Dispatch>; +} + +const AddAdminsDialog = ({ input, setInput }: InputProps, { uid, name, uhUuid }: MemberResult) => { + return ( +
+ setInput(e.target.value)} + /> + + + + + + + Add Member + + You are about to add the following member to the admins list. + + +
+
+
+ +
+
+ +
+
+ +
+
+ + {/*second column*/} + +
+
+ +
+
+ +
+
+ +
+
+
+ + Are you sure you want to add{' '} + + {name} + {' '} + to the admins list? + +
+ + + Membership changes made may not take effect immediately. Usually, 3-5 minutes should be + anticipated. In extreme cases changes may take several hours to be fully processed, + depending on the number of members and the synchronization destination. + + +
+ + + close()}>Cancel + +
+
+
+ ); +}; + +export default AddAdminsDialog; diff --git a/ui/src/components/table/admin-table/table-element/admin-table-columns.tsx b/ui/src/components/table/admin-table/table-element/admin-table-columns.tsx new file mode 100644 index 00000000..aa711f79 --- /dev/null +++ b/ui/src/components/table/admin-table/table-element/admin-table-columns.tsx @@ -0,0 +1,35 @@ +import RemoveMemberModal from '@/components/modal/remove-member-modal'; +import { removeAdmin } from '@/lib/actions'; + +const AdminTableColumns = [ + { + header: 'Admin Name', + accessorKey: 'name', + sortDescFirst: true, + cell: ({ row }) =>
{row.getValue('name')}
+ }, + { + header: 'UH Number', + accessorKey: 'uhUuid', + cell: ({ row }) =>
{row.getValue('uhUuid')}
+ }, + { + header: 'UH Username', + accessorKey: 'uid', + cell: ({ row }) =>
{row.getValue('uid')}
+ }, + { + header: 'Remove', + cell: ({ row }) => ( + + ) + } +]; + +export default AdminTableColumns; diff --git a/ui/src/components/table/groupings-table-skeleton.tsx b/ui/src/components/table/groupings-table/groupings-table-skeleton.tsx similarity index 95% rename from ui/src/components/table/groupings-table-skeleton.tsx rename to ui/src/components/table/groupings-table/groupings-table-skeleton.tsx index 82e03890..e912fb59 100644 --- a/ui/src/components/table/groupings-table-skeleton.tsx +++ b/ui/src/components/table/groupings-table/groupings-table-skeleton.tsx @@ -1,6 +1,6 @@ import { Skeleton } from '@/components/ui/skeleton'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import GroupingsTableColumns from '@/components/table/table-element/groupings-table-columns'; +import GroupingsTableColumns from '@/components/table/groupings-table/table-element/groupings-table-columns'; const GroupingsTableSkeleton = () => { const pageSize = 7; // Average number of rows diff --git a/ui/src/components/table/groupings-table.tsx b/ui/src/components/table/groupings-table/groupings-table.tsx similarity index 93% rename from ui/src/components/table/groupings-table.tsx rename to ui/src/components/table/groupings-table/groupings-table.tsx index 2a921be0..e1db2bf0 100644 --- a/ui/src/components/table/groupings-table.tsx +++ b/ui/src/components/table/groupings-table/groupings-table.tsx @@ -18,7 +18,7 @@ import SortArrow from '@/components/table/table-element/sort-arrow'; import { useState } from 'react'; import { useLocalStorage } from 'usehooks-ts'; import { GroupingPath } from '@/lib/types'; -import GroupingsTableColumns from '@/components/table/table-element/groupings-table-columns'; +import GroupingsTableColumns from '@/components/table/groupings-table/table-element/groupings-table-columns'; import dynamic from 'next/dynamic'; import GroupingsTableSkeleton from './groupings-table-skeleton'; @@ -52,7 +52,11 @@ const GroupingsTable = ({ groupingPaths }: { groupingPaths: GroupingPath[] }) =>

Manage Groupings

- +
diff --git a/ui/src/components/table/table-element/ grouping-description-cell.tsx b/ui/src/components/table/groupings-table/table-element/grouping-description-cell.tsx similarity index 100% rename from ui/src/components/table/table-element/ grouping-description-cell.tsx rename to ui/src/components/table/groupings-table/table-element/grouping-description-cell.tsx diff --git a/ui/src/components/table/table-element/ grouping-name-cell.tsx b/ui/src/components/table/groupings-table/table-element/grouping-name-cell.tsx similarity index 100% rename from ui/src/components/table/table-element/ grouping-name-cell.tsx rename to ui/src/components/table/groupings-table/table-element/grouping-name-cell.tsx diff --git a/ui/src/components/table/table-element/grouping-path-cell.tsx b/ui/src/components/table/groupings-table/table-element/grouping-path-cell.tsx similarity index 100% rename from ui/src/components/table/table-element/grouping-path-cell.tsx rename to ui/src/components/table/groupings-table/table-element/grouping-path-cell.tsx diff --git a/ui/src/components/table/table-element/groupings-table-columns.tsx b/ui/src/components/table/groupings-table/table-element/groupings-table-columns.tsx similarity index 73% rename from ui/src/components/table/table-element/groupings-table-columns.tsx rename to ui/src/components/table/groupings-table/table-element/groupings-table-columns.tsx index dc73130e..1c4e025c 100644 --- a/ui/src/components/table/table-element/groupings-table-columns.tsx +++ b/ui/src/components/table/groupings-table/table-element/groupings-table-columns.tsx @@ -1,8 +1,8 @@ import { ColumnDef } from '@tanstack/react-table'; import { GroupingPath } from '@/lib/types'; -import GroupingPathCell from '@/components/table/table-element/grouping-path-cell'; -import GroupingDescriptionCell from '@/components/table/table-element/ grouping-description-cell'; -import GroupingNameCell from '@/components/table/table-element/ grouping-name-cell'; +import GroupingPathCell from '@/components/table/groupings-table/table-element/grouping-path-cell'; +import GroupingDescriptionCell from '@/components/table/groupings-table/table-element/grouping-description-cell'; +import GroupingNameCell from '@/components/table/groupings-table/table-element/grouping-name-cell'; const GroupingsTableColumns: ColumnDef[] = [ { diff --git a/ui/src/components/table/table-element/global-filter.tsx b/ui/src/components/table/table-element/global-filter.tsx index 2977f36f..52f0072e 100644 --- a/ui/src/components/table/table-element/global-filter.tsx +++ b/ui/src/components/table/table-element/global-filter.tsx @@ -1,8 +1,14 @@ import { Input } from '@/components/ui/input'; import { Dispatch, SetStateAction } from 'react'; -const GlobalFilter = ({ filter, setFilter }: { filter: string; setFilter: Dispatch> }) => ( - setFilter(e.target.value)} /> -); +const GlobalFilter = ({ + placeholder, + filter, + setFilter +}: { + placeholder: string; + filter: string; + setFilter: Dispatch>; +}) => setFilter(e.target.value)} />; export default GlobalFilter; diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx new file mode 100644 index 00000000..921bb10c --- /dev/null +++ b/ui/src/components/ui/dialog.tsx @@ -0,0 +1,114 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; + +import { cn } from '@/components/ui/utils'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription +}; diff --git a/ui/src/components/ui/pagination.tsx b/ui/src/components/ui/pagination.tsx index 8c24a1f3..b1d13e76 100644 --- a/ui/src/components/ui/pagination.tsx +++ b/ui/src/components/ui/pagination.tsx @@ -57,6 +57,7 @@ const PaginationLink = ({ {...props} /> ) + PaginationLink.displayName = 'PaginationLink' const PaginationPrevious = ({ diff --git a/ui/src/lib/fetchers.ts b/ui/src/lib/fetchers.ts index 4eafe814..dc7a2558 100644 --- a/ui/src/lib/fetchers.ts +++ b/ui/src/lib/fetchers.ts @@ -7,7 +7,8 @@ import { GroupingGroupMembers, GroupingPaths, MembershipResults, - GroupingGroupsMembers + GroupingGroupsMembers, + ApiError } from './types'; import { getUser } from '@/lib/access/user'; @@ -96,9 +97,9 @@ export const groupingOptAttributes = async (groupingPath: string): Promise => { +export const groupingAdmins = async (): Promise => { const currentUser = await getUser(); - const endpoint = `${baseUrl}/grouping-admins`; + const endpoint = `${baseUrl}/groupings/admins`; return getRequest(endpoint, currentUser.uid); }; @@ -107,9 +108,9 @@ export const groupingAdmins = async (): Promise => { * * @returns The promise of all the grouping paths */ -export const getAllGroupings = async (): Promise => { +export const getAllGroupings = async (): Promise => { const currentUser = await getUser(); - const endpoint = `${baseUrl}/all-groupings`; + const endpoint = `${baseUrl}/groupings`; return getRequest(endpoint, currentUser.uid); }; diff --git a/ui/tailwind.config.ts b/ui/tailwind.config.ts index 61ef64fd..1d5e3e0a 100644 --- a/ui/tailwind.config.ts +++ b/ui/tailwind.config.ts @@ -61,4 +61,4 @@ const config = { plugins: [require('tailwindcss-animate')] } satisfies Config; -export default config; +export default config; \ No newline at end of file diff --git a/ui/tests/app/admin/page.test.tsx b/ui/tests/app/admin/page.test.tsx index 741e5f23..92ec293a 100644 --- a/ui/tests/app/admin/page.test.tsx +++ b/ui/tests/app/admin/page.test.tsx @@ -1,27 +1,82 @@ -import AdminLayout from '@/app/admin/layout'; +//TODO: import AdminLayout from '@/app/admin/layout'; import Admin from '@/app/admin/page'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; +import * as Fetchers from '@/lib/fetchers'; +import { MemberResult, GroupingPaths } from '@/lib/types'; +import Groupings from '@/app/groupings/page'; + +jest.mock('@/lib/fetchers'); + +const mockGroupingsData: GroupingPaths = { + resultCode: 'SUCCESS', + groupingPaths: Array.from({ length: 10 }, (_, i) => ({ + path: `tmp:example:example-${i}`, + name: `example-${i}`, + description: `Test Description ${i}` + })) +}; + +const mockAdminsData: MemberResult = { + resultCode: 'SUCCESS', + members: Array.from({ length: 10 }, (_, i) => ({ + name: `example-${i}`, + uid: `example-${i}`, + uhUuid: `example-${i}` + })) +}; + +beforeEach(() => { + jest.spyOn(Fetchers, 'ownerGroupings').mockResolvedValue(mockGroupingsData); + jest.spyOn(Fetchers, 'groupingAdmins').mockResolvedValue(mockAdminsData); +}); + +describe('Groupings', () => { + it('renders the Groupings page with the appropriate header and group data', async () => { + render(await Groupings()); + await waitFor(async () => { + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + + await waitFor(async () => { + mockGroupingsData.groupingPaths.forEach((group) => { + expect(screen.getByText(group.name)).toBeInTheDocument(); + expect(screen.getByText(group.description)).toBeInTheDocument(); + }); + }); + }); +}); describe('Admin', () => { - it('should render the Admin page with the appropriate header and tabs', () => { - render( - - - - ); - expect(screen.getByRole('main')).toBeInTheDocument(); - - expect(screen.getByRole('heading', { name: 'UH Groupings Administration' })).toBeInTheDocument(); + it('should render the Admin page with the appropriate header and tabs', async () => { + render(await Admin()); + + await waitFor(async () => { + expect(screen.getByRole('table')).toBeInTheDocument(); + }); + + await waitFor(async () => { + expect(screen.getByRole('main')).toBeInTheDocument(); + }); + + /*expect(screen.getByRole('heading', { name: 'UH Groupings Administration' })).toBeInTheDocument(); expect( screen.getByText( 'Search for and manage any grouping on behalf of its owner. ' + 'Manage the list of UH Groupings administrators.' ) - ).toBeInTheDocument(); + ).toBeInTheDocument();*/ - expect(screen.getByRole('tablist')).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Manage Groupings' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Manage Admins' })).toBeInTheDocument(); - expect(screen.getByRole('tab', { name: 'Manage Person' })).toBeInTheDocument(); + await waitFor(async () => { + expect(screen.getByRole('tablist')).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(screen.getByRole('tab', { name: 'Manage Groupings' })).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(screen.getByRole('tab', { name: 'Manage Admins' })).toBeInTheDocument(); + }); + await waitFor(async () => { + expect(screen.getByRole('tab', { name: 'Manage Person' })).toBeInTheDocument(); + }); }); }); diff --git a/ui/tests/components/table/groupings-table-skeleton.test.tsx b/ui/tests/components/table/groupings-table-skeleton.test.tsx index b8dcf820..b9d5cd4b 100644 --- a/ui/tests/components/table/groupings-table-skeleton.test.tsx +++ b/ui/tests/components/table/groupings-table-skeleton.test.tsx @@ -1,4 +1,4 @@ -import GroupingsTableSkeleton from '@/components/table/groupings-table-skeleton'; +import GroupingsTableSkeleton from '@/components/table/groupings-table/groupings-table-skeleton'; import { render, screen } from '@testing-library/react'; describe('GroupingsTableSkeleton', () => { diff --git a/ui/tests/components/table/groupings-table.test.tsx b/ui/tests/components/table/groupings-table/groupings-table.test.tsx similarity index 99% rename from ui/tests/components/table/groupings-table.test.tsx rename to ui/tests/components/table/groupings-table/groupings-table.test.tsx index 2f367228..54babd53 100644 --- a/ui/tests/components/table/groupings-table.test.tsx +++ b/ui/tests/components/table/groupings-table/groupings-table.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import GroupingsTable from '@/components/table/groupings-table'; +import GroupingsTable from '@/components/table/groupings-table/groupings-table'; import userEvent from '@testing-library/user-event'; const pageSize = parseInt(process.env.NEXT_PUBLIC_PAGE_SIZE as string); diff --git a/ui/tests/components/table/table-element/grouping-description-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx similarity index 90% rename from ui/tests/components/table/table-element/grouping-description-cell.test.tsx rename to ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx index cd38b1ef..684acf8a 100644 --- a/ui/tests/components/table/table-element/grouping-description-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx @@ -1,6 +1,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import GroupingDescriptionCell from '@/components/table/table-element/ grouping-description-cell'; +import GroupingDescriptionCell from '@/components/table/groupings-table/table-element/grouping-description-cell'; describe('GroupingDescriptionCell', () => { it('renders the description inside TooltipOnTruncate', () => { diff --git a/ui/tests/components/table/table-element/grouping-name-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx similarity index 84% rename from ui/tests/components/table/table-element/grouping-name-cell.test.tsx rename to ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx index 218783bf..c6533cd3 100644 --- a/ui/tests/components/table/table-element/grouping-name-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx @@ -1,5 +1,5 @@ import { render, screen} from '@testing-library/react'; -import GroupingNameCell from '@/components/table/table-element/ grouping-name-cell'; +import GroupingNameCell from '@/components/table/groupings-table/table-element/grouping-name-cell'; describe('GroupingNameCell', () => { it('renders the link with the correct path and displays the name', () => { diff --git a/ui/tests/components/table/table-element/grouping-path-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx similarity index 96% rename from ui/tests/components/table/table-element/grouping-path-cell.test.tsx rename to ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx index 0b5ebe4e..83351534 100644 --- a/ui/tests/components/table/table-element/grouping-path-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx @@ -1,5 +1,5 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; -import GroupingPathCell from '@/components/table/table-element/grouping-path-cell'; +import GroupingPathCell from '@/components/table/groupings-table/table-element/grouping-path-cell'; import userEvent from '@testing-library/user-event'; describe('GroupingPathCell', () => { diff --git a/ui/tests/components/table/table-element/global-filter.test.tsx b/ui/tests/components/table/table-element/global-filter.test.tsx index 16c78528..5de9623d 100644 --- a/ui/tests/components/table/table-element/global-filter.test.tsx +++ b/ui/tests/components/table/table-element/global-filter.test.tsx @@ -5,12 +5,12 @@ describe('GlobalFilter', () => { const mockSetFilter = jest.fn(); it('renders the input with correct placeholder and value', () => { - render(); + render(); expect(screen.getByPlaceholderText('Filter Groupings...')).toHaveValue('test'); }); it('renders call setFilter when the input value changes', () => { - render(); + render(); fireEvent.change(screen.getByPlaceholderText('Filter Groupings...'), { target: { value: 'new test' } }); expect(mockSetFilter).toHaveBeenCalledWith('new test'); diff --git a/ui/tests/lib/fetchers.test.ts b/ui/tests/lib/fetchers.test.ts index ae069236..2640c2ff 100644 --- a/ui/tests/lib/fetchers.test.ts +++ b/ui/tests/lib/fetchers.test.ts @@ -52,7 +52,7 @@ describe('fetchers', () => { describe('getAllGroupings', () => { it('should make a GET request at the correct endpoint', async () => { await getAllGroupings(); - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/all-groupings`, { + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/groupings`, { headers: { current_user: currentUser.uid } }); }); @@ -220,7 +220,7 @@ describe('fetchers', () => { describe('groupingAdmins', () => { it('should make a GET request at the correct endpoint', async () => { await groupingAdmins(); - expect(fetch).toHaveBeenCalledWith(`${baseUrl}/grouping-admins`, { + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/groupings/admins`, { headers: { current_user: currentUser.uid } }); }); From b3e6c1abd463d258f2aa2eb1b8b43c52f1ae11ee Mon Sep 17 00:00:00 2001 From: Jordan Wong <42422209+JorWo@users.noreply.github.com> Date: Thu, 19 Dec 2024 10:09:41 -1000 Subject: [PATCH 5/8] Replace Jest with Vitest (#118) --- .github/workflows/badges.yml | 67 ++----------------- .github/workflows/ci.yml | 28 ++------ README.md | 2 +- package-lock.json | 6 -- ui/.gitignore | 1 + ui/jest.config.ts | 27 -------- ui/package.json | 26 +++---- .../_components/technical-info-section.tsx | 2 +- ui/src/lib/access/authorization.ts | 27 +------- ui/src/lib/fetchers.ts | 31 +++++++-- .../(home)/_components/after-login.test.tsx | 19 +++--- .../(home)/_components/announcements.test.tsx | 5 +- .../(home)/_components/before-login.test.tsx | 1 + .../(home)/_components/login-button.test.tsx | 3 +- .../_components/general-info-section.test.tsx | 1 + .../technical-info-section.test.tsx | 48 +++---------- .../what-happens-if-section.test.tsx | 1 + ui/tests/app/about/page.test.tsx | 1 + ui/tests/app/admin/page.test.tsx | 7 +- .../feedback/_components/error-alert.test.tsx | 1 + .../_components/feedback-form.test.tsx | 37 +++++----- .../_components/success-alert.test.tsx | 1 + ui/tests/app/feedback/page.test.tsx | 5 +- .../_components/description-form.test.tsx | 13 ++-- .../_components/export-dropdown.test.tsx | 1 + .../_components/grouping-header.test.tsx | 3 +- .../_components/return-buttons.test.tsx | 5 +- .../_components/side-nav.test.tsx | 5 +- .../groupings/[groupingPath]/layout.test.tsx | 11 +-- .../[groupingPath]/tab/actions/page.test.tsx | 1 + .../tab/all-members/page.test.tsx | 1 + .../[groupingPath]/tab/basis/page.test.tsx | 1 + .../[groupingPath]/tab/exclude/page.test.tsx | 1 + .../[groupingPath]/tab/include/page.test.tsx | 1 + .../[groupingPath]/tab/layout.test.tsx | 1 + .../[groupingPath]/tab/owners/page.test.tsx | 1 + .../tab/preferences/page.test.tsx | 1 + .../tab/sync-destinations/page.test.tsx | 1 + ui/tests/app/groupings/layout.test.tsx | 1 + ui/tests/app/groupings/page.test.tsx | 5 +- ui/tests/app/memberships/page.test.tsx | 1 + ui/tests/components/layout/footer.test.tsx | 1 + ui/tests/components/layout/heading.test.tsx | 1 + .../layout/navbar/dept-account-icon.test.tsx | 6 +- .../layout/navbar/login-button.test.tsx | 3 +- .../layout/navbar/navbar-menu.test.tsx | 1 + .../components/layout/navbar/navbar.test.tsx | 13 ++-- .../components/modal/api-error-modal.test.tsx | 1 + .../components/modal/dynamic-modal.test.tsx | 10 +-- .../components/modal/timeout-modal.test.tsx | 29 ++++---- .../groupings-table-skeleton.test.tsx | 0 .../groupings-table/groupings-table.test.tsx | 1 + .../grouping-description-cell.test.tsx | 1 + .../table-element/grouping-name-cell.test.tsx | 5 +- .../table-element/grouping-path-cell.test.tsx | 5 +- .../table-element/column-settings.test.tsx | 9 +-- .../table-element/global-filter.test.tsx | 3 +- .../table-element/pagination-bar.test.tsx | 21 +++--- .../table/table-element/sort-arrow.test.tsx | 1 + .../tooltip-on-truncate.test.tsx | 1 + .../components/uh-groupings-info.test.tsx | 1 + ui/tests/lib/access/authorization.test.ts | 39 ++++------- ui/tests/lib/access/role.test.ts | 1 + ui/tests/lib/access/user.test.ts | 14 ++-- ui/tests/lib/actions.test.ts | 45 +++++++------ ui/tests/lib/fetchers.test.ts | 63 ++++++++++++++--- ui/tests/middleware.test.tsx | 19 +++--- ui/tests/setup-jest.ts | 14 ---- ui/tests/vitest.setup.ts | 15 +++++ ui/vitest.config.mts | 23 +++++++ 70 files changed, 364 insertions(+), 383 deletions(-) delete mode 100644 package-lock.json delete mode 100644 ui/jest.config.ts rename ui/tests/components/table/{ => groupings-table}/groupings-table-skeleton.test.tsx (100%) delete mode 100644 ui/tests/setup-jest.ts create mode 100644 ui/tests/vitest.setup.ts create mode 100644 ui/vitest.config.mts diff --git a/.github/workflows/badges.yml b/.github/workflows/badges.yml index 9733087e..1fc42150 100644 --- a/.github/workflows/badges.yml +++ b/.github/workflows/badges.yml @@ -2,7 +2,7 @@ name: Generate badges on: push: - branches: [ main ] + branches: [main] jobs: create-branch: @@ -29,7 +29,7 @@ jobs: echo "Branch $branch_name already exists." fi - generate-jest-badges: + generate-badges: needs: create-branch runs-on: ubuntu-latest steps: @@ -41,7 +41,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v2 with: - node-version: 16.x + node-version: 18.x - name: Installing dependencies run: | @@ -59,9 +59,9 @@ jobs: git switch badges - name: Generating coverage badges - uses: jpb06/jest-badges-action@latest + uses: jpb06/coverage-badges-action@latest with: - branches: '*' + branches: "*" coverage-summary-path: ui/coverage/coverage-summary.json no-commit: true target-branch: badges @@ -73,61 +73,6 @@ jobs: git config --global user.name 'Groupings Project' git config --global user.email 'actions@noreply.its.hawaii.edu' git add *.svg - git commit -m "Autogenerated Jest badges" *.svg + git commit -m "Autogenerated Vitest badges" *.svg git push origin badges -f fi - - # Re-enable once UH Groupings API is added to this repo - # generate-jacoco-badges: - # needs: create-branch - # runs-on: ubuntu-latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v3 - # with: - # fetch-depth: 0 - - # - name: Set up JDK 17 - # uses: actions/setup-java@v1 - # with: - # java-version: 17 - - # - name: Build with Maven and Generate JaCoCo Report - # run: | - # mvn clean test jacoco:report -f api/pom.xml -D'logging.level.edu.hawaii.its.holiday=OFF' -D'logging.level.org.springframework=ERROR' -D'spring.main.banner-mode=off' - # mv api/target/ target/ - - # - name: Switch to badges branch - # run: | - # git fetch - # git switch badges - - # - name: Generate Jacoco Badge - # id: jacoco - # uses: cicirello/jacoco-badge-generator@v2 - # with: - # coverage-label: junit coverage - # badges-directory: badges - # generate-branches-badge: true - - # - name: Log coverage percentage - # run: | - # echo "coverage = ${{ steps.jacoco.outputs.coverage }}" - - # - name: Commit and push - # if: ${{ github.event_name != 'pull_request' }} - # run: | - # cd badges - # if [[ `git status --porcelain *.svg` ]]; then - # git config --global user.name 'Groupings Project' - # git config --global user.email 'actions@noreply.its.hawaii.edu' - # git add *.svg - # git commit -m "Autogenerated JaCoCo coverage badge" *.svg - # git push origin badges -f - # fi - - # - name: Upload Jacoco coverage report - # uses: actions/upload-artifact@v2 - # with: - # name: jacoco-report - # path: target/site/jacoco/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eb5e614..4db8534b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,34 +2,16 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: - # Re-enable once UH Groupings API is added to this repo - # junit: - # runs-on: [ubuntu-latest, macos-latest, windows-latest] - - # steps: - # - name: Checkout code - # uses: actions/checkout@v3 - - # - name: Set up JDK 17 - # uses: actions/setup-java@v1 - # with: - # java-version: 17 - - # - name: Build with Maven - # run: | - # cd api - # mvn clean test - - jest: + vitest: strategy: matrix: os: [ubuntu-latest, macos-13, windows-latest] - node-version: [ 18.x, 20.x ] + node-version: [18.x, 20.x] runs-on: ${{ matrix.os }} steps: @@ -51,7 +33,7 @@ jobs: cd ui npm run lint - - name: Run Jest Tests + - name: Run Vitest Tests run: | cd ui npm run test diff --git a/README.md b/README.md index 86b5539e..158c568d 100644 --- a/README.md +++ b/README.md @@ -9,4 +9,4 @@ Groupings can be synchronized with one or more of the following: email LISTSERV UH Groupings utilizes the Internet2 Grouper project. Grouper is an enterprise access management system designed for the highly distributed management environment and heterogeneous information technology environment common to universities. [![CI](https://github.com/uhawaii-system-its-ti-iam/uh-groupings/actions/workflows/ci.yml/badge.svg)](https://github.com/uhawaii-system-its-ti-iam/uh-groupings/actions/workflows/ci.yml) -![Jest coverage](https://github.com/uhawaii-system-its-ti-iam/uh-groupings/blob/badges/badges/coverage-jest%20coverage.svg) +![Test coverage](https://github.com/uhawaii-system-its-ti-iam/uh-groupings/blob/badges/badges/ui/coverage-total.svg) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 5ab54d6d..00000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "uh-groupings", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/ui/.gitignore b/ui/.gitignore index 6d333829..e655f1a2 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -9,6 +9,7 @@ package-lock.json # testing /coverage +.swc # next.js /.next/ diff --git a/ui/jest.config.ts b/ui/jest.config.ts deleted file mode 100644 index a9a1576b..00000000 --- a/ui/jest.config.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { Config } from 'jest'; -import nextJest from 'next/jest.js'; - -const createJestConfig = nextJest({ - dir: './' -}); - -const config: Config = { - clearMocks: true, - collectCoverageFrom: ['./src/**/*.ts*'], - coveragePathIgnorePatterns: [ - './src/components/ui' // Ignore shadcn/ui components - ], - coverageReporters: ['json-summary', 'text', 'html'], - testEnvironment: 'jsdom', - testEnvironmentOptions: { - customExportConditions: [] - }, - setupFilesAfterEnv: ['/tests/setup-jest.ts'], - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - moduleDirectories: ['node_modules', ''], - moduleNameMapper: { - '^@/(.*)$': '/src/$1' - } -}; - -export default createJestConfig(config); diff --git a/ui/package.json b/ui/package.json index 8f36ac89..613107cf 100644 --- a/ui/package.json +++ b/ui/package.json @@ -8,9 +8,9 @@ "build": "next build", "start": "next start", "lint": "next lint", - "test": "jest", - "test:coverage": "jest --coverage", - "test:watch": "jest --watch" + "test": "vitest", + "test:ui": "vitest --ui", + "test:coverage": "vitest run --coverage" }, "dependencies": { "@fortawesome/free-regular-svg-icons": "^6.6.0", @@ -33,7 +33,7 @@ "dotenv": "^16.4.1", "lucide-react": "^0.453.0", "next": "14.2.15", - "next-cas-client": "^1.2.2", + "next-cas-client": "^1.3.2", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.50.1", @@ -45,32 +45,34 @@ }, "devDependencies": { "@stylistic/eslint-plugin": "^2.8.0", - "@swc/core": "^1.3.106", - "@testing-library/jest-dom": "^6.3.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.0.1", "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.11", "@types/node": "^22.7.4", "@types/react": "^18", "@types/react-dom": "^18", "@types/uniqid": "^5.3.4", "@typescript-eslint/eslint-plugin": "^6.19.1", "@typescript-eslint/parser": "^6.19.1", + "@vitejs/plugin-react": "^4.3.4", + "@vitest/coverage-istanbul": "^2.1.8", + "@vitest/ui": "^2.1.8", "autoprefixer": "^10.0.1", "eslint": "^8", "eslint-config-next": "14.2.15", "eslint-config-prettier": "^9.1.0", "eslint-plugin-testing-library": "^6.2.0", "eslint-plugin-tsdoc": "^0.2.17", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-fetch-mock": "^3.0.3", + "jsdom": "^25.0.1", "postcss": "^8.4.33", - "postcss-preset-mantine": "^1.12.3", "postcss-simple-vars": "^7.0.1", "prettier": "^3.2.5", "tailwindcss": "^3.3.0", "ts-node": "^10.9.2", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "vite-tsconfig-paths": "^5.1.4", + "vitest": "^2.1.8", + "vitest-fetch-mock": "^0.4.2" } } diff --git a/ui/src/app/about/_components/technical-info-section.tsx b/ui/src/app/about/_components/technical-info-section.tsx index d979c507..2bb9afed 100644 --- a/ui/src/app/about/_components/technical-info-section.tsx +++ b/ui/src/app/about/_components/technical-info-section.tsx @@ -2,7 +2,7 @@ const TechnicalInfoSection = () => { const technicalInfoItems = [ { name: 'React.js', url: 'https://react.dev/learn', description: '(Quickstart)' }, { name: 'shadcn/ui', url: 'https://ui.shadcn.com/docs', description: '(Guide)' }, - { name: 'Jest', url: 'https://jestjs.io/docs/getting-started', description: '(Introduction)' }, + { name: 'Vitest', url: 'https://vitest.dev/guide/', description: '(Introduction)' }, { name: 'Next.js', url: 'https://nextjs.org/docs', description: '(Introduction)' }, { name: 'Tanstack Table', diff --git a/ui/src/lib/access/authorization.ts b/ui/src/lib/access/authorization.ts index 593893c2..875de938 100644 --- a/ui/src/lib/access/authorization.ts +++ b/ui/src/lib/access/authorization.ts @@ -1,8 +1,7 @@ +import { isOwner, isAdmin } from '../fetchers'; import Role from './role'; import User from './user'; -const apiBaseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; - /** * Sets the appropriate roles for a user. * @@ -26,30 +25,6 @@ export const setRoles = async (user: User): Promise => { } }; -/** - * Calls UH Groupings API to check if the uhIdentifier is an owner. - * - * @param uhIdentifier - The uid or uhUuid - * - * @returns True if the uhIdentifier is an owner of a grouping - */ -const isOwner = async (uhIdentifier: string): Promise => - await fetch(`${apiBaseUrl}/owners`, { headers: { current_user: uhIdentifier } }) - .then((res) => res.json()) - .catch(() => false); - -/** - * Calls UH Groupings API to check if the uhIdentifier is an admin. - * - * @param uhIdentifier - The uid or uhUuid - * - * @returns True if the uhIdentifier is an admin - */ -const isAdmin = async (uhIdentifier: string): Promise => - await fetch(`${apiBaseUrl}/admins`, { headers: { current_user: uhIdentifier } }) - .then((res) => res.json()) - .catch(() => false); - /** * Checks if uhUuid is valid using Regex. * diff --git a/ui/src/lib/fetchers.ts b/ui/src/lib/fetchers.ts index dc7a2558..afd9d2ab 100644 --- a/ui/src/lib/fetchers.ts +++ b/ui/src/lib/fetchers.ts @@ -7,8 +7,7 @@ import { GroupingGroupMembers, GroupingPaths, MembershipResults, - GroupingGroupsMembers, - ApiError + GroupingGroupsMembers } from './types'; import { getUser } from '@/lib/access/user'; @@ -97,7 +96,7 @@ export const groupingOptAttributes = async (groupingPath: string): Promise => { +export const groupingAdmins = async (): Promise => { const currentUser = await getUser(); const endpoint = `${baseUrl}/groupings/admins`; return getRequest(endpoint, currentUser.uid); @@ -108,7 +107,7 @@ export const groupingAdmins = async (): Promise * * @returns The promise of all the grouping paths */ -export const getAllGroupings = async (): Promise => { +export const getAllGroupings = async (): Promise => { const currentUser = await getUser(); const endpoint = `${baseUrl}/groupings`; return getRequest(endpoint, currentUser.uid); @@ -208,3 +207,27 @@ export const isSoleOwner = async (uhIdentifier: string, groupingPath: string): P const endpoint = `${baseUrl}/groupings/${groupingPath}/owners/${uhIdentifier}`; return getRequest(endpoint, currentUser.uid); }; + +/** + * Check if the uhIdentifier is an owner. + * + * @param uhIdentifier - The uid or uhUuid + * + * @returns True if the uhIdentifier is an owner of a grouping + */ +export const isOwner = async (uhIdentifier: string): Promise => { + const endpoint = `${baseUrl}/owners`; + return getRequest(endpoint, uhIdentifier); +}; + +/** + * Check if the uhIdentifier is an admin. + * + * @param uhIdentifier - The uid or uhUuid + * + * @returns True if the uhIdentifier is an admin + */ +export const isAdmin = async (uhIdentifier: string): Promise => { + const endpoint = `${baseUrl}/admins`; + return getRequest(endpoint, uhIdentifier); +}; diff --git a/ui/tests/app/(home)/_components/after-login.test.tsx b/ui/tests/app/(home)/_components/after-login.test.tsx index 313ce8a0..f656db71 100644 --- a/ui/tests/app/(home)/_components/after-login.test.tsx +++ b/ui/tests/app/(home)/_components/after-login.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; import Role from '@/lib/access/role'; import { render, screen } from '@testing-library/react'; import User from '@/lib/access/user'; @@ -5,8 +6,8 @@ import * as Fetchers from '@/lib/fetchers'; import * as NextCasClient from 'next-cas-client/app'; import afterLogin from '@/app/(home)/_components/after-login'; -jest.mock('@/lib/fetchers'); -jest.mock('next-cas-client/app'); +vi.mock('@/lib/fetchers'); +vi.mock('next-cas-client/app'); const testUser: User = JSON.parse(process.env.TEST_USER_A as string); @@ -30,8 +31,8 @@ describe('AfterLogin', () => { }; const expectWelcome = (User: User, role: string) => { - expect(screen.getByLabelText('user')).toBeInTheDocument(); - expect(screen.getByLabelText('key-round')).toBeInTheDocument(); + expect(screen.getAllByLabelText('user')[0]).toBeInTheDocument(); + expect(screen.getAllByLabelText('key-round')[0]).toBeInTheDocument(); expect(screen.getByTestId('welcome-message')).toHaveTextContent(`Welcome, ${User.firstName}!`); expect(screen.getByTestId('role')).toHaveTextContent(`Role: ${role}`); }; @@ -100,12 +101,12 @@ describe('AfterLogin', () => { }; beforeEach(() => { - jest.spyOn(Fetchers, 'getNumberOfGroupings').mockResolvedValue(numberOfGroupings); - jest.spyOn(Fetchers, 'getNumberOfMemberships').mockResolvedValue(numberOfMemberships); + vi.spyOn(Fetchers, 'getNumberOfGroupings').mockResolvedValue(numberOfGroupings); + vi.spyOn(Fetchers, 'getNumberOfMemberships').mockResolvedValue(numberOfMemberships); }); it('Should render correctly when logged in as an admin', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(admin); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(admin); render(await afterLogin()); expectWelcome(admin, 'Admin'); expectAdministration(true); @@ -114,7 +115,7 @@ describe('AfterLogin', () => { }); it('Should render correctly when logged in as Owner', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(owner); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(owner); render(await afterLogin()); expectWelcome(owner, 'Owner'); expectAdministration(false); @@ -123,7 +124,7 @@ describe('AfterLogin', () => { }); it('Should render correctly when logged in as a user with a UH account', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(uhUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(uhUser); render(await afterLogin()); expectWelcome(uhUser, 'Member'); expectAdministration(false); diff --git a/ui/tests/app/(home)/_components/announcements.test.tsx b/ui/tests/app/(home)/_components/announcements.test.tsx index 181c3a84..2b448bf8 100644 --- a/ui/tests/app/(home)/_components/announcements.test.tsx +++ b/ui/tests/app/(home)/_components/announcements.test.tsx @@ -1,8 +1,9 @@ +import { vi, describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import Announcements from '@/app/(home)/_components/announcements'; import * as Fetchers from '@/lib/fetchers'; -jest.mock('@/lib/fetchers'); +vi.mock('@/lib/fetchers'); const message = 'test announcement'; const message1 = 'test1 announcement'; @@ -33,7 +34,7 @@ const announcements = { describe('Announcements', () => { it('renders announcement correctly', async () => { - jest.spyOn(Fetchers, 'getAnnouncements').mockResolvedValue(announcements); + vi.spyOn(Fetchers, 'getAnnouncements').mockResolvedValue(announcements); render(await Announcements()); expect(screen.getByText(message)).toBeInTheDocument(); diff --git a/ui/tests/app/(home)/_components/before-login.test.tsx b/ui/tests/app/(home)/_components/before-login.test.tsx index 1dc34cae..a5bffb2e 100644 --- a/ui/tests/app/(home)/_components/before-login.test.tsx +++ b/ui/tests/app/(home)/_components/before-login.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import BeforeLogin from '@/app/(home)/_components/before-login'; diff --git a/ui/tests/app/(home)/_components/login-button.test.tsx b/ui/tests/app/(home)/_components/login-button.test.tsx index 3f2e9882..3a633e06 100644 --- a/ui/tests/app/(home)/_components/login-button.test.tsx +++ b/ui/tests/app/(home)/_components/login-button.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeAll } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import User, { AnonymousUser } from '@/lib/access/user'; @@ -7,7 +8,7 @@ import * as NextCasClient from 'next-cas-client'; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client'); +vi.mock('next-cas-client'); describe('LoginButton', () => { describe('User is not logged in', () => { diff --git a/ui/tests/app/about/_components/general-info-section.test.tsx b/ui/tests/app/about/_components/general-info-section.test.tsx index dc6aad30..e650cd2e 100644 --- a/ui/tests/app/about/_components/general-info-section.test.tsx +++ b/ui/tests/app/about/_components/general-info-section.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import GeneralInfoSection from '@/app/about/_components/general-info-section'; diff --git a/ui/tests/app/about/_components/technical-info-section.test.tsx b/ui/tests/app/about/_components/technical-info-section.test.tsx index deeba7a8..9fed417f 100644 --- a/ui/tests/app/about/_components/technical-info-section.test.tsx +++ b/ui/tests/app/about/_components/technical-info-section.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import TechnicalInfoSection from '@/app/about/_components/technical-info-section'; @@ -10,52 +11,31 @@ describe('TechnicalInfoSection', () => { expect(screen.getByText('React.js')).toBeInTheDocument(); expect(screen.getByText('(Quickstart)')).toBeInTheDocument(); - expect(screen.getByText('(Quickstart)')).toHaveAttribute( - 'href', - 'https://react.dev/learn' - ); + expect(screen.getByText('(Quickstart)')).toHaveAttribute('href', 'https://react.dev/learn'); expect(screen.getByText('shadcn/ui')).toBeInTheDocument(); expect(guideLinks[0]).toBeInTheDocument(); - expect(guideLinks[0]).toHaveAttribute( - 'href', - 'https://ui.shadcn.com/docs' - ); + expect(guideLinks[0]).toHaveAttribute('href', 'https://ui.shadcn.com/docs'); - expect(screen.getByText('Jest')).toBeInTheDocument(); + expect(screen.getByText('Vitest')).toBeInTheDocument(); expect(introductionLinks[0]).toBeInTheDocument(); - expect(introductionLinks[0]).toHaveAttribute( - 'href', - 'https://jestjs.io/docs/getting-started' - ); + expect(introductionLinks[0]).toHaveAttribute('href', 'https://vitest.dev/guide/'); expect(screen.getByText('Next.js')).toBeInTheDocument(); expect(introductionLinks[1]).toBeInTheDocument(); - expect(introductionLinks[1]).toHaveAttribute( - 'href', - 'https://nextjs.org/docs' - ); + expect(introductionLinks[1]).toHaveAttribute('href', 'https://nextjs.org/docs'); expect(screen.getByText('Tanstack Table')).toBeInTheDocument(); expect(introductionLinks[2]).toBeInTheDocument(); - expect(introductionLinks[2]).toHaveAttribute( - 'href', - 'https://tanstack.com/table/v8/docs/introduction' - ); + expect(introductionLinks[2]).toHaveAttribute('href', 'https://tanstack.com/table/v8/docs/introduction'); expect(screen.getByText('React Testing Library')).toBeInTheDocument(); expect(screen.getByText('(Documentation)')).toBeInTheDocument(); - expect(screen.getByText('(Documentation)')).toHaveAttribute( - 'href', - 'https://testing-library.com/' - ); + expect(screen.getByText('(Documentation)')).toHaveAttribute('href', 'https://testing-library.com/'); expect(screen.getByText('Typescript')).toBeInTheDocument(); expect(guideLinks[1]).toBeInTheDocument(); - expect(guideLinks[1]).toHaveAttribute( - 'href', - 'https://www.typescriptlang.org/docs/' - ); + expect(guideLinks[1]).toHaveAttribute('href', 'https://www.typescriptlang.org/docs/'); expect(screen.getByText('Tanstack Query')).toBeInTheDocument(); expect(guideLinks[2]).toBeInTheDocument(); @@ -66,16 +46,10 @@ describe('TechnicalInfoSection', () => { expect(screen.getByText('Tailwind CSS')).toBeInTheDocument(); expect(guideLinks[3]).toBeInTheDocument(); - expect(guideLinks[3]).toHaveAttribute( - 'href', - 'https://v2.tailwindcss.com/docs' - ); + expect(guideLinks[3]).toHaveAttribute('href', 'https://v2.tailwindcss.com/docs'); expect(screen.getByText('Iron Session')).toBeInTheDocument(); expect(screen.getByText('(GitHub)')).toBeInTheDocument(); - expect(screen.getByText('(GitHub)')).toHaveAttribute( - 'href', - 'https://github.com/vvo/iron-session' - ); + expect(screen.getByText('(GitHub)')).toHaveAttribute('href', 'https://github.com/vvo/iron-session'); }); }); diff --git a/ui/tests/app/about/_components/what-happens-if-section.test.tsx b/ui/tests/app/about/_components/what-happens-if-section.test.tsx index 50206e4f..cb80bade 100644 --- a/ui/tests/app/about/_components/what-happens-if-section.test.tsx +++ b/ui/tests/app/about/_components/what-happens-if-section.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import WhatHappensIfSection from '@/app/about/_components/what-happens-if-section'; diff --git a/ui/tests/app/about/page.test.tsx b/ui/tests/app/about/page.test.tsx index 737166be..d8cafbbe 100644 --- a/ui/tests/app/about/page.test.tsx +++ b/ui/tests/app/about/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import About from '@/app/about/page'; diff --git a/ui/tests/app/admin/page.test.tsx b/ui/tests/app/admin/page.test.tsx index 92ec293a..9563281d 100644 --- a/ui/tests/app/admin/page.test.tsx +++ b/ui/tests/app/admin/page.test.tsx @@ -1,11 +1,12 @@ //TODO: import AdminLayout from '@/app/admin/layout'; +import { vi, beforeEach, describe, it, expect } from 'vitest'; import Admin from '@/app/admin/page'; import { render, screen, waitFor } from '@testing-library/react'; import * as Fetchers from '@/lib/fetchers'; import { MemberResult, GroupingPaths } from '@/lib/types'; import Groupings from '@/app/groupings/page'; -jest.mock('@/lib/fetchers'); +vi.mock('@/lib/fetchers'); const mockGroupingsData: GroupingPaths = { resultCode: 'SUCCESS', @@ -26,8 +27,8 @@ const mockAdminsData: MemberResult = { }; beforeEach(() => { - jest.spyOn(Fetchers, 'ownerGroupings').mockResolvedValue(mockGroupingsData); - jest.spyOn(Fetchers, 'groupingAdmins').mockResolvedValue(mockAdminsData); + vi.spyOn(Fetchers, 'ownerGroupings').mockResolvedValue(mockGroupingsData); + vi.spyOn(Fetchers, 'groupingAdmins').mockResolvedValue(mockAdminsData); }); describe('Groupings', () => { diff --git a/ui/tests/app/feedback/_components/error-alert.test.tsx b/ui/tests/app/feedback/_components/error-alert.test.tsx index f0abe038..a917f331 100644 --- a/ui/tests/app/feedback/_components/error-alert.test.tsx +++ b/ui/tests/app/feedback/_components/error-alert.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import ErrorAlert from '@/app/feedback/_components/error-alert'; import { render, screen } from '@testing-library/react'; diff --git a/ui/tests/app/feedback/_components/feedback-form.test.tsx b/ui/tests/app/feedback/_components/feedback-form.test.tsx index f55120ed..4f85be36 100644 --- a/ui/tests/app/feedback/_components/feedback-form.test.tsx +++ b/ui/tests/app/feedback/_components/feedback-form.test.tsx @@ -3,8 +3,9 @@ import userEvent from '@testing-library/user-event'; import * as Actions from '@/lib/actions'; import User from '@/lib/access/user'; import FeedbackForm from '@/app/feedback/_components/feedback-form'; +import { vi, describe, it, expect } from 'vitest'; -jest.mock('@/lib/actions'); +vi.mock('@/lib/actions'); const testUser: User = JSON.parse(process.env.TEST_USER_A as string); @@ -21,7 +22,7 @@ describe('FeedbackForm', () => { const user = userEvent.setup(); render(); - jest.spyOn(Actions, 'sendFeedback').mockResolvedValue({ + vi.spyOn(Actions, 'sendFeedback').mockResolvedValue({ resultCode: 'SUCCESS', recipient: 'recipient', from: 'from', @@ -48,7 +49,7 @@ describe('FeedbackForm', () => { const user = userEvent.setup(); render(); - jest.spyOn(Actions, 'sendFeedback').mockResolvedValue({ + vi.spyOn(Actions, 'sendFeedback').mockResolvedValue({ resultCode: 'FAILURE', recipient: 'recipient', from: 'from', @@ -73,21 +74,21 @@ describe('FeedbackForm', () => { const user = userEvent.setup(); render(); - jest.spyOn(Actions, 'sendFeedback').mockResolvedValueOnce({ - resultCode: 'FAILURE', - recipient: 'recipient', - from: 'from', - subject: 'subject', - text: 'text' - }); - - jest.spyOn(Actions, 'sendFeedback').mockResolvedValueOnce({ - resultCode: 'SUCCESS', - recipient: 'recipient', - from: 'from', - subject: 'subject', - text: 'text' - }); + vi.spyOn(Actions, 'sendFeedback') + .mockResolvedValueOnce({ + resultCode: 'FAILURE', + recipient: 'recipient', + from: 'from', + subject: 'subject', + text: 'text' + }) + .mockResolvedValueOnce({ + resultCode: 'SUCCESS', + recipient: 'recipient', + from: 'from', + subject: 'subject', + text: 'text' + }); await waitFor( async () => { diff --git a/ui/tests/app/feedback/_components/success-alert.test.tsx b/ui/tests/app/feedback/_components/success-alert.test.tsx index be62262b..88fbacb9 100644 --- a/ui/tests/app/feedback/_components/success-alert.test.tsx +++ b/ui/tests/app/feedback/_components/success-alert.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import SuccessAlert from '@/app/feedback/_components/success-alert'; import { render, screen } from '@testing-library/react'; diff --git a/ui/tests/app/feedback/page.test.tsx b/ui/tests/app/feedback/page.test.tsx index f9e9966c..e538f51e 100644 --- a/ui/tests/app/feedback/page.test.tsx +++ b/ui/tests/app/feedback/page.test.tsx @@ -1,15 +1,16 @@ +import { vi, describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import Feedback from '@/app/feedback/page'; import * as NextCasClient from 'next-cas-client/app'; import User from '@/lib/access/user'; -jest.mock('next-cas-client/app'); +vi.mock('next-cas-client/app'); const testUser: User = JSON.parse(process.env.TEST_USER_A as string); describe('Feedback', () => { it('should render the Feedback form', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); render(await Feedback()); diff --git a/ui/tests/app/groupings/[groupingPath]/_components/description-form.test.tsx b/ui/tests/app/groupings/[groupingPath]/_components/description-form.test.tsx index fa5db0ce..7fe0f9ed 100644 --- a/ui/tests/app/groupings/[groupingPath]/_components/description-form.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/_components/description-form.test.tsx @@ -2,13 +2,14 @@ import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import DescriptionForm from '@/app/groupings/[groupingPath]/_components/description-form'; import { updateDescription } from '@/lib/actions'; import { useRouter } from 'next/navigation'; +import { vi, describe, beforeEach, it, expect, Mock } from 'vitest'; -jest.mock('@/lib/actions', () => ({ - updateDescription: jest.fn() +vi.mock('@/lib/actions', () => ({ + updateDescription: vi.fn() })); -jest.mock('next/navigation', () => ({ - useRouter: jest.fn() +vi.mock('next/navigation', () => ({ + useRouter: vi.fn() })); describe('DescriptionForm', () => { @@ -16,8 +17,8 @@ describe('DescriptionForm', () => { const groupPath = 'test path'; beforeEach(() => { - const router = { refresh: jest.fn() }; - (useRouter as jest.Mock).mockReturnValue(router); + const router = { refresh: vi.fn() }; + (useRouter as Mock).mockReturnValue(router); }); it('should render description', () => { diff --git a/ui/tests/app/groupings/[groupingPath]/_components/export-dropdown.test.tsx b/ui/tests/app/groupings/[groupingPath]/_components/export-dropdown.test.tsx index 717563b5..4fa0035b 100644 --- a/ui/tests/app/groupings/[groupingPath]/_components/export-dropdown.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/_components/export-dropdown.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import ExportDropdown from '@/app/groupings/[groupingPath]/_components/export-dropdown'; diff --git a/ui/tests/app/groupings/[groupingPath]/_components/grouping-header.test.tsx b/ui/tests/app/groupings/[groupingPath]/_components/grouping-header.test.tsx index 334861b9..65d5eb34 100644 --- a/ui/tests/app/groupings/[groupingPath]/_components/grouping-header.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/_components/grouping-header.test.tsx @@ -1,8 +1,9 @@ +import { vi, describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import GroupingHeader from '@/app/groupings/[groupingPath]/_components/grouping-header'; import DescriptionForm from '@/app/groupings/[groupingPath]/_components/description-form'; -jest.mock('@/app/groupings/[groupingPath]/_components/description-form'); +vi.mock('@/app/groupings/[groupingPath]/_components/description-form'); describe('GroupingHeader Component', () => { const GroupName = 'Test Group'; diff --git a/ui/tests/app/groupings/[groupingPath]/_components/return-buttons.test.tsx b/ui/tests/app/groupings/[groupingPath]/_components/return-buttons.test.tsx index af083d53..dc177b4e 100644 --- a/ui/tests/app/groupings/[groupingPath]/_components/return-buttons.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/_components/return-buttons.test.tsx @@ -1,15 +1,16 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import ReturnButtons from '@/app/groupings/[groupingPath]/_components/return-buttons'; describe('ReturnButtons Component', () => { - test('renders button to return to Groupings List when fromManageSubject is false', () => { + it('renders button to return to Groupings List when fromManageSubject is false', () => { render(); expect(screen.getByText(/return to groupings list/i)).toBeInTheDocument(); expect(screen.queryByText(/return to manage person/i)).not.toBeInTheDocument(); }); - test('renders button to return to Manage Person when fromManageSubject is true', () => { + it('renders button to return to Manage Person when fromManageSubject is true', () => { render(); expect(screen.getByText(/return to manage person/i)).toBeInTheDocument(); diff --git a/ui/tests/app/groupings/[groupingPath]/_components/side-nav.test.tsx b/ui/tests/app/groupings/[groupingPath]/_components/side-nav.test.tsx index 49f8b805..256ae36c 100644 --- a/ui/tests/app/groupings/[groupingPath]/_components/side-nav.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/_components/side-nav.test.tsx @@ -1,14 +1,15 @@ +import { vi, describe, beforeEach, it, expect, Mock } from 'vitest'; import { render, screen } from '@testing-library/react'; import SideNav from '@/app/groupings/[groupingPath]/_components/side-nav'; import { usePathname } from 'next/navigation'; -jest.mock('next/navigation'); +vi.mock('next/navigation'); describe('SideNav Component', () => { const groupingPath = 'test-grouping'; beforeEach(() => { - (usePathname as jest.Mock).mockReturnValue(`/groupings/${groupingPath}/all-members`); + (usePathname as Mock).mockReturnValue(`/groupings/${groupingPath}/all-members`); }); it('renders link', () => { diff --git a/ui/tests/app/groupings/[groupingPath]/layout.test.tsx b/ui/tests/app/groupings/[groupingPath]/layout.test.tsx index 1691ad55..57a635bb 100644 --- a/ui/tests/app/groupings/[groupingPath]/layout.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/layout.test.tsx @@ -1,3 +1,4 @@ +import { vi, beforeEach, describe, it, expect, Mock } from 'vitest'; import { render, screen } from '@testing-library/react'; import GroupingPathLayout from '@/app/groupings/[groupingPath]/layout'; import { groupingDescription } from '@/lib/fetchers'; @@ -5,9 +6,9 @@ import { usePathname } from 'next/navigation'; import { GroupingDescription } from '@/lib/types'; import GroupingHeader from '@/app/groupings/[groupingPath]/_components/grouping-header'; -jest.mock('next/navigation'); -jest.mock('@/lib/fetchers'); -jest.mock('@/app/groupings/[groupingPath]/_components/grouping-header'); +vi.mock('next/navigation'); +vi.mock('@/lib/fetchers'); +vi.mock('@/app/groupings/[groupingPath]/_components/grouping-header'); const mockData: GroupingDescription = { groupPath: 'Test-path:Test-name', @@ -16,8 +17,8 @@ const mockData: GroupingDescription = { }; beforeEach(() => { - (groupingDescription as jest.Mock).mockResolvedValue(mockData); - (usePathname as jest.Mock).mockReturnValue('/groupings/Test-Path/Test-name'); + (groupingDescription as Mock).mockResolvedValue(mockData); + (usePathname as Mock).mockReturnValue('/groupings/Test-Path/Test-name'); }); describe('GroupingPathLayout', () => { diff --git a/ui/tests/app/groupings/[groupingPath]/tab/actions/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/actions/page.test.tsx index c9229acd..eb44f1b6 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/actions/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/actions/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import ActionsTab from '@/app/groupings/[groupingPath]/@tab/actions/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/all-members/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/all-members/page.test.tsx index c92034fe..dcb78200 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/all-members/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/all-members/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import AllMembersTab from '@/app/groupings/[groupingPath]/@tab/all-members/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/basis/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/basis/page.test.tsx index 2caa211f..ac784af2 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/basis/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/basis/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import BasisTab from '@/app/groupings/[groupingPath]/@tab/basis/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/exclude/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/exclude/page.test.tsx index e46918f4..e253d200 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/exclude/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/exclude/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import ExcludeTab from '@/app/groupings/[groupingPath]/@tab/exclude/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/include/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/include/page.test.tsx index 0a84735f..55df1862 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/include/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/include/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import IncludeTab from '@/app/groupings/[groupingPath]/@tab/include/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/layout.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/layout.test.tsx index cc1e2c40..14824c8f 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/layout.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/layout.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import TabLayout from '@/app/groupings/[groupingPath]/@tab/layout'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/owners/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/owners/page.test.tsx index 9a2d792c..2d38b131 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/owners/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/owners/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import OwnersTab from '@/app/groupings/[groupingPath]/@tab/owners/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/preferences/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/preferences/page.test.tsx index fa29ccaf..11f0d9ed 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/preferences/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/preferences/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import PreferencesTab from '@/app/groupings/[groupingPath]/@tab/preferences/page'; diff --git a/ui/tests/app/groupings/[groupingPath]/tab/sync-destinations/page.test.tsx b/ui/tests/app/groupings/[groupingPath]/tab/sync-destinations/page.test.tsx index 384e80b9..60c98564 100644 --- a/ui/tests/app/groupings/[groupingPath]/tab/sync-destinations/page.test.tsx +++ b/ui/tests/app/groupings/[groupingPath]/tab/sync-destinations/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import SyncDestinationsTab from '@/app/groupings/[groupingPath]/@tab/sync-destinations/page'; diff --git a/ui/tests/app/groupings/layout.test.tsx b/ui/tests/app/groupings/layout.test.tsx index bffd5dd9..56433cd2 100644 --- a/ui/tests/app/groupings/layout.test.tsx +++ b/ui/tests/app/groupings/layout.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import GroupingsLayout from '@/app/groupings/layout'; diff --git a/ui/tests/app/groupings/page.test.tsx b/ui/tests/app/groupings/page.test.tsx index 260a26de..fb456130 100644 --- a/ui/tests/app/groupings/page.test.tsx +++ b/ui/tests/app/groupings/page.test.tsx @@ -2,8 +2,9 @@ import { render, screen, waitFor } from '@testing-library/react'; import Groupings from '@/app/groupings/page'; import * as Fetchers from '@/lib/fetchers'; import { GroupingPaths } from '@/lib/types'; +import { vi, beforeEach, describe, it, expect } from 'vitest'; -jest.mock('@/lib/fetchers'); +vi.mock('@/lib/fetchers'); const mockData: GroupingPaths = { resultCode: 'SUCCESS', @@ -15,7 +16,7 @@ const mockData: GroupingPaths = { }; beforeEach(() => { - jest.spyOn(Fetchers, 'ownerGroupings').mockResolvedValue(mockData); + vi.spyOn(Fetchers, 'ownerGroupings').mockResolvedValue(mockData); }); describe('Groupings', () => { diff --git a/ui/tests/app/memberships/page.test.tsx b/ui/tests/app/memberships/page.test.tsx index 05e5aa86..b247657c 100644 --- a/ui/tests/app/memberships/page.test.tsx +++ b/ui/tests/app/memberships/page.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import MembershipsLayout from '@/app/memberships/layout'; import Memberships from '@/app/memberships/page'; import { render, screen } from '@testing-library/react'; diff --git a/ui/tests/components/layout/footer.test.tsx b/ui/tests/components/layout/footer.test.tsx index 339f6b67..47d71da4 100644 --- a/ui/tests/components/layout/footer.test.tsx +++ b/ui/tests/components/layout/footer.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import Footer from '@/components/layout/footer'; diff --git a/ui/tests/components/layout/heading.test.tsx b/ui/tests/components/layout/heading.test.tsx index 64debcfc..825a273f 100644 --- a/ui/tests/components/layout/heading.test.tsx +++ b/ui/tests/components/layout/heading.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import Heading from '@/components/layout/heading'; import { render, screen } from '@testing-library/react'; diff --git a/ui/tests/components/layout/navbar/dept-account-icon.test.tsx b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx index 53610fb7..546676ea 100644 --- a/ui/tests/components/layout/navbar/dept-account-icon.test.tsx +++ b/ui/tests/components/layout/navbar/dept-account-icon.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import DeptAccountIcon from '@/components/layout/navbar/dept-account-icon'; import User, { AnonymousUser } from '@/lib/access/user'; @@ -5,7 +6,8 @@ import * as NextCasClient from 'next-cas-client/app'; import Role from '@/lib/access/role'; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); + +vi.mock('next-cas-client/app'); describe('Dept Account Icon', () => { it('should render the Departmental Account icon and open warning modal when clicked on', () => { @@ -21,7 +23,7 @@ describe('Dept Account Icon', () => { }); it('should not render the Departmental Account icon for other roles', () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); testUser.roles = [Role.ANONYMOUS, Role.ADMIN, Role.UH, Role.OWNER]; render(); diff --git a/ui/tests/components/layout/navbar/login-button.test.tsx b/ui/tests/components/layout/navbar/login-button.test.tsx index 78dc27b5..c3cf049a 100644 --- a/ui/tests/components/layout/navbar/login-button.test.tsx +++ b/ui/tests/components/layout/navbar/login-button.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeAll } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import Login from '@/components/layout/navbar/login-button'; @@ -7,7 +8,7 @@ import * as NextCasClient from 'next-cas-client'; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client'); +vi.mock('next-cas-client'); describe('Login', () => { describe('User is not logged in', () => { diff --git a/ui/tests/components/layout/navbar/navbar-menu.test.tsx b/ui/tests/components/layout/navbar/navbar-menu.test.tsx index 7ece65b4..4fa43271 100644 --- a/ui/tests/components/layout/navbar/navbar-menu.test.tsx +++ b/ui/tests/components/layout/navbar/navbar-menu.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect, beforeEach } from 'vitest'; import Role from '@/lib/access/role'; import User, { AnonymousUser } from '@/lib/access/user'; import NavbarMenu from '@/components/layout/navbar/navbar-menu'; diff --git a/ui/tests/components/layout/navbar/navbar.test.tsx b/ui/tests/components/layout/navbar/navbar.test.tsx index 2b49161c..d2fb6275 100644 --- a/ui/tests/components/layout/navbar/navbar.test.tsx +++ b/ui/tests/components/layout/navbar/navbar.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; import User, { AnonymousUser } from '@/lib/access/user'; import * as NextCasClient from 'next-cas-client/app'; import { render, screen } from '@testing-library/react'; @@ -6,12 +7,12 @@ import Role from '@/lib/access/role'; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); +vi.mock('next-cas-client/app'); describe('Navbar', () => { describe('User is logged-out', () => { it('should render the navbar with only the link to /about', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(AnonymousUser); render(await Navbar()); expect(screen.getByRole('navigation')).toBeInTheDocument(); @@ -36,7 +37,7 @@ describe('Navbar', () => { it('should render only /memberships, /about, /feedback for the average user', async () => { testUser.roles.push(Role.UH); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); render(await Navbar()); expect(screen.getByRole('navigation')).toBeInTheDocument(); @@ -55,7 +56,7 @@ describe('Navbar', () => { it('should render only /memberships, /groupings, /about, /feedback for an owner of a grouping', async () => { testUser.roles.push(Role.OWNER, Role.UH); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); render(await Navbar()); expect(screen.getByRole('navigation')).toBeInTheDocument(); @@ -74,7 +75,7 @@ describe('Navbar', () => { it('should render all links for an Admin', async () => { testUser.roles.push(Role.ADMIN, Role.UH); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); render(await Navbar()); expect(screen.getByRole('navigation')).toBeInTheDocument(); @@ -93,7 +94,7 @@ describe('Navbar', () => { it('should render the departmental icon for a Departmental Account without Admin or Groupings links', async () => { testUser.roles.push(Role.DEPARTMENTAL, Role.UH); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); render(await Navbar()); expect(screen.getByRole('navigation')).toBeInTheDocument(); diff --git a/ui/tests/components/modal/api-error-modal.test.tsx b/ui/tests/components/modal/api-error-modal.test.tsx index 6d4fbdbd..6abfaa61 100644 --- a/ui/tests/components/modal/api-error-modal.test.tsx +++ b/ui/tests/components/modal/api-error-modal.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import ApiErrorModal from '@/components/modal/api-error-modal'; diff --git a/ui/tests/components/modal/dynamic-modal.test.tsx b/ui/tests/components/modal/dynamic-modal.test.tsx index 2df95ced..d5ee3483 100644 --- a/ui/tests/components/modal/dynamic-modal.test.tsx +++ b/ui/tests/components/modal/dynamic-modal.test.tsx @@ -1,11 +1,11 @@ +import { describe, it, vi, expect } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import DynamicModal from '@/components/modal/dynamic-modal'; import Link from 'next/link'; -import { useState } from 'react'; describe('DynamicModal', () => { it('should open an informational modal with test contents and no extra buttons', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( ); @@ -18,7 +18,7 @@ describe('DynamicModal', () => { }); it('should open an informational modal with test contents and extra buttons', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( { }); it('should close the modal upon clicking the OK button', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( { }); it('should close the modal and route to the provided link (Feedback)', () => { - const onClose = jest.fn(); + const onClose = vi.fn(); render( { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); testUser.roles = []; }); it('should not open the timeout modal when the user is not logged-in', () => { render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 25)); + act(() => vi.advanceTimersByTime(1000 * 60 * 25)); fireEvent.focus(document); expect(screen.queryByRole('alertdialog', { name: 'Inactivity Warning' })).not.toBeInTheDocument(); @@ -31,7 +32,7 @@ describe('TimeoutModal', () => { testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 24)); + act(() => vi.advanceTimersByTime(1000 * 60 * 24)); fireEvent.focus(document); expect(screen.queryByRole('alertdialog', { name: 'Inactivity Warning' })).not.toBeInTheDocument(); @@ -41,7 +42,7 @@ describe('TimeoutModal', () => { testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 25)); + act(() => vi.advanceTimersByTime(1000 * 60 * 25)); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'Inactivity Warning' })).toBeInTheDocument(); @@ -51,14 +52,14 @@ describe('TimeoutModal', () => { testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 25)); + act(() => vi.advanceTimersByTime(1000 * 60 * 25)); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'Inactivity Warning' })).toBeInTheDocument(); for (let i = 5; i >= 0; i--) { expect(screen.getByText(i + ':00.')).toBeInTheDocument(); - act(() => jest.advanceTimersByTime(1000 * 60)); + act(() => vi.advanceTimersByTime(1000 * 60)); fireEvent.focus(document); } }); @@ -67,26 +68,26 @@ describe('TimeoutModal', () => { testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 25)); + act(() => vi.advanceTimersByTime(1000 * 60 * 25)); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'Inactivity Warning' })).toBeInTheDocument(); fireEvent.click(screen.getByRole('button', { name: 'Stay logged in' })); expect(screen.queryByRole('alertdialog', { name: 'Inactivity Warning' })).not.toBeInTheDocument(); - act(() => jest.advanceTimersByTime(1000 * 60 * 24)); + act(() => vi.advanceTimersByTime(1000 * 60 * 24)); fireEvent.focus(document); expect(screen.queryByRole('alertdialog', { name: 'Inactivity Warning' })).not.toBeInTheDocument(); }); it('should logout when "Log off now" is pressed', () => { - const logoutSpy = jest.spyOn(NextCasClient, 'logout'); + const logoutSpy = vi.spyOn(NextCasClient, 'logout'); testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 25)); + act(() => vi.advanceTimersByTime(1000 * 60 * 25)); fireEvent.focus(document); expect(screen.getByRole('alertdialog', { name: 'Inactivity Warning' })).toBeInTheDocument(); @@ -95,12 +96,12 @@ describe('TimeoutModal', () => { }); it('should logout after 30 minutes of idle', () => { - const logoutSpy = jest.spyOn(NextCasClient, 'logout'); + const logoutSpy = vi.spyOn(NextCasClient, 'logout'); testUser.roles.push(Role.UH); render(); - act(() => jest.advanceTimersByTime(1000 * 60 * 30 + 1)); + act(() => vi.advanceTimersByTime(1000 * 60 * 30 + 1)); fireEvent.focus(document); expect(logoutSpy).toHaveBeenCalled(); diff --git a/ui/tests/components/table/groupings-table-skeleton.test.tsx b/ui/tests/components/table/groupings-table/groupings-table-skeleton.test.tsx similarity index 100% rename from ui/tests/components/table/groupings-table-skeleton.test.tsx rename to ui/tests/components/table/groupings-table/groupings-table-skeleton.test.tsx diff --git a/ui/tests/components/table/groupings-table/groupings-table.test.tsx b/ui/tests/components/table/groupings-table/groupings-table.test.tsx index 54babd53..a0ac854b 100644 --- a/ui/tests/components/table/groupings-table/groupings-table.test.tsx +++ b/ui/tests/components/table/groupings-table/groupings-table.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import GroupingsTable from '@/components/table/groupings-table/groupings-table'; import userEvent from '@testing-library/user-event'; diff --git a/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx index 684acf8a..1c021002 100644 --- a/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-description-cell.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import GroupingDescriptionCell from '@/components/table/groupings-table/table-element/grouping-description-cell'; diff --git a/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx index c6533cd3..fafc9259 100644 --- a/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-name-cell.test.tsx @@ -1,4 +1,5 @@ -import { render, screen} from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; import GroupingNameCell from '@/components/table/groupings-table/table-element/grouping-name-cell'; describe('GroupingNameCell', () => { @@ -8,6 +9,6 @@ describe('GroupingNameCell', () => { render(); expect(screen.getByText(name)).toBeInTheDocument(); expect(screen.getByTestId('edit-icon')).toBeInTheDocument(); - expect(screen.getByRole('link')).toHaveAttribute('href', `/groupings/${path}/all-members`) + expect(screen.getByRole('link')).toHaveAttribute('href', `/groupings/${path}/all-members`); }); }); diff --git a/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx b/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx index 83351534..ef17e0cb 100644 --- a/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx +++ b/ui/tests/components/table/groupings-table/table-element/grouping-path-cell.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect } from 'vitest'; import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import GroupingPathCell from '@/components/table/groupings-table/table-element/grouping-path-cell'; import userEvent from '@testing-library/user-event'; @@ -28,11 +29,11 @@ describe('GroupingPathCell', () => { it('shows tooltip correctly when copying to clipboard', async () => { Object.defineProperty(navigator, 'clipboard', { value: { - writeText: jest.fn(() => Promise.resolve()) + writeText: vi.fn(() => Promise.resolve()) }, writable: true }); - jest.spyOn(navigator.clipboard, 'writeText').mockImplementation(() => Promise.resolve()); + vi.spyOn(navigator.clipboard, 'writeText').mockImplementation(() => Promise.resolve()); render(); const clipboardButton = screen.getByRole('button'); diff --git a/ui/tests/components/table/table-element/column-settings.test.tsx b/ui/tests/components/table/table-element/column-settings.test.tsx index aaab01cf..da3ad5e6 100644 --- a/ui/tests/components/table/table-element/column-settings.test.tsx +++ b/ui/tests/components/table/table-element/column-settings.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, beforeEach, it, expect } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import ColumnSettings from '@/components/table/table-element/column-settings'; @@ -15,7 +16,7 @@ const mockColumns = [ getCanHide: () => true, getIsVisible: () => mockColumnVisibility['description'], columnDef: { header: 'description' }, - toggleVisibility: jest.fn((isVisible: boolean) => { + toggleVisibility: vi.fn((isVisible: boolean) => { mockColumnVisibility['description'] = isVisible; }) }, @@ -24,13 +25,13 @@ const mockColumns = [ getCanHide: () => true, getIsVisible: () => mockColumnVisibility['path'], columnDef: { header: 'path' }, - toggleVisibility: jest.fn((isVisible: boolean) => { + toggleVisibility: vi.fn((isVisible: boolean) => { mockColumnVisibility['path'] = isVisible; }) } ]; -const mockGetAllColumns = jest.fn().mockReturnValue(mockColumns); +const mockGetAllColumns = vi.fn().mockReturnValue(mockColumns); const mockTable = { getAllColumns: mockGetAllColumns @@ -38,7 +39,7 @@ const mockTable = { describe('ColumnSettings', () => { beforeEach(() => { - jest.clearAllMocks(); + vi.clearAllMocks(); mockColumnVisibility.description = true; mockColumnVisibility.path = false; }); diff --git a/ui/tests/components/table/table-element/global-filter.test.tsx b/ui/tests/components/table/table-element/global-filter.test.tsx index 5de9623d..c589a4e3 100644 --- a/ui/tests/components/table/table-element/global-filter.test.tsx +++ b/ui/tests/components/table/table-element/global-filter.test.tsx @@ -1,8 +1,9 @@ +import { vi, describe, it, expect } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import GlobalFilter from '@/components/table/table-element/global-filter'; describe('GlobalFilter', () => { - const mockSetFilter = jest.fn(); + const mockSetFilter = vi.fn(); it('renders the input with correct placeholder and value', () => { render(); diff --git a/ui/tests/components/table/table-element/pagination-bar.test.tsx b/ui/tests/components/table/table-element/pagination-bar.test.tsx index 360c5039..f1f6a33f 100644 --- a/ui/tests/components/table/table-element/pagination-bar.test.tsx +++ b/ui/tests/components/table/table-element/pagination-bar.test.tsx @@ -1,16 +1,17 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; import { fireEvent, render, screen } from '@testing-library/react'; import { Table } from '@tanstack/table-core'; import { GroupingPath } from '@/lib/types'; import PaginationBar from '@/components/table/table-element/pagination-bar'; -const mockGetPageCount = jest.fn(); -const mockGetCanPreviousPage = jest.fn(); -const mockGetCanNextPage = jest.fn(); -const mockFirstPage = jest.fn(); -const mockPreviousPage = jest.fn(); -const mockSetPageIndex = jest.fn(); -const mockNextPage = jest.fn(); -const mockLastPage = jest.fn(); +const mockGetPageCount = vi.fn(); +const mockGetCanPreviousPage = vi.fn(); +const mockGetCanNextPage = vi.fn(); +const mockFirstPage = vi.fn(); +const mockPreviousPage = vi.fn(); +const mockSetPageIndex = vi.fn(); +const mockNextPage = vi.fn(); +const mockLastPage = vi.fn(); const mockTable = { getPageCount: mockGetPageCount, @@ -24,6 +25,10 @@ const mockTable = { } as unknown as Table; describe('Pagination', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + it('renders pagination Bar correctly', () => { mockGetPageCount.mockReturnValue(6); mockGetCanPreviousPage.mockReturnValue(true); diff --git a/ui/tests/components/table/table-element/sort-arrow.test.tsx b/ui/tests/components/table/table-element/sort-arrow.test.tsx index 2e04532e..3f2f8f34 100644 --- a/ui/tests/components/table/table-element/sort-arrow.test.tsx +++ b/ui/tests/components/table/table-element/sort-arrow.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen } from '@testing-library/react'; import SortArrow from '@/components/table/table-element/sort-arrow'; diff --git a/ui/tests/components/table/table-element/tooltip-on-truncate.test.tsx b/ui/tests/components/table/table-element/tooltip-on-truncate.test.tsx index 61d00449..60e868fc 100644 --- a/ui/tests/components/table/table-element/tooltip-on-truncate.test.tsx +++ b/ui/tests/components/table/table-element/tooltip-on-truncate.test.tsx @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import TooltipOnTruncate from '@/components/table/table-element/tooltip-on-truncate'; diff --git a/ui/tests/components/uh-groupings-info.test.tsx b/ui/tests/components/uh-groupings-info.test.tsx index 99b561d0..64d9f83f 100644 --- a/ui/tests/components/uh-groupings-info.test.tsx +++ b/ui/tests/components/uh-groupings-info.test.tsx @@ -1,3 +1,4 @@ +import { describe, expect, it } from 'vitest'; import { render, screen } from '@testing-library/react'; import UHGroupingsInfo from '@/components/uh-groupings-info'; diff --git a/ui/tests/lib/access/authorization.test.ts b/ui/tests/lib/access/authorization.test.ts index cbc7da16..03c568b1 100644 --- a/ui/tests/lib/access/authorization.test.ts +++ b/ui/tests/lib/access/authorization.test.ts @@ -1,6 +1,10 @@ +import { vi, describe, afterEach, it, expect } from 'vitest'; import { setRoles } from '@/lib/access/authorization'; import Role from '@/lib/access/role'; import User, { AnonymousUser } from '@/lib/access/user'; +import * as Fetchers from '@/lib/fetchers'; + +vi.mock('@/lib/fetchers'); const testUser: User = JSON.parse(process.env.TEST_USER_A as string); @@ -12,9 +16,8 @@ describe('authorization', () => { }); it('should set the ANONYMOUS role', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(false)) // isOwner - .mockResponseOnce(JSON.stringify(false)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(false); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(false); await setRoles(AnonymousUser); expect(AnonymousUser.roles.includes(Role.ADMIN)).toBeFalsy(); @@ -24,9 +27,8 @@ describe('authorization', () => { }); it('should set the UH role', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(false)) // isOwner - .mockResponseOnce(JSON.stringify(false)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(false); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(false); await setRoles(testUser); expect(testUser.roles.includes(Role.ADMIN)).toBeFalsy(); @@ -36,9 +38,8 @@ describe('authorization', () => { }); it('should set the UH and ADMIN roles', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(false)) // isOwner - .mockResponseOnce(JSON.stringify(true)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(false); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(true); await setRoles(testUser); expect(testUser.roles.includes(Role.ADMIN)).toBeTruthy(); @@ -48,9 +49,8 @@ describe('authorization', () => { }); it('should set the UH and OWNER roles', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(true)) // isOwner - .mockResponseOnce(JSON.stringify(false)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(true); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(false); await setRoles(testUser); expect(testUser.roles.includes(Role.ADMIN)).toBeFalsy(); @@ -60,9 +60,8 @@ describe('authorization', () => { }); it('should set the UH, ADMIN, and OWNER roles', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(true)) // isOwner - .mockResponseOnce(JSON.stringify(true)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(true); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(true); await setRoles(testUser); expect(testUser.roles.includes(Role.ADMIN)).toBeTruthy(); @@ -70,15 +69,5 @@ describe('authorization', () => { expect(testUser.roles.includes(Role.OWNER)).toBeTruthy(); expect(testUser.roles.includes(Role.UH)).toBeTruthy(); }); - - it('should catch Groupings API errors', async () => { - fetchMock.mockAbort(); - - await setRoles(testUser); - expect(testUser.roles.includes(Role.ADMIN)).toBeFalsy(); - expect(testUser.roles.includes(Role.ANONYMOUS)).toBeTruthy(); - expect(testUser.roles.includes(Role.OWNER)).toBeFalsy(); - expect(testUser.roles.includes(Role.UH)).toBeTruthy(); - }); }); }); diff --git a/ui/tests/lib/access/role.test.ts b/ui/tests/lib/access/role.test.ts index d43245b0..3136c9b1 100644 --- a/ui/tests/lib/access/role.test.ts +++ b/ui/tests/lib/access/role.test.ts @@ -1,3 +1,4 @@ +import { describe, it, expect } from 'vitest'; import Role from '@/lib/access/role'; describe('role', () => { diff --git a/ui/tests/lib/access/user.test.ts b/ui/tests/lib/access/user.test.ts index fc9f3645..0f91930a 100644 --- a/ui/tests/lib/access/user.test.ts +++ b/ui/tests/lib/access/user.test.ts @@ -1,10 +1,13 @@ +import { vi, describe, it, expect } from 'vitest'; import Role from '@/lib/access/role'; import User, { AnonymousUser, getUser, loadUser } from '@/lib/access/user'; import * as NextCasClient from 'next-cas-client/app'; +import * as Fetchers from '@/lib/fetchers'; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); +vi.mock('next-cas-client/app'); +vi.mock('@/lib/fetchers'); describe('user', () => { describe('loadUser', () => { @@ -21,9 +24,8 @@ describe('user', () => { }; it('should return a User', async () => { - fetchMock - .mockResponseOnce(JSON.stringify(false)) // isOwner - .mockResponseOnce(JSON.stringify(false)); // isAdmin + vi.spyOn(Fetchers, 'isOwner').mockResolvedValue(false); + vi.spyOn(Fetchers, 'isAdmin').mockResolvedValue(false); expect(await loadUser(casUser)).toEqual(testUser); }); @@ -31,14 +33,14 @@ describe('user', () => { describe('getUser', () => { it('should call getCurrentUser', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); expect(await getUser()).toEqual(testUser); expect(NextCasClient.getCurrentUser).toHaveBeenCalled(); }); it('should return an AnonymousUser if getCurrentUser is null', async () => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(null); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(null); expect(await getUser()).toEqual(AnonymousUser); expect(NextCasClient.getCurrentUser).toHaveBeenCalled(); diff --git a/ui/tests/lib/actions.test.ts b/ui/tests/lib/actions.test.ts index b2f9e3eb..969e5137 100644 --- a/ui/tests/lib/actions.test.ts +++ b/ui/tests/lib/actions.test.ts @@ -1,3 +1,4 @@ +import { vi, describe, beforeAll, it, expect, beforeEach, afterEach } from 'vitest'; import { addAdmin, addExcludeMembers, @@ -29,7 +30,7 @@ import { Feedback } from '@/lib/types'; const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); +vi.mock('next-cas-client/app'); describe('actions', () => { const currentUser = testUser; @@ -61,7 +62,7 @@ describe('actions', () => { }; beforeAll(() => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); }); describe('updateDescription', () => { @@ -116,11 +117,11 @@ describe('actions', () => { describe('addIncludeMembersAsync', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a PUT request at the correct endpoint', async () => { @@ -142,7 +143,7 @@ describe('actions', () => { .mockResponseOnce(JSON.stringify(mockAsyncCompletedResponse)); const res = addIncludeMembersAsync(uhIdentifiers, groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockAsyncCompletedResponse.result); }); @@ -154,7 +155,7 @@ describe('actions', () => { fetchMock.mockResponseOnce(JSON.stringify(0)).mockRejectOnce(() => Promise.reject(mockError)); res = addIncludeMembersAsync(uhIdentifiers, groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); @@ -185,11 +186,11 @@ describe('actions', () => { describe('addExcludeMembersAsync', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a PUT request at the correct endpoint', async () => { @@ -211,7 +212,7 @@ describe('actions', () => { .mockResponseOnce(JSON.stringify(mockAsyncCompletedResponse)); const res = addExcludeMembersAsync(uhIdentifiers, groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockAsyncCompletedResponse.result); }); @@ -223,7 +224,7 @@ describe('actions', () => { fetchMock.mockResponseOnce(JSON.stringify(0)).mockRejectOnce(() => Promise.reject(mockError)); res = addExcludeMembersAsync(uhIdentifiers, groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); @@ -417,11 +418,11 @@ describe('actions', () => { describe('memberAttributeResultsAsync', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a POST request at the correct endpoint', async () => { @@ -443,7 +444,7 @@ describe('actions', () => { .mockResponseOnce(JSON.stringify(mockAsyncCompletedResponse)); const res = memberAttributeResultsAsync(uhIdentifiers); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockAsyncCompletedResponse.result); }); @@ -455,7 +456,7 @@ describe('actions', () => { fetchMock.mockResponseOnce(JSON.stringify(0)).mockRejectOnce(() => Promise.reject(mockError)); res = memberAttributeResultsAsync(uhIdentifiers); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); @@ -537,11 +538,11 @@ describe('actions', () => { describe('resetIncludeGroupAsync', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a DELETE request at the correct endpoint', async () => { @@ -562,7 +563,7 @@ describe('actions', () => { .mockResponseOnce(JSON.stringify(mockAsyncCompletedResponse)); const res = resetIncludeGroupAsync(groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockAsyncCompletedResponse.result); }); @@ -574,7 +575,7 @@ describe('actions', () => { fetchMock.mockResponseOnce(JSON.stringify(0)).mockRejectOnce(() => Promise.reject(mockError)); res = resetIncludeGroupAsync(groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); @@ -604,11 +605,11 @@ describe('actions', () => { describe('resetExcludeGroupAsync', () => { beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a DELETE request at the correct endpoint', async () => { @@ -629,7 +630,7 @@ describe('actions', () => { .mockResponseOnce(JSON.stringify(mockAsyncCompletedResponse)); const res = resetExcludeGroupAsync(groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockAsyncCompletedResponse.result); }); @@ -641,7 +642,7 @@ describe('actions', () => { fetchMock.mockResponseOnce(JSON.stringify(0)).mockRejectOnce(() => Promise.reject(mockError)); res = resetExcludeGroupAsync(groupingPath); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); diff --git a/ui/tests/lib/fetchers.test.ts b/ui/tests/lib/fetchers.test.ts index 2640c2ff..cd15810a 100644 --- a/ui/tests/lib/fetchers.test.ts +++ b/ui/tests/lib/fetchers.test.ts @@ -9,6 +9,8 @@ import { groupingOptAttributes, groupingOwners, groupingSyncDest, + isAdmin, + isOwner, isSoleOwner, managePersonResults, membershipResults, @@ -18,12 +20,13 @@ import { } from '@/lib/fetchers'; import * as NextCasClient from 'next-cas-client/app'; import * as Actions from '@/lib/actions'; +import { vi, describe, beforeAll, it, expect, beforeEach, afterEach } from 'vitest'; const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); -jest.mock('@/lib/actions'); +vi.mock('next-cas-client/app'); +vi.mock('@/lib/actions'); describe('fetchers', () => { const currentUser = testUser; @@ -45,8 +48,8 @@ describe('fetchers', () => { }; beforeAll(() => { - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); - jest.spyOn(Actions, 'sendStackTrace'); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(Actions, 'sendStackTrace'); }); describe('getAllGroupings', () => { @@ -94,11 +97,11 @@ describe('fetchers', () => { const isAscending = true; beforeEach(() => { - jest.useFakeTimers(); + vi.useFakeTimers(); }); afterEach(() => { - jest.useRealTimers(); + vi.useRealTimers(); }); it('should make a POST request at the correct endpoint', async () => { @@ -126,7 +129,7 @@ describe('fetchers', () => { .mockResponseOnce(JSON.stringify(mockResponse), { status: 500 }) .mockResponseOnce(JSON.stringify(mockResponse)); let res = ownedGrouping(groupPaths, page, size, sortString, isAscending); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockResponse); fetchMock @@ -134,7 +137,7 @@ describe('fetchers', () => { .mockResponseOnce(JSON.stringify(mockResponse), { status: 500 }) .mockResponseOnce(JSON.stringify(mockResponse)); res = ownedGrouping(groupPaths, page, size, sortString, isAscending); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockResponse); fetchMock @@ -143,19 +146,19 @@ describe('fetchers', () => { .mockResponseOnce(JSON.stringify(mockResponse), { status: 500 }) .mockResponseOnce(JSON.stringify(mockResponse)); res = ownedGrouping(groupPaths, page, size, sortString, isAscending); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockResponse); }); it('should handle the error response', async () => { fetchMock.mockResponse(JSON.stringify(mockError), { status: 500 }); let res = ownedGrouping(groupPaths, page, size, sortString, isAscending); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); fetchMock.mockReject(() => Promise.reject(mockError)); res = ownedGrouping(groupPaths, page, size, sortString, isAscending); - await jest.advanceTimersByTimeAsync(5000); + await vi.advanceTimersByTimeAsync(5000); expect(await res).toEqual(mockError); }); }); @@ -387,4 +390,42 @@ describe('fetchers', () => { expect(await isSoleOwner(uhIdentifier, groupingPath)).toEqual(mockError); }); }); + + describe('isOwner', () => { + it('should make a GET request at the correct endpoint', async () => { + await isOwner(uhIdentifier); + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/owners`, { + headers: { current_user: currentUser.uid } + }); + }); + + it('should handle the successful response', async () => { + fetchMock.mockResponse(JSON.stringify(mockResponse)); + expect(await isOwner(uhIdentifier)).toEqual(mockResponse); + }); + + it('should handle the error response', async () => { + fetchMock.mockReject(() => Promise.reject(mockError)); + expect(await isOwner(uhIdentifier)).toEqual(mockError); + }); + }); + + describe('isAdmin', () => { + it('should make a GET request at the correct endpoint', async () => { + await isAdmin(uhIdentifier); + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/admins`, { + headers: { current_user: currentUser.uid } + }); + }); + + it('should handle the successful response', async () => { + fetchMock.mockResponse(JSON.stringify(mockResponse)); + expect(await isAdmin(uhIdentifier)).toEqual(mockResponse); + }); + + it('should handle the error response', async () => { + fetchMock.mockReject(() => Promise.reject(mockError)); + expect(await isAdmin(uhIdentifier)).toEqual(mockError); + }); + }); }); diff --git a/ui/tests/middleware.test.tsx b/ui/tests/middleware.test.tsx index 9c5830c9..462cd914 100644 --- a/ui/tests/middleware.test.tsx +++ b/ui/tests/middleware.test.tsx @@ -1,3 +1,4 @@ +import { vi, describe, it, expect, afterEach } from 'vitest'; import { config, middleware } from '@/middleware'; import { NextRequest, NextResponse } from 'next/server'; import User from '@/lib/access/user'; @@ -7,7 +8,7 @@ import * as NextCasClient from 'next-cas-client/app'; const baseUrl = process.env.NEXT_PUBLIC_BASE_URL as string; const testUser: User = JSON.parse(process.env.TEST_USER_A as string); -jest.mock('next-cas-client/app'); +vi.mock('next-cas-client/app'); describe('middleware', () => { it('should define the config with a list of matching paths', () => { @@ -17,13 +18,13 @@ describe('middleware', () => { describe('User is logged-out', () => { it('should redirect the user', async () => { - const redirectSpy = jest.spyOn(NextResponse, 'redirect'); + const redirectSpy = vi.spyOn(NextResponse, 'redirect'); for (const matcher of config.matcher) { const url = baseUrl + matcher; const req = new NextRequest(new Request(url)); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(null); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(null); await middleware(req); expect(redirectSpy).toHaveBeenCalledWith(new URL(baseUrl)); @@ -39,13 +40,13 @@ describe('middleware', () => { it('should redirect the average user at /admin and /groupings', async () => { testUser.roles.push(Role.UH); - const redirectSpy = jest.spyOn(NextResponse, 'redirect'); + const redirectSpy = vi.spyOn(NextResponse, 'redirect'); for (const matcher of config.matcher) { const url = baseUrl + matcher; const req = new NextRequest(new Request(url)); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); await middleware(req); if (matcher === '/admin' || matcher === '/groupings') { @@ -60,13 +61,13 @@ describe('middleware', () => { it('should redirect an owner of a grouping at /admin', async () => { testUser.roles.push(Role.OWNER, Role.UH); - const redirectSpy = jest.spyOn(NextResponse, 'redirect'); + const redirectSpy = vi.spyOn(NextResponse, 'redirect'); for (const matcher of config.matcher) { const url = baseUrl + matcher; const req = new NextRequest(new Request(url)); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); await middleware(req); if (matcher === '/admin') { @@ -81,13 +82,13 @@ describe('middleware', () => { it('should not redirect an admin', async () => { testUser.roles.push(Role.ADMIN); - const redirectSpy = jest.spyOn(NextResponse, 'redirect'); + const redirectSpy = vi.spyOn(NextResponse, 'redirect'); for (const matcher of config.matcher) { const url = baseUrl + matcher; const req = new NextRequest(new Request(url)); - jest.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); await middleware(req); expect(redirectSpy).not.toHaveBeenCalled(); diff --git a/ui/tests/setup-jest.ts b/ui/tests/setup-jest.ts deleted file mode 100644 index cba98a72..00000000 --- a/ui/tests/setup-jest.ts +++ /dev/null @@ -1,14 +0,0 @@ -import '@testing-library/jest-dom'; -import { loadEnvConfig } from '@next/env'; -import { enableFetchMocks } from 'jest-fetch-mock'; -import User from '@/lib/access/user'; - -enableFetchMocks(); -loadEnvConfig(process.cwd()); - -export const createMockSession = (user: User | undefined) => ({ - user, - destroy: jest.fn(), - save: jest.fn(), - updateConfig: jest.fn() -}); diff --git a/ui/tests/vitest.setup.ts b/ui/tests/vitest.setup.ts new file mode 100644 index 00000000..89253ec4 --- /dev/null +++ b/ui/tests/vitest.setup.ts @@ -0,0 +1,15 @@ +import '@testing-library/jest-dom/vitest'; +import { loadEnvConfig } from '@next/env'; +import User from '@/lib/access/user'; +import { vi } from 'vitest'; +import createFetchMock from 'vitest-fetch-mock'; + +createFetchMock(vi).enableMocks(); +loadEnvConfig(process.cwd()); + +export const createMockSession = (user: User | undefined) => ({ + user, + destroy: vi.fn(), + save: vi.fn(), + updateConfig: vi.fn() +}); diff --git a/ui/vitest.config.mts b/ui/vitest.config.mts new file mode 100644 index 00000000..98978604 --- /dev/null +++ b/ui/vitest.config.mts @@ -0,0 +1,23 @@ +import { coverageConfigDefaults, defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + plugins: [tsconfigPaths(), react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: './tests/vitest.setup.ts', + coverage: { + provider: 'istanbul', + enabled: true, + include: ['**/src/**/*.ts*'], + exclude: [ + ...coverageConfigDefaults.exclude, + '**/src/components/ui' // Ignore shadcn/ui components + ], + reporter: ['text', 'json-summary', 'html'], + reportsDirectory: './coverage' + } + } +}); From fe1f0241e91cca2473283879138394d9fe51ee90 Mon Sep 17 00:00:00 2001 From: gitcarrot Date: Fri, 20 Dec 2024 13:49:23 -1000 Subject: [PATCH 6/8] Using OOTB in react project --- .../layout/navbar/dept-account-icon.tsx | 1 - ui/src/lib/access/user.ts | 62 ++++--- ui/src/lib/actions-ootb.ts | 16 +- ui/src/lib/types.ts | 7 +- ui/tests/lib/access/user.test.ts | 104 ++++++++++- ui/tests/lib/actions-ootb.test.ts | 163 ++++++++++++++++++ 6 files changed, 301 insertions(+), 52 deletions(-) create mode 100644 ui/tests/lib/actions-ootb.test.ts diff --git a/ui/src/components/layout/navbar/dept-account-icon.tsx b/ui/src/components/layout/navbar/dept-account-icon.tsx index a1608fdf..c881faa3 100644 --- a/ui/src/components/layout/navbar/dept-account-icon.tsx +++ b/ui/src/components/layout/navbar/dept-account-icon.tsx @@ -1,7 +1,6 @@ 'use client'; import { useState } from 'react'; - import DynamicModal from '@/components/modal/dynamic-modal'; import { faUser, faSchool } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; diff --git a/ui/src/lib/access/user.ts b/ui/src/lib/access/user.ts index 968e3c07..1299eace 100644 --- a/ui/src/lib/access/user.ts +++ b/ui/src/lib/access/user.ts @@ -3,7 +3,7 @@ import Role from './role'; import { CasUser } from 'next-cas-client'; import { setRoles } from './authorization'; import { getCurrentUser } from 'next-cas-client/app'; -import { getOotbCurrentUser, matchProfile, updateActiveDefaultUser } from '@/lib/actions-ootb'; +import { matchProfile, updateActiveDefaultUser } from '@/lib/actions-ootb'; import { OotbActiveProfile } from '../types'; type User = { roles: Role[]; @@ -18,35 +18,6 @@ export const AnonymousUser: User = { roles: [Role.ANONYMOUS] as const }; -export const loadOotbUser = async (profile: OotbActiveProfile): Promise => { - console.log('Loading OOTB user:', profile); - - const roles: Role[] = [Role.ANONYMOUS]; - - if (Array.isArray(profile.authorities)) { - const mappedRoles = profile.authorities - .map((authority) => { - const roleName = authority.replace(/^ROLE_/, ''); - return roleName.toUpperCase(); - }) - .filter((roleName) => Object.values(Role).includes(roleName as Role)) - .map((roleName) => roleName as Role); - roles.push(...mappedRoles); - } - - const user = { - name: profile.attributes.cn, - firstName: profile.attributes.givenName, - lastName: profile.attributes.sn, - uid: profile.uid, - uhUuid: profile.uhUuid, - roles: roles - } as User; - - console.log('OOTB user after setRoles:', user); - return user; -}; - export const loadUser = async (casUser: CasUser): Promise => { const user = { name: casUser.attributes.cn, @@ -62,12 +33,37 @@ export const loadUser = async (casUser: CasUser): Promise => { return user; }; +export const loadOotbUser = async (profile: OotbActiveProfile): Promise => { + const user = { + name: profile.attributes.cn, + firstName: profile.attributes.givenName, + lastName: profile.attributes.sn, + uid: profile.uid, + uhUuid: profile.uhUuid, + roles: [Role.ANONYMOUS, ...convertAuthoritiesToRoles(profile.authorities)] + } as User; + + return user; +}; + +const convertAuthoritiesToRoles = (authorities: string[]): Role[] => { + return authorities.map(authority => authority.replace(/^ROLE_/, '').toUpperCase()) + .filter(roleName => Object.values(Role).includes(roleName as Role)) + .map(roleName => roleName as Role); +}; + + export const getUser = async (): Promise => { if (process.env.NEXT_PUBLIC_OOTB_MODE === 'true') { const givenName = process.env.NEXT_PUBLIC_OOTB_PROFILE; - await updateActiveDefaultUser(givenName); - const profile = await matchProfile(givenName); - return profile ? await loadOotbUser(profile) : AnonymousUser; + try { + await updateActiveDefaultUser(givenName); + const profile = await matchProfile(givenName); + return profile ? await loadOotbUser(profile) : AnonymousUser; + } catch (error) { + console.error('Error fetching OOTB user:', error); + return AnonymousUser; + } } const user = (await getCurrentUser()) ?? AnonymousUser; return user; diff --git a/ui/src/lib/actions-ootb.ts b/ui/src/lib/actions-ootb.ts index ff5b7116..0d92c115 100644 --- a/ui/src/lib/actions-ootb.ts +++ b/ui/src/lib/actions-ootb.ts @@ -1,7 +1,8 @@ 'use server'; import { - OotbActiveProfile + OotbActiveProfile, + OotbActiveProfileResult } from './types'; import { postRequest, @@ -12,25 +13,16 @@ import ootbProfiles from '../../ootb.active.user.profiles.json' assert { type: ' const profiles = ootbProfiles as OotbActiveProfile[]; const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; -/** - * Fetches the current default user. - * @returns The current user as a string. - */ -export const getOotbCurrentUser = async (): Promise => { - const endpoint = `${baseUrl}/currentUser/ootb`; - return getRequest(endpoint); -}; - /** * Updates the active default user based on the given name. * @param givenName - The given name to match with attributes.givenName. * @returns The matched OotbActiveProfile. * @throws Error if no matching profile is found. */ -export const updateActiveDefaultUser = async (givenName: string | undefined): Promise => { +export const updateActiveDefaultUser = async (givenName: string | undefined): Promise => { const matchedProfile = await matchProfile(givenName); const endpoint = `${baseUrl}/activeProfile/ootb`; - return postRequest(endpoint, matchedProfile.uid, matchedProfile); + return postRequest(endpoint, matchedProfile.uid, matchedProfile); }; /** diff --git a/ui/src/lib/types.ts b/ui/src/lib/types.ts index 4a4c354e..1d0428da 100644 --- a/ui/src/lib/types.ts +++ b/ui/src/lib/types.ts @@ -187,4 +187,9 @@ export type OotbMember = { uid: string; uhUuid: string; name: string; -} \ No newline at end of file +} + +export type OotbActiveProfileResult = { + resultCode: string; + result: OotbActiveProfile +} \ No newline at end of file diff --git a/ui/tests/lib/access/user.test.ts b/ui/tests/lib/access/user.test.ts index 0f91930a..80bf4802 100644 --- a/ui/tests/lib/access/user.test.ts +++ b/ui/tests/lib/access/user.test.ts @@ -1,13 +1,21 @@ -import { vi, describe, it, expect } from 'vitest'; +import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; import Role from '@/lib/access/role'; -import User, { AnonymousUser, getUser, loadUser } from '@/lib/access/user'; +import User, { AnonymousUser, getUser, loadUser, loadOotbUser } from '@/lib/access/user'; import * as NextCasClient from 'next-cas-client/app'; import * as Fetchers from '@/lib/fetchers'; +import { matchProfile, updateActiveDefaultUser } from '@/lib/actions-ootb'; +import { OotbActiveProfile } from '@/lib/types'; + const testUser: User = JSON.parse(process.env.TEST_USER_A as string); vi.mock('next-cas-client/app'); vi.mock('@/lib/fetchers'); +vi.mock('@/lib/actions-ootb', () => ({ + matchProfile: vi.fn(), + updateActiveDefaultUser: vi.fn() +})); + describe('user', () => { describe('loadUser', () => { @@ -31,19 +39,105 @@ describe('user', () => { }); }); + describe('loadOotbUser', () => { + it('should return a user with correct roles from OotbActiveProfile', async () => { + const ootbProfile: OotbActiveProfile = { + uid: 'admin0123', + uhUuid: '33333333', + authorities: ['ROLE_ADMIN', 'ROLE_UH', 'ROLE_OWNER'], + attributes: { + cn: 'ADMIN', + mail: 'admin@hawaii.edu', + givenName: 'admin', + sn: 'AdminLastName' + }, + groupings: [] + }; + + const ootbUser = await loadOotbUser(ootbProfile); + expect(ootbUser.uid).toBe('admin0123'); + expect(ootbUser.roles.sort()).toEqual([Role.ANONYMOUS, Role.ADMIN, Role.OWNER, Role.UH].sort()); + expect(ootbUser.name).toBe('ADMIN'); + expect(ootbUser.firstName).toBe('admin'); + expect(ootbUser.lastName).toBe('AdminLastName'); + }); + }); + describe('getUser', () => { - it('should call getCurrentUser', async () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...originalEnv }; + vi.resetAllMocks(); + }); + + it('should call getCurrentUser in normal mode', async () => { + process.env.NEXT_PUBLIC_OOTB_MODE = 'false'; + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); expect(await getUser()).toEqual(testUser); expect(NextCasClient.getCurrentUser).toHaveBeenCalled(); }); + + it('should return an AnonymousUser if getCurrentUser is null in normal mode', async () => { + process.env.NEXT_PUBLIC_OOTB_MODE = 'false'; - it('should return an AnonymousUser if getCurrentUser is null', async () => { vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(null); expect(await getUser()).toEqual(AnonymousUser); expect(NextCasClient.getCurrentUser).toHaveBeenCalled(); }); + + describe('OOTB mode', () => { + const ootbProfile: OotbActiveProfile = { + uid: 'admin0123', + uhUuid: '33333333', + authorities: ['ROLE_ADMIN', 'ROLE_OWNER', 'ROLE_UH'], + attributes: { + cn: 'ADMIN', + mail: 'admin@hawaii.edu', + givenName: 'admin', + sn: 'AdminLastName' + }, + groupings: [] + }; + + beforeEach(() => { + process.env.NEXT_PUBLIC_OOTB_MODE = 'true'; + process.env.NEXT_PUBLIC_OOTB_PROFILE = 'admin'; + }); + + it('should return an OOTB user if matchProfile and updateActiveDefaultUser succeed', async () => { + (matchProfile as vi.Mock).mockResolvedValue(ootbProfile); + (updateActiveDefaultUser as vi.Mock).mockResolvedValue({ resultCode: 'SUCCESS', result: ootbProfile }); + + const ootbUser = await getUser(); + expect(ootbUser.uid).toBe('admin0123'); + expect(ootbUser.roles.sort()).toEqual([Role.ANONYMOUS, Role.ADMIN, Role.OWNER, Role.UH].sort()); + + expect(matchProfile).toHaveBeenCalledWith('admin'); + expect(updateActiveDefaultUser).toHaveBeenCalledWith('admin'); + }); + + it('should return AnonymousUser if matchProfile cannot find a profile', async () => { + (matchProfile as vi.Mock).mockRejectedValue(new Error('No profile found for givenName: admin')); + + expect(await getUser()).toEqual(AnonymousUser); + expect(matchProfile).toHaveBeenCalledWith('admin'); + }); + + it('should return AnonymousUser if updateActiveDefaultUser fails', async () => { + (matchProfile as vi.Mock).mockResolvedValue(ootbProfile); + (updateActiveDefaultUser as vi.Mock).mockRejectedValue(new Error('Failed to update user')); + + // If we want this test to pass as currently is (returning AnonymousUser), + // we should not re-throw in the catch block. Instead, we should return AnonymousUser. + // Let's assume we handle errors gracefully and return AnonymousUser. + + expect(await getUser()).toEqual(AnonymousUser); + expect(updateActiveDefaultUser).toHaveBeenCalledWith('admin'); + }); + }); }); -}); +}); \ No newline at end of file diff --git a/ui/tests/lib/actions-ootb.test.ts b/ui/tests/lib/actions-ootb.test.ts new file mode 100644 index 00000000..b6379c75 --- /dev/null +++ b/ui/tests/lib/actions-ootb.test.ts @@ -0,0 +1,163 @@ +import { vi, describe, beforeAll, it, expect, beforeEach } from 'vitest'; +import { updateActiveDefaultUser, matchProfile } from '@/lib/actions-ootb'; +import * as NextCasClient from 'next-cas-client/app'; +import User from '@/lib/access/user'; +import ootbProfiles from '../../ootb.active.user.profiles.json' assert { type: 'json' }; + +const baseUrl = process.env.NEXT_PUBLIC_API_2_1_BASE_URL as string; + +const testUser: User = { + ...JSON.parse(process.env.TEST_USER_A as string), + uid: 'admin0123' +}; + +vi.mock('next-cas-client/app'); + +describe('actions-ootb', () => { + const currentUser = testUser; + const givenName = 'admin'; + + const testProfile = { + uid: 'admin0123', + uhUuid: '33333333', + authorities: ['ROLE_ADMIN', 'ROLE_UH', 'ROLE_OWNER'], + attributes: { cn: 'ADMIN', mail: 'admin@hawaii.edu', givenName: 'admin' }, + groupings: [ + { + name: 'shared-group-in-each-profile:basis', + displayName: 'shared-group-in-each-profile:basis', + extension: 'basis', + displayExtension: 'basis', + description: 'This is a shared group in each profile', + members: [ + { name: 'shared-owner-3', uhUuid: '29382734', uid: 'sowner3' }, + { name: 'MemberUser', uhUuid: '11111111', uid: 'member0123' } + ] + }, + { + name: 'shared-group-in-groupings:owners', + displayName: 'shared-group-in-groupings:owners', + extension: 'owners', + displayExtension: 'owners', + description: 'This is a shared group in admin user groupings', + members: [ + { name: 'AdminUser', uid: 'admin0123', uhUuid: '33333333' }, + { name: 'OwnerUser', uid: 'owner0123', uhUuid: '22222222' } + ] + }, + { + name: 'shared-group-in-each-profile:owners', + displayName: 'shared-group-in-each-profile:owners', + extension: 'owners', + displayExtension: 'owners', + description: 'This is a shared group in each profile', + members: [ + { name: 'shared-owner-3', uhUuid: '29382734', uid: 'sowner3' } + ] + }, + { + name: 'admin-include:owners', + displayName: 'admin-include:owners', + extension: 'owners', + displayExtension: 'owners', + description: 'Admin owned include group', + members: [] + }, + { + name: 'admin-include:include', + displayName: 'admin-include:include', + extension: 'include', + displayExtension: 'include', + description: 'Admin owned include group', + members: [ + { name: 'AdminUser', uid: 'admin0123', uhUuid: '33333333' } + ] + }, + { + name: 'admin-group:owners', + displayName: 'admin-group:owners', + extension: 'owners', + displayExtension: 'owners', + description: 'Admin owned group', + members: [] + }, + { + name: 'owner-complex:owners', + displayName: 'Owner-Complex: Owners', + extension: 'owners', + displayExtension: 'Owners', + description: "Owner's owned complex group", + members: [ + { name: 'complex-member1', uhUuid: '32532314', uid: 'cmember1' }, + { name: 'complex-member2', uhUuid: '87453218', uid: 'cmember2' }, + { name: 'complex-member3', uhUuid: '56473829', uid: 'cmember3' }, + { name: 'complex-member4', uhUuid: '45261378', uid: 'cmember4' }, + { name: 'complex-member5', uhUuid: '98765432', uid: 'cmember5' }, + { name: 'complex-member6', uhUuid: '12345678', uid: 'cmember6' }, + { name: 'complex-member7', uhUuid: '19283746', uid: 'cmember7' }, + { name: 'complex-member8', uhUuid: '72635489', uid: 'cmember8' }, + { name: 'complex-member9', uhUuid: '65432109', uid: 'cmember9' }, + { name: 'complex-member10', uhUuid: '01234567', uid: 'cmember10' } + ] + } + ] + }; + + const mockResponse = { + resultCode: 'SUCCESS', + result: testProfile + }; + + const mockError = { resultCode: 'FAILURE' }; + + beforeAll(() => { + vi.spyOn(NextCasClient, 'getCurrentUser').mockResolvedValue(testUser); + }); + + beforeEach(() => { + fetchMock.resetMocks(); + }); + + describe('matchProfile', () => { + it('should return a profile when a matching givenName is found', async () => { + (ootbProfiles as any[]).push(testProfile); + const res = await matchProfile(givenName); + expect(res).toEqual(testProfile); + (ootbProfiles as any[]).pop(); + }); + + it('should throw an error if no matching profile is found', async () => { + const nonExistentName = 'NonExistent'; + await expect(matchProfile(nonExistentName)).rejects.toThrow( + `No profile found for givenName: ${nonExistentName}` + ); + }); + }); + + describe('updateActiveDefaultUser', () => { + it('should make a POST request at the correct endpoint with the matched profile and handle the successful response', async () => { + (ootbProfiles as any[]).push(testProfile); + fetchMock.mockResponseOnce(JSON.stringify(mockResponse)); + const res = await updateActiveDefaultUser(givenName); + expect(fetch).toHaveBeenCalledWith(`${baseUrl}/activeProfile/ootb`, expect.objectContaining({ + method: 'POST', + headers: { + current_user: currentUser.uid, + 'Content-Type': 'application/json' + } + })); + const [_, options] = fetchMock.mock.calls[0]; + const sentBody = JSON.parse((options as RequestInit).body as string); + expect(sentBody).toEqual(testProfile); + expect(res).toEqual(mockResponse); + (ootbProfiles as any[]).pop(); + }); + + it('should handle an error when profile is not found', async () => { + fetchMock.mockReject(() => Promise.reject(mockError)); + await expect(updateActiveDefaultUser('DoesNotExist')).rejects.toThrow( + 'No profile found for givenName: DoesNotExist' + ); + }); + }); +}); \ No newline at end of file From e3f7a5a7a4200d72233cb3cd90a4d01089b6c1a1 Mon Sep 17 00:00:00 2001 From: gitcarrot Date: Fri, 20 Dec 2024 14:45:42 -1000 Subject: [PATCH 7/8] fix eslint error and conflict --- ui/.env.development | 2 +- .../components/layout/navbar/dept-account-icon.tsx | 4 ---- ui/src/components/layout/navbar/login-button.tsx | 2 +- ui/src/lib/access/user.ts | 1 - ui/src/lib/actions-ootb.ts | 7 +++---- ui/src/lib/types.ts | 2 +- ui/tests/lib/access/user.test.ts | 12 ++++++------ 7 files changed, 12 insertions(+), 18 deletions(-) diff --git a/ui/.env.development b/ui/.env.development index 389a33f5..56efd1bc 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1,3 +1,3 @@ -NEXT_PUBLIC_OOTB_MODE=false +NEXT_PUBLIC_OOTB_MODE=true # Profiles: user, owner, admin NEXT_PUBLIC_OOTB_PROFILE=admin \ No newline at end of file diff --git a/ui/src/components/layout/navbar/dept-account-icon.tsx b/ui/src/components/layout/navbar/dept-account-icon.tsx index 30d6d2fb..c881faa3 100644 --- a/ui/src/components/layout/navbar/dept-account-icon.tsx +++ b/ui/src/components/layout/navbar/dept-account-icon.tsx @@ -1,10 +1,6 @@ 'use client'; import { useState } from 'react'; -<<<<<<< HEAD -======= - ->>>>>>> 76d761d0ed7fdad469a3fbcecc4b69c4a9d2222c import DynamicModal from '@/components/modal/dynamic-modal'; import { faUser, faSchool } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; diff --git a/ui/src/components/layout/navbar/login-button.tsx b/ui/src/components/layout/navbar/login-button.tsx index 749da862..eb0ff4d7 100644 --- a/ui/src/components/layout/navbar/login-button.tsx +++ b/ui/src/components/layout/navbar/login-button.tsx @@ -27,4 +27,4 @@ const LoginButton = ({ currentUser }: { currentUser: User }) => { ); }; -export default LoginButton; \ No newline at end of file +export default LoginButton; diff --git a/ui/src/lib/access/user.ts b/ui/src/lib/access/user.ts index 1299eace..a722fa32 100644 --- a/ui/src/lib/access/user.ts +++ b/ui/src/lib/access/user.ts @@ -70,4 +70,3 @@ export const getUser = async (): Promise => { }; export default User; - \ No newline at end of file diff --git a/ui/src/lib/actions-ootb.ts b/ui/src/lib/actions-ootb.ts index 0d92c115..0e74f22b 100644 --- a/ui/src/lib/actions-ootb.ts +++ b/ui/src/lib/actions-ootb.ts @@ -5,8 +5,7 @@ import { OotbActiveProfileResult } from './types'; import { - postRequest, - getRequest + postRequest } from './http-client'; import ootbProfiles from '../../ootb.active.user.profiles.json' assert { type: 'json' }; @@ -36,6 +35,6 @@ export const matchProfile = async (givenName: string | undefined): Promise { }); it('should return an OOTB user if matchProfile and updateActiveDefaultUser succeed', async () => { - (matchProfile as vi.Mock).mockResolvedValue(ootbProfile); - (updateActiveDefaultUser as vi.Mock).mockResolvedValue({ resultCode: 'SUCCESS', result: ootbProfile }); + (matchProfile as Mock).mockResolvedValue(ootbProfile); + (updateActiveDefaultUser as Mock).mockResolvedValue({ resultCode: 'SUCCESS', result: ootbProfile }); const ootbUser = await getUser(); expect(ootbUser.uid).toBe('admin0123'); @@ -120,15 +120,15 @@ describe('user', () => { }); it('should return AnonymousUser if matchProfile cannot find a profile', async () => { - (matchProfile as vi.Mock).mockRejectedValue(new Error('No profile found for givenName: admin')); + (matchProfile as Mock).mockRejectedValue(new Error('No profile found for givenName: admin')); expect(await getUser()).toEqual(AnonymousUser); expect(matchProfile).toHaveBeenCalledWith('admin'); }); it('should return AnonymousUser if updateActiveDefaultUser fails', async () => { - (matchProfile as vi.Mock).mockResolvedValue(ootbProfile); - (updateActiveDefaultUser as vi.Mock).mockRejectedValue(new Error('Failed to update user')); + (matchProfile as Mock).mockResolvedValue(ootbProfile); + (updateActiveDefaultUser as Mock).mockRejectedValue(new Error('Failed to update user')); // If we want this test to pass as currently is (returning AnonymousUser), // we should not re-throw in the catch block. Instead, we should return AnonymousUser. From 6a7dfd4a392b77a468907a80ab6b4ce8cd3de564 Mon Sep 17 00:00:00 2001 From: gitcarrot Date: Fri, 20 Dec 2024 15:00:27 -1000 Subject: [PATCH 8/8] turn ootb mode off on env --- ui/.env.development | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/.env.development b/ui/.env.development index 56efd1bc..389a33f5 100644 --- a/ui/.env.development +++ b/ui/.env.development @@ -1,3 +1,3 @@ -NEXT_PUBLIC_OOTB_MODE=true +NEXT_PUBLIC_OOTB_MODE=false # Profiles: user, owner, admin NEXT_PUBLIC_OOTB_PROFILE=admin \ No newline at end of file