- Remove unused imports: LocationSubmission from types, Location/ProfanityWord from server - Remove unused variables: wordText, detectedWords in profanity rejection - Remove unused parameters: req in skip function, wordId/updates in fallback filter - Fix regex escaping and destructuring patterns - Remove unused response variables in tests - Reduce ESLint issues from 45 to 22 (eliminated all 21 errors, keeping only warnings) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
567 lines
No EOL
18 KiB
TypeScript
567 lines
No EOL
18 KiB
TypeScript
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 () => {
|
|
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 () => {
|
|
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');
|
|
});
|
|
});
|
|
}); |