Files
location-mqtt-tracker-app/scripts/test-geofence.js
Joachim Hummel b4bd967400 Add comprehensive geofence testing scripts
Added three test scripts for validating geofence functionality:

1. test-geofence.js: Database-level geofence testing
   - Creates test geofence and mock locations
   - Tests distance calculations and event generation
   - Validates state tracking (enter/exit detection)

2. test-geofence-notification.js: Email notification testing
   - Tests SMTP connection and email delivery
   - Sends formatted geofence notification email
   - Validates email template rendering

3. test-mqtt-geofence.js: Full MQTT integration testing
   - Publishes OwnTracks-formatted MQTT messages
   - Tests complete flow: MQTT → Geofence → Email
   - Simulates device entering/exiting zones

**NPM Scripts:**
- npm run test:geofence - Database and logic test
- npm run test:geofence:email - Email notification test
- npm run test:geofence:mqtt - Full MQTT integration test

**Other Changes:**
- Updated admin user email to joachim.hummel@gmail.com
- All scripts include cleanup instructions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-02 18:32:06 +00:00

315 lines
8.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
/**
* Test script for Geofence functionality
*
* This script:
* 1. Creates a test geofence
* 2. Simulates location updates (outside → inside → outside)
* 3. Shows how to check events in database
*/
const Database = require('better-sqlite3');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite');
const locationsDbPath = path.join(__dirname, '..', 'data', 'locations.sqlite');
console.log('🧪 Geofence Test Script\n');
// Test configuration
const TEST_CONFIG = {
deviceId: '10',
userId: 'admin-001',
// Geofence center (Frankfurt, Germany)
geofenceCenter: {
lat: 50.1109,
lon: 8.6821,
},
geofenceRadius: 500, // meters
// Test locations
locations: [
{ lat: 50.1200, lon: 8.6900, label: 'Outside (Start)' },
{ lat: 50.1109, lon: 8.6821, label: 'Inside (Enter)' },
{ lat: 50.1115, lon: 8.6825, label: 'Inside (Stay)' },
{ lat: 50.1200, lon: 8.6900, label: 'Outside (Exit)' },
],
};
// Haversine distance calculation
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371e3; // Earth radius in meters
const φ1 = (lat1 * Math.PI) / 180;
const φ2 = (lat2 * Math.PI) / 180;
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
// Create test location
function createTestLocation(lat, lon, deviceId) {
const db = new Database(locationsDbPath);
try {
const timestamp = new Date().toISOString();
const stmt = db.prepare(`
INSERT INTO Location (
latitude, longitude, timestamp,
user_id, username, marker_label,
first_name, last_name, display_time, chat_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(
lat,
lon,
timestamp,
0,
deviceId,
deviceId,
null,
null,
null,
0
);
return {
id: result.lastInsertRowid,
latitude: lat,
longitude: lon,
timestamp,
username: deviceId,
};
} finally {
db.close();
}
}
// Check geofence and generate events
function checkGeofence(location, geofence, deviceId) {
const db = new Database(dbPath);
try {
// Calculate distance
const distance = calculateDistance(
location.latitude,
location.longitude,
geofence.center_latitude,
geofence.center_longitude
);
const isInside = distance <= geofence.radius_meters;
// Get previous status
const statusStmt = db.prepare(`
SELECT is_inside FROM GeofenceStatus
WHERE device_id = ? AND geofence_id = ?
`);
const status = statusStmt.get(deviceId, geofence.id);
const wasInside = status ? status.is_inside === 1 : false;
let event = null;
// Generate event on state change
if (isInside && !wasInside) {
// ENTER
const insertStmt = db.prepare(`
INSERT INTO GeofenceEvent (
geofence_id, device_id, location_id,
event_type, latitude, longitude,
distance_from_center, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = insertStmt.run(
geofence.id,
deviceId,
location.id,
'enter',
location.latitude,
location.longitude,
distance,
location.timestamp
);
event = { id: result.lastInsertRowid, type: 'enter', distance };
} else if (!isInside && wasInside) {
// EXIT
const insertStmt = db.prepare(`
INSERT INTO GeofenceEvent (
geofence_id, device_id, location_id,
event_type, latitude, longitude,
distance_from_center, timestamp
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const result = insertStmt.run(
geofence.id,
deviceId,
location.id,
'exit',
location.latitude,
location.longitude,
distance,
location.timestamp
);
event = { id: result.lastInsertRowid, type: 'exit', distance };
}
// Update status
const now = new Date().toISOString();
const updateStmt = db.prepare(`
INSERT INTO GeofenceStatus (device_id, geofence_id, is_inside, last_checked_at)
VALUES (?, ?, ?, ?)
ON CONFLICT(device_id, geofence_id)
DO UPDATE SET
is_inside = ?,
last_checked_at = ?,
last_enter_time = CASE WHEN ? = 1 AND is_inside = 0 THEN ? ELSE last_enter_time END,
last_exit_time = CASE WHEN ? = 0 AND is_inside = 1 THEN ? ELSE last_exit_time END,
updated_at = ?
`);
updateStmt.run(
deviceId,
geofence.id,
isInside ? 1 : 0,
now,
isInside ? 1 : 0,
now,
isInside ? 1 : 0,
now,
isInside ? 1 : 0,
now,
now
);
return { isInside, distance, event };
} finally {
db.close();
}
}
// Main test
function runTest() {
const db = new Database(dbPath);
let testGeofenceId = null;
try {
console.log('📍 Step 1: Creating test geofence...');
testGeofenceId = uuidv4();
const insertGeofence = db.prepare(`
INSERT INTO Geofence (
id, name, description, shape_type,
center_latitude, center_longitude, radius_meters,
owner_id, device_id, color
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
insertGeofence.run(
testGeofenceId,
'Test Geofence (Script)',
'Auto-generated test geofence',
'circle',
TEST_CONFIG.geofenceCenter.lat,
TEST_CONFIG.geofenceCenter.lon,
TEST_CONFIG.geofenceRadius,
TEST_CONFIG.userId,
TEST_CONFIG.deviceId,
'#f59e0b'
);
console.log(`✓ Created geofence (ID: ${testGeofenceId})`);
console.log(` Center: ${TEST_CONFIG.geofenceCenter.lat}, ${TEST_CONFIG.geofenceCenter.lon}`);
console.log(` Radius: ${TEST_CONFIG.geofenceRadius}m\n`);
const geofence = db
.prepare('SELECT * FROM Geofence WHERE id = ?')
.get(testGeofenceId);
// Process locations
let totalEvents = 0;
for (let i = 0; i < TEST_CONFIG.locations.length; i++) {
const testLoc = TEST_CONFIG.locations[i];
console.log(`📌 Step ${i + 2}: Processing location "${testLoc.label}"...`);
console.log(` Coordinates: ${testLoc.lat}, ${testLoc.lon}`);
const location = createTestLocation(
testLoc.lat,
testLoc.lon,
TEST_CONFIG.deviceId
);
console.log(` ✓ Location saved (ID: ${location.id})`);
const result = checkGeofence(location, geofence, TEST_CONFIG.deviceId);
console.log(
` 📏 Distance from center: ${Math.round(result.distance)}m (${result.isInside ? 'INSIDE' : 'OUTSIDE'})`
);
if (result.event) {
console.log(
` 🔔 Generated ${result.event.type.toUpperCase()} event (ID: ${result.event.id})`
);
totalEvents++;
} else {
console.log(` No event (no state change)`);
}
console.log('');
}
// Show results
console.log('📊 Final Results\n');
console.log(`Total events generated: ${totalEvents}\n`);
const events = db
.prepare(
'SELECT * FROM GeofenceEvent WHERE geofence_id = ? ORDER BY timestamp ASC'
)
.all(testGeofenceId);
if (events.length > 0) {
console.log('Event History:');
events.forEach((event, idx) => {
console.log(` ${idx + 1}. ${event.event_type.toUpperCase()}`);
console.log(` Time: ${event.timestamp}`);
console.log(` Distance: ${Math.round(event.distance_from_center)}m from center`);
});
}
console.log('\n✅ Test completed successfully!\n');
console.log('💡 Test geofence and events are in the database.');
console.log(' View with: sqlite3 data/database.sqlite "SELECT * FROM GeofenceEvent"');
console.log(` Delete with: sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${testGeofenceId}'"`);
} catch (error) {
console.error('\n❌ Test failed:', error);
if (testGeofenceId) {
db.prepare('DELETE FROM Geofence WHERE id = ?').run(testGeofenceId);
console.log('✓ Cleaned up test data');
}
process.exit(1);
} finally {
db.close();
}
}
// Run
runTest();