ice/tests/unit/models/Location.test.ts
Claude Code 30fdd72cc5 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>
2025-07-05 22:12:37 -04:00

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');
});
});
});