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

Binary file added .yarn/install-state.gz
Binary file not shown.
6 changes: 6 additions & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
approvedGitRepositories:

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.

I don't think we need this file or the install-state.gz. Can you try deleting these and then running yarn install to see if it reinstalls them? If not, let's keep them removed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not quite sure how to delete these from the PR.

- "**"

enableScripts: true

nodeLinker: node-modules
88 changes: 88 additions & 0 deletions apps/backend/src/volunteers/dto/update-volunteer.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { Homebase } from '../../types';
import { FosterType } from '../volunteers.types';
import {
IsOptional,
IsString,
IsNumber,
IsEnum,
IsPositive,
IsNotEmpty,
MaxLength,
IsPhoneNumber,
IsEmail,
} from 'class-validator';

export class UpdateVolunteerDto {
@IsNumber()
@IsPositive()
volunteerId?: number;
Comment thread
shreeyaadhikari marked this conversation as resolved.
Comment thread
shreeyaadhikari marked this conversation as resolved.

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

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

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

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

@IsOptional()
@IsString()
@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;
}
100 changes: 96 additions & 4 deletions apps/backend/src/volunteers/volunteers.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,117 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { VolunteersController } from './volunteers.controller';
import { VolunteersService } from './volunteers.service';
import { FosterVolunteer } from './volunteers.entity';

describe('VolunteersController', () => {
let controller: VolunteersController;

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

const mockVolunteersService = {
getVolunteerById: jest.fn(),
updateVolunteerById: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [VolunteersController],
providers: [
{
provide: VolunteersService,
useValue: {},
},
{ provide: VolunteersService, useValue: mockVolunteersService },
],
}).compile();

controller = module.get<VolunteersController>(VolunteersController);
});

afterEach(() => {
jest.clearAllMocks();
});

it('should be defined', () => {
expect(controller).toBeDefined();
});

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 = { volunteerId: 1, notes: 'updated notes' };
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, { volunteerId: 999, notes: 'x' }),
).rejects.toThrow(NotFoundException);
});

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

it('should throw BadRequestException when path and body ids do not match', async () => {
await expect(
controller.updateVolunteerById(1, { volunteerId: 2, notes: 'x' }),
).rejects.toThrow(BadRequestException);
expect(mockVolunteersService.updateVolunteerById).not.toHaveBeenCalled();
});

it('should throw BadRequestException when no fields besides volunteerId are provided', async () => {
await expect(
controller.updateVolunteerById(1, { volunteerId: 1 }),
).rejects.toThrow(BadRequestException);
expect(mockVolunteersService.updateVolunteerById).not.toHaveBeenCalled();
});
});
});
64 changes: 58 additions & 6 deletions apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,67 @@
import { Controller } from '@nestjs/common';
import {
Controller,
Patch,
Get,
Param,
Body,
ParseIntPipe,
BadRequestException,
} from '@nestjs/common';
import { ApiOperation, ApiParam, ApiResponse } from '@nestjs/swagger';
import { VolunteersService } from './volunteers.service';
import { validateId } from '../utils/validation.utils';
import { UpdateVolunteerDto } from './dto/update-volunteer.dto';
import { FosterVolunteer } from './volunteers.entity';

// @ApiTags('Volunteers')
// @ApiBearerAuth()
@Controller('volunteers')
export class VolunteersController {
constructor(private volunteersService: VolunteersService) {}

// Example endpoint
// @Get('/:userId')
// async getUser(@Param('userId', ParseIntPipe) userId: number): Promise<User> {
// return this.usersService.findOne(userId);
// }
@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: 200,
description: 'The volunteer was found',
type: FosterVolunteer,
})
async getVolunteerById(
@Param('volunteerId', ParseIntPipe) volunteerId: number,
): Promise<FosterVolunteer> {
validateId(volunteerId, 'FosterVolunteer');
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: 200,
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, 'FosterVolunteer');
if (dto.volunteerId !== volunteerId) {
throw new BadRequestException('volunteerId in path and body must match');
}
const fields = Object.keys(dto).filter((key) => key !== 'volunteerId');
if (fields.length === 0) {
throw new BadRequestException('At least one field must be provided');
}
return this.volunteersService.updateVolunteerById(volunteerId, dto);
}
}
3 changes: 2 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,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { FosterVolunteer } from './volunteers.entity';
import { VolunteersController } from './volunteers.controller';
import { VolunteersService } from './volunteers.service';
import { FosterCoordinator } from '../coordinators/coordinators.entity';

@Module({
imports: [TypeOrmModule.forFeature([FosterVolunteer])],
imports: [TypeOrmModule.forFeature([FosterVolunteer, FosterCoordinator])],
controllers: [VolunteersController],
providers: [VolunteersService],
exports: [VolunteersService],
Expand Down
Loading
Loading