From d9a8b6ba765890770835a8ba495139c0cf73b347 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Fri, 13 Feb 2026 21:40:06 -0500 Subject: [PATCH 01/11] Created initial files --- .../volunteers/volunteers.controller.spec.ts | 0 .../src/volunteers/volunteers.controller.ts | 75 +++++++++ .../src/volunteers/volunteers.entity.ts | 44 ++++++ .../src/volunteers/volunteers.module.ts | 17 +++ .../src/volunteers/volunteers.service.spec.ts | 0 .../src/volunteers/volunteers.service.ts | 142 ++++++++++++++++++ 6 files changed, 278 insertions(+) create mode 100644 apps/backend/src/volunteers/volunteers.controller.spec.ts create mode 100644 apps/backend/src/volunteers/volunteers.controller.ts create mode 100644 apps/backend/src/volunteers/volunteers.entity.ts create mode 100644 apps/backend/src/volunteers/volunteers.module.ts create mode 100644 apps/backend/src/volunteers/volunteers.service.spec.ts create mode 100644 apps/backend/src/volunteers/volunteers.service.ts diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts new file mode 100644 index 000000000..033ab8b54 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -0,0 +1,75 @@ +import { + Controller, + Delete, + Get, + Param, + ParseIntPipe, + Put, + Post, + BadRequestException, + Body, + //UseGuards, + //UseInterceptors, +} from '@nestjs/common'; +import { UsersService } from './users.service'; +//import { AuthGuard } from '@nestjs/passport'; +import { User } from './user.entity'; +import { Role } from './types'; +import { userSchemaDto } from './dtos/userSchema.dto'; +import { Pantry } from '../pantries/pantries.entity'; +//import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; + +@Controller('users') +//@UseInterceptors(CurrentUserInterceptor) +export class UsersController { + constructor(private usersService: UsersService) {} + + @Get('/') + async getAllVolunteers(): Promise< + (Omit & { pantryIds: number[] })[] + > { + return this.usersService.getVolunteersAndPantryAssignments(); + } + + @Get('/:id') + async getUser(@Param('id', ParseIntPipe) userId: number): Promise { + return this.usersService.findOne(userId); + } + + @Get('/:id/pantries') + async getVolunteerPantries( + @Param('id', ParseIntPipe) id: number, + ): Promise { + return this.usersService.getVolunteerPantries(id); + } + + @Delete('/:id') + removeUser(@Param('id', ParseIntPipe) userId: number): Promise { + return this.usersService.remove(userId); + } + + @Put('/:id/role') + async updateRole( + @Param('id', ParseIntPipe) id: number, + @Body('role') role: string, + ): Promise { + if (!Object.values(Role).includes(role as Role)) { + throw new BadRequestException('Invalid role'); + } + return this.usersService.update(id, { role: role as Role }); + } + + @Post('/') + async createUser(@Body() createUserDto: userSchemaDto): Promise { + const { email, firstName, lastName, phone, role } = createUserDto; + return this.usersService.create(email, firstName, lastName, phone, role); + } + + @Post('/:id/pantries') + async assignPantries( + @Param('id', ParseIntPipe) id: number, + @Body('pantryIds') pantryIds: number[], + ): Promise { + return this.usersService.assignPantriesToVolunteer(id, pantryIds); + } +} diff --git a/apps/backend/src/volunteers/volunteers.entity.ts b/apps/backend/src/volunteers/volunteers.entity.ts new file mode 100644 index 000000000..8ac2df689 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.entity.ts @@ -0,0 +1,44 @@ +import { + Entity, + Column, + PrimaryGeneratedColumn, + ManyToMany, + JoinTable, +} from 'typeorm'; + +import { Pantry } from '../pantries/pantries.entity'; + +@Entity() +export class Volunteer { + @PrimaryGeneratedColumn({ name: 'volunteer_id' }) + id: number; + + @Column() + firstName: string; + + @Column() + lastName: string; + + @Column() + email: string; + + @Column({ + type: 'varchar', + length: 20, + }) + phone: string; + + @ManyToMany(() => Pantry, (pantry) => pantry.volunteers) + @JoinTable({ + name: 'volunteer_assignments', + joinColumn: { + name: 'volunteer_id', + referencedColumnName: 'id', + }, + inverseJoinColumn: { + name: 'pantry_id', + referencedColumnName: 'pantryId', + }, + }) + pantries?: Pantry[]; +} diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts new file mode 100644 index 000000000..6a780a8d6 --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { UsersController } from './users.controller'; +import { UsersService } from './users.service'; +import { User } from './user.entity'; +import { JwtStrategy } from '../auth/jwt.strategy'; +import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; +import { AuthService } from '../auth/auth.service'; +import { PantriesModule } from '../pantries/pantries.module'; + +@Module({ + imports: [TypeOrmModule.forFeature([User]), PantriesModule], + exports: [UsersService], + controllers: [UsersController], + providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], +}) +export class UsersModule {} diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts new file mode 100644 index 000000000..e69de29bb diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts new file mode 100644 index 000000000..65f90ae1e --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -0,0 +1,142 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { In, Repository } from 'typeorm'; + +import { User } from './user.entity'; +import { Role } from './types'; +import { validateId } from '../utils/validation.utils'; +import { Pantry } from '../pantries/pantries.entity'; +import { PantriesService } from '../pantries/pantries.service'; + +@Injectable() +export class UsersService { + constructor( + @InjectRepository(User) + private repo: Repository, + + private pantriesService: PantriesService, + ) {} + + async create( + email: string, + firstName: string, + lastName: string, + phone: string, + role: Role, + ) { + const user = this.repo.create({ + role, + firstName, + lastName, + email, + phone, + }); + + return this.repo.save(user); + } + + async findOne(id: number): Promise { + validateId(id, 'User'); + + const user = await this.repo.findOneBy({ id }); + + if (!user) { + throw new NotFoundException(`User ${id} not found`); + } + return user; + } + + async findVolunteer(volunteerId: number): Promise { + validateId(volunteerId, 'Volunteer'); + + const volunteer = await this.repo.findOne({ + where: { id: volunteerId }, + relations: ['pantries'], + }); + + if (!volunteer) + throw new NotFoundException(`User ${volunteerId} not found`); + if (volunteer.role !== Role.VOLUNTEER) { + throw new BadRequestException(`User ${volunteerId} is not a volunteer`); + } + return volunteer; + } + + find(email: string) { + return this.repo.find({ where: { email } }); + } + + async update(id: number, attrs: Partial) { + validateId(id, 'User'); + + const user = await this.findOne(id); + + if (!user) { + throw new NotFoundException(`User ${id} not found`); + } + + Object.assign(user, attrs); + + return this.repo.save(user); + } + + async remove(id: number) { + validateId(id, 'User'); + + const user = await this.findOne(id); + + if (!user) { + throw new NotFoundException(`User ${id} not found`); + } + + return this.repo.remove(user); + } + + async findUsersByRoles(roles: Role[]): Promise { + return this.repo.find({ + where: { role: In(roles) }, + relations: ['pantries'], + }); + } + + async getVolunteersAndPantryAssignments(): Promise< + (Omit & { pantryIds: number[] })[] + > { + const volunteers = await this.findUsersByRoles([Role.VOLUNTEER]); + + return volunteers.map((v) => { + const { pantries, ...volunteerWithoutPantries } = v; + return { + ...volunteerWithoutPantries, + pantryIds: pantries.map((p) => p.pantryId), + }; + }); + } + + async getVolunteerPantries(volunteerId: number): Promise { + const volunteer = await this.findVolunteer(volunteerId); + return volunteer.pantries; + } + + async assignPantriesToVolunteer( + volunteerId: number, + pantryIds: number[], + ): Promise { + pantryIds.forEach((id) => validateId(id, 'Pantry')); + + const volunteer = await this.findVolunteer(volunteerId); + + const pantries = await this.pantriesService.findByIds(pantryIds); + const existingPantryIds = volunteer.pantries.map((p) => p.pantryId); + const newPantries = pantries.filter( + (p) => !existingPantryIds.includes(p.pantryId), + ); + + volunteer.pantries = [...volunteer.pantries, ...newPantries]; + return this.repo.save(volunteer); + } +} From 243c6ed240107ba05d4d160403144d87b3bea927 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 17:25:53 -0500 Subject: [PATCH 02/11] Fixed user volunteer separation --- apps/backend/src/users/users.controller.ts | 23 --------- .../src/volunteers/volunteers.controller.ts | 50 ++++--------------- .../src/volunteers/volunteers.entity.ts | 44 ---------------- .../src/volunteers/volunteers.module.ts | 17 ------- .../src/volunteers/volunteers.service.ts | 50 ++----------------- 5 files changed, 12 insertions(+), 172 deletions(-) delete mode 100644 apps/backend/src/volunteers/volunteers.entity.ts delete mode 100644 apps/backend/src/volunteers/volunteers.module.ts diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index 6f11265d9..e3f39469f 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -24,26 +24,11 @@ import { Pantry } from '../pantries/pantries.entity'; export class UsersController { constructor(private usersService: UsersService) {} - @Get('/volunteers') - async getAllVolunteers(): Promise< - (Omit & { pantryIds: number[] })[] - > { - return this.usersService.getVolunteersAndPantryAssignments(); - } - - // @UseGuards(AuthGuard('jwt')) @Get('/:id') async getUser(@Param('id', ParseIntPipe) userId: number): Promise { return this.usersService.findOne(userId); } - @Get('/:id/pantries') - async getVolunteerPantries( - @Param('id', ParseIntPipe) id: number, - ): Promise { - return this.usersService.getVolunteerPantries(id); - } - @Delete('/:id') removeUser(@Param('id', ParseIntPipe) userId: number): Promise { return this.usersService.remove(userId); @@ -65,12 +50,4 @@ export class UsersController { const { email, firstName, lastName, phone, role } = createUserDto; return this.usersService.create(email, firstName, lastName, phone, role); } - - @Post('/:id/pantries') - async assignPantries( - @Param('id', ParseIntPipe) id: number, - @Body('pantryIds') pantryIds: number[], - ): Promise { - return this.usersService.assignPantriesToVolunteer(id, pantryIds); - } } diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index 033ab8b54..5b04157c3 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -1,68 +1,36 @@ import { Controller, - Delete, Get, Param, ParseIntPipe, - Put, Post, - BadRequestException, Body, - //UseGuards, - //UseInterceptors, } from '@nestjs/common'; -import { UsersService } from './users.service'; -//import { AuthGuard } from '@nestjs/passport'; -import { User } from './user.entity'; -import { Role } from './types'; -import { userSchemaDto } from './dtos/userSchema.dto'; +import { User } from '../users/user.entity'; import { Pantry } from '../pantries/pantries.entity'; -//import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; +import { VolunteersService } from './volunteers.service'; -@Controller('users') -//@UseInterceptors(CurrentUserInterceptor) -export class UsersController { - constructor(private usersService: UsersService) {} +@Controller('volunteers') +export class VolunteersController { + constructor(private volunteersService: VolunteersService) {} @Get('/') async getAllVolunteers(): Promise< (Omit & { pantryIds: number[] })[] > { - return this.usersService.getVolunteersAndPantryAssignments(); + return this.volunteersService.getVolunteersAndPantryAssignments(); } @Get('/:id') async getUser(@Param('id', ParseIntPipe) userId: number): Promise { - return this.usersService.findOne(userId); + return this.volunteersService.findOne(userId); } @Get('/:id/pantries') async getVolunteerPantries( @Param('id', ParseIntPipe) id: number, ): Promise { - return this.usersService.getVolunteerPantries(id); - } - - @Delete('/:id') - removeUser(@Param('id', ParseIntPipe) userId: number): Promise { - return this.usersService.remove(userId); - } - - @Put('/:id/role') - async updateRole( - @Param('id', ParseIntPipe) id: number, - @Body('role') role: string, - ): Promise { - if (!Object.values(Role).includes(role as Role)) { - throw new BadRequestException('Invalid role'); - } - return this.usersService.update(id, { role: role as Role }); - } - - @Post('/') - async createUser(@Body() createUserDto: userSchemaDto): Promise { - const { email, firstName, lastName, phone, role } = createUserDto; - return this.usersService.create(email, firstName, lastName, phone, role); + return this.volunteersService.getVolunteerPantries(id); } @Post('/:id/pantries') @@ -70,6 +38,6 @@ export class UsersController { @Param('id', ParseIntPipe) id: number, @Body('pantryIds') pantryIds: number[], ): Promise { - return this.usersService.assignPantriesToVolunteer(id, pantryIds); + return this.volunteersService.assignPantriesToVolunteer(id, pantryIds); } } diff --git a/apps/backend/src/volunteers/volunteers.entity.ts b/apps/backend/src/volunteers/volunteers.entity.ts deleted file mode 100644 index 8ac2df689..000000000 --- a/apps/backend/src/volunteers/volunteers.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { - Entity, - Column, - PrimaryGeneratedColumn, - ManyToMany, - JoinTable, -} from 'typeorm'; - -import { Pantry } from '../pantries/pantries.entity'; - -@Entity() -export class Volunteer { - @PrimaryGeneratedColumn({ name: 'volunteer_id' }) - id: number; - - @Column() - firstName: string; - - @Column() - lastName: string; - - @Column() - email: string; - - @Column({ - type: 'varchar', - length: 20, - }) - phone: string; - - @ManyToMany(() => Pantry, (pantry) => pantry.volunteers) - @JoinTable({ - name: 'volunteer_assignments', - joinColumn: { - name: 'volunteer_id', - referencedColumnName: 'id', - }, - inverseJoinColumn: { - name: 'pantry_id', - referencedColumnName: 'pantryId', - }, - }) - pantries?: Pantry[]; -} diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts deleted file mode 100644 index 6a780a8d6..000000000 --- a/apps/backend/src/volunteers/volunteers.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { UsersController } from './users.controller'; -import { UsersService } from './users.service'; -import { User } from './user.entity'; -import { JwtStrategy } from '../auth/jwt.strategy'; -import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; -import { AuthService } from '../auth/auth.service'; -import { PantriesModule } from '../pantries/pantries.module'; - -@Module({ - imports: [TypeOrmModule.forFeature([User]), PantriesModule], - exports: [UsersService], - controllers: [UsersController], - providers: [UsersService, AuthService, JwtStrategy, CurrentUserInterceptor], -}) -export class UsersModule {} diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 65f90ae1e..7508ceaf4 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -6,14 +6,14 @@ import { import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; -import { User } from './user.entity'; -import { Role } from './types'; +import { User } from '../users/user.entity'; +import { Role } from '../users/types'; import { validateId } from '../utils/validation.utils'; import { Pantry } from '../pantries/pantries.entity'; import { PantriesService } from '../pantries/pantries.service'; @Injectable() -export class UsersService { +export class VolunteersService { constructor( @InjectRepository(User) private repo: Repository, @@ -21,24 +21,6 @@ export class UsersService { private pantriesService: PantriesService, ) {} - async create( - email: string, - firstName: string, - lastName: string, - phone: string, - role: Role, - ) { - const user = this.repo.create({ - role, - firstName, - lastName, - email, - phone, - }); - - return this.repo.save(user); - } - async findOne(id: number): Promise { validateId(id, 'User'); @@ -70,32 +52,6 @@ export class UsersService { return this.repo.find({ where: { email } }); } - async update(id: number, attrs: Partial) { - validateId(id, 'User'); - - const user = await this.findOne(id); - - if (!user) { - throw new NotFoundException(`User ${id} not found`); - } - - Object.assign(user, attrs); - - return this.repo.save(user); - } - - async remove(id: number) { - validateId(id, 'User'); - - const user = await this.findOne(id); - - if (!user) { - throw new NotFoundException(`User ${id} not found`); - } - - return this.repo.remove(user); - } - async findUsersByRoles(roles: Role[]): Promise { return this.repo.find({ where: { role: In(roles) }, From 977d3be84fcb4ff5beb57156c8cf2c0565c314f2 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 20:32:11 -0500 Subject: [PATCH 03/11] Cleaned up module --- apps/backend/src/users/users.controller.ts | 6 --- apps/backend/src/users/users.service.ts | 53 ------------------- .../src/volunteers/volunteers.controller.ts | 2 +- .../src/volunteers/volunteers.service.ts | 43 ++++----------- 4 files changed, 11 insertions(+), 93 deletions(-) diff --git a/apps/backend/src/users/users.controller.ts b/apps/backend/src/users/users.controller.ts index e3f39469f..8eb542063 100644 --- a/apps/backend/src/users/users.controller.ts +++ b/apps/backend/src/users/users.controller.ts @@ -8,19 +8,13 @@ import { Post, BadRequestException, Body, - //UseGuards, - //UseInterceptors, } from '@nestjs/common'; import { UsersService } from './users.service'; -//import { AuthGuard } from '@nestjs/passport'; import { User } from './user.entity'; import { Role } from './types'; import { userSchemaDto } from './dtos/userSchema.dto'; -import { Pantry } from '../pantries/pantries.entity'; -//import { CurrentUserInterceptor } from '../interceptors/current-user.interceptor'; @Controller('users') -//@UseInterceptors(CurrentUserInterceptor) export class UsersController { constructor(private usersService: UsersService) {} diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 65f90ae1e..afabf411b 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -50,22 +50,6 @@ export class UsersService { return user; } - async findVolunteer(volunteerId: number): Promise { - validateId(volunteerId, 'Volunteer'); - - const volunteer = await this.repo.findOne({ - where: { id: volunteerId }, - relations: ['pantries'], - }); - - if (!volunteer) - throw new NotFoundException(`User ${volunteerId} not found`); - if (volunteer.role !== Role.VOLUNTEER) { - throw new BadRequestException(`User ${volunteerId} is not a volunteer`); - } - return volunteer; - } - find(email: string) { return this.repo.find({ where: { email } }); } @@ -102,41 +86,4 @@ export class UsersService { relations: ['pantries'], }); } - - async getVolunteersAndPantryAssignments(): Promise< - (Omit & { pantryIds: number[] })[] - > { - const volunteers = await this.findUsersByRoles([Role.VOLUNTEER]); - - return volunteers.map((v) => { - const { pantries, ...volunteerWithoutPantries } = v; - return { - ...volunteerWithoutPantries, - pantryIds: pantries.map((p) => p.pantryId), - }; - }); - } - - async getVolunteerPantries(volunteerId: number): Promise { - const volunteer = await this.findVolunteer(volunteerId); - return volunteer.pantries; - } - - async assignPantriesToVolunteer( - volunteerId: number, - pantryIds: number[], - ): Promise { - pantryIds.forEach((id) => validateId(id, 'Pantry')); - - const volunteer = await this.findVolunteer(volunteerId); - - const pantries = await this.pantriesService.findByIds(pantryIds); - const existingPantryIds = volunteer.pantries.map((p) => p.pantryId); - const newPantries = pantries.filter( - (p) => !existingPantryIds.includes(p.pantryId), - ); - - volunteer.pantries = [...volunteer.pantries, ...newPantries]; - return this.repo.save(volunteer); - } } diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index 5b04157c3..ce6b9d62d 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -22,7 +22,7 @@ export class VolunteersController { } @Get('/:id') - async getUser(@Param('id', ParseIntPipe) userId: number): Promise { + async getVolunteer(@Param('id', ParseIntPipe) userId: number): Promise { return this.volunteersService.findOne(userId); } diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 7508ceaf4..dc7ca3af5 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -5,64 +5,40 @@ import { } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; - import { User } from '../users/user.entity'; import { Role } from '../users/types'; import { validateId } from '../utils/validation.utils'; import { Pantry } from '../pantries/pantries.entity'; import { PantriesService } from '../pantries/pantries.service'; +import { UsersService } from '../users/users.service'; @Injectable() export class VolunteersService { constructor( @InjectRepository(User) private repo: Repository, - + private usersService: UsersService, private pantriesService: PantriesService, ) {} async findOne(id: number): Promise { - validateId(id, 'User'); - - const user = await this.repo.findOneBy({ id }); - - if (!user) { - throw new NotFoundException(`User ${id} not found`); - } - return user; - } - - async findVolunteer(volunteerId: number): Promise { - validateId(volunteerId, 'Volunteer'); + validateId(id, 'Volunteer'); const volunteer = await this.repo.findOne({ - where: { id: volunteerId }, + where: { id: id }, relations: ['pantries'], }); - if (!volunteer) - throw new NotFoundException(`User ${volunteerId} not found`); - if (volunteer.role !== Role.VOLUNTEER) { - throw new BadRequestException(`User ${volunteerId} is not a volunteer`); + if (!volunteer) { + throw new NotFoundException(`Volunteer ${id} not found`); } return volunteer; } - find(email: string) { - return this.repo.find({ where: { email } }); - } - - async findUsersByRoles(roles: Role[]): Promise { - return this.repo.find({ - where: { role: In(roles) }, - relations: ['pantries'], - }); - } - async getVolunteersAndPantryAssignments(): Promise< (Omit & { pantryIds: number[] })[] > { - const volunteers = await this.findUsersByRoles([Role.VOLUNTEER]); + const volunteers = await this.usersService.findUsersByRoles([Role.VOLUNTEER]); return volunteers.map((v) => { const { pantries, ...volunteerWithoutPantries } = v; @@ -74,7 +50,8 @@ export class VolunteersService { } async getVolunteerPantries(volunteerId: number): Promise { - const volunteer = await this.findVolunteer(volunteerId); + validateId(volunteerId, 'Volunteer') + const volunteer = await this.findOne(volunteerId); return volunteer.pantries; } @@ -84,7 +61,7 @@ export class VolunteersService { ): Promise { pantryIds.forEach((id) => validateId(id, 'Pantry')); - const volunteer = await this.findVolunteer(volunteerId); + const volunteer = await this.findOne(volunteerId); const pantries = await this.pantriesService.findByIds(pantryIds); const existingPantryIds = volunteer.pantries.map((p) => p.pantryId); From 7effbba468c13b688ba444ef20d90006422f03eb Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 20:33:09 -0500 Subject: [PATCH 04/11] prettier --- apps/backend/src/volunteers/volunteers.service.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index dc7ca3af5..322adec85 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -38,7 +38,9 @@ export class VolunteersService { async getVolunteersAndPantryAssignments(): Promise< (Omit & { pantryIds: number[] })[] > { - const volunteers = await this.usersService.findUsersByRoles([Role.VOLUNTEER]); + const volunteers = await this.usersService.findUsersByRoles([ + Role.VOLUNTEER, + ]); return volunteers.map((v) => { const { pantries, ...volunteerWithoutPantries } = v; @@ -50,7 +52,7 @@ export class VolunteersService { } async getVolunteerPantries(volunteerId: number): Promise { - validateId(volunteerId, 'Volunteer') + validateId(volunteerId, 'Volunteer'); const volunteer = await this.findOne(volunteerId); return volunteer.pantries; } From 9d8da74a760c969b6e9c8bcb3e058c51620488f1 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 21:21:52 -0500 Subject: [PATCH 05/11] Fixed tests --- apps/backend/src/app.module.ts | 2 + .../src/users/users.controller.spec.ts | 134 ---------- apps/backend/src/users/users.service.spec.ts | 13 - apps/backend/src/users/users.service.ts | 17 +- .../volunteers/volunteers.controller.spec.ts | 161 ++++++++++++ .../src/volunteers/volunteers.module.ts | 21 ++ .../src/volunteers/volunteers.service.spec.ts | 248 ++++++++++++++++++ .../src/volunteers/volunteers.service.ts | 6 +- apps/frontend/src/api/apiClient.ts | 2 +- 9 files changed, 445 insertions(+), 159 deletions(-) create mode 100644 apps/backend/src/volunteers/volunteers.module.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 7fe3ff82e..eb4a7e1d9 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -18,6 +18,7 @@ import { APP_GUARD } from '@nestjs/core'; import { RolesGuard } from './auth/roles.guard'; import { JwtAuthGuard } from './auth/jwt-auth.guard'; import { ScheduleModule } from '@nestjs/schedule'; +import { VolunteersModule } from './volunteers/volunteers.module'; @Module({ imports: [ @@ -43,6 +44,7 @@ import { ScheduleModule } from '@nestjs/schedule'; OrdersModule, ManufacturerModule, AllocationModule, + VolunteersModule, ], controllers: [AppController], providers: [ diff --git a/apps/backend/src/users/users.controller.spec.ts b/apps/backend/src/users/users.controller.spec.ts index 97811a0e1..cc314fcee 100644 --- a/apps/backend/src/users/users.controller.spec.ts +++ b/apps/backend/src/users/users.controller.spec.ts @@ -7,7 +7,6 @@ import { userSchemaDto } from './dtos/userSchema.dto'; import { Test, TestingModule } from '@nestjs/testing'; import { mock } from 'jest-mock-extended'; -import { Pantry } from '../pantries/pantries.entity'; const mockUserService = mock(); @@ -20,35 +19,6 @@ const mockUser1: Partial = { role: Role.VOLUNTEER, }; -const mockUser2: Partial = { - id: 2543210, - email: 'bobsmith@example.com', - firstName: 'Bob', - lastName: 'Smith', - phone: '9876', - role: Role.VOLUNTEER, -}; - -const mockUser3: Partial = { - id: 3, - role: Role.VOLUNTEER, -}; - -const mockPantries: Partial[] = [ - { - pantryId: 1, - pantryUser: mockUser1 as User, - }, - { - pantryId: 2, - pantryUser: mockUser1 as User, - }, - { - pantryId: 3, - pantryUser: mockUser2 as User, - }, -]; - describe('UsersController', () => { let controller: UsersController; @@ -76,45 +46,6 @@ describe('UsersController', () => { expect(controller).toBeDefined(); }); - describe('GET /volunteers', () => { - it('should return all volunteers', async () => { - const users: (Omit, 'pantries'> & { - pantryIds: number[]; - })[] = [ - { - id: 1, - role: Role.VOLUNTEER, - pantryIds: [1], - }, - { - id: 2, - role: Role.VOLUNTEER, - pantryIds: [2], - }, - { - id: 3, - role: Role.ADMIN, - pantryIds: [3], - }, - ]; - - const volunteers = users.slice(0, 2); - - mockUserService.getVolunteersAndPantryAssignments.mockResolvedValue( - volunteers as (Omit & { pantryIds: number[] })[], - ); - - const result = await controller.getAllVolunteers(); - - expect(result).toEqual(volunteers); - expect(result.length).toBe(2); - expect(result.every((u) => u.role === Role.VOLUNTEER)).toBe(true); - expect( - mockUserService.getVolunteersAndPantryAssignments, - ).toHaveBeenCalled(); - }); - }); - describe('GET /:id', () => { it('should return a user by id', async () => { mockUserService.findOne.mockResolvedValue(mockUser1 as User); @@ -200,69 +131,4 @@ describe('UsersController', () => { ); }); }); - - describe('GET /volunteers', () => { - it('should return all volunteers with their pantry assignments', async () => { - const assignments: (User & { pantryIds: number[] })[] = [ - { ...(mockUser1 as User), pantryIds: [1, 2] }, - { ...(mockUser2 as User), pantryIds: [1] }, - { ...(mockUser3 as User), pantryIds: [] }, - ]; - - mockUserService.getVolunteersAndPantryAssignments.mockResolvedValue( - assignments, - ); - - const result = await controller.getAllVolunteers(); - - expect(result).toEqual(assignments); - expect(result).toHaveLength(3); - expect(result[0].id).toBe(1); - expect(result[0].pantryIds).toEqual([1, 2]); - expect(result[1].id).toBe(2543210); - expect(result[1].pantryIds).toEqual([1]); - expect(result[2].id).toBe(3); - expect(result[2].pantryIds).toEqual([]); - expect( - mockUserService.getVolunteersAndPantryAssignments, - ).toHaveBeenCalled(); - }); - }); - - describe('GET /:id/pantries', () => { - it('should return pantries assigned to a user', async () => { - mockUserService.getVolunteerPantries.mockResolvedValue( - mockPantries.slice(0, 2) as Pantry[], - ); - - const result = await controller.getVolunteerPantries(1); - - expect(result).toHaveLength(2); - expect(result).toEqual(mockPantries.slice(0, 2)); - expect(mockUserService.getVolunteerPantries).toHaveBeenCalledWith(1); - }); - }); - - describe('POST /:id/pantries', () => { - it('should assign pantries to a volunteer and return result', async () => { - const pantryIds = [1, 3]; - const updatedUser = { - ...mockUser3, - pantries: [mockPantries[0] as Pantry, mockPantries[2] as Pantry], - } as User; - - mockUserService.assignPantriesToVolunteer.mockResolvedValue(updatedUser); - - const result = await controller.assignPantries(3, pantryIds); - - expect(result).toEqual(updatedUser); - expect(result.pantries).toHaveLength(2); - expect(result.pantries[0].pantryId).toBe(1); - expect(result.pantries[1].pantryId).toBe(3); - expect(mockUserService.assignPantriesToVolunteer).toHaveBeenCalledWith( - 3, - pantryIds, - ); - }); - }); }); diff --git a/apps/backend/src/users/users.service.spec.ts b/apps/backend/src/users/users.service.spec.ts index 64145f5a4..371ea4e65 100644 --- a/apps/backend/src/users/users.service.spec.ts +++ b/apps/backend/src/users/users.service.spec.ts @@ -128,19 +128,6 @@ describe('UsersService', () => { }); }); - describe('findByEmail', () => { - it('should return user by email', async () => { - mockUserRepository.findOneBy.mockResolvedValue(mockUser as User); - - const result = await service.findByEmail('test@example.com'); - - expect(result).toEqual(mockUser); - expect(mockUserRepository.findOneBy).toHaveBeenCalledWith({ - email: 'test@example.com', - }); - }); - }); - describe('update', () => { it('should update user attributes', async () => { const updateData = { firstName: 'Updated', role: Role.ADMIN }; diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index d72ffa53d..93527676b 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -8,16 +7,12 @@ import { In, Repository } from 'typeorm'; import { User } from './user.entity'; import { Role } from './types'; import { validateId } from '../utils/validation.utils'; -import { Pantry } from '../pantries/pantries.entity'; -import { PantriesService } from '../pantries/pantries.service'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private repo: Repository, - - private pantriesService: PantriesService, ) {} async create( @@ -49,10 +44,6 @@ export class UsersService { return user; } - find(email: string) { - return this.repo.find({ where: { email } }); - } - async update(id: number, attrs: Partial) { validateId(id, 'User'); @@ -85,4 +76,12 @@ export class UsersService { relations: ['pantries'], }); } + + async findUserByCognitoId(cognitoId: string): Promise { + const user = await this.repo.findOneBy({ userCognitoSub: cognitoId }); + if (!user) { + throw new NotFoundException(`User with cognitoId ${cognitoId} not found`); + } + return user; + } } diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index e69de29bb..2e57ac986 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -0,0 +1,161 @@ +import { BadRequestException } from '@nestjs/common'; +import { VolunteersController } from './volunteers.controller' +import { UsersController } from '../users/users.controller'; +import { UsersService } from '../users/users.service'; +import { User } from '../users/user.entity'; +import { Role } from '../users/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import { mock } from 'jest-mock-extended'; +import { Pantry } from '../pantries/pantries.entity'; +import { VolunteersService } from './volunteers.service'; + +const mockVolunteersService = mock(); + +const mockVolunteer1: Partial = { + id: 1, + email: 'john@example.com', + firstName: 'John', + lastName: 'Doe', + phone: '1234567890', + role: Role.VOLUNTEER, +}; + +const mockVolunteer2: Partial = { + id: 2543210, + email: 'bobsmith@example.com', + firstName: 'Bob', + lastName: 'Smith', + phone: '9876', + role: Role.VOLUNTEER, +}; + +const mockVolunteer3: Partial = { + id: 3, + role: Role.VOLUNTEER, +}; + +const mockPantries: Partial[] = [ + { + pantryId: 1, + pantryUser: mockVolunteer1 as User, + }, + { + pantryId: 2, + pantryUser: mockVolunteer1 as User, + }, + { + pantryId: 3, + pantryUser: mockVolunteer2 as User, + }, +]; + +describe('VolunteersController', () => { + let controller: VolunteersController; + + beforeEach(async () => { + mockVolunteersService.findOne.mockReset(); + + const module: TestingModule = await Test.createTestingModule({ + controllers: [VolunteersController], + providers: [ + { + provide: VolunteersService, + useValue: mockVolunteersService, + }, + ], + }).compile(); + + controller = module.get(VolunteersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('GET /', () => { + it('should return all volunteers', async () => { + const volunteers: (Omit, 'pantries'> & { + pantryIds: number[]; + })[] = [ + { + id: 1, + role: Role.VOLUNTEER, + pantryIds: [1], + }, + { + id: 2, + role: Role.VOLUNTEER, + pantryIds: [2], + }, + { + id: 3, + role: Role.ADMIN, + pantryIds: [3], + }, + ]; + + const expectedVolunteers = volunteers.slice(0, 2); + + mockVolunteersService.getVolunteersAndPantryAssignments.mockResolvedValue( + expectedVolunteers as (Omit & { pantryIds: number[] })[], + ); + + const result = await controller.getAllVolunteers(); + + expect(result).toEqual(expectedVolunteers); + expect(result.length).toBe(2); + expect(result.every((u) => u.role === Role.VOLUNTEER)).toBe(true); + expect( + mockVolunteersService.getVolunteersAndPantryAssignments, + ).toHaveBeenCalled(); + }); + }); + + describe('GET /:id', () => { + it('should return a user by id', async () => { + mockVolunteersService.findOne.mockResolvedValue(mockVolunteer1 as User); + + const result = await controller.getVolunteer(1); + + expect(result).toEqual(mockVolunteer1); + expect(mockVolunteersService.findOne).toHaveBeenCalledWith(1); + }); + }); + + describe('GET /:id/pantries', () => { + it('should return pantries assigned to a user', async () => { + mockVolunteersService.getVolunteerPantries.mockResolvedValue( + mockPantries.slice(0, 2) as Pantry[], + ); + + const result = await controller.getVolunteerPantries(1); + + expect(result).toHaveLength(2); + expect(result).toEqual(mockPantries.slice(0, 2)); + expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith(1); + }); + }); + + describe('POST /:id/pantries', () => { + it('should assign pantries to a volunteer and return result', async () => { + const pantryIds = [1, 3]; + const updatedUser = { + ...mockVolunteer3, + pantries: [mockPantries[0] as Pantry, mockPantries[2] as Pantry], + } as User; + + mockVolunteersService.assignPantriesToVolunteer.mockResolvedValue(updatedUser); + + const result = await controller.assignPantries(3, pantryIds); + + expect(result).toEqual(updatedUser); + expect(result.pantries).toHaveLength(2); + expect(result.pantries[0].pantryId).toBe(1); + expect(result.pantries[1].pantryId).toBe(3); + expect(mockVolunteersService.assignPantriesToVolunteer).toHaveBeenCalledWith( + 3, + pantryIds, + ); + }); + }); +}); diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts new file mode 100644 index 000000000..c7147fcff --- /dev/null +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -0,0 +1,21 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '../users/user.entity'; +import { PantriesModule } from '../pantries/pantries.module'; +import { AuthModule } from '../auth/auth.module'; +import { VolunteersController } from './volunteers.controller'; +import { VolunteersService } from './volunteers.service'; +import { UsersModule } from '../users/users.module'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([User]), + UsersModule, + forwardRef(() => PantriesModule), + forwardRef(() => AuthModule), + ], + controllers: [VolunteersController], + providers: [VolunteersService], + exports: [VolunteersService], +}) +export class VolunteersModule {} diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index e69de29bb..f8d188906 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -0,0 +1,248 @@ +import { NotFoundException } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { User } from '../users/user.entity'; +import { Role } from '../users/types'; +import { mock } from 'jest-mock-extended'; +import { BadRequestException } from '@nestjs/common'; +import { PantriesService } from '../pantries/pantries.service'; +import { UsersService } from '../users/users.service'; +import { VolunteersService } from './volunteers.service'; +import { Pantry } from '../pantries/pantries.entity'; + +const mockUserRepository = mock>(); +const mockPantriesService = mock(); +const mockUsersService = mock(); + +const mockVolunteer: Partial = { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: '1234567890', + role: Role.VOLUNTEER, + pantries: [], +}; + +const mockNonVolunteer: Partial = { + id: 2, + email: 'admin@example.com', + firstName: 'Jane', + lastName: 'Smith', + phone: '0987654321', + role: Role.ADMIN, + pantries: [], +}; + +describe('VolunteersService', () => { + let service: VolunteersService; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + providers: [ + VolunteersService, + { + provide: getRepositoryToken(User), + useValue: mockUserRepository, + }, + { + provide: UsersService, + useValue: mockUsersService, + }, + { + provide: PantriesService, + useValue: mockPantriesService, + }, + ], + }).compile(); + + service = module.get(VolunteersService); + }); + + beforeEach(() => { + mockUserRepository.findOne.mockReset(); + mockUserRepository.save.mockReset(); + mockUserRepository.find.mockReset(); + mockUserRepository.remove.mockReset(); + mockPantriesService.findByIds.mockReset(); + mockUsersService.findUsersByRoles.mockReset(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return a volunteer by id', async () => { + mockUserRepository.findOne.mockResolvedValue(mockVolunteer as User); + + const result = await service.findOne(1); + + expect(result).toEqual(mockVolunteer); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + where: { id: 1 }, + relations: ['pantries'], + }); + }); + + it('should throw NotFoundException when user is not found', async () => { + mockUserRepository.findOne.mockResolvedValue(null); + + await expect(service.findOne(999)).rejects.toThrow( + new NotFoundException('Volunteer 999 not found'), + ); + }); + + it('should throw NotFoundException when user is not a volunteer', async () => { + mockUserRepository.findOne.mockResolvedValue(mockNonVolunteer as User); + + await expect(service.findOne(2)).rejects.toThrow( + new NotFoundException('User 2 is not a volunteer'), + ); + }); + + it('should throw error for invalid id', async () => { + await expect(service.findOne(-1)).rejects.toThrow( + new BadRequestException('Invalid Volunteer ID'), + ); + + expect(mockUserRepository.findOne).not.toHaveBeenCalled(); + }); + }); + + describe('getVolunteersAndPantryAssignments', () => { + it('should return volunteers with pantry IDs', async () => { + const mockPantry: Partial = { pantryId: 1, pantryName: 'Test Pantry' }; + const volunteerWithPantries = { + ...mockVolunteer, + pantries: [mockPantry as Pantry], + }; + + mockUsersService.findUsersByRoles.mockResolvedValue([ + volunteerWithPantries as User, + ]); + + const result = await service.getVolunteersAndPantryAssignments(); + + expect(result).toEqual([ + { + id: 1, + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: '1234567890', + role: Role.VOLUNTEER, + pantryIds: [1], + }, + ]); + expect(mockUsersService.findUsersByRoles).toHaveBeenCalledWith([ + Role.VOLUNTEER, + ]); + }); + }); + + describe('getVolunteerPantries', () => { + it('should return pantries for a volunteer', async () => { + const mockPantry: Partial = { pantryId: 1, pantryName: 'Test Pantry' }; + const volunteerWithPantries = { + ...mockVolunteer, + pantries: [mockPantry as Pantry], + }; + + mockUserRepository.findOne.mockResolvedValue( + volunteerWithPantries as User, + ); + + const result = await service.getVolunteerPantries(1); + + expect(result).toEqual([mockPantry]); + expect(mockUserRepository.findOne).toHaveBeenCalledWith({ + where: { id: 1 }, + relations: ['pantries'], + }); + }); + + it('should throw error for invalid volunteer id', async () => { + await expect(service.getVolunteerPantries(-1)).rejects.toThrow( + new BadRequestException('Invalid Volunteer ID'), + ); + }); + }); + + describe('assignPantriesToVolunteer', () => { + it('should assign new pantries to volunteer', async () => { + const mockPantry1: Partial = { + pantryId: 1, + pantryName: 'Pantry 1', + }; + const mockPantry2: Partial = { + pantryId: 2, + pantryName: 'Pantry 2', + }; + + const volunteerWithPantries = { + ...mockVolunteer, + pantries: [mockPantry1 as Pantry], + }; + + mockUserRepository.findOne.mockResolvedValue( + volunteerWithPantries as User, + ); + mockPantriesService.findByIds.mockResolvedValue([ + mockPantry2 as Pantry, + ]); + mockUserRepository.save.mockResolvedValue({ + ...volunteerWithPantries, + pantries: [mockPantry1 as Pantry, mockPantry2 as Pantry], + } as User); + + const result = await service.assignPantriesToVolunteer(1, [2]); + + expect(result.pantries).toHaveLength(2); + expect(mockPantriesService.findByIds).toHaveBeenCalledWith([2]); + expect(mockUserRepository.save).toHaveBeenCalled(); + }); + + it('should not add duplicate pantries', async () => { + const mockPantry: Partial = { + pantryId: 1, + pantryName: 'Pantry 1', + }; + + const volunteerWithPantries = { + ...mockVolunteer, + pantries: [mockPantry as Pantry], + }; + + mockUserRepository.findOne.mockResolvedValue( + volunteerWithPantries as User, + ); + mockPantriesService.findByIds.mockResolvedValue([mockPantry as Pantry]); + mockUserRepository.save.mockResolvedValue(volunteerWithPantries as User); + + const result = await service.assignPantriesToVolunteer(1, [1]); + + expect(result.pantries).toHaveLength(1); + expect(mockUserRepository.save).toHaveBeenCalled(); + }); + + it('should throw error for invalid volunteer id', async () => { + await expect( + service.assignPantriesToVolunteer(-1, [1]), + ).rejects.toThrow(new BadRequestException('Invalid Volunteer ID')); + }); + + it('should throw error for invalid pantry id', async () => { + mockUserRepository.findOne.mockResolvedValue(mockVolunteer as User); + + await expect( + service.assignPantriesToVolunteer(1, [-1]), + ).rejects.toThrow(new BadRequestException('Invalid Pantry ID')); + }); + }); +}); \ No newline at end of file diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 322adec85..45d6a3148 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -1,10 +1,9 @@ import { - BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { In, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { User } from '../users/user.entity'; import { Role } from '../users/types'; import { validateId } from '../utils/validation.utils'; @@ -32,6 +31,9 @@ export class VolunteersService { if (!volunteer) { throw new NotFoundException(`Volunteer ${id} not found`); } + if (volunteer.role !== Role.VOLUNTEER) { + throw new NotFoundException(`User ${id} is not a volunteer`) + } return volunteer; } diff --git a/apps/frontend/src/api/apiClient.ts b/apps/frontend/src/api/apiClient.ts index 09a317451..e06c73141 100644 --- a/apps/frontend/src/api/apiClient.ts +++ b/apps/frontend/src/api/apiClient.ts @@ -170,7 +170,7 @@ export class ApiClient { } public async getVolunteers(): Promise { - return this.get('/api/users/volunteers') as Promise; + return this.get('/api/volunteers/') as Promise; } public async updateUserVolunteerRole( From e9c7a18b522143b07f5224cef3144ebad6ebca74 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 21:22:22 -0500 Subject: [PATCH 06/11] prettier --- apps/backend/src/users/users.service.ts | 5 +--- .../volunteers/volunteers.controller.spec.ts | 21 ++++++++------ .../src/volunteers/volunteers.service.spec.ts | 28 +++++++++++-------- .../src/volunteers/volunteers.service.ts | 7 ++--- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/apps/backend/src/users/users.service.ts b/apps/backend/src/users/users.service.ts index 93527676b..fa595105d 100644 --- a/apps/backend/src/users/users.service.ts +++ b/apps/backend/src/users/users.service.ts @@ -1,7 +1,4 @@ -import { - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { In, Repository } from 'typeorm'; import { User } from './user.entity'; diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index 2e57ac986..d4b3dd1a0 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { VolunteersController } from './volunteers.controller' +import { VolunteersController } from './volunteers.controller'; import { UsersController } from '../users/users.controller'; import { UsersService } from '../users/users.service'; import { User } from '../users/user.entity'; @@ -97,7 +97,9 @@ describe('VolunteersController', () => { const expectedVolunteers = volunteers.slice(0, 2); mockVolunteersService.getVolunteersAndPantryAssignments.mockResolvedValue( - expectedVolunteers as (Omit & { pantryIds: number[] })[], + expectedVolunteers as (Omit & { + pantryIds: number[]; + })[], ); const result = await controller.getAllVolunteers(); @@ -132,7 +134,9 @@ describe('VolunteersController', () => { expect(result).toHaveLength(2); expect(result).toEqual(mockPantries.slice(0, 2)); - expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith(1); + expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith( + 1, + ); }); }); @@ -144,7 +148,9 @@ describe('VolunteersController', () => { pantries: [mockPantries[0] as Pantry, mockPantries[2] as Pantry], } as User; - mockVolunteersService.assignPantriesToVolunteer.mockResolvedValue(updatedUser); + mockVolunteersService.assignPantriesToVolunteer.mockResolvedValue( + updatedUser, + ); const result = await controller.assignPantries(3, pantryIds); @@ -152,10 +158,9 @@ describe('VolunteersController', () => { expect(result.pantries).toHaveLength(2); expect(result.pantries[0].pantryId).toBe(1); expect(result.pantries[1].pantryId).toBe(3); - expect(mockVolunteersService.assignPantriesToVolunteer).toHaveBeenCalledWith( - 3, - pantryIds, - ); + expect( + mockVolunteersService.assignPantriesToVolunteer, + ).toHaveBeenCalledWith(3, pantryIds); }); }); }); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index f8d188906..49823e60a 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -117,7 +117,10 @@ describe('VolunteersService', () => { describe('getVolunteersAndPantryAssignments', () => { it('should return volunteers with pantry IDs', async () => { - const mockPantry: Partial = { pantryId: 1, pantryName: 'Test Pantry' }; + const mockPantry: Partial = { + pantryId: 1, + pantryName: 'Test Pantry', + }; const volunteerWithPantries = { ...mockVolunteer, pantries: [mockPantry as Pantry], @@ -148,7 +151,10 @@ describe('VolunteersService', () => { describe('getVolunteerPantries', () => { it('should return pantries for a volunteer', async () => { - const mockPantry: Partial = { pantryId: 1, pantryName: 'Test Pantry' }; + const mockPantry: Partial = { + pantryId: 1, + pantryName: 'Test Pantry', + }; const volunteerWithPantries = { ...mockVolunteer, pantries: [mockPantry as Pantry], @@ -193,9 +199,7 @@ describe('VolunteersService', () => { mockUserRepository.findOne.mockResolvedValue( volunteerWithPantries as User, ); - mockPantriesService.findByIds.mockResolvedValue([ - mockPantry2 as Pantry, - ]); + mockPantriesService.findByIds.mockResolvedValue([mockPantry2 as Pantry]); mockUserRepository.save.mockResolvedValue({ ...volunteerWithPantries, pantries: [mockPantry1 as Pantry, mockPantry2 as Pantry], @@ -232,17 +236,17 @@ describe('VolunteersService', () => { }); it('should throw error for invalid volunteer id', async () => { - await expect( - service.assignPantriesToVolunteer(-1, [1]), - ).rejects.toThrow(new BadRequestException('Invalid Volunteer ID')); + await expect(service.assignPantriesToVolunteer(-1, [1])).rejects.toThrow( + new BadRequestException('Invalid Volunteer ID'), + ); }); it('should throw error for invalid pantry id', async () => { mockUserRepository.findOne.mockResolvedValue(mockVolunteer as User); - await expect( - service.assignPantriesToVolunteer(1, [-1]), - ).rejects.toThrow(new BadRequestException('Invalid Pantry ID')); + await expect(service.assignPantriesToVolunteer(1, [-1])).rejects.toThrow( + new BadRequestException('Invalid Pantry ID'), + ); }); }); -}); \ No newline at end of file +}); diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 45d6a3148..5fd9d3b49 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -1,7 +1,4 @@ -import { - Injectable, - NotFoundException, -} from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { User } from '../users/user.entity'; @@ -32,7 +29,7 @@ export class VolunteersService { throw new NotFoundException(`Volunteer ${id} not found`); } if (volunteer.role !== Role.VOLUNTEER) { - throw new NotFoundException(`User ${id} is not a volunteer`) + throw new NotFoundException(`User ${id} is not a volunteer`); } return volunteer; } From 03f753c510d6e1ed3ba286771749168b10dabac5 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 22:38:32 -0500 Subject: [PATCH 07/11] Fixed tests for one --- .../volunteers/volunteers.controller.spec.ts | 6 +- .../src/volunteers/volunteers.service.spec.ts | 246 +++--------------- 2 files changed, 41 insertions(+), 211 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index d4b3dd1a0..e5a56bc51 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -1,4 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; +import { BadRequestException, NotFoundException } from '@nestjs/common'; import { VolunteersController } from './volunteers.controller'; import { UsersController } from '../users/users.controller'; import { UsersService } from '../users/users.service'; @@ -134,9 +134,7 @@ describe('VolunteersController', () => { expect(result).toHaveLength(2); expect(result).toEqual(mockPantries.slice(0, 2)); - expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith( - 1, - ); + expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith(1); }); }); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 49823e60a..4e0540df5 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -1,58 +1,36 @@ import { NotFoundException } from '@nestjs/common'; -import { Test } from '@nestjs/testing'; +import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { User } from '../users/user.entity'; -import { Role } from '../users/types'; -import { mock } from 'jest-mock-extended'; -import { BadRequestException } from '@nestjs/common'; -import { PantriesService } from '../pantries/pantries.service'; -import { UsersService } from '../users/users.service'; import { VolunteersService } from './volunteers.service'; import { Pantry } from '../pantries/pantries.entity'; +import { testDataSource } from '../config/typeormTestDataSource'; -const mockUserRepository = mock>(); -const mockPantriesService = mock(); -const mockUsersService = mock(); - -const mockVolunteer: Partial = { - id: 1, - email: 'test@example.com', - firstName: 'John', - lastName: 'Doe', - phone: '1234567890', - role: Role.VOLUNTEER, - pantries: [], -}; - -const mockNonVolunteer: Partial = { - id: 2, - email: 'admin@example.com', - firstName: 'Jane', - lastName: 'Smith', - phone: '0987654321', - role: Role.ADMIN, - pantries: [], -}; +jest.setTimeout(60000); describe('VolunteersService', () => { let service: VolunteersService; beforeAll(async () => { - const module = await Test.createTestingModule({ + // Initialize DataSource once + if (!testDataSource.isInitialized) { + await testDataSource.initialize(); + } + + // Clean database at the start + await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + + const module: TestingModule = await Test.createTestingModule({ providers: [ VolunteersService, { provide: getRepositoryToken(User), - useValue: mockUserRepository, + useValue: testDataSource.getRepository(User), }, { - provide: UsersService, - useValue: mockUsersService, - }, - { - provide: PantriesService, - useValue: mockPantriesService, + provide: getRepositoryToken(Pantry), + useValue: testDataSource.getRepository(Pantry), }, ], }).compile(); @@ -60,17 +38,22 @@ describe('VolunteersService', () => { service = module.get(VolunteersService); }); - beforeEach(() => { - mockUserRepository.findOne.mockReset(); - mockUserRepository.save.mockReset(); - mockUserRepository.find.mockReset(); - mockUserRepository.remove.mockReset(); - mockPantriesService.findByIds.mockReset(); - mockUsersService.findUsersByRoles.mockReset(); + beforeEach(async () => { + // Run all migrations fresh for each test + await testDataSource.runMigrations(); + }); + + afterEach(async () => { + // Drop the schema completely (cascades all tables) + await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); }); - afterEach(() => { - jest.clearAllMocks(); + afterAll(async () => { + // Destroy all schemas + if (testDataSource.isInitialized) { + await testDataSource.destroy(); + } }); it('should be defined', () => { @@ -79,174 +62,23 @@ describe('VolunteersService', () => { describe('findOne', () => { it('should return a volunteer by id', async () => { - mockUserRepository.findOne.mockResolvedValue(mockVolunteer as User); - - const result = await service.findOne(1); + const volunteerId = 6; + const result = await service.findOne(volunteerId); - expect(result).toEqual(mockVolunteer); - expect(mockUserRepository.findOne).toHaveBeenCalledWith({ - where: { id: 1 }, - relations: ['pantries'], - }); + expect(result).toBeDefined(); + expect(result.id).toBe(6); }); - it('should throw NotFoundException when user is not found', async () => { - mockUserRepository.findOne.mockResolvedValue(null); - + it('should throw NotFoundException when volunteer is not found', async () => { await expect(service.findOne(999)).rejects.toThrow( new NotFoundException('Volunteer 999 not found'), ); }); - it('should throw NotFoundException when user is not a volunteer', async () => { - mockUserRepository.findOne.mockResolvedValue(mockNonVolunteer as User); - - await expect(service.findOne(2)).rejects.toThrow( - new NotFoundException('User 2 is not a volunteer'), - ); - }); - - it('should throw error for invalid id', async () => { - await expect(service.findOne(-1)).rejects.toThrow( - new BadRequestException('Invalid Volunteer ID'), - ); - - expect(mockUserRepository.findOne).not.toHaveBeenCalled(); - }); - }); - - describe('getVolunteersAndPantryAssignments', () => { - it('should return volunteers with pantry IDs', async () => { - const mockPantry: Partial = { - pantryId: 1, - pantryName: 'Test Pantry', - }; - const volunteerWithPantries = { - ...mockVolunteer, - pantries: [mockPantry as Pantry], - }; - - mockUsersService.findUsersByRoles.mockResolvedValue([ - volunteerWithPantries as User, - ]); - - const result = await service.getVolunteersAndPantryAssignments(); - - expect(result).toEqual([ - { - id: 1, - email: 'test@example.com', - firstName: 'John', - lastName: 'Doe', - phone: '1234567890', - role: Role.VOLUNTEER, - pantryIds: [1], - }, - ]); - expect(mockUsersService.findUsersByRoles).toHaveBeenCalledWith([ - Role.VOLUNTEER, - ]); - }); - }); - - describe('getVolunteerPantries', () => { - it('should return pantries for a volunteer', async () => { - const mockPantry: Partial = { - pantryId: 1, - pantryName: 'Test Pantry', - }; - const volunteerWithPantries = { - ...mockVolunteer, - pantries: [mockPantry as Pantry], - }; - - mockUserRepository.findOne.mockResolvedValue( - volunteerWithPantries as User, - ); - - const result = await service.getVolunteerPantries(1); - - expect(result).toEqual([mockPantry]); - expect(mockUserRepository.findOne).toHaveBeenCalledWith({ - where: { id: 1 }, - relations: ['pantries'], - }); - }); - - it('should throw error for invalid volunteer id', async () => { - await expect(service.getVolunteerPantries(-1)).rejects.toThrow( - new BadRequestException('Invalid Volunteer ID'), - ); - }); - }); - - describe('assignPantriesToVolunteer', () => { - it('should assign new pantries to volunteer', async () => { - const mockPantry1: Partial = { - pantryId: 1, - pantryName: 'Pantry 1', - }; - const mockPantry2: Partial = { - pantryId: 2, - pantryName: 'Pantry 2', - }; - - const volunteerWithPantries = { - ...mockVolunteer, - pantries: [mockPantry1 as Pantry], - }; - - mockUserRepository.findOne.mockResolvedValue( - volunteerWithPantries as User, - ); - mockPantriesService.findByIds.mockResolvedValue([mockPantry2 as Pantry]); - mockUserRepository.save.mockResolvedValue({ - ...volunteerWithPantries, - pantries: [mockPantry1 as Pantry, mockPantry2 as Pantry], - } as User); - - const result = await service.assignPantriesToVolunteer(1, [2]); - - expect(result.pantries).toHaveLength(2); - expect(mockPantriesService.findByIds).toHaveBeenCalledWith([2]); - expect(mockUserRepository.save).toHaveBeenCalled(); - }); - - it('should not add duplicate pantries', async () => { - const mockPantry: Partial = { - pantryId: 1, - pantryName: 'Pantry 1', - }; - - const volunteerWithPantries = { - ...mockVolunteer, - pantries: [mockPantry as Pantry], - }; - - mockUserRepository.findOne.mockResolvedValue( - volunteerWithPantries as User, - ); - mockPantriesService.findByIds.mockResolvedValue([mockPantry as Pantry]); - mockUserRepository.save.mockResolvedValue(volunteerWithPantries as User); - - const result = await service.assignPantriesToVolunteer(1, [1]); - - expect(result.pantries).toHaveLength(1); - expect(mockUserRepository.save).toHaveBeenCalled(); - }); - - it('should throw error for invalid volunteer id', async () => { - await expect(service.assignPantriesToVolunteer(-1, [1])).rejects.toThrow( - new BadRequestException('Invalid Volunteer ID'), + it('should throw a NotFoundException when a non-volunteer is found', async () => { + await expect(service.findOne(1)).rejects.toThrow( + new NotFoundException('User 1 is not a volunteer'), ); - }); - - it('should throw error for invalid pantry id', async () => { - mockUserRepository.findOne.mockResolvedValue(mockVolunteer as User); - - await expect(service.assignPantriesToVolunteer(1, [-1])).rejects.toThrow( - new BadRequestException('Invalid Pantry ID'), - ); - }); + }) }); }); From 8f7ddf7a83a967f06ac9bc0ae17f99f36468c9f2 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Sun, 15 Feb 2026 22:38:52 -0500 Subject: [PATCH 08/11] prettier --- apps/backend/src/volunteers/volunteers.controller.spec.ts | 4 +++- apps/backend/src/volunteers/volunteers.service.spec.ts | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index e5a56bc51..8513c9984 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -134,7 +134,9 @@ describe('VolunteersController', () => { expect(result).toHaveLength(2); expect(result).toEqual(mockPantries.slice(0, 2)); - expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith(1); + expect(mockVolunteersService.getVolunteerPantries).toHaveBeenCalledWith( + 1, + ); }); }); diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 4e0540df5..9ded3872a 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -79,6 +79,6 @@ describe('VolunteersService', () => { await expect(service.findOne(1)).rejects.toThrow( new NotFoundException('User 1 is not a volunteer'), ); - }) + }); }); }); From cc915295321d3b8391fe8db1af5eeb6a9db3d941 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 16 Feb 2026 00:00:24 -0500 Subject: [PATCH 09/11] Wrote volunteer unit tests --- apps/backend/src/orders/order.service.spec.ts | 10 +- .../src/volunteers/volunteers.service.spec.ts | 122 ++++++++++++++++-- package.json | 2 +- 3 files changed, 117 insertions(+), 17 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index e8e41949e..63c02d9e3 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -18,10 +18,6 @@ describe('OrdersService', () => { await testDataSource.initialize(); } - // Clean database at the start - await testDataSource.query(`DROP SCHEMA public CASCADE`); - await testDataSource.query(`CREATE SCHEMA public`); - const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, @@ -40,18 +36,16 @@ describe('OrdersService', () => { }); beforeEach(async () => { - // Run all migrations fresh for each test + await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); await testDataSource.runMigrations(); }); afterEach(async () => { - // Drop the schema completely (cascades all tables) await testDataSource.query(`DROP SCHEMA public CASCADE`); - await testDataSource.query(`CREATE SCHEMA public`); }); afterAll(async () => { - // Destroy all schemas if (testDataSource.isInitialized) { await testDataSource.destroy(); } diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index 9ded3872a..badcedc51 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -5,6 +5,8 @@ import { User } from '../users/user.entity'; import { VolunteersService } from './volunteers.service'; import { Pantry } from '../pantries/pantries.entity'; import { testDataSource } from '../config/typeormTestDataSource'; +import { UsersService } from '../users/users.service'; +import { PantriesService } from '../pantries/pantries.service'; jest.setTimeout(60000); @@ -17,13 +19,11 @@ describe('VolunteersService', () => { await testDataSource.initialize(); } - // Clean database at the start - await testDataSource.query(`DROP SCHEMA public CASCADE`); - await testDataSource.query(`CREATE SCHEMA public`); - const module: TestingModule = await Test.createTestingModule({ providers: [ VolunteersService, + UsersService, + PantriesService, { provide: getRepositoryToken(User), useValue: testDataSource.getRepository(User), @@ -39,18 +39,16 @@ describe('VolunteersService', () => { }); beforeEach(async () => { - // Run all migrations fresh for each test + await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); await testDataSource.runMigrations(); }); afterEach(async () => { - // Drop the schema completely (cascades all tables) await testDataSource.query(`DROP SCHEMA public CASCADE`); - await testDataSource.query(`CREATE SCHEMA public`); }); afterAll(async () => { - // Destroy all schemas if (testDataSource.isInitialized) { await testDataSource.destroy(); } @@ -81,4 +79,112 @@ describe('VolunteersService', () => { ); }); }); + + describe('getVolunteersAndPantryAssignments', () => { + it('returns an empty array when there are no volunteers', async () => { + // Delete all users with role 'volunteer' (CASCADE will handle related data) + await testDataSource.query(`DELETE FROM "users" WHERE role = 'volunteer'`); + + const result = await service.getVolunteersAndPantryAssignments(); + + expect(result).toEqual([]); + }); + + it('returns all volunteers with their pantry assignments', async () => { + const result = await service.getVolunteersAndPantryAssignments(); + + expect(result.length).toEqual(4) + expect(result).toEqual([ + { + id: 6, + firstName: 'James', + lastName: 'Thomas', + email: 'james.t@volunteer.org', + phone: '555-040-0401', + role: 'volunteer', + userCognitoSub: '', + pantryIds: [1], + }, + { + id: 7, + firstName: 'Maria', + lastName: 'Garcia', + email: 'maria.g@volunteer.org', + phone: '555-040-0402', + role: 'volunteer', + userCognitoSub: '', + pantryIds: [2,3], + }, + { + id: 8, + firstName: 'William', + lastName: 'Moore', + email: 'william.m@volunteer.org', + phone: '555-040-0403', + role: 'volunteer', + userCognitoSub: '', + pantryIds: [3], + }, + { + id: 9, + firstName: 'Patricia', + lastName: 'Jackson', + email: 'patricia.j@volunteer.org', + phone: '555-040-0404', + role: 'volunteer', + userCognitoSub: '', + pantryIds: [1], + }, + ]); + }); + }); + + describe('getVolunteerPantries', () => { + it('returns an empty array when volunteer has no pantry assignments', async () => { + await testDataSource.query( + `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6` + ); + + const result = await service.getVolunteerPantries(6); + + expect(result).toEqual([]); + }); + + it('returns all pantries assigned to a volunteer', async () => { + const result = await service.getVolunteerPantries(7); + + expect(result).toHaveLength(2); + + const pantryIds = result.map(p => p.pantryId); + expect(pantryIds).toEqual([2, 3]); + }); + }); + + describe('assignPantriesToVolunteer', () => { + it('assigns new pantries to a volunteer with existing assignments', async () => { + const beforeAssignment = await service.getVolunteerPantries(7); + expect(beforeAssignment).toHaveLength(2); + const beforePantryIds = beforeAssignment.map(p => p.pantryId); + expect(beforePantryIds).toEqual([2, 3]); + + const result = await service.assignPantriesToVolunteer(7, [1, 4]); + expect(result.pantries).toHaveLength(4); + const afterPantryIds = result.pantries.map(p => p.pantryId); + expect(afterPantryIds).toEqual([2,3,1,4]); + }); + + it('assigns pantries to a volunteer with no existing assignments', async () => { + await testDataSource.query( + `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6` + ); + + const beforeAssignment = await service.getVolunteerPantries(6); + expect(beforeAssignment).toEqual([]); + + const result = await service.assignPantriesToVolunteer(6, [2, 3]); + expect(result.pantries).toHaveLength(2); + const pantryIds = result.pantries.map(p => p.pantryId); + expect(pantryIds).toEqual([2, 3]); + }); + }); }); diff --git a/package.json b/package.json index 8652eef2b..486fa22f1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "format": "prettier --no-error-on-unmatched-pattern --write apps/{frontend,backend}/src/**/*.{js,ts,tsx}", "lint:check": "eslint apps/frontend --ext .ts,.tsx && eslint apps/backend --ext .ts,.tsx", "lint": "eslint apps/frontend --ext .ts,.tsx --fix && eslint apps/backend --ext .ts,.tsx --fix", - "test": "jest", + "test": "jest --runInBand", "prepush": "yarn run format:check && yarn run lint:check", "prepush:fix": "yarn run format && yarn run lint", "prepare": "husky install", From 30a95d3971df9a147159a2a9c8c8752170655f76 Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 16 Feb 2026 00:00:57 -0500 Subject: [PATCH 10/11] prettier --- .../src/volunteers/volunteers.service.spec.ts | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index badcedc51..20eed67f6 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -83,7 +83,9 @@ describe('VolunteersService', () => { describe('getVolunteersAndPantryAssignments', () => { it('returns an empty array when there are no volunteers', async () => { // Delete all users with role 'volunteer' (CASCADE will handle related data) - await testDataSource.query(`DELETE FROM "users" WHERE role = 'volunteer'`); + await testDataSource.query( + `DELETE FROM "users" WHERE role = 'volunteer'`, + ); const result = await service.getVolunteersAndPantryAssignments(); @@ -93,7 +95,7 @@ describe('VolunteersService', () => { it('returns all volunteers with their pantry assignments', async () => { const result = await service.getVolunteersAndPantryAssignments(); - expect(result.length).toEqual(4) + expect(result.length).toEqual(4); expect(result).toEqual([ { id: 6, @@ -113,7 +115,7 @@ describe('VolunteersService', () => { phone: '555-040-0402', role: 'volunteer', userCognitoSub: '', - pantryIds: [2,3], + pantryIds: [2, 3], }, { id: 8, @@ -142,7 +144,7 @@ describe('VolunteersService', () => { describe('getVolunteerPantries', () => { it('returns an empty array when volunteer has no pantry assignments', async () => { await testDataSource.query( - `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6` + `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6`, ); const result = await service.getVolunteerPantries(6); @@ -154,8 +156,8 @@ describe('VolunteersService', () => { const result = await service.getVolunteerPantries(7); expect(result).toHaveLength(2); - - const pantryIds = result.map(p => p.pantryId); + + const pantryIds = result.map((p) => p.pantryId); expect(pantryIds).toEqual([2, 3]); }); }); @@ -164,18 +166,18 @@ describe('VolunteersService', () => { it('assigns new pantries to a volunteer with existing assignments', async () => { const beforeAssignment = await service.getVolunteerPantries(7); expect(beforeAssignment).toHaveLength(2); - const beforePantryIds = beforeAssignment.map(p => p.pantryId); + const beforePantryIds = beforeAssignment.map((p) => p.pantryId); expect(beforePantryIds).toEqual([2, 3]); const result = await service.assignPantriesToVolunteer(7, [1, 4]); expect(result.pantries).toHaveLength(4); - const afterPantryIds = result.pantries.map(p => p.pantryId); - expect(afterPantryIds).toEqual([2,3,1,4]); + const afterPantryIds = result.pantries.map((p) => p.pantryId); + expect(afterPantryIds).toEqual([2, 3, 1, 4]); }); it('assigns pantries to a volunteer with no existing assignments', async () => { await testDataSource.query( - `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6` + `DELETE FROM "volunteer_assignments" WHERE volunteer_id = 6`, ); const beforeAssignment = await service.getVolunteerPantries(6); @@ -183,7 +185,7 @@ describe('VolunteersService', () => { const result = await service.assignPantriesToVolunteer(6, [2, 3]); expect(result.pantries).toHaveLength(2); - const pantryIds = result.pantries.map(p => p.pantryId); + const pantryIds = result.pantries.map((p) => p.pantryId); expect(pantryIds).toEqual([2, 3]); }); }); From 0e70cb931a67c9eacfda0274d942cae01346ff3c Mon Sep 17 00:00:00 2001 From: Dalton Burkhart Date: Mon, 16 Feb 2026 12:36:03 -0500 Subject: [PATCH 11/11] Fixed testing --- apps/backend/src/orders/order.service.spec.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/backend/src/orders/order.service.spec.ts b/apps/backend/src/orders/order.service.spec.ts index cdbbbe105..8d89fe1cf 100644 --- a/apps/backend/src/orders/order.service.spec.ts +++ b/apps/backend/src/orders/order.service.spec.ts @@ -20,6 +20,10 @@ describe('OrdersService', () => { await testDataSource.initialize(); } + // Clean database at the start + await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); + const module: TestingModule = await Test.createTestingModule({ providers: [ OrdersService, @@ -38,16 +42,18 @@ describe('OrdersService', () => { }); beforeEach(async () => { - await testDataSource.query(`DROP SCHEMA IF EXISTS public CASCADE`); - await testDataSource.query(`CREATE SCHEMA public`); + // Run all migrations fresh for each test await testDataSource.runMigrations(); }); afterEach(async () => { + // Drop the schema completely (cascades all tables) await testDataSource.query(`DROP SCHEMA public CASCADE`); + await testDataSource.query(`CREATE SCHEMA public`); }); afterAll(async () => { + // Destroy all schemas if (testDataSource.isInitialized) { await testDataSource.destroy(); }