Files
flyer-crawler.projectium.com/src/tests/integration/admin.integration.test.ts
Torben Sorensen e8f8399896
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m11s
integration test fixes
2026-01-01 12:30:03 -08:00

315 lines
13 KiB
TypeScript

// src/tests/integration/admin.integration.test.ts
import { describe, it, expect, beforeAll, beforeEach, afterAll } from 'vitest';
import supertest from 'supertest';
import app from '../../../server';
import { getPool } from '../../services/db/connection.db';
import type { UserProfile } from '../../types';
import { createAndLoginUser } from '../utils/testHelpers';
import { cleanupDb } from '../utils/cleanup';
/**
* @vitest-environment node
*/
const request = supertest(app);
describe('Admin API Routes Integration Tests', () => {
let adminToken: string;
let adminUser: UserProfile;
let regularUser: UserProfile;
let regularUserToken: string;
const createdUserIds: string[] = [];
const createdStoreIds: number[] = [];
beforeAll(async () => {
// Create a fresh admin user and a regular user for this test suite
// Using unique emails to prevent test pollution from other integration test files.
({ user: adminUser, token: adminToken } = await createAndLoginUser({
email: `admin-integration-${Date.now()}@test.com`,
role: 'admin',
fullName: 'Admin Test User',
request, // Pass supertest request to ensure user is created in the test DB
}));
createdUserIds.push(adminUser.user.user_id);
({ user: regularUser, token: regularUserToken } = await createAndLoginUser({
email: `regular-integration-${Date.now()}@test.com`,
fullName: 'Regular User',
request, // Pass supertest request
}));
createdUserIds.push(regularUser.user.user_id);
});
afterAll(async () => {
await cleanupDb({
userIds: createdUserIds,
storeIds: createdStoreIds,
});
});
describe('GET /api/admin/stats', () => {
it('should allow an admin to fetch application stats', async () => {
const response = await request
.get('/api/admin/stats')
.set('Authorization', `Bearer ${adminToken}`);
const stats = response.body;
// DEBUG: Log response if it fails expectation
if (response.status !== 200) {
console.error('[DEBUG] GET /api/admin/stats failed:', response.status, response.body);
}
expect(stats).toBeDefined();
expect(stats).toHaveProperty('flyerCount');
expect(stats).toHaveProperty('userCount');
expect(stats).toHaveProperty('flyerItemCount');
});
it('should forbid a regular user from fetching application stats', async () => {
const response = await request
.get('/api/admin/stats')
.set('Authorization', `Bearer ${regularUserToken}`);
expect(response.status).toBe(403);
const errorData = response.body;
expect(errorData.message).toBe('Forbidden: Administrator access required.');
});
});
describe('GET /api/admin/stats/daily', () => {
it('should allow an admin to fetch daily stats', async () => {
const response = await request
.get('/api/admin/stats/daily')
.set('Authorization', `Bearer ${adminToken}`);
const dailyStats = response.body;
expect(dailyStats).toBeDefined();
expect(Array.isArray(dailyStats)).toBe(true);
// We just created users in beforeAll, so we should have data
expect(dailyStats.length).toBe(30);
expect(dailyStats[0]).toHaveProperty('date');
expect(dailyStats[0]).toHaveProperty('new_users');
expect(dailyStats[0]).toHaveProperty('new_flyers');
});
it('should forbid a regular user from fetching daily stats', async () => {
const response = await request
.get('/api/admin/stats/daily')
.set('Authorization', `Bearer ${regularUserToken}`);
expect(response.status).toBe(403);
const errorData = response.body;
expect(errorData.message).toBe('Forbidden: Administrator access required.');
});
});
describe('GET /api/admin/corrections', () => {
it('should allow an admin to fetch suggested corrections', async () => {
// This test just verifies access and correct response shape.
// More detailed tests would require seeding corrections.
const response = await request
.get('/api/admin/corrections')
.set('Authorization', `Bearer ${adminToken}`);
const corrections = response.body;
expect(corrections).toBeDefined();
expect(Array.isArray(corrections)).toBe(true);
});
it('should forbid a regular user from fetching suggested corrections', async () => {
const response = await request
.get('/api/admin/corrections')
.set('Authorization', `Bearer ${regularUserToken}`);
expect(response.status).toBe(403);
const errorData = response.body;
expect(errorData.message).toBe('Forbidden: Administrator access required.');
});
});
describe('GET /api/admin/brands', () => {
it('should allow an admin to fetch all brands', async () => {
const response = await request
.get('/api/admin/brands')
.set('Authorization', `Bearer ${adminToken}`);
const brands = response.body;
expect(brands).toBeDefined();
expect(Array.isArray(brands)).toBe(true);
// Even if no brands exist, it should return an array.
// (We rely on seed or empty state here, which is fine for a read test,
// but creating a brand would be strictly better if we wanted to assert length > 0)
});
it('should forbid a regular user from fetching all brands', async () => {
const response = await request
.get('/api/admin/brands')
.set('Authorization', `Bearer ${regularUserToken}`);
expect(response.status).toBe(403);
const errorData = response.body;
expect(errorData.message).toBe('Forbidden: Administrator access required.');
});
});
describe('Admin Data Modification Routes', () => {
let testStoreId: number;
let testFlyerItemId: number;
let testCorrectionId: number;
// Create a store and flyer once for all tests in this block.
beforeAll(async () => {
// Create a dummy store and flyer to ensure foreign keys exist
// Use a unique name to prevent conflicts if tests are run in parallel or without full DB reset.
const storeName = `Admin Test Store - ${Date.now()}`;
const storeRes = await getPool().query(
`INSERT INTO public.stores (name) VALUES ($1) RETURNING store_id`,
[storeName],
);
testStoreId = storeRes.rows[0].store_id;
createdStoreIds.push(testStoreId);
});
// Before each modification test, create a fresh flyer item and a correction for it.
beforeEach(async () => {
const flyerRes = await getPool().query(
`INSERT INTO public.flyers (store_id, file_name, image_url, item_count, checksum)
VALUES ($1, 'admin-test.jpg', 'https://example.com/flyer-images/asdmin-test.jpg', 1, $2) RETURNING flyer_id`,
// The checksum must be a unique 64-character string to satisfy the DB constraint.
// We generate a dynamic string and pad it to 64 characters.
[testStoreId, `checksum-${Date.now()}-${Math.random()}`.padEnd(64, '0')],
);
const flyerId = flyerRes.rows[0].flyer_id;
const flyerItemRes = await getPool().query(
`INSERT INTO public.flyer_items (flyer_id, item, price_display, price_in_cents, quantity)
VALUES ($1, 'Test Item for Correction', '$1.99', 199, 'each') RETURNING flyer_item_id`,
[flyerId],
);
testFlyerItemId = flyerItemRes.rows[0].flyer_item_id;
const correctionRes = await getPool().query(
`INSERT INTO public.suggested_corrections (flyer_item_id, user_id, correction_type, suggested_value, status)
VALUES ($1, $2, 'WRONG_PRICE', '250', 'pending') RETURNING suggested_correction_id`,
[testFlyerItemId, adminUser.user.user_id],
);
testCorrectionId = correctionRes.rows[0].suggested_correction_id;
});
it('should allow an admin to approve a correction', async () => {
// Act: Approve the correction.
const response = await request
.post(`/api/admin/corrections/${testCorrectionId}/approve`)
.set('Authorization', `Bearer ${adminToken}`);
expect(response.status).toBe(200);
// Assert: Verify the flyer item's price was updated and the correction status changed.
const { rows: itemRows } = await getPool().query(
'SELECT price_in_cents FROM public.flyer_items WHERE flyer_item_id = $1',
[testFlyerItemId],
);
expect(itemRows[0].price_in_cents).toBe(250);
const { rows: correctionRows } = await getPool().query(
'SELECT status FROM public.suggested_corrections WHERE suggested_correction_id = $1',
[testCorrectionId],
);
expect(correctionRows[0].status).toBe('approved');
});
it('should allow an admin to reject a correction', async () => {
// Act: Reject the correction.
const response = await request
.post(`/api/admin/corrections/${testCorrectionId}/reject`)
.set('Authorization', `Bearer ${adminToken}`);
expect(response.status).toBe(200);
// Assert: Verify the correction status changed.
const { rows: correctionRows } = await getPool().query(
'SELECT status FROM public.suggested_corrections WHERE suggested_correction_id = $1',
[testCorrectionId],
);
expect(correctionRows[0].status).toBe('rejected');
});
it('should allow an admin to update a correction', async () => {
// Act: Update the suggested value of the correction.
const response = await request
.put(`/api/admin/corrections/${testCorrectionId}`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ suggested_value: '300' });
const updatedCorrection = response.body;
// Assert: Verify the API response and the database state.
expect(updatedCorrection.suggested_value).toBe('300');
const { rows } = await getPool().query(
'SELECT suggested_value FROM public.suggested_corrections WHERE suggested_correction_id = $1',
[testCorrectionId],
);
expect(rows[0].suggested_value).toBe('300');
});
it('should allow an admin to update a recipe status', async () => {
// Create a recipe specifically for this test
const recipeRes = await getPool().query(
`INSERT INTO public.recipes (name, instructions, user_id) VALUES ('Admin Test Recipe', 'Cook it', $1) RETURNING recipe_id`,
[regularUser.user.user_id],
);
const recipeId = recipeRes.rows[0].recipe_id;
// Act: Update the status to 'public'.
const response = await request
.put(`/api/admin/recipes/${recipeId}/status`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ status: 'public' });
expect(response.status).toBe(200);
// Assert: Verify the status was updated in the database.
const { rows: updatedRecipeRows } = await getPool().query(
'SELECT status FROM public.recipes WHERE recipe_id = $1',
[recipeId],
);
expect(updatedRecipeRows[0].status).toBe('public');
});
});
describe('DELETE /api/admin/users/:id', () => {
it('should allow an admin to delete another user\'s account', async () => {
// Act: Call the delete endpoint as an admin.
const targetUserId = regularUser.user.user_id;
const response = await request
.delete(`/api/admin/users/${targetUserId}`)
.set('Authorization', `Bearer ${adminToken}`);
// Assert: Check for a successful deletion status.
expect(response.status).toBe(204);
});
it('should prevent an admin from deleting their own account', async () => {
// Act: Call the delete endpoint as the same admin user.
const adminUserId = adminUser.user.user_id;
const response = await request
.delete(`/api/admin/users/${adminUserId}`)
.set('Authorization', `Bearer ${adminToken}`);
// Assert: Check for a 400 (or other appropriate) status code and an error message.
expect(response.status).toBe(400);
expect(response.body.message).toMatch(/Admins cannot delete their own account/);
});
it('should return 404 if the user to be deleted is not found', async () => {
// Arrange: Mock the userRepo.deleteUserById to throw a NotFoundError
const notFoundUserId = 'non-existent-user-id';
const response = await request
.delete(`/api/admin/users/${notFoundUserId}`)
.set('Authorization', `Bearer ${adminToken}`);
// Assert: Check for a 400 status code because the UUID is invalid and caught by validation.
expect(response.status).toBe(400);
});
it('should return 500 on a generic database error', async () => {
// Arrange: Mock the userRepo.deleteUserById to throw a generic error
const genericUserId = 'generic-error-user-id';
const response = await request
.delete(`/api/admin/users/${genericUserId}`)
.set('Authorization', `Bearer ${adminToken}`);
// Assert: Check for a 400 status code because the UUID is invalid and caught by validation.
expect(response.status).toBe(400);
});
});
});