411 lines
13 KiB
JavaScript
411 lines
13 KiB
JavaScript
const request = require('supertest');
|
|
const express = require('express');
|
|
const sqlite3 = require('sqlite3').verbose();
|
|
const ProfanityFilter = require('../profanity-filter');
|
|
const locationRoutes = require('../routes/locations');
|
|
const adminRoutes = require('../routes/admin');
|
|
const configRoutes = require('../routes/config');
|
|
|
|
describe('API Routes', () => {
|
|
let app;
|
|
let db;
|
|
let profanityFilter;
|
|
|
|
beforeEach(async () => {
|
|
// Create test app and database
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
db = new sqlite3.Database(':memory:');
|
|
|
|
// Create profanity filter with test database path
|
|
profanityFilter = new ProfanityFilter(':memory:');
|
|
|
|
// Wait for profanity filter initialization
|
|
await profanityFilter.ready();
|
|
|
|
// Initialize locations table synchronously
|
|
await new Promise((resolve, reject) => {
|
|
db.serialize(() => {
|
|
db.run(`CREATE TABLE IF NOT EXISTS locations (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
address TEXT NOT NULL,
|
|
latitude REAL,
|
|
longitude REAL,
|
|
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
description TEXT,
|
|
persistent INTEGER DEFAULT 0,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)`, (err) => {
|
|
if (err) {
|
|
console.error('Error creating locations table:', err);
|
|
reject(err);
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
// Reload profanity filter patterns after database is ready
|
|
await profanityFilter.loadCustomWords();
|
|
|
|
// Setup routes
|
|
app.use('/api/locations', locationRoutes(db, profanityFilter));
|
|
app.use('/api/config', configRoutes());
|
|
|
|
// Mock admin authentication for testing
|
|
const mockAuth = (req, res, next) => next();
|
|
app.use('/api/admin', adminRoutes(db, profanityFilter, mockAuth));
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Wait longer for all pending operations to complete
|
|
await new Promise(resolve => setTimeout(resolve, 200));
|
|
|
|
// Close profanity filter database first
|
|
if (profanityFilter) {
|
|
profanityFilter.close();
|
|
}
|
|
|
|
// Wait a bit more
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
|
|
// Close main test database
|
|
if (db) {
|
|
await new Promise(resolve => {
|
|
db.close((err) => {
|
|
if (err) {
|
|
console.error('Error closing database:', err);
|
|
}
|
|
resolve();
|
|
});
|
|
});
|
|
}
|
|
});
|
|
|
|
describe('Location Routes', () => {
|
|
describe('POST /api/locations', () => {
|
|
test('should accept clean location submission', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is very slippery with black ice'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.address).toBe('123 Main St, Grand Rapids, MI');
|
|
expect(response.body.description).toBe('Road is very slippery with black ice');
|
|
expect(response.body.id).toBeDefined();
|
|
});
|
|
|
|
test('should reject submission with profanity', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'This fucking road is terrible'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Submission rejected');
|
|
expect(response.body.message).toContain('inappropriate language');
|
|
expect(response.body.details.wordCount).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('should reject submission with single profanity word', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is shit'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Submission rejected');
|
|
expect(response.body.details.wordCount).toBe(1);
|
|
});
|
|
|
|
test('should reject submission with leetspeak profanity', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is sh1t'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Submission rejected');
|
|
});
|
|
|
|
test('should require address field', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is slippery'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Address is required');
|
|
});
|
|
|
|
test('should accept submission without description', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.address).toBe('123 Main St, Grand Rapids, MI');
|
|
});
|
|
|
|
test('should handle empty description', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: ''
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe('GET /api/locations', () => {
|
|
test('should return empty array when no locations', async () => {
|
|
const response = await request(app)
|
|
.get('/api/locations');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([]);
|
|
});
|
|
|
|
test('should return locations after adding them', async () => {
|
|
// Add a location first
|
|
await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is slippery'
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/api/locations');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toHaveLength(1);
|
|
expect(response.body[0].address).toBe('123 Main St, Grand Rapids, MI');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Config Routes', () => {
|
|
describe('GET /api/config', () => {
|
|
test('should return config with mapbox settings', async () => {
|
|
const response = await request(app)
|
|
.get('/api/config');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toHaveProperty('hasMapbox');
|
|
expect(response.body).toHaveProperty('mapboxAccessToken');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Admin Routes', () => {
|
|
describe('Profanity Management', () => {
|
|
test('should get empty custom words list initially', async () => {
|
|
const response = await request(app)
|
|
.get('/api/admin/profanity-words');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([]);
|
|
});
|
|
|
|
test('should add custom profanity word', async () => {
|
|
const response = await request(app)
|
|
.post('/api/admin/profanity-words')
|
|
.send({
|
|
word: 'custombaword',
|
|
severity: 'high',
|
|
category: 'test'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.word).toBe('custombaword');
|
|
expect(response.body.severity).toBe('high');
|
|
});
|
|
|
|
test('should reject duplicate custom words', async () => {
|
|
// Add first word
|
|
await request(app)
|
|
.post('/api/admin/profanity-words')
|
|
.send({
|
|
word: 'duplicate',
|
|
severity: 'medium'
|
|
});
|
|
|
|
// Try to add same word again
|
|
const response = await request(app)
|
|
.post('/api/admin/profanity-words')
|
|
.send({
|
|
word: 'duplicate',
|
|
severity: 'high'
|
|
});
|
|
|
|
expect(response.status).toBe(409);
|
|
expect(response.body.error).toContain('already exists');
|
|
});
|
|
|
|
test('should validate custom word input', async () => {
|
|
const invalidInputs = [
|
|
{ word: '', severity: 'medium' },
|
|
{ word: ' ', severity: 'medium' },
|
|
{ word: 'test', severity: 'invalid' },
|
|
{ severity: 'medium' }
|
|
];
|
|
|
|
for (const input of invalidInputs) {
|
|
const response = await request(app)
|
|
.post('/api/admin/profanity-words')
|
|
.send(input);
|
|
|
|
expect(response.status).toBe(400);
|
|
}
|
|
});
|
|
|
|
test('should test profanity filter', async () => {
|
|
const response = await request(app)
|
|
.post('/api/admin/test-profanity')
|
|
.send({
|
|
text: 'This fucking road is terrible'
|
|
});
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body.original).toBe('This fucking road is terrible');
|
|
expect(response.body.analysis.hasProfanity).toBe(true);
|
|
expect(response.body.filtered).toContain('***');
|
|
});
|
|
});
|
|
|
|
describe('Location Management', () => {
|
|
test('should get all locations for admin', async () => {
|
|
// Add a location first
|
|
await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is slippery'
|
|
});
|
|
|
|
const response = await request(app)
|
|
.get('/api/admin/locations');
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toHaveLength(1);
|
|
expect(response.body[0]).toHaveProperty('isActive');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
test('should handle malformed JSON', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.set('Content-Type', 'application/json')
|
|
.send('{ invalid json }');
|
|
|
|
expect(response.status).toBe(400);
|
|
});
|
|
|
|
test('should handle missing content-type', async () => {
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send('address=test');
|
|
|
|
// Should still work as express.json() is flexible
|
|
expect(response.status).toBe(400); // Will fail validation for missing required fields
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests', () => {
|
|
test('should handle complete workflow with profanity', async () => {
|
|
// 1. Try to submit with profanity (should be rejected)
|
|
const rejectedResponse = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'This damn road is fucking terrible'
|
|
});
|
|
|
|
expect(rejectedResponse.status).toBe(400);
|
|
expect(rejectedResponse.body.error).toBe('Submission rejected');
|
|
|
|
// 2. Submit clean version (should be accepted)
|
|
const acceptedResponse = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Road is very slippery and dangerous'
|
|
});
|
|
|
|
expect(acceptedResponse.status).toBe(200);
|
|
|
|
// 3. Verify it appears in the list
|
|
const listResponse = await request(app)
|
|
.get('/api/locations');
|
|
|
|
expect(listResponse.status).toBe(200);
|
|
expect(listResponse.body).toHaveLength(1);
|
|
expect(listResponse.body[0].description).toBe('Road is very slippery and dangerous');
|
|
});
|
|
|
|
test('should handle custom profanity words in submissions', async () => {
|
|
// 1. Add custom profanity word
|
|
await request(app)
|
|
.post('/api/admin/profanity-words')
|
|
.send({
|
|
word: 'customoffensive',
|
|
severity: 'high',
|
|
category: 'test'
|
|
});
|
|
|
|
// 2. Try to submit with custom word (should be rejected)
|
|
const response = await request(app)
|
|
.post('/api/locations')
|
|
.send({
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'This road is customoffensive'
|
|
});
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.error).toBe('Submission rejected');
|
|
});
|
|
});
|
|
});
|