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>
315 lines
8.3 KiB
JavaScript
315 lines
8.3 KiB
JavaScript
#!/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();
|