Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2bed4ed
Delete .github/dependabot.yml
dburkhart07 Sep 21, 2026
91a46b2
created update volunteer dto
shreeyaadhikari Sep 24, 2026
abb1c10
add service logic for get/update volunteer
shreeyaadhikari Sep 25, 2026
c8a565b
added not found exception for coordinator not found
shreeyaadhikari Sep 25, 2026
833a402
added controller logic
shreeyaadhikari Sep 25, 2026
0a2009f
service tests
shreeyaadhikari Sep 25, 2026
c548f81
service and controller tests
shreeyaadhikari Sep 25, 2026
9f773f2
..
shreeyaadhikari Sep 26, 2026
e229d8b
reverted changes meant for another branch
shreeyaadhikari Sep 26, 2026
e93101f
Merge branch 'main' of https://github.com/Code-4-Community/mspca into…
shreeyaadhikari Sep 26, 2026
26a1740
fixed prettier checks
shreeyaadhikari Sep 26, 2026
58956c5
Merge branch 'main' of https://github.com/Code-4-Community/mspca into…
shreeyaadhikari Sep 30, 2026
ba4b369
resolved dto comments
shreeyaadhikari Sep 30, 2026
1a7b55d
service.ts resolved
shreeyaadhikari Sep 30, 2026
fc1fcb1
controller + specs
shreeyaadhikari Sep 30, 2026
2f25424
removed yarn/yml files
shreeyaadhikari Oct 1, 2026
0454032
dto fixes
shreeyaadhikari Oct 1, 2026
df9447a
volunteer controller fixes
shreeyaadhikari Oct 1, 2026
be0d504
specs
shreeyaadhikari Oct 1, 2026
dd3f822
Merge branch 'main' of https://github.com/Code-4-Community/mspca into…
shreeyaadhikari Oct 1, 2026
efc5f8b
controller fix
shreeyaadhikari Oct 1, 2026
43f6006
fixes
shreeyaadhikari Oct 2, 2026
ba68448
plural naming strat
shreeyaadhikari Oct 2, 2026
d073472
added docstrings
shreeyaadhikari Oct 4, 2026
87bb1ce
Merge branch 'main' of https://github.com/Code-4-Community/mspca into…
shreeyaadhikari Oct 4, 2026
a1bd218
fixes
shreeyaadhikari Oct 5, 2026
5507313
Merge branch 'main' of https://github.com/Code-4-Community/mspca into…
shreeyaadhikari Oct 8, 2026
079fb85
used findByIdOrFail in volunteer get/update and update tests
shreeyaadhikari Oct 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 0 additions & 14 deletions .github/dependabot.yml

This file was deleted.

79 changes: 79 additions & 0 deletions apps/backend/src/volunteers/dto/update-volunteer.dto.ts
Comment thread
shreeyaadhikari marked this conversation as resolved.
Comment thread
shreeyaadhikari marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Homebase } from '../../types';
import { FosterType } from '../volunteers.types';
import {
IsOptional,
IsString,
IsEnum,
IsNotEmpty,
MaxLength,
IsPhoneNumber,
IsEmail,
} from 'class-validator';

export class UpdateVolunteerDto {
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(255)
firstName?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(255)
lastName?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US')
phone?: string;
Comment thread
shreeyaadhikari marked this conversation as resolved.

@IsOptional()
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US')
secondaryPhone?: string;
Comment thread
shreeyaadhikari marked this conversation as resolved.
Comment thread
shreeyaadhikari marked this conversation as resolved.

@IsOptional()
@IsNotEmpty()
@MaxLength(255)
@IsEmail()
email?: string;
Comment thread
shreeyaadhikari marked this conversation as resolved.

@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(255)
address?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(255)
city?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(10)
zipcode?: string;

@IsOptional()
@IsEnum(Homebase)
homebase?: Homebase;

@IsOptional()
@IsString()
@IsNotEmpty()
residentAnimals?: string;

@IsOptional()
@IsString()
@IsNotEmpty()
notes?: string;

@IsOptional()
@IsEnum(FosterType)
fosterType?: FosterType;
}
124 changes: 103 additions & 21 deletions apps/backend/src/volunteers/volunteers.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException } from '@nestjs/common';
import { NotFoundException, BadRequestException } 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';
import { FosterVolunteer } from './volunteers.entity';
import { UpdateVolunteerDto } from './dto/update-volunteer.dto';

describe('VolunteersController', () => {
let controller: VolunteersController;
let volunteersService: { findByIdOrFail: jest.Mock };
let matchesService: { findByVolunteerId: jest.Mock };

const mockVolunteer = {
Comment thread
shreeyaadhikari marked this conversation as resolved.
volunteerId: 1,
firstName: 'Jane',
lastName: 'Doe',
notes: 'likes cats',
} as FosterVolunteer;

let mockVolunteersService: {
findByIdOrFail: jest.Mock;
getVolunteerById: jest.Mock;
updateVolunteerById: jest.Mock;
};
let mockMatchesService: { findByVolunteerId: jest.Mock };

beforeEach(async () => {
volunteersService = { findByIdOrFail: jest.fn() };
matchesService = { findByVolunteerId: jest.fn() };
mockVolunteersService = {
findByIdOrFail: jest.fn(),
getVolunteerById: jest.fn(),
updateVolunteerById: jest.fn(),
};
mockMatchesService = { findByVolunteerId: jest.fn() };

const module: TestingModule = await Test.createTestingModule({
controllers: [VolunteersController],
providers: [
{
provide: VolunteersService,
useValue: volunteersService,
},
{
provide: MatchesService,
useValue: matchesService,
},
{ provide: VolunteersService, useValue: mockVolunteersService },
{ provide: MatchesService, useValue: mockMatchesService },
],
}).compile();

Expand All @@ -47,32 +59,102 @@ describe('VolunteersController', () => {
deniedReason: 'Schedule conflict',
},
] as Match[];
volunteersService.findByIdOrFail.mockResolvedValue({});
matchesService.findByVolunteerId.mockResolvedValue(matches);
mockVolunteersService.findByIdOrFail.mockResolvedValue({});
mockMatchesService.findByVolunteerId.mockResolvedValue(matches);

const result = await controller.getVolunteerMatches(7);

expect(result).toBe(matches);
expect(volunteersService.findByIdOrFail).toHaveBeenCalledWith(7);
expect(matchesService.findByVolunteerId).toHaveBeenCalledWith(7);
expect(mockVolunteersService.findByIdOrFail).toHaveBeenCalledWith(7);
expect(mockMatchesService.findByVolunteerId).toHaveBeenCalledWith(7);
});

it('returns an empty array when the volunteer exists but has no matches', async () => {
volunteersService.findByIdOrFail.mockResolvedValue({});
matchesService.findByVolunteerId.mockResolvedValue([]);
mockVolunteersService.findByIdOrFail.mockResolvedValue({});
mockMatchesService.findByVolunteerId.mockResolvedValue([]);

await expect(controller.getVolunteerMatches(7)).resolves.toEqual([]);
});

it('throws NotFoundException when the volunteer does not exist', async () => {
volunteersService.findByIdOrFail.mockRejectedValue(
mockVolunteersService.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();
expect(mockMatchesService.findByVolunteerId).not.toHaveBeenCalled();
});
});

describe('getVolunteerById', () => {
it('should return a volunteer when found', async () => {
mockVolunteersService.getVolunteerById.mockResolvedValue(mockVolunteer);

const result = await controller.getVolunteerById(1);

expect(result).toEqual(mockVolunteer);
expect(mockVolunteersService.getVolunteerById).toHaveBeenCalledWith(1);
});

it('should throw NotFoundException when volunteer does not exist', async () => {
mockVolunteersService.getVolunteerById.mockRejectedValue(
new NotFoundException('Volunteer with id 999 not found'),
);

await expect(controller.getVolunteerById(999)).rejects.toThrow(
NotFoundException,
);
});

it('should throw BadRequestException when id is invalid', async () => {
await expect(controller.getVolunteerById(0)).rejects.toThrow(
BadRequestException,
);
expect(mockVolunteersService.getVolunteerById).not.toHaveBeenCalled();
});
});

describe('updateVolunteerById', () => {
it('should update and return the volunteer when found', async () => {
const dto = { notes: 'updated notes' } as UpdateVolunteerDto;
const updatedVolunteer = { ...mockVolunteer, ...dto } as FosterVolunteer;
mockVolunteersService.updateVolunteerById.mockResolvedValue(
updatedVolunteer,
);

const result = await controller.updateVolunteerById(1, dto);

expect(result).toEqual(updatedVolunteer);
expect(mockVolunteersService.updateVolunteerById).toHaveBeenCalledWith(
1,
dto,
);
});

it('should throw NotFoundException when volunteer does not exist', async () => {
mockVolunteersService.updateVolunteerById.mockRejectedValue(
new NotFoundException('Volunteer with id 999 not found'),
);

await expect(
controller.updateVolunteerById(999, { notes: 'x' }),
).rejects.toThrow(NotFoundException);
});

it('should throw BadRequestException when id is invalid', async () => {
await expect(
controller.updateVolunteerById(0, { notes: 'x' }),
).rejects.toThrow(BadRequestException);
expect(mockVolunteersService.updateVolunteerById).not.toHaveBeenCalled();
});

it('should throw BadRequestException when no fields are provided', async () => {
await expect(
controller.updateVolunteerById(1, {} as UpdateVolunteerDto),
).rejects.toThrow(BadRequestException);
expect(mockVolunteersService.updateVolunteerById).not.toHaveBeenCalled();
});
});
});
49 changes: 48 additions & 1 deletion apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import {
Controller,
Patch,
Get,
HttpStatus,
Param,
Body,
HttpStatus,
ParseIntPipe,
BadRequestException,
} 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';
import { UpdateVolunteerDto } from './dto/update-volunteer.dto';
import { FosterVolunteer } from './volunteers.entity';

@ApiTags('Volunteers')
// @ApiBearerAuth()
Expand Down Expand Up @@ -41,4 +46,46 @@ export class VolunteersController {

return this.matchesService.findByVolunteerId(volunteerId);
}

@Get('/:volunteerId')
Comment thread
shreeyaadhikari marked this conversation as resolved.
@ApiOperation({ summary: 'Get a volunteer by ID' })
@ApiParam({
name: 'volunteerId',
type: Number,
description: 'ID of the volunteer',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'The volunteer was found',
type: FosterVolunteer,
})
async getVolunteerById(
@Param('volunteerId', ParseIntPipe) volunteerId: number,
): Promise<FosterVolunteer> {
validateId(volunteerId, 'Volunteer');
return this.volunteersService.getVolunteerById(volunteerId);
}

@Patch('/:volunteerId')
Comment thread
shreeyaadhikari marked this conversation as resolved.
@ApiOperation({ summary: 'Update a volunteer by ID' })
@ApiParam({
name: 'volunteerId',
type: Number,
description: 'ID of the volunteer',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'The volunteer was updated',
type: FosterVolunteer,
})
async updateVolunteerById(
@Param('volunteerId', ParseIntPipe) volunteerId: number,
@Body() dto: UpdateVolunteerDto,
Comment thread
shreeyaadhikari marked this conversation as resolved.
): Promise<FosterVolunteer> {
validateId(volunteerId, 'Volunteer');
if (Object.keys(dto).length === 0) {
throw new BadRequestException('At least one field must be provided');
}
return this.volunteersService.updateVolunteerById(volunteerId, dto);
}
}
Loading
Loading