diff --git a/api/main_endpoints/routes/User.js b/api/main_endpoints/routes/User.js index 80fdf0e43..928908360 100644 --- a/api/main_endpoints/routes/User.js +++ b/api/main_endpoints/routes/User.js @@ -128,6 +128,57 @@ router.post('/search', async function(req, res) { }); }); +router.post('/admins/validate', async function(req, res) { + const decoded = await decodeToken(req, membershipState.NON_MEMBER); + if (decoded.status !== OK) { + return res.sendStatus(decoded.status); + } + + if (!Array.isArray(req.body.ids)) { + return res.status(BAD_REQUEST).send({ message: 'ids must be an array.' }); + } + + const requestedIds = []; + const invalidIds = []; + const seen = new Set(); + + req.body.ids.forEach((id) => { + if (typeof id !== 'string' || !id.trim()) { + invalidIds.push(id); + return; + } + + const normalizedId = id.trim(); + if (seen.has(normalizedId)) { + return; + } + seen.add(normalizedId); + requestedIds.push(normalizedId); + }); + + const objectIdPattern = /^[0-9a-fA-F]{24}$/; + const candidateIds = requestedIds.filter((id) => objectIdPattern.test(id)); + invalidIds.push(...requestedIds.filter((id) => !objectIdPattern.test(id))); + + try { + const validAdmins = await User.find({ + _id: { $in: candidateIds }, + accessLevel: { $gte: membershipState.OFFICER } + }, '_id firstName lastName email accessLevel').lean(); + + const validIdSet = new Set(validAdmins.map((user) => user._id.toString())); + invalidIds.push(...candidateIds.filter((id) => !validIdSet.has(id))); + + return res.status(OK).send({ + validAdmins, + invalidIds + }); + } catch (err) { + logger.error('/admins/validate had an error:', err); + return res.sendStatus(BAD_REQUEST); + } +}); + // Search for all members router.post('/users', async function(req, res) { const decoded = await decodeToken(req, membershipState.OFFICER); @@ -147,6 +198,19 @@ router.post('/users', async function(req, res) { }; } + if (req.body.minRole !== undefined && req.body.minRole !== null) { + if (Object.keys(maybeOr).length > 0) { + maybeOr = { + $and: [ + maybeOr, + { accessLevel: { $gte: req.body.minRole } } + ] + }; + } else { + maybeOr = { accessLevel: { $gte: req.body.minRole } }; + } + } + const sortColumn = req.query.sort || 'joinDate'; const orderToInteger = { diff --git a/src/APIFunctions/SCEvents.js b/src/APIFunctions/SCEvents.js index 08c7823e8..48c0b3e4b 100644 --- a/src/APIFunctions/SCEvents.js +++ b/src/APIFunctions/SCEvents.js @@ -16,7 +16,7 @@ export async function getAllSCEvents(token, { startDate, endDate } = {}) { url.searchParams.set('endDate', endDate); } - const res = await fetch(url.pathname + url.search, { + const res = await fetch(url.href, { headers, }); diff --git a/src/APIFunctions/User.js b/src/APIFunctions/User.js index bcb9ebbd1..23ac761f3 100644 --- a/src/APIFunctions/User.js +++ b/src/APIFunctions/User.js @@ -13,6 +13,7 @@ export async function getAllUsers({ page = null, sortColumn = null, sortOrder = null, + minRole = null, }) { const url = new URL('/api/User/users', BASE_API_URL); @@ -35,6 +36,7 @@ export async function getAllUsers({ body: JSON.stringify({ query, page, + minRole, }), }); if (res.ok) { @@ -49,6 +51,29 @@ export async function getAllUsers({ return status; } +export async function validateEventAdmins(token, ids) { + let status = new UserApiResponse(); + const url = new URL('/api/User/admins/validate', BASE_API_URL); + try { + const res = await fetch(url.href, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ ids }), + }); + const result = await res.json(); + status.responseData = result; + if (!res.ok) { + status.error = true; + } + } catch(err) { + status.error = true; + } + return status; +} + /** * Edit an existing users * @param {Object} userToEdit - The user that is to be updated diff --git a/src/Pages/Events/CalendarView.js b/src/Pages/Events/CalendarView.js index 90a89720b..6ed00ee6b 100644 --- a/src/Pages/Events/CalendarView.js +++ b/src/Pages/Events/CalendarView.js @@ -160,7 +160,7 @@ function getBadgeText(event, isAdminView) { return ''; } -function getRegistrationCta(event) { +function getRegistrationCta(event, isAdminView) { const registrationStatus = event?.registration_status || 'none'; switch (registrationStatus) { @@ -183,6 +183,13 @@ function getRegistrationCta(event) { className: 'border border-violet-400/30 bg-violet-500/10 text-violet-200', }; case 'rejected': + if (isAdminView) { + return { + label: 'Register', + disabled: false, + className: '', + }; + } return { label: 'Unavailable', disabled: true, @@ -205,6 +212,7 @@ function getRegistrationCta(event) { */ function canUserManageEvent(event, user) { const userId = user?._id != null ? String(user._id) : ''; + const access = user?.accessLevel ?? 0; const eventAdmins = Array.isArray(event.admins) ? event.admins.map((id) => String(id)) : []; // 1. If user is explicitly listed as an admin for this event @@ -212,8 +220,12 @@ function canUserManageEvent(event, user) { return true; } + if (event.all_org_admins_can_edit && access >= membershipState.OFFICER) { + return true; + } + // 2. If the event has no admins, allow users with level 3 (ADMIN) or higher to manage it - if (eventAdmins.length === 0 && (user?.accessLevel ?? 0) >= membershipState.ADMIN) { + if (eventAdmins.length === 0 && access >= membershipState.ADMIN) { return true; } @@ -239,7 +251,7 @@ function EventPopup({ event, onClose, isAdminView, user }) { const eventId = event?.id || event?._id; const authToken = user?.token; const { maxAttendees, hasCapacityLimit, remainingSpots, isFull } = calculateEventCapacity(event, attendeeCount); - const registrationCta = getRegistrationCta(event); + const registrationCta = getRegistrationCta(event, isAdminView); const shouldShowWaitlistJoin = !canManageEvent && event.status === 'published' && diff --git a/src/Pages/Events/CreateEventPage.js b/src/Pages/Events/CreateEventPage.js index 3c2dedb1c..c7b455c11 100644 --- a/src/Pages/Events/CreateEventPage.js +++ b/src/Pages/Events/CreateEventPage.js @@ -1,8 +1,9 @@ /* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { createSCEvent } from '../../APIFunctions/SCEvents'; +import { getAllUsers } from '../../APIFunctions/User'; import { membershipState } from '../../Enums'; import { useEventQuestions, toApiRegistrationForm } from './useEventQuestions'; import { getApiErrorMessage } from './eventUtils'; @@ -37,6 +38,11 @@ function defaultQuestions() { ]; } +function userDisplayName(admin) { + const name = [admin.firstName, admin.lastName].filter(Boolean).join(' ').trim(); + return name || admin.email || admin._id; +} + export default function CreateEventPage() { const { user } = useSCE(); const history = useHistory(); @@ -61,7 +67,6 @@ export default function CreateEventPage() { const [maxAttendees, setMaxAttendees] = useState(UNLIMITED_ATTENDEES); const [waitlistEnabled, setWaitlistEnabled] = useState(false); const [waitlistSize, setWaitlistSize] = useState(10); - const { questions, addQuestion, @@ -70,15 +75,94 @@ export default function CreateEventPage() { updateQuestionType, updateAnswerOption, addAnswerOption, - removeAnswerOption + removeAnswerOption, } = useEventQuestions(defaultQuestions()); - + const [eventAdmins, setEventAdmins] = useState([]); + const [allOrgAdminsCanEdit, setAllOrgAdminsCanEdit] = useState(false); + const [adminSearch, setAdminSearch] = useState(''); + const [adminSearchResults, setAdminSearchResults] = useState([]); + const [adminSearchError, setAdminSearchError] = useState(''); + const [adminSearching, setAdminSearching] = useState(false); const [submitError, setSubmitError] = useState(''); const [submitting, setSubmitting] = useState(false); + const debounceRef = useRef(null); const isOfficerOrAdmin = user?.accessLevel >= membershipState.OFFICER; const adminId = useMemo(() => (user?._id != null ? String(user._id) : ''), [user]); + const eventAdminIds = useMemo( + () => eventAdmins.map((admin) => String(admin._id)), + [eventAdmins], + ); + + useEffect(() => { + if (!adminId || allOrgAdminsCanEdit) return; + setEventAdmins((prev) => { + if (prev.some((admin) => String(admin._id) === adminId)) { + return prev; + } + return [ + ...prev, + { + _id: adminId, + firstName: user?.firstName || '', + lastName: user?.lastName || '', + email: user?.email || '', + accessLevel: user?.accessLevel, + }, + ]; + }); + }, [adminId, user, allOrgAdminsCanEdit]); + + function addEventAdmin(admin) { + setEventAdmins((prev) => { + if (prev.some((selected) => String(selected._id) === String(admin._id))) { + return prev; + } + return [...prev, admin]; + }); + setAdminSearchResults((prev) => ( + prev.filter((candidate) => String(candidate._id) !== String(admin._id)) + )); + } + + function removeEventAdmin(id) { + if (String(id) === adminId) { + return; + } + setEventAdmins((prev) => prev.filter((admin) => String(admin._id) !== String(id))); + } + + async function performAdminSearch(query) { + setAdminSearching(true); + const token = window.localStorage.getItem('jwtToken'); + const result = await getAllUsers({ token, query, minRole: membershipState.OFFICER }); + setAdminSearching(false); + if (result.error) { + setAdminSearchError('Failed to search admins.'); + return; + } + const users = Array.isArray(result.responseData?.items) ? result.responseData.items : []; + setAdminSearchResults( + users.filter((candidate) => ( + !eventAdminIds.includes(String(candidate._id)) + )), + ); + } + + function handleAdminSearchChange(value) { + setAdminSearch(value); + setAdminSearchError(''); + if (debounceRef.current) clearTimeout(debounceRef.current); + const query = value.trim(); + if (query.length < 2) { + setAdminSearchResults([]); + setAdminSearching(false); + return; + } + setAdminSearching(true); + debounceRef.current = setTimeout(() => performAdminSearch(query), 300); + } async function handleCreateEvent() { setSubmitError(''); @@ -90,6 +174,10 @@ export default function CreateEventPage() { setSubmitError('Could not resolve your user id.'); return; } + if (!allOrgAdminsCanEdit && eventAdminIds.length === 0) { + setSubmitError('Please select at least one event admin, or allow all officers and administrators to edit.'); + return; + } if (visibility === 'private' && !minimumVisibleRole) { setSubmitError('Please select a minimum visible role for private events.'); return; @@ -110,7 +198,8 @@ export default function CreateEventPage() { time, location: location.trim(), description: description.trim(), - admins: [adminId], // The event creator becomes the initial event admin in SCEvents + admins: allOrgAdminsCanEdit ? [] : eventAdminIds, + all_org_admins_can_edit: allOrgAdminsCanEdit, registration_form: toApiRegistrationForm(questions), max_attendees: maxAttendees === UNLIMITED_ATTENDEES ? UNLIMITED_ATTENDEES : Number(maxAttendees), @@ -202,6 +291,27 @@ export default function CreateEventPage() { addAnswerOption, removeAnswerOption, }} + adminActions={{ + eventAdmins, + userDisplayName, + adminSearch, + adminSearchResults, + adminSearchError, + adminSearching, + debounceRef, + handleAdminSearchChange, + performAdminSearch, + addEventAdmin, + removeEventAdmin, + isRemoveDisabledForAdmin: (admin) => String(admin._id) === adminId, + allOrgAdminsCanEdit, + setAllOrgAdminsCanEdit: (next) => { + setAllOrgAdminsCanEdit(next); + if (next) { + setEventAdmins([]); + } + }, + }} /> ); } diff --git a/src/Pages/Events/EditEventPage.js b/src/Pages/Events/EditEventPage.js index 8beaf7426..0e1d03a2c 100644 --- a/src/Pages/Events/EditEventPage.js +++ b/src/Pages/Events/EditEventPage.js @@ -1,8 +1,9 @@ /* eslint-disable camelcase -- mirrors SCEvents JSON field names in state and payloads */ -import { useMemo, useState, useEffect } from 'react'; +import { useMemo, useState, useEffect, useRef } from 'react'; import { Link, useHistory, useParams } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { getEventByID, updateSCEvent } from '../../APIFunctions/SCEvents'; +import { getAllUsers, validateEventAdmins } from '../../APIFunctions/User'; import { membershipState } from '../../Enums'; import { toApiRegistrationForm, useEventQuestions } from './useEventQuestions'; import { getApiErrorMessage } from './eventUtils'; @@ -11,6 +12,11 @@ import EventEditorForm from './EventEditorForm'; /** Matches SCEvents `max_attendees` when there is no cap. */ const UNLIMITED_ATTENDEES = -1; +function userDisplayName(admin) { + const name = [admin.firstName, admin.lastName].filter(Boolean).join(' ').trim(); + return name || admin.email || admin._id; +} + export default function EditEventPage() { const { id } = useParams(); const { user } = useSCE(); @@ -31,6 +37,11 @@ export default function EditEventPage() { const [waitlistEnabled, setWaitlistEnabled] = useState(false); const [waitlistSize, setWaitlistSize] = useState(10); const [eventAdmins, setEventAdmins] = useState([]); + const [allOrgAdminsCanEdit, setAllOrgAdminsCanEdit] = useState(false); + const [adminSearch, setAdminSearch] = useState(''); + const [adminSearchResults, setAdminSearchResults] = useState([]); + const [adminSearchError, setAdminSearchError] = useState(''); + const [adminSearching, setAdminSearching] = useState(false); const { questions, setQuestions, @@ -40,14 +51,26 @@ export default function EditEventPage() { updateQuestionType, updateAnswerOption, addAnswerOption, - removeAnswerOption + removeAnswerOption, } = useEventQuestions([]); const [submitError, setSubmitError] = useState(''); const [submitting, setSubmitting] = useState(false); + const debounceRef = useRef(null); - const isOfficerOrAdmin = user?.accessLevel >= membershipState.OFFICER; const userId = useMemo(() => (user?._id != null ? String(user._id) : ''), [user]); + const eventAdminIds = useMemo( + () => eventAdmins.map((admin) => String(admin._id)), + [eventAdmins], + ); + + const canEditThisEvent = useMemo(() => { + if (!userId) return false; + if (allOrgAdminsCanEdit && (user?.accessLevel ?? 0) >= membershipState.OFFICER) { + return true; + } + return eventAdminIds.includes(userId); + }, [allOrgAdminsCanEdit, user, userId, eventAdminIds]); useEffect(() => { async function loadEvent() { @@ -80,11 +103,75 @@ export default function EditEventPage() { typeof evt.waitlist_size === 'number' && evt.waitlist_size > 0 ? evt.waitlist_size : 10, ); setQuestions(evt.registration_form || []); - setEventAdmins(evt.admins || []); + const adminIds = Array.isArray(evt.admins) ? evt.admins.map(String) : []; + setAllOrgAdminsCanEdit(!!evt.all_org_admins_can_edit); + setEventAdmins(adminIds.map((adminId) => ({ _id: adminId }))); + + const adminResult = await validateEventAdmins(token, adminIds); + if (!adminResult.error) { + const validAdmins = Array.isArray(adminResult.responseData?.validAdmins) + ? adminResult.responseData.validAdmins + : []; + const validAdminIds = validAdmins.map((admin) => String(admin._id)); + setEventAdmins(validAdmins); + } } loadEvent(); - }, [id]); + }, [id, userId]); + + function addEventAdmin(admin) { + if (allOrgAdminsCanEdit) return; + setEventAdmins((prev) => { + if (prev.some((selected) => String(selected._id) === String(admin._id))) { + return prev; + } + return [...prev, admin]; + }); + setAdminSearchResults((prev) => ( + prev.filter((candidate) => String(candidate._id) !== String(admin._id)) + )); + } + + function removeEventAdmin(adminId) { + if (allOrgAdminsCanEdit) return; + if (eventAdminIds.length <= 1) { + window.alert('An event must have at least one admin.'); + return; + } + setEventAdmins((prev) => prev.filter((admin) => String(admin._id) !== String(adminId))); + } + + async function performAdminSearch(query) { + setAdminSearching(true); + const token = window.localStorage.getItem('jwtToken'); + const result = await getAllUsers({ token, query, minRole: membershipState.OFFICER }); + setAdminSearching(false); + if (result.error) { + setAdminSearchError('Failed to search admins.'); + return; + } + const users = Array.isArray(result.responseData?.items) ? result.responseData.items : []; + setAdminSearchResults( + users.filter((candidate) => ( + !eventAdminIds.includes(String(candidate._id)) + )), + ); + } + + function handleAdminSearchChange(value) { + setAdminSearch(value); + setAdminSearchError(''); + if (debounceRef.current) clearTimeout(debounceRef.current); + const query = value.trim(); + if (query.length < 2) { + setAdminSearchResults([]); + setAdminSearching(false); + return; + } + setAdminSearching(true); + debounceRef.current = setTimeout(() => performAdminSearch(query), 300); + } async function handleUpdateEvent() { setSubmitError(''); @@ -103,6 +190,18 @@ export default function EditEventPage() { return; } + if (!allOrgAdminsCanEdit && eventAdminIds.length === 0) { + setSubmitError('Please select at least one event admin, or allow all officers and administrators to edit.'); + return; + } + + if (!allOrgAdminsCanEdit && !eventAdminIds.includes(userId)) { + const confirmed = window.confirm('You will lose edit access to this event after saving.'); + if (!confirmed) { + return; + } + } + const payload = { name: eventName.trim(), date, @@ -117,6 +216,8 @@ export default function EditEventPage() { minimum_visible_role: visibility === 'private' ? minimumVisibleRole : '', waitlist_enabled: waitlistEnabled, waitlist_size: waitlistEnabled ? Number(waitlistSize) : 0, + admins: allOrgAdminsCanEdit ? [] : eventAdminIds, + all_org_admins_can_edit: allOrgAdminsCanEdit, }; setSubmitting(true); @@ -135,22 +236,6 @@ export default function EditEventPage() { history.push('/events'); } - if (!isOfficerOrAdmin) { - return ( -
-

- Edit event -

-

- Only officers and administrators can edit events. -

- - Back to events - -
- ); - } - if (isLoading) { return
Loading event details...
; } @@ -167,9 +252,7 @@ export default function EditEventPage() { ); } - // Edit access: only users listed in event.admins can update an event - const isEventAdmin = eventAdmins.includes(userId); - if (!isEventAdmin) { + if (!canEditThisEvent) { return (

@@ -233,6 +316,40 @@ export default function EditEventPage() { addAnswerOption, removeAnswerOption, }} + adminActions={{ + eventAdmins, + userDisplayName, + adminSearch, + adminSearchResults, + adminSearchError, + adminSearching, + debounceRef, + handleAdminSearchChange, + performAdminSearch, + addEventAdmin, + removeEventAdmin, + allOrgAdminsCanEdit, + setAllOrgAdminsCanEdit: (next) => { + setAllOrgAdminsCanEdit(next); + if (next) { + setEventAdmins([]); + } else if (user) { + setEventAdmins((prev) => { + if (prev.some((a) => String(a._id) === userId)) return prev; + return [ + ...prev, + { + _id: userId, + firstName: user.firstName || '', + lastName: user.lastName || '', + email: user.email || '', + accessLevel: user.accessLevel, + }, + ]; + }); + } + }, + }} /> ); } diff --git a/src/Pages/Events/EventEditorForm.js b/src/Pages/Events/EventEditorForm.js index 3890764ef..70c73fee2 100644 --- a/src/Pages/Events/EventEditorForm.js +++ b/src/Pages/Events/EventEditorForm.js @@ -5,6 +5,7 @@ export default function EventEditorForm({ meta, form, questionActions, + adminActions, }) { const { title, @@ -284,6 +285,99 @@ export default function EventEditorForm({ )} + + {adminActions && ( +
+ {typeof adminActions.setAllOrgAdminsCanEdit === 'function' && ( + + )} +
+ Event admins +
+
+ {adminActions.eventAdmins.map((admin) => ( +
+
+

+ {adminActions.userDisplayName(admin)} +

+ {admin.email && ( +

{admin.email}

+ )} +
+ +
+ ))} +
+
+ adminActions.handleAdminSearchChange(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + if (adminActions.debounceRef.current) clearTimeout(adminActions.debounceRef.current); + const query = adminActions.adminSearch.trim(); + if (query.length >= 2) adminActions.performAdminSearch(query); + } + }} + placeholder="Search admins by name or email" + /> + {adminActions.adminSearch.trim().length >= 2 && (adminActions.adminSearching || adminActions.adminSearchResults.length > 0 || adminActions.adminSearchError) && ( +
+ {adminActions.adminSearching && ( +
Searching…
+ )} + {!adminActions.adminSearching && adminActions.adminSearchError && ( +
{adminActions.adminSearchError}
+ )} + {!adminActions.adminSearching && adminActions.adminSearchResults.map((admin) => ( + + ))} +
+ )} +
+
+ )}

diff --git a/src/Pages/Events/Events.js b/src/Pages/Events/Events.js index e9b0794a3..b69f75830 100644 --- a/src/Pages/Events/Events.js +++ b/src/Pages/Events/Events.js @@ -10,16 +10,18 @@ function canUserSeeEvent(event, user) { const userAccess = user?.accessLevel ?? membershipState.NON_MEMBER; const isGlobalAdmin = userAccess >= membershipState.ADMIN; + const isOfficerOrAbove = userAccess >= membershipState.OFFICER; const isEventAdmin = Array.isArray(event.admins) && userId ? event.admins.includes(userId) : false; + const allOrgAdminsCanEdit = !!event.all_org_admins_can_edit; const status = event.status || 'draft'; const visibility = event.visibility || 'public'; const minimumVisibleRole = event.minimum_visible_role || ''; if (status === 'draft') { - return isGlobalAdmin || isEventAdmin; + return isGlobalAdmin || isEventAdmin || (allOrgAdminsCanEdit && isOfficerOrAbove); } if (visibility === 'public') { diff --git a/src/Pages/Events/EventsRegistration.js b/src/Pages/Events/EventsRegistration.js index 75d3a438f..41d346758 100644 --- a/src/Pages/Events/EventsRegistration.js +++ b/src/Pages/Events/EventsRegistration.js @@ -3,6 +3,7 @@ import { useState, useEffect } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { useSCE } from '../../Components/context/SceContext'; import { getEventByID, getEventAttendanceSummary, registerForEvent } from '../../APIFunctions/SCEvents'; +import { membershipState } from '../../Enums'; import { calculateEventCapacity, getApiErrorMessage } from './eventUtils'; import { ArrowLeftIcon } from './EventIcons'; @@ -223,7 +224,10 @@ export default function EventRegistration() { ); } - if (registrationStatus === 'rejected') { + const canCreateEvent = user?.accessLevel >= membershipState.OFFICER; + const isAdminView = canCreateEvent; + + if (registrationStatus === 'rejected' && !isAdminView) { return ( { }); }); + describe('/POST admins/validate', () => { + it('Should return statusCode 401 if no token is passed in', async () => { + const result = await test.sendPostRequest( + '/api/User/admins/validate', { ids: [] }); + expect(result).to.have.status(UNAUTHORIZED); + }); + + it('Should return statusCode 403 if an invalid token was passed in', async () => { + setTokenStatus(null); + const result = await test.sendPostRequestWithToken( + token, '/api/User/admins/validate', { ids: [] }); + expect(result).to.have.status(FORBIDDEN); + }); + + it('Should return statusCode 400 if ids is not an array', async () => { + setTokenStatus(true, { accessLevel: MEMBERSHIP_STATE.ADMIN }); + const result = await test.sendPostRequestWithToken( + token, '/api/User/admins/validate', { ids: 'not-array' }); + expect(result).to.have.status(BAD_REQUEST); + }); + + it('Should return valid admin users and invalid ids', async () => { + await User.deleteMany({}); + + const admin = await new User({ + email: 'admin@sce.dev', + password: 'Passw0rd', + firstName: 'Ada', + lastName: 'Admin', + major: 'Computer Science', + accessLevel: MEMBERSHIP_STATE.ADMIN, + }).save(); + const officer = await new User({ + email: 'officer@sce.dev', + password: 'Passw0rd', + firstName: 'Ollie', + lastName: 'Officer', + major: 'Computer Science', + accessLevel: MEMBERSHIP_STATE.OFFICER, + }).save(); + const missingId = new mongoose.Types.ObjectId().toString(); + + setTokenStatus(true, { accessLevel: MEMBERSHIP_STATE.ADMIN }); + const result = await test.sendPostRequestWithToken( + token, + '/api/User/admins/validate', + { + ids: [ + admin._id.toString(), + officer._id.toString(), + missingId, + 'not-object-id', + admin._id.toString() + ] + } + ); + + expect(result).to.have.status(OK); + expect(result.body.validAdmins).to.have.length(2); + expect(result.body.validAdmins).to.deep.include.members([ + { + _id: admin._id.toString(), + firstName: 'Ada', + lastName: 'Admin', + email: 'admin@sce.dev', + accessLevel: MEMBERSHIP_STATE.ADMIN + }, + { + _id: officer._id.toString(), + firstName: 'Ollie', + lastName: 'Officer', + email: 'officer@sce.dev', + accessLevel: MEMBERSHIP_STATE.OFFICER + } + ]); + expect(result.body.invalidIds).to.have.members([ + missingId, + 'not-object-id' + ]); + }); + }); + describe('/POST edit', () => { it('Should return statusCode 401 if no token is passed in', async () => { const user = {