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
306 changes: 306 additions & 0 deletions src/app/api/approvals/__tests__/route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { GET, POST, PATCH } from '../route';

// Mock the repository
vi.mock('@/lib/db/repositories/approvals.repository', () => ({
findAll: vi.fn(),
findById: vi.fn(),
create: vi.fn(),
review: vi.fn(),
}));

// Mock rate limiting
vi.mock('@/lib/ratelimit', () => ({
withRateLimit: vi.fn(() => ({
addHeaders: (response: Response) => response,
rateLimitResponse: null,
})),
}));

// Mock audit logging
vi.mock('@/middleware/audit', () => ({
logAuditMutation: vi.fn(),
}));

import * as approvalsRepo from '@/lib/db/repositories/approvals.repository';

describe('/api/approvals', () => {
beforeEach(() => {
vi.clearAllMocks();
});

describe('GET', () => {
it('should return all approvals', async () => {
const mockApprovals = [
{
id: 'approval-1',
contentId: 'course-1',
contentType: 'COURSE',
title: 'Test Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'PENDING',
},
];

vi.mocked(approvalsRepo.findAll).mockResolvedValue(mockApprovals);

const request = new Request('http://localhost/api/approvals');
const response = await GET(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data).toEqual(mockApprovals);
expect(approvalsRepo.findAll).toHaveBeenCalledWith(undefined);
});

it('should filter approvals by status', async () => {
const mockApprovals = [
{
id: 'approval-1',
contentId: 'course-1',
contentType: 'COURSE',
title: 'Test Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'PENDING',
},
];

vi.mocked(approvalsRepo.findAll).mockResolvedValue(mockApprovals);

const request = new Request('http://localhost/api/approvals?status=PENDING');
const response = await GET(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.success).toBe(true);
expect(approvalsRepo.findAll).toHaveBeenCalledWith('PENDING');
});

it('should return 400 for invalid status', async () => {
const request = new Request('http://localhost/api/approvals?status=INVALID');
const response = await GET(request);

expect(response.status).toBe(400);
});

it('should return 500 on database error', async () => {
vi.mocked(approvalsRepo.findAll).mockRejectedValue(new Error('DB error'));

const request = new Request('http://localhost/api/approvals');
const response = await GET(request);

expect(response.status).toBe(500);
});
});

describe('POST', () => {
it('should create a new approval request', async () => {
const mockApproval = {
id: 'approval-123',
contentId: 'course-1',
contentType: 'COURSE' as const,
title: 'New Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'PENDING' as const,
};

vi.mocked(approvalsRepo.create).mockResolvedValue(mockApproval);

const request = new Request('http://localhost/api/approvals', {
method: 'POST',
body: JSON.stringify({
contentId: 'course-1',
contentType: 'COURSE',
title: 'New Course',
submittedBy: 'user-1',
}),
});

const response = await POST(request);
const json = await response.json();

expect(response.status).toBe(201);
expect(json.success).toBe(true);
expect(json.data.status).toBe('PENDING');
});

it('should return 400 for missing required fields', async () => {
const request = new Request('http://localhost/api/approvals', {
method: 'POST',
body: JSON.stringify({
contentId: 'course-1',
// missing contentType, title, submittedBy
}),
});

const response = await POST(request);
expect(response.status).toBe(400);
});

it('should return 500 on database error', async () => {
vi.mocked(approvalsRepo.create).mockRejectedValue(new Error('DB error'));

const request = new Request('http://localhost/api/approvals', {
method: 'POST',
body: JSON.stringify({
contentId: 'course-1',
contentType: 'COURSE',
title: 'New Course',
submittedBy: 'user-1',
}),
});

const response = await POST(request);
expect(response.status).toBe(500);
});
});

describe('PATCH', () => {
it('should approve a pending approval', async () => {
const existingApproval = {
id: 'approval-123',
contentId: 'course-1',
contentType: 'COURSE' as const,
title: 'Test Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'PENDING' as const,
};

const updatedApproval = {
...existingApproval,
status: 'APPROVED' as const,
reviewedBy: 'admin-1',
reviewedAt: '2024-01-02T00:00:00.000Z',
};

vi.mocked(approvalsRepo.findById).mockResolvedValue(existingApproval);
vi.mocked(approvalsRepo.review).mockResolvedValue(updatedApproval);

const request = new Request('http://localhost/api/approvals', {
method: 'PATCH',
body: JSON.stringify({
id: 'approval-123',
status: 'APPROVED',
reviewedBy: 'admin-1',
}),
});

const response = await PATCH(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.success).toBe(true);
expect(json.data.status).toBe('APPROVED');
});

it('should reject a pending approval with note', async () => {
const existingApproval = {
id: 'approval-123',
contentId: 'course-1',
contentType: 'COURSE' as const,
title: 'Test Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'PENDING' as const,
};

const updatedApproval = {
...existingApproval,
status: 'REJECTED' as const,
reviewedBy: 'admin-1',
reviewedAt: '2024-01-02T00:00:00.000Z',
reviewNote: 'Content does not meet guidelines',
};

vi.mocked(approvalsRepo.findById).mockResolvedValue(existingApproval);
vi.mocked(approvalsRepo.review).mockResolvedValue(updatedApproval);

const request = new Request('http://localhost/api/approvals', {
method: 'PATCH',
body: JSON.stringify({
id: 'approval-123',
status: 'REJECTED',
reviewedBy: 'admin-1',
reviewNote: 'Content does not meet guidelines',
}),
});

const response = await PATCH(request);
const json = await response.json();

expect(response.status).toBe(200);
expect(json.data.reviewNote).toBe('Content does not meet guidelines');
});

it('should return 404 for non-existent approval', async () => {
vi.mocked(approvalsRepo.findById).mockResolvedValue(null);

const request = new Request('http://localhost/api/approvals', {
method: 'PATCH',
body: JSON.stringify({
id: 'non-existent',
status: 'APPROVED',
reviewedBy: 'admin-1',
}),
});

const response = await PATCH(request);
const json = await response.json();

expect(response.status).toBe(404);
expect(json.message).toBe('Approval not found');
});

it('should return 409 for already reviewed approval', async () => {
const existingApproval = {
id: 'approval-123',
contentId: 'course-1',
contentType: 'COURSE' as const,
title: 'Test Course',
submittedBy: 'user-1',
submittedAt: '2024-01-01T00:00:00.000Z',
status: 'APPROVED' as const,
reviewedBy: 'admin-1',
reviewedAt: '2024-01-02T00:00:00.000Z',
};

vi.mocked(approvalsRepo.findById).mockResolvedValue(existingApproval);

const request = new Request('http://localhost/api/approvals', {
method: 'PATCH',
body: JSON.stringify({
id: 'approval-123',
status: 'REJECTED',
reviewedBy: 'admin-2',
}),
});

const response = await PATCH(request);
const json = await response.json();

expect(response.status).toBe(409);
expect(json.message).toBe('Only PENDING approvals can be reviewed');
});

it('should return 500 on database error', async () => {
vi.mocked(approvalsRepo.findById).mockRejectedValue(new Error('DB error'));

const request = new Request('http://localhost/api/approvals', {
method: 'PATCH',
body: JSON.stringify({
id: 'approval-123',
status: 'APPROVED',
reviewedBy: 'admin-1',
}),
});

const response = await PATCH(request);
expect(response.status).toBe(500);
});
});
});
Loading
Loading