diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index faede2748..de33e1d0b 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule, ConfigService } from '@nestjs/config'; import typeorm from './config/typeorm'; import { CognitoModule } from './aws/cognito/cognito.module'; +import { VolunteersModule } from './volunteers/volunteers.module'; @Module({ imports: [ @@ -16,6 +17,7 @@ import { CognitoModule } from './aws/cognito/cognito.module'; configService.getOrThrow('typeorm'), }), CognitoModule, + VolunteersModule, ], }) export class AppModule {} diff --git a/apps/backend/src/matches/matches.service.spec.ts b/apps/backend/src/matches/matches.service.spec.ts index b2d520821..093c550e5 100644 --- a/apps/backend/src/matches/matches.service.spec.ts +++ b/apps/backend/src/matches/matches.service.spec.ts @@ -2,17 +2,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { MatchesService } from './matches.service'; import { Match } from './matches.entity'; +import { MatchStatus } from './matches.types'; describe('MatchesService', () => { let service: MatchesService; + let repo: { find: jest.Mock }; beforeEach(async () => { + repo = { find: jest.fn() }; + const module: TestingModule = await Test.createTestingModule({ providers: [ MatchesService, { provide: getRepositoryToken(Match), - useValue: {}, + useValue: repo, }, ], }).compile(); @@ -23,4 +27,37 @@ describe('MatchesService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findByVolunteerId', () => { + it('returns the matches for the volunteer', async () => { + const matches = [ + { + matchId: 1, + volunteerId: 7, + chameleonAnimalId: 42, + status: MatchStatus.PENDING, + deniedReason: null, + }, + { + matchId: 2, + volunteerId: 7, + chameleonAnimalId: 43, + status: MatchStatus.DENIED, + deniedReason: 'Resident dog is not cat-friendly', + }, + ] as Match[]; + repo.find.mockResolvedValue(matches); + + const result = await service.findByVolunteerId(7); + + expect(result).toBe(matches); + expect(repo.find).toHaveBeenCalledWith({ where: { volunteerId: 7 } }); + }); + + it('returns an empty array when the volunteer has no matches', async () => { + repo.find.mockResolvedValue([]); + + await expect(service.findByVolunteerId(7)).resolves.toEqual([]); + }); + }); }); diff --git a/apps/backend/src/matches/matches.service.ts b/apps/backend/src/matches/matches.service.ts index e91ccaf65..da322e372 100644 --- a/apps/backend/src/matches/matches.service.ts +++ b/apps/backend/src/matches/matches.service.ts @@ -9,4 +9,8 @@ export class MatchesService { @InjectRepository(Match) private repo: Repository, ) {} + + findByVolunteerId(volunteerId: number): Promise { + return this.repo.find({ where: { volunteerId } }); + } } diff --git a/apps/backend/src/strategies/plural-naming.strategy.ts b/apps/backend/src/strategies/plural-naming.strategy.ts index 1a7c0a156..03599d836 100644 --- a/apps/backend/src/strategies/plural-naming.strategy.ts +++ b/apps/backend/src/strategies/plural-naming.strategy.ts @@ -7,12 +7,4 @@ export class PluralNamingStrategy tableName(targetName: string, userSpecifiedName: string | undefined): string { return userSpecifiedName || targetName.toLowerCase() + 's'; // Pluralize the table name } - - columnName(propertyName: string): string { - return propertyName; - } - - relationName(propertyName: string): string { - return propertyName; - } } diff --git a/apps/backend/src/volunteers/volunteers.controller.spec.ts b/apps/backend/src/volunteers/volunteers.controller.spec.ts index a3cdb3daa..f7ece72f3 100644 --- a/apps/backend/src/volunteers/volunteers.controller.spec.ts +++ b/apps/backend/src/volunteers/volunteers.controller.spec.ts @@ -1,17 +1,30 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; import { VolunteersController } from './volunteers.controller'; import { VolunteersService } from './volunteers.service'; +import { MatchesService } from '../matches/matches.service'; +import { Match } from '../matches/matches.entity'; +import { MatchStatus } from '../matches/matches.types'; describe('VolunteersController', () => { let controller: VolunteersController; + let volunteersService: { findByIdOrFail: jest.Mock }; + let matchesService: { findByVolunteerId: jest.Mock }; beforeEach(async () => { + volunteersService = { findByIdOrFail: jest.fn() }; + matchesService = { findByVolunteerId: jest.fn() }; + const module: TestingModule = await Test.createTestingModule({ controllers: [VolunteersController], providers: [ { provide: VolunteersService, - useValue: {}, + useValue: volunteersService, + }, + { + provide: MatchesService, + useValue: matchesService, }, ], }).compile(); @@ -22,4 +35,44 @@ describe('VolunteersController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + describe('getVolunteerMatches', () => { + it("returns the volunteer's matches", async () => { + const matches = [ + { + matchId: 1, + volunteerId: 7, + chameleonAnimalId: 42, + status: MatchStatus.DENIED, + deniedReason: 'Schedule conflict', + }, + ] as Match[]; + volunteersService.findByIdOrFail.mockResolvedValue({}); + matchesService.findByVolunteerId.mockResolvedValue(matches); + + const result = await controller.getVolunteerMatches(7); + + expect(result).toBe(matches); + expect(volunteersService.findByIdOrFail).toHaveBeenCalledWith(7); + expect(matchesService.findByVolunteerId).toHaveBeenCalledWith(7); + }); + + it('returns an empty array when the volunteer exists but has no matches', async () => { + volunteersService.findByIdOrFail.mockResolvedValue({}); + matchesService.findByVolunteerId.mockResolvedValue([]); + + await expect(controller.getVolunteerMatches(7)).resolves.toEqual([]); + }); + + it('throws NotFoundException when the volunteer does not exist', async () => { + volunteersService.findByIdOrFail.mockRejectedValue( + new NotFoundException('Volunteer with ID 999 not found'), + ); + + await expect(controller.getVolunteerMatches(999)).rejects.toThrow( + new NotFoundException('Volunteer with ID 999 not found'), + ); + expect(matchesService.findByVolunteerId).not.toHaveBeenCalled(); + }); + }); }); diff --git a/apps/backend/src/volunteers/volunteers.controller.ts b/apps/backend/src/volunteers/volunteers.controller.ts index d2c246fc8..a28c8aaa1 100644 --- a/apps/backend/src/volunteers/volunteers.controller.ts +++ b/apps/backend/src/volunteers/volunteers.controller.ts @@ -1,15 +1,44 @@ -import { Controller } from '@nestjs/common'; +import { + Controller, + Get, + HttpStatus, + Param, + ParseIntPipe, +} from '@nestjs/common'; +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { VolunteersService } from './volunteers.service'; +import { MatchesService } from '../matches/matches.service'; +import { Match } from '../matches/matches.entity'; +import { validateId } from '../utils/validation.utils'; -// @ApiTags('Volunteers') +@ApiTags('Volunteers') // @ApiBearerAuth() @Controller('volunteers') export class VolunteersController { - constructor(private volunteersService: VolunteersService) {} + constructor( + private volunteersService: VolunteersService, + private matchesService: MatchesService, + ) {} - // Example endpoint - // @Get('/:userId') - // async getUser(@Param('userId', ParseIntPipe) userId: number): Promise { - // return this.usersService.findOne(userId); - // } + @Get('/:volunteerId/matches') + @ApiOperation({ summary: 'Get all Matches for a Volunteer' }) + @ApiParam({ + name: 'volunteerId', + type: Number, + description: 'ID of the Volunteer', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: "The Volunteer's Matches", + type: [Match], + }) + async getVolunteerMatches( + @Param('volunteerId', ParseIntPipe) volunteerId: number, + ): Promise { + validateId(volunteerId, 'Volunteer'); + + await this.volunteersService.findByIdOrFail(volunteerId); + + return this.matchesService.findByVolunteerId(volunteerId); + } } diff --git a/apps/backend/src/volunteers/volunteers.module.ts b/apps/backend/src/volunteers/volunteers.module.ts index d3fef67b4..4697b4cdd 100644 --- a/apps/backend/src/volunteers/volunteers.module.ts +++ b/apps/backend/src/volunteers/volunteers.module.ts @@ -3,9 +3,15 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FosterVolunteer } from './volunteers.entity'; import { VolunteersController } from './volunteers.controller'; import { VolunteersService } from './volunteers.service'; +import { CoordinatorsModule } from '../coordinators/coordinators.module'; +import { MatchesModule } from '../matches/matches.module'; @Module({ - imports: [TypeOrmModule.forFeature([FosterVolunteer])], + imports: [ + TypeOrmModule.forFeature([FosterVolunteer]), + CoordinatorsModule, + MatchesModule, + ], controllers: [VolunteersController], providers: [VolunteersService], exports: [VolunteersService], diff --git a/apps/backend/src/volunteers/volunteers.service.spec.ts b/apps/backend/src/volunteers/volunteers.service.spec.ts index fda546818..7ff6ef097 100644 --- a/apps/backend/src/volunteers/volunteers.service.spec.ts +++ b/apps/backend/src/volunteers/volunteers.service.spec.ts @@ -1,18 +1,22 @@ import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; import { getRepositoryToken } from '@nestjs/typeorm'; import { VolunteersService } from './volunteers.service'; import { FosterVolunteer } from './volunteers.entity'; describe('VolunteersService', () => { let service: VolunteersService; + let repo: { findOneBy: jest.Mock }; beforeEach(async () => { + repo = { findOneBy: jest.fn() }; + const module: TestingModule = await Test.createTestingModule({ providers: [ VolunteersService, { provide: getRepositoryToken(FosterVolunteer), - useValue: {}, + useValue: repo, }, ], }).compile(); @@ -23,4 +27,25 @@ describe('VolunteersService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + describe('findByIdOrFail', () => { + it('returns the volunteer when one with the id exists', async () => { + const volunteer = { volunteerId: 7 } as FosterVolunteer; + repo.findOneBy.mockResolvedValue(volunteer); + + const result = await service.findByIdOrFail(7); + + expect(result).toBe(volunteer); + expect(repo.findOneBy).toHaveBeenCalledWith({ volunteerId: 7 }); + }); + + it('throws NotFoundException when no volunteer with the id exists', async () => { + repo.findOneBy.mockResolvedValue(null); + + await expect(service.findByIdOrFail(7)).rejects.toThrow( + new NotFoundException('Volunteer with ID 7 not found'), + ); + expect(repo.findOneBy).toHaveBeenCalledWith({ volunteerId: 7 }); + }); + }); }); diff --git a/apps/backend/src/volunteers/volunteers.service.ts b/apps/backend/src/volunteers/volunteers.service.ts index 1c6f17a61..6da786853 100644 --- a/apps/backend/src/volunteers/volunteers.service.ts +++ b/apps/backend/src/volunteers/volunteers.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotFoundException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { FosterVolunteer } from './volunteers.entity'; @@ -10,20 +10,13 @@ export class VolunteersService { private repo: Repository, ) {} - // Example service functions - // find(email: string) { - // return this.repo.find({ where: { email } }); - // } + async findByIdOrFail(id: number): Promise { + const volunteer = await this.repo.findOneBy({ volunteerId: id }); - // async update(id: number, attrs: Partial) { - // const user = await this.findOne(id); + if (!volunteer) { + throw new NotFoundException(`Volunteer with ID ${id} not found`); + } - // if (!user) { - // throw new NotFoundException('User not found'); - // } - - // Object.assign(user, attrs); - - // return this.repo.save(user); - // } + return volunteer; + } }