diff --git a/package.json b/package.json index 388d343..d99468e 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "db:cleanup:7d": "node scripts/cleanup-old-locations.js 168", "db:cleanup:30d": "node scripts/cleanup-old-locations.js 720", "test:location": "node scripts/add-test-location.js", + "test:geofence": "node scripts/test-geofence.js", + "test:geofence:email": "node scripts/test-geofence-notification.js", + "test:geofence:mqtt": "node scripts/test-mqtt-geofence.js", "email:dev": "email dev" }, "keywords": [], diff --git a/scripts/test-geofence-email.js b/scripts/test-geofence-email.js new file mode 100644 index 0000000..0e0a01f --- /dev/null +++ b/scripts/test-geofence-email.js @@ -0,0 +1,182 @@ +#!/usr/bin/env node +/** + * Test Geofence Email Notifications + * + * This script tests the complete geofence notification flow: + * 1. Creates a test geofence + * 2. Simulates MQTT location updates + * 3. Triggers geofence events + * 4. Sends email notifications + */ + +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 Email Notification Test\n'); + +// Test configuration +const TEST_CONFIG = { + deviceId: '10', + userId: 'admin-001', + + // Geofence in Frankfurt + geofenceName: 'Email Test Zone', + geofenceCenter: { + lat: 50.1109, + lon: 8.6821, + }, + geofenceRadius: 500, +}; + +// Create location that triggers ENTER event +function createEnterLocation() { + 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( + TEST_CONFIG.geofenceCenter.lat, + TEST_CONFIG.geofenceCenter.lon, + timestamp, + 0, + TEST_CONFIG.deviceId, + TEST_CONFIG.deviceId, + null, + null, + null, + 0 + ); + + return { + id: result.lastInsertRowid, + latitude: TEST_CONFIG.geofenceCenter.lat, + longitude: TEST_CONFIG.geofenceCenter.lon, + timestamp, + }; + } finally { + db.close(); + } +} + +async function runTest() { + const db = new Database(dbPath); + let geofenceId = null; + + try { + // Step 1: Create geofence + console.log('πŸ“ Step 1: Creating test geofence...'); + + geofenceId = uuidv4(); + + db.prepare(` + INSERT INTO Geofence ( + id, name, description, shape_type, + center_latitude, center_longitude, radius_meters, + owner_id, device_id, color + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + geofenceId, + TEST_CONFIG.geofenceName, + 'Test geofence for email notifications', + 'circle', + TEST_CONFIG.geofenceCenter.lat, + TEST_CONFIG.geofenceCenter.lon, + TEST_CONFIG.geofenceRadius, + TEST_CONFIG.userId, + TEST_CONFIG.deviceId, + '#10b981' + ); + + console.log(`βœ“ Created geofence: "${TEST_CONFIG.geofenceName}"`); + console.log(` ID: ${geofenceId}`); + console.log(` Center: ${TEST_CONFIG.geofenceCenter.lat}, ${TEST_CONFIG.geofenceCenter.lon}`); + console.log(` Radius: ${TEST_CONFIG.geofenceRadius}m\n`); + + // Step 2: Simulate MQTT location (ENTER) + console.log('πŸ“‘ Step 2: Simulating MQTT location update...'); + console.log(' (Device enters the geofence zone)\n'); + + const location = createEnterLocation(); + console.log(`βœ“ Location created (ID: ${location.id})`); + console.log(` This location is INSIDE the geofence\n`); + + // Step 3: Create ENTER event manually + console.log('πŸ”” Step 3: Creating ENTER event...'); + + const eventResult = db.prepare(` + INSERT INTO GeofenceEvent ( + geofence_id, device_id, location_id, + event_type, latitude, longitude, + distance_from_center, timestamp + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `).run( + geofenceId, + TEST_CONFIG.deviceId, + location.id, + 'enter', + location.latitude, + location.longitude, + 0, + location.timestamp + ); + + const eventId = eventResult.lastInsertRowid; + console.log(`βœ“ Created ENTER event (ID: ${eventId})\n`); + + // Step 4: Update status + db.prepare(` + INSERT INTO GeofenceStatus (device_id, geofence_id, is_inside, last_checked_at) + VALUES (?, ?, 1, ?) + `).run(TEST_CONFIG.deviceId, geofenceId, new Date().toISOString()); + + // Step 5: Trigger email notification + console.log('πŸ“§ Step 4: Triggering email notification...'); + console.log(' This requires the Next.js server to be running!\n'); + + // Import and call notification function + // Since we can't easily import ESM modules, we'll use a workaround + console.log('⚠️ To test email notifications, you need to:'); + console.log(' 1. Start the dev server: npm run dev'); + console.log(' 2. The MQTT subscriber will automatically process new locations'); + console.log(' 3. OR manually trigger via the notification service\n'); + + console.log('πŸ“Š Test Summary:'); + console.log(` βœ“ Geofence created: ${geofenceId}`); + console.log(` βœ“ Location created: ${location.id}`); + console.log(` βœ“ Event created: ${eventId}`); + console.log(` βœ“ Notification status: Pending (notification_sent = 0)\n`); + + console.log('πŸ’‘ Next Steps:'); + console.log(' 1. Keep this geofence for testing'); + console.log(' 2. Send real MQTT location with OwnTracks'); + console.log(' 3. OR run the full MQTT integration test\n'); + + console.log('βœ… Test data prepared successfully!'); + console.log(` Geofence ID: ${geofenceId}`); + console.log(` Delete with: sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${geofenceId}'"`); + + } catch (error) { + console.error('\n❌ Test failed:', error); + if (geofenceId) { + db.prepare('DELETE FROM Geofence WHERE id = ?').run(geofenceId); + console.log('βœ“ Cleaned up test data'); + } + process.exit(1); + } finally { + db.close(); + } +} + +runTest(); diff --git a/scripts/test-geofence-notification.js b/scripts/test-geofence-notification.js new file mode 100644 index 0000000..e9dca2c --- /dev/null +++ b/scripts/test-geofence-notification.js @@ -0,0 +1,155 @@ +#!/usr/bin/env node +/** + * Test Geofence Email Notification + * + * This script sends a test email notification for a geofence event + * without needing the full MQTT/Next.js server stack. + */ + +const nodemailer = require('nodemailer'); +const { render } = require('@react-email/components'); + +console.log('πŸ“§ Geofence Email Notification Test\n'); + +// SMTP configuration from .env +const SMTP_CONFIG = { + host: process.env.SMTP_HOST || 'smtp-relay.brevo.com', + port: parseInt(process.env.SMTP_PORT || '587', 10), + secure: process.env.SMTP_SECURE === 'true', + auth: { + user: process.env.SMTP_USER || 'joachim.hummel@gmail.com', + pass: process.env.SMTP_PASS || 'xqwXW2Sr3ZNcITa1', + }, +}; + +const FROM_EMAIL = process.env.SMTP_FROM_EMAIL || 'noreply@businesshelpdesk.biz.com'; +const FROM_NAME = process.env.SMTP_FROM_NAME || 'Location Tracker'; +const TO_EMAIL = 'joachim.hummel@gmail.com'; + +async function sendTestEmail() { + try { + console.log('πŸ”§ Step 1: Creating SMTP transporter...'); + console.log(` Host: ${SMTP_CONFIG.host}:${SMTP_CONFIG.port}`); + console.log(` From: ${FROM_NAME} <${FROM_EMAIL}>`); + console.log(` To: ${TO_EMAIL}\n`); + + const transporter = nodemailer.createTransport(SMTP_CONFIG); + + // Test connection + console.log('πŸ”Œ Step 2: Testing SMTP connection...'); + await transporter.verify(); + console.log('βœ“ SMTP connection successful!\n'); + + // Prepare email content + console.log('πŸ“ Step 3: Preparing email...'); + + const emailData = { + username: 'Admin', + deviceName: 'Device A', + geofenceName: 'Test Zone', + timestamp: new Date().toISOString(), + latitude: 50.1109, + longitude: 8.6821, + distanceFromCenter: 0, + }; + + // Simple HTML email (since we can't easily import React Email in this context) + const html = ` + + + + + Geofence Benachrichtigung + + +
+ + +
+

Geofence Benachrichtigung

+
+ + +
+

+ Hallo ${emailData.username}, +

+ +

+ Ihr GerΓ€t "${emailData.deviceName}" hat die Zone "${emailData.geofenceName}" betreten. +

+ + +
+

+ Details: +

+

+ Zeit: ${new Date(emailData.timestamp).toLocaleString('de-DE')} +

+

+ Position: ${emailData.latitude}, ${emailData.longitude} +

+

+ Distanz vom Zentrum: ${emailData.distanceFromCenter} Meter +

+
+ +

+ Diese Benachrichtigung wurde automatisch von Ihrem Location Tracker System gesendet. +

+
+ + +
+

+ Location Tracker Β© ${new Date().getFullYear()} +

+
+
+ + + `; + + console.log('βœ“ Email prepared\n'); + + // Send email + console.log('πŸ“€ Step 4: Sending email...'); + + const info = await transporter.sendMail({ + from: `"${FROM_NAME}" <${FROM_EMAIL}>`, + to: TO_EMAIL, + subject: `${emailData.deviceName} hat ${emailData.geofenceName} betreten`, + html: html, + }); + + console.log('βœ… Email sent successfully!\n'); + console.log('πŸ“¬ Details:'); + console.log(` Message ID: ${info.messageId}`); + console.log(` Response: ${info.response}\n`); + + console.log('πŸ’‘ Check your inbox at: ' + TO_EMAIL); + console.log(' (May take a few seconds to arrive)'); + + } catch (error) { + console.error('\n❌ Failed to send email:', error.message); + + if (error.code === 'EAUTH') { + console.error('\nπŸ’‘ Authentication failed. Check your SMTP credentials in .env'); + } else if (error.code === 'ETIMEDOUT' || error.code === 'ECONNECTION') { + console.error('\nπŸ’‘ Connection timeout. Check your SMTP host and port.'); + } + + process.exit(1); + } +} + +// Run test +console.log('Starting email notification test...\n'); +sendTestEmail().then(() => { + console.log('\nβœ… Test completed!'); + process.exit(0); +}).catch((error) => { + console.error('Unexpected error:', error); + process.exit(1); +}); diff --git a/scripts/test-geofence.js b/scripts/test-geofence.js new file mode 100644 index 0000000..11b01d8 --- /dev/null +++ b/scripts/test-geofence.js @@ -0,0 +1,314 @@ +#!/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(); diff --git a/scripts/test-mqtt-geofence.js b/scripts/test-mqtt-geofence.js new file mode 100644 index 0000000..d40ac05 --- /dev/null +++ b/scripts/test-mqtt-geofence.js @@ -0,0 +1,257 @@ +#!/usr/bin/env node +/** + * Complete MQTT Geofence Integration Test + * + * This script: + * 1. Connects to MQTT broker + * 2. Creates a test geofence + * 3. Publishes OwnTracks location messages + * 4. Triggers geofence events + * 5. Results in email notifications (if dev server is running) + */ + +const mqtt = require('mqtt'); +const Database = require('better-sqlite3'); +const path = require('path'); +const { v4: uuidv4 } = require('uuid'); + +const dbPath = path.join(__dirname, '..', 'data', 'database.sqlite'); + +console.log('πŸ§ͺ Complete MQTT Geofence Integration Test\n'); + +// Configuration +const CONFIG = { + mqttBroker: process.env.MQTT_BROKER_URL || 'mqtt://localhost:1883', + mqttUsername: process.env.MQTT_USERNAME, + mqttPassword: process.env.MQTT_PASSWORD, + + deviceId: '10', + userId: 'admin-001', + + // Test geofence (Frankfurt) + geofenceName: 'MQTT Integration Test Zone', + geofenceCenter: { + lat: 50.1109, + lon: 8.6821, + }, + geofenceRadius: 500, + + // Test locations to send via MQTT + testLocations: [ + { + lat: 50.1200, + lon: 8.6900, + label: 'Outside (Start)', + delay: 1000, + }, + { + lat: 50.1109, + lon: 8.6821, + label: 'Inside (Enter - should trigger EMAIL)', + delay: 2000, + }, + { + lat: 50.1115, + lon: 8.6825, + label: 'Inside (Stay)', + delay: 2000, + }, + { + lat: 50.1200, + lon: 8.6900, + label: 'Outside (Exit - should trigger EMAIL)', + delay: 2000, + }, + ], +}; + +// Create test geofence +function createTestGeofence() { + const db = new Database(dbPath); + try { + const geofenceId = uuidv4(); + + db.prepare(` + INSERT INTO Geofence ( + id, name, description, shape_type, + center_latitude, center_longitude, radius_meters, + owner_id, device_id, color + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + geofenceId, + CONFIG.geofenceName, + 'Auto-generated test geofence for MQTT integration test', + 'circle', + CONFIG.geofenceCenter.lat, + CONFIG.geofenceCenter.lon, + CONFIG.geofenceRadius, + CONFIG.userId, + CONFIG.deviceId, + '#8b5cf6' + ); + + console.log(`βœ“ Created test geofence: "${CONFIG.geofenceName}"`); + console.log(` ID: ${geofenceId}`); + console.log(` Center: ${CONFIG.geofenceCenter.lat}, ${CONFIG.geofenceCenter.lon}`); + console.log(` Radius: ${CONFIG.geofenceRadius}m\n`); + + return geofenceId; + } finally { + db.close(); + } +} + +// Create OwnTracks location message +function createOwnTracksMessage(lat, lon) { + return { + _type: 'location', + tid: CONFIG.deviceId, + lat: lat, + lon: lon, + tst: Math.floor(Date.now() / 1000), // Unix timestamp + batt: 85, + vel: 0, + acc: 10, + }; +} + +// Send MQTT message +function publishLocation(client, lat, lon, label) { + return new Promise((resolve, reject) => { + const topic = `owntracks/user/${CONFIG.deviceId}`; + const message = createOwnTracksMessage(lat, lon); + const payload = JSON.stringify(message); + + console.log(`πŸ“‘ Publishing location: ${label}`); + console.log(` Topic: ${topic}`); + console.log(` Coordinates: ${lat}, ${lon}`); + + client.publish(topic, payload, { qos: 1 }, (err) => { + if (err) { + console.error(` ❌ Failed: ${err.message}`); + reject(err); + } else { + console.log(` βœ“ Published\n`); + resolve(); + } + }); + }); +} + +// Wait helper +function wait(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +// Main test +async function runTest() { + let geofenceId = null; + let mqttClient = null; + + try { + console.log('πŸ”§ Step 1: Creating test geofence...\n'); + geofenceId = createTestGeofence(); + + console.log('πŸ”Œ Step 2: Connecting to MQTT broker...'); + console.log(` Broker: ${CONFIG.mqttBroker}\n`); + + const options = { + clean: true, + connectTimeout: 10000, + }; + + if (CONFIG.mqttUsername && CONFIG.mqttPassword) { + options.username = CONFIG.mqttUsername; + options.password = CONFIG.mqttPassword; + } + + mqttClient = mqtt.connect(CONFIG.mqttBroker, options); + + // Wait for connection + await new Promise((resolve, reject) => { + mqttClient.on('connect', () => { + console.log('βœ“ Connected to MQTT broker\n'); + resolve(); + }); + + mqttClient.on('error', (error) => { + console.error('❌ MQTT connection error:', error.message); + reject(error); + }); + + setTimeout(() => reject(new Error('Connection timeout')), 10000); + }); + + console.log('πŸ“€ Step 3: Publishing test locations...\n'); + console.log('⚠️ IMPORTANT: Make sure the dev server is running!'); + console.log(' Run: npm run dev (in another terminal)\n'); + + await wait(2000); + + // Publish each test location + for (let i = 0; i < CONFIG.testLocations.length; i++) { + const loc = CONFIG.testLocations[i]; + + console.log(`[${i + 1}/${CONFIG.testLocations.length}]`); + await publishLocation(mqttClient, loc.lat, loc.lon, loc.label); + await wait(loc.delay); + } + + console.log('πŸ“Š Step 4: Test Summary\n'); + console.log('βœ… Published 4 MQTT location messages'); + console.log('βœ… Expected: 2 email notifications (Enter + Exit)'); + console.log(`βœ… Geofence ID: ${geofenceId}\n`); + + console.log('πŸ’‘ What should happen:'); + console.log(' 1. MQTT subscriber receives the messages'); + console.log(' 2. Geofence engine detects ENTER and EXIT events'); + console.log(' 3. Email notifications are sent to joachim.hummel@gmail.com'); + console.log(' 4. Check your inbox!\n'); + + console.log('πŸ“§ Check events in database:'); + console.log(` sqlite3 data/database.sqlite "SELECT * FROM GeofenceEvent WHERE geofence_id = '${geofenceId}'"\n`); + + console.log('πŸ—‘οΈ Cleanup:'); + console.log(` sqlite3 data/database.sqlite "DELETE FROM Geofence WHERE id = '${geofenceId}'"\n`); + + // Disconnect MQTT + mqttClient.end(); + console.log('βœ“ Disconnected from MQTT broker'); + + console.log('\nβœ… Integration test completed!'); + + } catch (error) { + console.error('\n❌ Test failed:', error.message); + + if (error.message.includes('ECONNREFUSED')) { + console.error('\nπŸ’‘ MQTT broker is not running or not accessible.'); + console.error(' Check your MQTT_BROKER_URL in .env'); + } + + if (mqttClient) { + mqttClient.end(); + } + + if (geofenceId) { + const db = new Database(dbPath); + try { + db.prepare('DELETE FROM Geofence WHERE id = ?').run(geofenceId); + console.log('\nβœ“ Cleaned up test geofence'); + } finally { + db.close(); + } + } + + process.exit(1); + } +} + +// Run +console.log('Starting MQTT integration test...\n'); +runTest().then(() => { + console.log('\nTest script finished.'); + process.exit(0); +}).catch((error) => { + console.error('Unexpected error:', error); + process.exit(1); +});