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.

7 changes: 7 additions & 0 deletions apps/backend/src/matches/deny-match.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class DenyMatchDto {
@IsNotEmpty()
@IsString()
deniedReason!: string;
}
116 changes: 110 additions & 6 deletions apps/backend/src/matches/matches.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,129 @@
import { Test, TestingModule } from '@nestjs/testing';
import { NotFoundException, BadRequestException } from '@nestjs/common';
import { validate } from 'class-validator';
import { MatchesController } from './matches.controller';
import { MatchesService } from './matches.service';
import { DenyMatchDto } from './deny-match.dto';
import { MatchStatus } from './matches.types';

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

const mockMatch = {
matchId: 1,
status: MatchStatus.COMPLETE,
deniedReason: null,
};

const mockMatchesService = {
approveMatch: jest.fn(),
denyMatch: jest.fn(),
};

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

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

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

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

describe('approveMatch', () => {
it('should approve and return the match when found', async () => {
mockMatchesService.approveMatch.mockResolvedValue(mockMatch);

const result = await controller.approveMatch('1');

expect(result).toEqual(mockMatch);
expect(mockMatchesService.approveMatch).toHaveBeenCalledWith(1);
});

it('should throw NotFoundException when match does not exist', async () => {
mockMatchesService.approveMatch.mockResolvedValue(null);

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

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

describe('denyMatch', () => {
it('should deny and return the match when found', async () => {
const deniedMatch = {
...mockMatch,
status: MatchStatus.DENIED,
deniedReason: 'Not enough space',
};
mockMatchesService.denyMatch.mockResolvedValue(deniedMatch);

const dto: DenyMatchDto = { deniedReason: 'Not enough space' };
const result = await controller.denyMatch('1', dto);

expect(result).toEqual(deniedMatch);
expect(mockMatchesService.denyMatch).toHaveBeenCalledWith(
1,
'Not enough space',
);
});

it('should throw NotFoundException when match does not exist', async () => {
mockMatchesService.denyMatch.mockResolvedValue(null);

const dto: DenyMatchDto = { deniedReason: 'Some reason' };
await expect(controller.denyMatch('999', dto)).rejects.toThrow(
NotFoundException,
);
});

it('should throw BadRequestException when id is invalid', async () => {
const dto: DenyMatchDto = { deniedReason: 'Some reason' };
await expect(controller.denyMatch('0', dto)).rejects.toThrow(
BadRequestException,
);
});
});

describe('DenyMatchDto validation', () => {
it('should fail validation when deniedReason is empty', async () => {
const dto = new DenyMatchDto();
dto.deniedReason = '';

const errors = await validate(dto);

expect(errors.length).toBeGreaterThan(0);
expect(errors[0].property).toBe('deniedReason');
});

it('should fail validation when deniedReason is missing', async () => {
const dto = new DenyMatchDto();

const errors = await validate(dto);

expect(errors.length).toBeGreaterThan(0);
});

it('should pass validation when deniedReason is a non-empty string', async () => {
const dto = new DenyMatchDto();
dto.deniedReason = 'Not enough space';

const errors = await validate(dto);

expect(errors.length).toBe(0);
});
});
});
37 changes: 36 additions & 1 deletion apps/backend/src/matches/matches.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,42 @@
import { Controller } from '@nestjs/common';
import {
Controller,
Patch,
Param,
Body,
NotFoundException,
} from '@nestjs/common';
import { MatchesService } from './matches.service';
import { validateId } from '../utils/validation.utils';
import { DenyMatchDto } from './deny-match.dto';

@Controller('matches')
export class MatchesController {
constructor(private matchesService: MatchesService) {}

@Patch(':matchId/approve')
async approveMatch(@Param('matchId') matchId: string) {
const id = Number(matchId);
validateId(id, 'Match');

const match = await this.matchesService.approveMatch(id);
if (!match) {
throw new NotFoundException('Match not found');
}
return match;
}

@Patch(':matchId/deny')
async denyMatch(
@Param('matchId') matchId: string,
@Body() dto: DenyMatchDto,
) {
const id = Number(matchId);
validateId(id, 'Match');

const match = await this.matchesService.denyMatch(id, dto.deniedReason);
if (!match) {
throw new NotFoundException('Match not found');
}
return match;
}
}
74 changes: 70 additions & 4 deletions apps/backend/src/matches/matches.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,91 @@ 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;

const mockMatch = {
matchId: 1,
status: MatchStatus.PENDING,
deniedReason: null,
};

const mockRepo = {
findOneBy: jest.fn(),
save: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
MatchesService,
{
provide: getRepositoryToken(Match),
useValue: {},
},
{ provide: getRepositoryToken(Match), useValue: mockRepo },
],
}).compile();

service = module.get<MatchesService>(MatchesService);
});

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

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

describe('approveMatch', () => {
it('should approve a match and return it with updated status', async () => {
mockRepo.findOneBy.mockResolvedValue({ ...mockMatch });
mockRepo.save.mockImplementation((m) => Promise.resolve(m));

const result = await service.approveMatch(1);

if (!result) {
throw new Error('Expected a match, got null');
}

expect(result.status).toBe(MatchStatus.COMPLETE);
expect(mockRepo.findOneBy).toHaveBeenCalledWith({ matchId: 1 });
expect(mockRepo.save).toHaveBeenCalled();
});

it('should return null when match does not exist', async () => {
mockRepo.findOneBy.mockResolvedValue(null);

const result = await service.approveMatch(999);

expect(result).toBeNull();
expect(mockRepo.save).not.toHaveBeenCalled();
});
});

describe('denyMatch', () => {
it('should deny a match and save the reason', async () => {
mockRepo.findOneBy.mockResolvedValue({ ...mockMatch });
mockRepo.save.mockImplementation((m) => Promise.resolve(m));

const result = await service.denyMatch(1, 'Not enough space in home');

if (!result) {
throw new Error('Expected a match, got null');
}

expect(result.status).toBe(MatchStatus.DENIED);
expect(result.deniedReason).toBe('Not enough space in home');
expect(mockRepo.findOneBy).toHaveBeenCalledWith({ matchId: 1 });
expect(mockRepo.save).toHaveBeenCalled();
});

it('should return null when match does not exist', async () => {
mockRepo.findOneBy.mockResolvedValue(null);

const result = await service.denyMatch(999, 'Some reason');

expect(result).toBeNull();
expect(mockRepo.save).not.toHaveBeenCalled();
});
});
});
22 changes: 22 additions & 0 deletions apps/backend/src/matches/matches.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,33 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Match } from './matches.entity';
import { MatchStatus } from './matches.types';

@Injectable()
export class MatchesService {
constructor(
@InjectRepository(Match)
private repo: Repository<Match>,
) {}

async approveMatch(id: number) {
const match = await this.repo.findOneBy({ matchId: id });
if (!match) {
return null;
}

match.status = MatchStatus.COMPLETE;
return this.repo.save(match);
}

async denyMatch(id: number, deniedReason: string) {
const match = await this.repo.findOneBy({ matchId: id });
if (!match) {
return null;
}

match.status = MatchStatus.DENIED;
match.deniedReason = deniedReason;
return this.repo.save(match);
}
}
7 changes: 6 additions & 1 deletion apps/backend/src/volunteers/volunteers.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Controller } from '@nestjs/common';
import { Controller, Get } from '@nestjs/common';
import { VolunteersService } from './volunteers.service';

// @ApiTags('Volunteers')
Expand All @@ -12,4 +12,9 @@ export class VolunteersController {
// async getUser(@Param('userId', ParseIntPipe) userId: number): Promise<User> {
// return this.usersService.findOne(userId);
// }

@Get()
async getAllVolunteers() {
return this.volunteersService.getAllVolunteers();
}
}
4 changes: 4 additions & 0 deletions apps/backend/src/volunteers/volunteers.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ export class VolunteersService {

// return this.repo.save(user);
// }

async getAllVolunteers() {
return this.repo.find();
}
}
Loading