Add coordinate validation and ESLint integration
- Add explicit latitude/longitude validation in location submissions - Implement ESLint with TypeScript support and flat config - Auto-fix 621 formatting issues across codebase - Add comprehensive tests for coordinate validation - Update documentation with lint scripts and validation rules - Maintain 128 passing tests with enhanced security 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
5176636f6d
commit
30fdd72cc5
20 changed files with 2171 additions and 599 deletions
|
@ -35,12 +35,12 @@ describe('Admin API Routes', () => {
|
|||
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();
|
||||
|
@ -59,7 +59,7 @@ describe('Admin API Routes', () => {
|
|||
const loginResponse = await request(app)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'test_admin_password' });
|
||||
|
||||
|
||||
authToken = loginResponse.body.token;
|
||||
});
|
||||
|
||||
|
@ -69,7 +69,7 @@ describe('Admin API Routes', () => {
|
|||
closedCount++;
|
||||
if (closedCount === 2) done();
|
||||
};
|
||||
|
||||
|
||||
db.close(checkBothClosed);
|
||||
profanityDb.close(checkBothClosed);
|
||||
});
|
||||
|
@ -481,7 +481,7 @@ describe('Admin API Routes', () => {
|
|||
// 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'))
|
||||
|
@ -497,7 +497,7 @@ describe('Admin API Routes', () => {
|
|||
const loginResponse = await request(brokenApp)
|
||||
.post('/api/admin/login')
|
||||
.send({ password: 'test_admin_password' });
|
||||
|
||||
|
||||
const brokenAuthToken = loginResponse.body.token;
|
||||
|
||||
const response = await request(brokenApp)
|
||||
|
@ -554,7 +554,7 @@ describe('Admin API Routes', () => {
|
|||
|
||||
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}`)
|
||||
|
|
|
@ -121,7 +121,7 @@ describe('Public API Routes', () => {
|
|||
// 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 = {
|
||||
getActive: jest.fn().mockRejectedValue(new Error('Database error'))
|
||||
|
@ -341,9 +341,9 @@ describe('Public API Routes', () => {
|
|||
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
.send({
|
||||
address: 'Test Address',
|
||||
description: longDescription
|
||||
description: longDescription
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
|
@ -372,5 +372,64 @@ describe('Public API Routes', () => {
|
|||
|
||||
expect(response.body.address).toBe(unicodeAddress);
|
||||
});
|
||||
|
||||
it('should reject invalid latitude values', async () => {
|
||||
const invalidLatitudes = [91, -91, 'invalid', null, true, []];
|
||||
|
||||
for (const latitude of invalidLatitudes) {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: 'Test Address',
|
||||
latitude: latitude,
|
||||
longitude: -85.6681
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Latitude must be a number between -90 and 90');
|
||||
}
|
||||
});
|
||||
|
||||
it('should reject invalid longitude values', async () => {
|
||||
const invalidLongitudes = [181, -181, 'invalid', null, true, []];
|
||||
|
||||
for (const longitude of invalidLongitudes) {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: 'Test Address',
|
||||
latitude: 42.9634,
|
||||
longitude: longitude
|
||||
})
|
||||
.expect(400);
|
||||
|
||||
expect(response.body).toHaveProperty('error');
|
||||
expect(response.body.error).toBe('Longitude must be a number between -180 and 180');
|
||||
}
|
||||
});
|
||||
|
||||
it('should accept valid latitude and longitude values', async () => {
|
||||
const validCoordinates = [
|
||||
{ latitude: 0, longitude: 0 },
|
||||
{ latitude: 90, longitude: 180 },
|
||||
{ latitude: -90, longitude: -180 },
|
||||
{ latitude: 42.9634, longitude: -85.6681 }
|
||||
];
|
||||
|
||||
for (const coords of validCoordinates) {
|
||||
const response = await request(app)
|
||||
.post('/api/locations')
|
||||
.send({
|
||||
address: 'Test Address',
|
||||
latitude: coords.latitude,
|
||||
longitude: coords.longitude
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.latitude).toBe(coords.latitude);
|
||||
expect(response.body.longitude).toBe(coords.longitude);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -16,7 +16,7 @@ export const createTestDatabase = (): Promise<Database> => {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create locations table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS locations (
|
||||
|
@ -48,7 +48,7 @@ export const createTestProfanityDatabase = (): Promise<Database> => {
|
|||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Create profanity_words table
|
||||
db.run(`
|
||||
CREATE TABLE IF NOT EXISTS profanity_words (
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('Location Model', () => {
|
|||
|
||||
// Check that we have all locations and they're ordered by created_at DESC
|
||||
expect(allLocations).toHaveLength(3);
|
||||
|
||||
|
||||
// The query uses ORDER BY created_at DESC, so the most recent should be first
|
||||
// Since they're created in the same moment, check that ordering is consistent
|
||||
expect(allLocations[0]).toHaveProperty('id');
|
||||
|
|
|
@ -5,7 +5,7 @@ describe('DatabaseService', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
databaseService = new DatabaseService();
|
||||
|
||||
|
||||
// Mock console methods to reduce test noise
|
||||
jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
@ -88,7 +88,7 @@ describe('DatabaseService', () => {
|
|||
it('should allow multiple instances', () => {
|
||||
const service1 = new DatabaseService();
|
||||
const service2 = new DatabaseService();
|
||||
|
||||
|
||||
expect(service1).toBeInstanceOf(DatabaseService);
|
||||
expect(service2).toBeInstanceOf(DatabaseService);
|
||||
expect(service1).not.toBe(service2);
|
||||
|
|
|
@ -27,10 +27,10 @@ describe('ProfanityFilterService', () => {
|
|||
|
||||
it('should load custom words during initialization', async () => {
|
||||
await profanityWordModel.create('customword', 'high', 'test');
|
||||
|
||||
|
||||
const newFilter = new ProfanityFilterService(profanityWordModel);
|
||||
await newFilter.initialize();
|
||||
|
||||
|
||||
expect(newFilter.containsProfanity('customword')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
@ -50,7 +50,7 @@ describe('ProfanityFilterService', () => {
|
|||
// Test basic profanity detection - may or may not be case insensitive
|
||||
const testWord = 'damn';
|
||||
expect(profanityFilter.containsProfanity(testWord)).toBe(true);
|
||||
|
||||
|
||||
// Test with sentences containing profanity
|
||||
expect(profanityFilter.containsProfanity('This is DAMN cold')).toBe(true);
|
||||
expect(profanityFilter.containsProfanity('What the HELL')).toBe(true);
|
||||
|
@ -104,7 +104,7 @@ describe('ProfanityFilterService', () => {
|
|||
it('should determine severity levels correctly', () => {
|
||||
const lowResult = profanityFilter.analyzeProfanity('damn');
|
||||
const mediumResult = profanityFilter.analyzeProfanity('shit');
|
||||
|
||||
|
||||
expect(lowResult.severity).toBe('low');
|
||||
expect(mediumResult.severity).toBe('medium');
|
||||
});
|
||||
|
@ -164,7 +164,7 @@ describe('ProfanityFilterService', () => {
|
|||
|
||||
it('should remove custom words', async () => {
|
||||
const added = await profanityFilter.addCustomWord('removeme', 'low', 'test');
|
||||
|
||||
|
||||
const result = await profanityFilter.removeCustomWord(added.id);
|
||||
|
||||
expect(result.deleted).toBe(true);
|
||||
|
@ -180,7 +180,7 @@ describe('ProfanityFilterService', () => {
|
|||
|
||||
it('should update custom words', async () => {
|
||||
const added = await profanityFilter.addCustomWord('updateme', 'low', 'test');
|
||||
|
||||
|
||||
const result = await profanityFilter.updateCustomWord(added.id, {
|
||||
word: 'updated',
|
||||
severity: 'high',
|
||||
|
@ -208,14 +208,14 @@ describe('ProfanityFilterService', () => {
|
|||
describe('text normalization', () => {
|
||||
it('should normalize text correctly', () => {
|
||||
const normalized = profanityFilter.normalizeText('Hello World!!!');
|
||||
|
||||
|
||||
expect(typeof normalized).toBe('string');
|
||||
expect(normalized.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should handle special characters', () => {
|
||||
const normalized = profanityFilter.normalizeText('h3ll0 w0rld');
|
||||
|
||||
|
||||
expect(normalized).toContain('hello world');
|
||||
});
|
||||
|
||||
|
@ -228,13 +228,13 @@ describe('ProfanityFilterService', () => {
|
|||
describe('severity and category helpers', () => {
|
||||
it('should get severity for words', () => {
|
||||
const severity = profanityFilter.getSeverity('damn');
|
||||
|
||||
|
||||
expect(['low', 'medium', 'high']).toContain(severity);
|
||||
});
|
||||
|
||||
it('should get category for words', () => {
|
||||
const category = profanityFilter.getCategory('damn');
|
||||
|
||||
|
||||
expect(typeof category).toBe('string');
|
||||
expect(category.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
@ -242,7 +242,7 @@ describe('ProfanityFilterService', () => {
|
|||
it('should return default values for unknown words', () => {
|
||||
const severity = profanityFilter.getSeverity('unknownword');
|
||||
const category = profanityFilter.getCategory('unknownword');
|
||||
|
||||
|
||||
expect(['low', 'medium', 'high']).toContain(severity);
|
||||
expect(typeof category).toBe('string');
|
||||
});
|
||||
|
@ -251,26 +251,26 @@ describe('ProfanityFilterService', () => {
|
|||
describe('utility methods', () => {
|
||||
it('should get all words', () => {
|
||||
const words = profanityFilter.getAllWords();
|
||||
|
||||
|
||||
expect(Array.isArray(words)).toBe(true);
|
||||
expect(words.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should get severity level as number', () => {
|
||||
const level = profanityFilter.getSeverityLevel();
|
||||
|
||||
|
||||
expect(typeof level).toBe('number');
|
||||
});
|
||||
|
||||
it('should get severity name', () => {
|
||||
const name = profanityFilter.getSeverityName();
|
||||
|
||||
|
||||
expect(typeof name).toBe('string');
|
||||
});
|
||||
|
||||
it('should have close method', () => {
|
||||
expect(typeof profanityFilter.close).toBe('function');
|
||||
|
||||
|
||||
// Should not throw
|
||||
profanityFilter.close();
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue