Skip to content
Merged
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
39 changes: 27 additions & 12 deletions backend/OpenAPI.json
Original file line number Diff line number Diff line change
Expand Up @@ -1617,42 +1617,57 @@
]
}
},
"/supervision-requests/pending-count/{userId}": {
"/supervision-requests/count/{userId}": {
"get": {
"description": "Returns the count of pending supervision requests for a specific user. Requesting a student gets their outgoing requests, requesting a supervisor gets their incoming requests.",
"operationId": "SupervisionRequestsController_getPendingRequestCountForUser",
"description": "Returns the count of supervision requests for a specific user filtered by state. Requesting a student gets their outgoing requests, requesting a supervisor gets their incoming requests.",
"operationId": "SupervisionRequestsController_getRequestCountForUser",
"parameters": [
{
"name": "userId",
"required": true,
"in": "path",
"description": "ID of the user to get pending request count for",
"description": "ID of the user to get request count for",
"schema": {
"format": "uuid",
"example": "123e4567-e89b-12d3-a456-426614174000",
"type": "string"
}
},
{
"name": "request_state",
"required": true,
"in": "query",
"description": "The request state to count",
"schema": {
"enum": [
"PENDING",
"ACCEPTED",
"REJECTED",
"WITHDRAWN"
],
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Returns the count of pending supervision requests",
"description": "Returns the count of supervision requests",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PendingRequestCountEntity"
"$ref": "#/components/schemas/SupervisionRequestCountEntity"
}
}
}
},
"400": {
"description": "Bad request - Invalid UUID or admin user requested"
"description": "Bad request - Invalid UUID, admin user requested, or missing request state"
},
"404": {
"description": "User not found"
}
},
"summary": "Get pending supervision request count for a user",
"summary": "Get supervision request count for a user by state",
"tags": [
"supervision-requests"
]
Expand Down Expand Up @@ -2877,18 +2892,18 @@
"request_state"
]
},
"PendingRequestCountEntity": {
"SupervisionRequestCountEntity": {
"type": "object",
"properties": {
"pending_count": {
"request_count": {
"type": "number",
"description": "Number of pending supervision requests for the user",
"description": "Number of supervision requests for the user with the specified state",
"example": 5,
"minimum": 0
}
},
"required": [
"pending_count"
"request_count"
]
}
}
Expand Down
14 changes: 14 additions & 0 deletions backend/src/modules/requests/supervision/dto/count-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
import { RequestState } from '@prisma/client';

export class CountQueryDto {
@ApiProperty({
description: 'The request state to count',
enum: RequestState,
example: RequestState.PENDING,
})
@IsEnum(RequestState, { message: 'Must be a valid request state' })
@IsNotEmpty({ message: 'Request state is required' })
request_state: RequestState;
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ApiProperty } from '@nestjs/swagger';

export class SupervisionRequestCountEntity {
@ApiProperty({
description: 'Number of supervision requests for the user with the specified state',
example: 5,
minimum: 0,
})
request_count: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { RequestState, Role, User } from '@prisma/client';
import { CreateSupervisionRequestDto } from './dto/create-supervision-request.dto';
import { UpdateSupervisionRequestDto } from './dto/update-supervision-request.dto';
import { SupervisionRequestQueryDto } from './dto/supervision-request-query.dto';
import { PendingRequestCountEntity } from './entities/pending-request-count.entity';
import { SupervisionRequestCountEntity } from './entities/supervision-request-count.entity';
import { AdminSupervisionRequestException } from '../../../common/exceptions/custom-exceptions/admin-supervision-request.exception';
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { StudentAlreadyHasAnAcceptedSupervisionRequestException } from '../../../common/exceptions/custom-exceptions/multiple-supervision-acceptances.exception';
Expand All @@ -19,7 +19,7 @@ describe('SupervisionRequestsController', () => {
findAllRequests: jest.fn(),
findRequestById: jest.fn(),
updateRequestState: jest.fn(),
countPendingRequestsForUser: jest.fn(),
countRequestsForUser: jest.fn(),
};

// Sample test data with proper UUIDs
Expand Down Expand Up @@ -381,98 +381,104 @@ describe('SupervisionRequestsController', () => {
});
});

describe('getPendingRequestCountForUser', () => {
describe('getRequestCountForUser', () => {
it('should return pending request count for a student user', async () => {
// Arrange
const expectedResponse: PendingRequestCountEntity = { pending_count: 3 };
mockSupervisionRequestsService.countPendingRequestsForUser.mockResolvedValue(
expectedResponse,
);
const expectedResponse: SupervisionRequestCountEntity = { request_count: 3 };
const queryParams = { request_state: RequestState.PENDING };
mockSupervisionRequestsService.countRequestsForUser.mockResolvedValue(expectedResponse);

// Act
const result = await controller.getPendingRequestCountForUser(STUDENT_USER_UUID);
const result = await controller.getRequestCountForUser(STUDENT_USER_UUID, queryParams);

// Assert
expect(result).toEqual(expectedResponse);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
STUDENT_USER_UUID,
RequestState.PENDING,
);
});

it('should return pending request count for a supervisor user', async () => {
it('should return accepted request count for a supervisor user', async () => {
// Arrange
const expectedResponse: PendingRequestCountEntity = { pending_count: 5 };
mockSupervisionRequestsService.countPendingRequestsForUser.mockResolvedValue(
expectedResponse,
);
const expectedResponse: SupervisionRequestCountEntity = { request_count: 5 };
const queryParams = { request_state: RequestState.ACCEPTED };
mockSupervisionRequestsService.countRequestsForUser.mockResolvedValue(expectedResponse);

// Act
const result = await controller.getPendingRequestCountForUser(SUPERVISOR_USER_UUID);
const result = await controller.getRequestCountForUser(SUPERVISOR_USER_UUID, queryParams);

// Assert
expect(result).toEqual(expectedResponse);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
SUPERVISOR_USER_UUID,
RequestState.ACCEPTED,
);
});

it('should return 0 pending requests when user has none', async () => {
it('should return 0 requests when user has none', async () => {
// Arrange
const expectedResponse: PendingRequestCountEntity = { pending_count: 0 };
mockSupervisionRequestsService.countPendingRequestsForUser.mockResolvedValue(
expectedResponse,
);
const expectedResponse: SupervisionRequestCountEntity = { request_count: 0 };
const queryParams = { request_state: RequestState.REJECTED };
mockSupervisionRequestsService.countRequestsForUser.mockResolvedValue(expectedResponse);

// Act
const result = await controller.getPendingRequestCountForUser(STUDENT_USER_UUID);
const result = await controller.getRequestCountForUser(STUDENT_USER_UUID, queryParams);

// Assert
expect(result).toEqual(expectedResponse);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
STUDENT_USER_UUID,
RequestState.REJECTED,
);
});

it('should pass through NotFoundException when user is not found', async () => {
// Arrange
const nonExistentUserId = 'non-existent-user-id';
const queryParams = { request_state: RequestState.PENDING };
const expectedError = new NotFoundException(`User with ID ${nonExistentUserId} not found`);
mockSupervisionRequestsService.countPendingRequestsForUser.mockRejectedValue(expectedError);
mockSupervisionRequestsService.countRequestsForUser.mockRejectedValue(expectedError);

// Act & Assert
await expect(controller.getPendingRequestCountForUser(nonExistentUserId)).rejects.toThrow(
expectedError,
);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
await expect(
controller.getRequestCountForUser(nonExistentUserId, queryParams),
).rejects.toThrow(expectedError);
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
nonExistentUserId,
RequestState.PENDING,
);
});

it('should pass through AdminSupervisionRequestException when admin user is requested', async () => {
// Arrange
const queryParams = { request_state: RequestState.PENDING };
const expectedError = new AdminSupervisionRequestException();
mockSupervisionRequestsService.countPendingRequestsForUser.mockRejectedValue(expectedError);
mockSupervisionRequestsService.countRequestsForUser.mockRejectedValue(expectedError);

// Act & Assert
await expect(controller.getPendingRequestCountForUser(ADMIN_USER_UUID)).rejects.toThrow(
await expect(controller.getRequestCountForUser(ADMIN_USER_UUID, queryParams)).rejects.toThrow(
expectedError,
);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
ADMIN_USER_UUID,
RequestState.PENDING,
);
});

it('should pass through any service errors', async () => {
// Arrange
const queryParams = { request_state: RequestState.WITHDRAWN };
const serviceError = new Error('Database connection failed');
mockSupervisionRequestsService.countPendingRequestsForUser.mockRejectedValue(serviceError);
mockSupervisionRequestsService.countRequestsForUser.mockRejectedValue(serviceError);

// Act & Assert
await expect(controller.getPendingRequestCountForUser(STUDENT_USER_UUID)).rejects.toThrow(
'Database connection failed',
);
expect(mockSupervisionRequestsService.countPendingRequestsForUser).toHaveBeenCalledWith(
await expect(
controller.getRequestCountForUser(STUDENT_USER_UUID, queryParams),
).rejects.toThrow('Database connection failed');
expect(mockSupervisionRequestsService.countRequestsForUser).toHaveBeenCalledWith(
STUDENT_USER_UUID,
RequestState.WITHDRAWN,
);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { SupervisionRequestsService } from './supervision-requests.service';
import { CreateSupervisionRequestDto } from './dto/create-supervision-request.dto';
import { UpdateSupervisionRequestDto } from './dto/update-supervision-request.dto';
import { SupervisionRequestQueryDto } from './dto/supervision-request-query.dto';
import { CountQueryDto } from './dto/count-query.dto';
import { SupervisionRequestEntity } from './entities/supervision-request.entity';
import { SupervisionRequestWithUsersEntity } from './entities/supervision-request-with-users.entity';
import { PendingRequestCountEntity } from './entities/pending-request-count.entity';
import { SupervisionRequestCountEntity } from './entities/supervision-request-count.entity';
import { CurrentUser } from '../../../common/decorators/current-user.decorator';
import { RequestState, User } from '@prisma/client';

Expand Down Expand Up @@ -137,35 +138,42 @@ export class SupervisionRequestsController {
);
}

@Get('pending-count/:userId')
@Get('count/:userId')
@ApiOperation({
summary: 'Get pending supervision request count for a user',
summary: 'Get supervision request count for a user by state',
description:
'Returns the count of pending supervision requests for a specific user. Requesting a student gets their outgoing requests, requesting a supervisor gets their incoming requests.',
'Returns the count of supervision requests for a specific user filtered by state. Requesting a student gets their outgoing requests, requesting a supervisor gets their incoming requests.',
})
@ApiParam({
name: 'userId',
description: 'ID of the user to get pending request count for',
description: 'ID of the user to get request count for',
type: String,
format: 'uuid',
example: '123e4567-e89b-12d3-a456-426614174000',
})
@ApiQuery({
name: 'request_state',
required: true,
enum: RequestState,
description: 'The request state to count',
})
@ApiResponse({
status: 200,
description: 'Returns the count of pending supervision requests',
type: PendingRequestCountEntity,
description: 'Returns the count of supervision requests',
type: SupervisionRequestCountEntity,
})
@ApiResponse({
status: 400,
description: 'Bad request - Invalid UUID or admin user requested',
description: 'Bad request - Invalid UUID, admin user requested, or missing request state',
})
@ApiResponse({
status: 404,
description: 'User not found',
})
async getPendingRequestCountForUser(
async getRequestCountForUser(
@Param('userId', ParseUUIDPipe) userId: string,
): Promise<PendingRequestCountEntity> {
return this.supervisionRequestsService.countPendingRequestsForUser(userId);
@Query() queryParams: CountQueryDto,
): Promise<SupervisionRequestCountEntity> {
return this.supervisionRequestsService.countRequestsForUser(userId, queryParams.request_state);
}
}
Loading