import request from 'supertest'; import express, { Application } from 'express'; import adminRoutes from '../../../src/routes/admin'; import Location from '../../../src/models/Location'; import ProfanityWord from '../../../src/models/ProfanityWord'; import ProfanityFilterService from '../../../src/services/ProfanityFilterService'; import { createTestDatabase, createTestProfanityDatabase } from '../../setup'; import { Database } from 'sqlite3'; describe('Admin API Routes', () => { let app: Application; let db: Database; let profanityDb: Database; let locationModel: Location; let profanityWordModel: ProfanityWord; let profanityFilterService: ProfanityFilterService; let authToken: string; beforeEach(async () => { // Setup Express app app = express(); app.use(express.json()); // Setup test databases and models db = await createTestDatabase(); profanityDb = await createTestProfanityDatabase(); locationModel = new Location(db); profanityWordModel = new ProfanityWord(profanityDb); profanityFilterService = new ProfanityFilterService(profanityWordModel); await profanityFilterService.initialize(); // Create mock authentication middleware const mockAuthMiddleware = (req: any, res: any, next: any) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ error: 'Access denied' }); } const token = authHeader.split(' ')[1]; if (!token || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ error: 'Access denied' }); } // Simple token validation for testing if (token === authToken) { next(); } else { res.status(401).json({ error: 'Invalid token' }); } }; // Setup routes app.use('/api/admin', adminRoutes(locationModel, profanityWordModel, profanityFilterService, mockAuthMiddleware)); // Set admin password for testing process.env.ADMIN_PASSWORD = 'test_admin_password'; // Login to get auth token const loginResponse = await request(app) .post('/api/admin/login') .send({ password: 'test_admin_password' }); authToken = loginResponse.body.token; }); afterEach((done) => { let closedCount = 0; const checkBothClosed = () => { closedCount++; if (closedCount === 2) done(); }; db.close(checkBothClosed); profanityDb.close(checkBothClosed); }); describe('POST /api/admin/login', () => { it('should authenticate with correct password', async () => { const response = await request(app) .post('/api/admin/login') .send({ password: 'test_admin_password' }) .expect(200); expect(response.body).toHaveProperty('token'); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Login successful'); expect(typeof response.body.token).toBe('string'); }); it('should reject invalid password', async () => { const response = await request(app) .post('/api/admin/login') .send({ password: 'wrong_password' }) .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Invalid password'); }); it('should reject missing password', async () => { const response = await request(app) .post('/api/admin/login') .send({}) .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Invalid password'); }); }); describe('Authentication middleware', () => { it('should reject requests without auth token', async () => { const response = await request(app) .get('/api/admin/locations') .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Access denied'); }); it('should reject requests with invalid auth token', async () => { const response = await request(app) .get('/api/admin/locations') .set('Authorization', 'Bearer invalid_token') .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Invalid token'); }); it('should accept requests with valid auth token', async () => { const response = await request(app) .get('/api/admin/locations') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(Array.isArray(response.body)).toBe(true); }); }); describe('GET /api/admin/locations', () => { beforeEach(async () => { await locationModel.create({ address: 'Admin Test Location 1', latitude: 42.9634, longitude: -85.6681, description: 'Test description 1' }); await locationModel.create({ address: 'Admin Test Location 2', latitude: 42.9584, longitude: -85.6706, description: 'Test description 2' }); }); it('should return all locations for admin', async () => { const response = await request(app) .get('/api/admin/locations') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body).toHaveLength(2); expect(response.body[0]).toHaveProperty('id'); expect(response.body[0]).toHaveProperty('address'); expect(response.body[0]).toHaveProperty('created_at'); }); it('should handle empty location list', async () => { // Clear all locations await locationModel.delete(1); await locationModel.delete(2); const response = await request(app) .get('/api/admin/locations') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body).toEqual([]); }); }); describe('PUT /api/admin/locations/:id', () => { let locationId: number; beforeEach(async () => { const location = await locationModel.create({ address: 'Original Address', description: 'Original Description' }); locationId = location.id; }); it('should update location successfully', async () => { const updateData = { address: 'Updated Address', latitude: 42.9634, longitude: -85.6681, description: 'Updated Description' }; const response = await request(app) .put(`/api/admin/locations/${locationId}`) .set('Authorization', `Bearer ${authToken}`) .send(updateData) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Location updated successfully'); }); it('should handle non-existent location', async () => { const updateData = { address: 'Updated Address' }; const response = await request(app) .put('/api/admin/locations/99999') .set('Authorization', `Bearer ${authToken}`) .send(updateData) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Location not found'); }); it('should validate required fields', async () => { const response = await request(app) .put(`/api/admin/locations/${locationId}`) .set('Authorization', `Bearer ${authToken}`) .send({}) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Address is required'); }); }); describe('PATCH /api/admin/locations/:id/persistent', () => { let locationId: number; beforeEach(async () => { const location = await locationModel.create({ address: 'Test Location' }); locationId = location.id; }); it('should toggle persistent to true', async () => { const response = await request(app) .patch(`/api/admin/locations/${locationId}/persistent`) .set('Authorization', `Bearer ${authToken}`) .send({ persistent: true }) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Persistent status updated successfully'); expect(response.body.persistent).toBe(true); }); it('should toggle persistent to false', async () => { const response = await request(app) .patch(`/api/admin/locations/${locationId}/persistent`) .set('Authorization', `Bearer ${authToken}`) .send({ persistent: false }) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Persistent status updated successfully'); expect(response.body.persistent).toBe(false); }); it('should handle non-existent location', async () => { const response = await request(app) .patch('/api/admin/locations/99999/persistent') .set('Authorization', `Bearer ${authToken}`) .send({ persistent: true }) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Location not found'); }); it('should validate persistent field', async () => { const response = await request(app) .patch(`/api/admin/locations/${locationId}/persistent`) .set('Authorization', `Bearer ${authToken}`) .send({}) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Persistent value must be a boolean'); }); }); describe('DELETE /api/admin/locations/:id', () => { let locationId: number; beforeEach(async () => { const location = await locationModel.create({ address: 'To Be Deleted' }); locationId = location.id; }); it('should delete location successfully', async () => { const response = await request(app) .delete(`/api/admin/locations/${locationId}`) .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body).toHaveProperty('message'); expect(response.body.message).toBe('Location deleted successfully'); }); it('should handle non-existent location', async () => { const response = await request(app) .delete('/api/admin/locations/99999') .set('Authorization', `Bearer ${authToken}`) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Location not found'); }); }); describe('GET /api/admin/profanity-words', () => { beforeEach(async () => { await profanityWordModel.create('word1', 'low', 'test'); await profanityWordModel.create('word2', 'high', 'offensive'); }); it('should return all profanity words', async () => { const response = await request(app) .get('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(Array.isArray(response.body)).toBe(true); expect(response.body).toHaveLength(2); expect(response.body[0]).toHaveProperty('id'); expect(response.body[0]).toHaveProperty('word'); expect(response.body[0]).toHaveProperty('severity'); expect(response.body[0]).toHaveProperty('category'); }); }); describe('POST /api/admin/profanity-words', () => { it('should create new profanity word', async () => { const wordData = { word: 'newbadword', severity: 'medium', category: 'test' }; const response = await request(app) .post('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .send(wordData) .expect(200); expect(response.body).toHaveProperty('id'); expect(response.body.word).toBe('newbadword'); expect(response.body.severity).toBe('medium'); expect(response.body.category).toBe('test'); }); it('should validate required fields', async () => { const response = await request(app) .post('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .send({}) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Word is required and must be a non-empty string'); }); it('should handle duplicate words', async () => { await profanityWordModel.create('duplicate', 'low', 'test'); const response = await request(app) .post('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .send({ word: 'duplicate', severity: 'high', category: 'test' }) .expect(409); expect(response.body).toHaveProperty('error'); }); }); describe('PUT /api/admin/profanity-words/:id', () => { let wordId: number; beforeEach(async () => { const word = await profanityWordModel.create('original', 'low', 'test'); wordId = word.id; }); it('should update profanity word successfully', async () => { const updateData = { word: 'updated', severity: 'high', category: 'updated' }; const response = await request(app) .put(`/api/admin/profanity-words/${wordId}`) .set('Authorization', `Bearer ${authToken}`) .send(updateData) .expect(200); expect(response.body).toHaveProperty('updated'); expect(response.body.updated).toBe(true); }); it('should handle non-existent word', async () => { const response = await request(app) .put('/api/admin/profanity-words/99999') .set('Authorization', `Bearer ${authToken}`) .send({ word: 'test', severity: 'low', category: 'test' }) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Word not found'); }); it('should validate required fields', async () => { const response = await request(app) .put(`/api/admin/profanity-words/${wordId}`) .set('Authorization', `Bearer ${authToken}`) .send({}) .expect(400); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Word is required and must be a non-empty string'); }); }); describe('DELETE /api/admin/profanity-words/:id', () => { let wordId: number; beforeEach(async () => { const word = await profanityWordModel.create('tobedeleted', 'low', 'test'); wordId = word.id; }); it('should delete profanity word successfully', async () => { const response = await request(app) .delete(`/api/admin/profanity-words/${wordId}`) .set('Authorization', `Bearer ${authToken}`) .expect(200); expect(response.body).toHaveProperty('deleted'); expect(response.body.deleted).toBe(true); }); it('should handle non-existent word', async () => { const response = await request(app) .delete('/api/admin/profanity-words/99999') .set('Authorization', `Bearer ${authToken}`) .expect(404); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Word not found'); }); }); describe('Error handling', () => { it('should handle database errors gracefully', async () => { // Create a new app with broken database to simulate error const brokenApp = express(); brokenApp.use(express.json()); // Create a broken location model that throws errors const brokenLocationModel = { getAll: jest.fn().mockRejectedValue(new Error('Database error')) }; const mockAuthMiddleware = (req: any, res: any, next: any) => { next(); // Always pass auth for broken app test }; brokenApp.use('/api/admin', adminRoutes(brokenLocationModel as any, profanityWordModel, profanityFilterService, mockAuthMiddleware)); // Login to get auth token for broken app const loginResponse = await request(brokenApp) .post('/api/admin/login') .send({ password: 'test_admin_password' }); const brokenAuthToken = loginResponse.body.token; const response = await request(brokenApp) .get('/api/admin/locations') .set('Authorization', `Bearer ${brokenAuthToken}`) .expect(500); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Internal server error'); }); it('should handle malformed JSON in request body', async () => { const response = await request(app) .post('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .set('Content-Type', 'application/json') .send('{"word": "test"') // Missing closing brace .expect(400); // Should return bad request for malformed JSON }); it('should handle missing content-type header', async () => { const response = await request(app) .post('/api/admin/profanity-words') .set('Authorization', `Bearer ${authToken}`) .send('word=test&severity=low&category=test') .expect(400); // Should handle form data appropriately or reject it }); }); describe('Authorization edge cases', () => { it('should handle malformed authorization header', async () => { const response = await request(app) .get('/api/admin/locations') .set('Authorization', 'InvalidFormat') .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Access denied'); }); it('should handle missing bearer prefix', async () => { const response = await request(app) .get('/api/admin/locations') .set('Authorization', authToken) .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Access denied'); }); it('should handle expired/tampered tokens gracefully', async () => { const tamperedToken = authToken.slice(0, -5) + 'XXXXX'; const response = await request(app) .get('/api/admin/locations') .set('Authorization', `Bearer ${tamperedToken}`) .expect(401); expect(response.body).toHaveProperty('error'); expect(response.body.error).toBe('Invalid token'); }); }); });