Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ticket says

Build PATCH /volunteers/:id endpoint. Address, city, zipcode, homebase, residentAnimals, notes, and fosterType should all be editable fields in the DTO.

but dto also exposes firstName, lastName, phone, secondaryPhone, email as editable fields - want to double check that's intended.. @dburkhart07

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still waiting on client communication for what fields we want to be editable. for right now, let's just say all the ones currently implemented here (there are a few noneditable fields that i already had removed), and we will limit scope later on as we get more information. good catch though, sorry about the inconsistency

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