Skip to content
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [
Expand All @@ -16,6 +17,7 @@ import { CognitoModule } from './aws/cognito/cognito.module';
configService.getOrThrow('typeorm'),
}),
CognitoModule,
VolunteersModule,
],
})
export class AppModule {}
39 changes: 38 additions & 1 deletion apps/backend/src/matches/matches.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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([]);
});
});
});
4 changes: 4 additions & 0 deletions apps/backend/src/matches/matches.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export class MatchesService {
@InjectRepository(Match)
private repo: Repository<Match>,
) {}

findByVolunteerId(volunteerId: number): Promise<Match[]> {
return this.repo.find({ where: { volunteerId } });
}
}
8 changes: 0 additions & 8 deletions apps/backend/src/strategies/plural-naming.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
55 changes: 54 additions & 1 deletion apps/backend/src/volunteers/volunteers.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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();
});
});
});
45 changes: 37 additions & 8 deletions apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
@@ -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<User> {
// 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<Match[]> {
validateId(volunteerId, 'Volunteer');

await this.volunteersService.findByIdOrFail(volunteerId);

return this.matchesService.findByVolunteerId(volunteerId);
}
}
8 changes: 7 additions & 1 deletion apps/backend/src/volunteers/volunteers.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
27 changes: 26 additions & 1 deletion apps/backend/src/volunteers/volunteers.service.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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 });
});
});
});
23 changes: 8 additions & 15 deletions apps/backend/src/volunteers/volunteers.service.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -10,20 +10,13 @@ export class VolunteersService {
private repo: Repository<FosterVolunteer>,
) {}

// Example service functions
// find(email: string) {
// return this.repo.find({ where: { email } });
// }
async findByIdOrFail(id: number): Promise<FosterVolunteer> {
const volunteer = await this.repo.findOneBy({ volunteerId: id });

// async update(id: number, attrs: Partial<User>) {
// 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;
}
}
Loading