- 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>
275 lines
No EOL
8.3 KiB
TypeScript
275 lines
No EOL
8.3 KiB
TypeScript
import Location from '../../../src/models/Location';
|
|
import { createTestDatabase } from '../../setup';
|
|
import { Database } from 'sqlite3';
|
|
|
|
describe('Location Model', () => {
|
|
let db: Database;
|
|
let locationModel: Location;
|
|
|
|
beforeEach(async () => {
|
|
db = await createTestDatabase();
|
|
locationModel = new Location(db);
|
|
});
|
|
|
|
afterEach((done) => {
|
|
db.close(done);
|
|
});
|
|
|
|
describe('create', () => {
|
|
it('should create a new location with all fields', async () => {
|
|
const locationData = {
|
|
address: '123 Main St, Grand Rapids, MI',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Black ice present'
|
|
};
|
|
|
|
const result = await locationModel.create(locationData);
|
|
|
|
expect(result).toMatchObject({
|
|
id: 1,
|
|
address: locationData.address,
|
|
latitude: locationData.latitude,
|
|
longitude: locationData.longitude,
|
|
description: locationData.description
|
|
});
|
|
});
|
|
|
|
it('should create a location with only required address field', async () => {
|
|
const locationData = {
|
|
address: 'Main St & Oak Ave'
|
|
};
|
|
|
|
const result = await locationModel.create(locationData);
|
|
|
|
expect(result).toMatchObject({
|
|
id: 1,
|
|
address: locationData.address
|
|
});
|
|
});
|
|
|
|
it('should handle undefined optional fields', async () => {
|
|
const locationData = {
|
|
address: 'Test Address',
|
|
latitude: undefined,
|
|
longitude: undefined,
|
|
description: undefined
|
|
};
|
|
|
|
const result = await locationModel.create(locationData);
|
|
|
|
expect(result).toMatchObject({
|
|
id: 1,
|
|
address: locationData.address
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('getActive', () => {
|
|
beforeEach(async () => {
|
|
// Insert test data with different timestamps
|
|
const now = new Date();
|
|
const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000);
|
|
const fiftyHoursAgo = new Date(now.getTime() - 50 * 60 * 60 * 1000);
|
|
|
|
// Active location (within 48 hours)
|
|
await new Promise<void>((resolve, reject) => {
|
|
db.run(
|
|
'INSERT INTO locations (address, created_at) VALUES (?, ?)',
|
|
['Active Location', twoHoursAgo.toISOString()],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
|
|
// Expired location (over 48 hours)
|
|
await new Promise<void>((resolve, reject) => {
|
|
db.run(
|
|
'INSERT INTO locations (address, created_at) VALUES (?, ?)',
|
|
['Expired Location', fiftyHoursAgo.toISOString()],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
|
|
// Persistent location (expired but persistent)
|
|
await new Promise<void>((resolve, reject) => {
|
|
db.run(
|
|
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
|
['Persistent Location', fiftyHoursAgo.toISOString(), 1],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
});
|
|
|
|
it('should return only active and persistent locations', async () => {
|
|
const activeLocations = await locationModel.getActive();
|
|
|
|
expect(activeLocations).toHaveLength(2);
|
|
expect(activeLocations.map(l => l.address)).toContain('Active Location');
|
|
expect(activeLocations.map(l => l.address)).toContain('Persistent Location');
|
|
expect(activeLocations.map(l => l.address)).not.toContain('Expired Location');
|
|
});
|
|
|
|
it('should respect custom hours threshold', async () => {
|
|
const activeLocations = await locationModel.getActive(1); // 1 hour threshold
|
|
|
|
expect(activeLocations).toHaveLength(1);
|
|
expect(activeLocations[0].address).toBe('Persistent Location');
|
|
});
|
|
});
|
|
|
|
describe('getAll', () => {
|
|
beforeEach(async () => {
|
|
await locationModel.create({ address: 'Location 1' });
|
|
await locationModel.create({ address: 'Location 2' });
|
|
await locationModel.create({ address: 'Location 3' });
|
|
});
|
|
|
|
it('should return all locations', async () => {
|
|
const allLocations = await locationModel.getAll();
|
|
|
|
expect(allLocations).toHaveLength(3);
|
|
expect(allLocations.map(l => l.address)).toEqual(
|
|
expect.arrayContaining(['Location 1', 'Location 2', 'Location 3'])
|
|
);
|
|
});
|
|
|
|
it('should return locations in reverse chronological order', async () => {
|
|
const allLocations = await locationModel.getAll();
|
|
|
|
// 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');
|
|
expect(allLocations[1]).toHaveProperty('id');
|
|
expect(allLocations[2]).toHaveProperty('id');
|
|
});
|
|
});
|
|
|
|
describe('update', () => {
|
|
let locationId: number;
|
|
|
|
beforeEach(async () => {
|
|
const location = await locationModel.create({
|
|
address: 'Original Address',
|
|
description: 'Original Description'
|
|
});
|
|
locationId = location.id;
|
|
});
|
|
|
|
it('should update a location successfully', async () => {
|
|
const updateData = {
|
|
address: 'Updated Address',
|
|
latitude: 42.9634,
|
|
longitude: -85.6681,
|
|
description: 'Updated Description'
|
|
};
|
|
|
|
const result = await locationModel.update(locationId, updateData);
|
|
|
|
expect(result.changes).toBe(1);
|
|
});
|
|
|
|
it('should return 0 changes for non-existent location', async () => {
|
|
const updateData = {
|
|
address: 'Updated Address'
|
|
};
|
|
|
|
const result = await locationModel.update(99999, updateData);
|
|
|
|
expect(result.changes).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('togglePersistent', () => {
|
|
let locationId: number;
|
|
|
|
beforeEach(async () => {
|
|
const location = await locationModel.create({
|
|
address: 'Test Location'
|
|
});
|
|
locationId = location.id;
|
|
});
|
|
|
|
it('should toggle persistent to true', async () => {
|
|
const result = await locationModel.togglePersistent(locationId, true);
|
|
|
|
expect(result.changes).toBe(1);
|
|
});
|
|
|
|
it('should toggle persistent to false', async () => {
|
|
const result = await locationModel.togglePersistent(locationId, false);
|
|
|
|
expect(result.changes).toBe(1);
|
|
});
|
|
|
|
it('should return 0 changes for non-existent location', async () => {
|
|
const result = await locationModel.togglePersistent(99999, true);
|
|
|
|
expect(result.changes).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('delete', () => {
|
|
let locationId: number;
|
|
|
|
beforeEach(async () => {
|
|
const location = await locationModel.create({
|
|
address: 'To Be Deleted'
|
|
});
|
|
locationId = location.id;
|
|
});
|
|
|
|
it('should delete a location successfully', async () => {
|
|
const result = await locationModel.delete(locationId);
|
|
|
|
expect(result.changes).toBe(1);
|
|
});
|
|
|
|
it('should return 0 changes for non-existent location', async () => {
|
|
const result = await locationModel.delete(99999);
|
|
|
|
expect(result.changes).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('cleanupExpired', () => {
|
|
beforeEach(async () => {
|
|
const now = new Date();
|
|
const fiftyHoursAgo = new Date(now.getTime() - 50 * 60 * 60 * 1000);
|
|
|
|
// Expired non-persistent location
|
|
await new Promise<void>((resolve, reject) => {
|
|
db.run(
|
|
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
|
['Expired Non-Persistent', fiftyHoursAgo.toISOString(), 0],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
|
|
// Expired persistent location
|
|
await new Promise<void>((resolve, reject) => {
|
|
db.run(
|
|
'INSERT INTO locations (address, created_at, persistent) VALUES (?, ?, ?)',
|
|
['Expired Persistent', fiftyHoursAgo.toISOString(), 1],
|
|
(err) => err ? reject(err) : resolve()
|
|
);
|
|
});
|
|
|
|
// Recent location
|
|
await locationModel.create({ address: 'Recent Location' });
|
|
});
|
|
|
|
it('should delete only expired non-persistent locations', async () => {
|
|
const result = await locationModel.cleanupExpired();
|
|
|
|
expect(result.changes).toBe(1);
|
|
|
|
const remaining = await locationModel.getAll();
|
|
expect(remaining).toHaveLength(2);
|
|
expect(remaining.map(l => l.address)).toContain('Expired Persistent');
|
|
expect(remaining.map(l => l.address)).toContain('Recent Location');
|
|
});
|
|
});
|
|
}); |