From 8860340cc4452a12e1b3dbc227dfbe50be66a64c Mon Sep 17 00:00:00 2001 From: Tour Date: Mon, 8 Dec 2025 08:44:09 +0100 Subject: [PATCH] fix-tests-cleanup --- docs/EMAIL_CONFIGURATION.md | 226 ++++++++++++++++++ .../java/auctiora/ImageProcessingService.java | 4 +- .../java/auctiora/db/AuctionRepository.java | 8 +- .../auctiora/NotificationServiceTest.java | 54 ++--- 4 files changed, 259 insertions(+), 33 deletions(-) create mode 100644 docs/EMAIL_CONFIGURATION.md diff --git a/docs/EMAIL_CONFIGURATION.md b/docs/EMAIL_CONFIGURATION.md new file mode 100644 index 0000000..38892b5 --- /dev/null +++ b/docs/EMAIL_CONFIGURATION.md @@ -0,0 +1,226 @@ +# Email Notification Configuration Guide + +## Overview +The application uses Gmail SMTP to send email notifications for auction alerts and lot updates. + +## Gmail App Password Setup (Required for michael@appmodel.nl) + +### Why App Passwords? +Google requires **App Passwords** instead of your regular Gmail password when using SMTP with 2-factor authentication enabled. + +### Steps to Generate Gmail App Password: + +1. **Enable 2-Factor Authentication** (if not already enabled) + - Go to https://myaccount.google.com/security + - Under "Signing in to Google", enable "2-Step Verification" + +2. **Generate App Password** + - Go to https://myaccount.google.com/apppasswords + - Or navigate: Google Account → Security → 2-Step Verification → App passwords + - Select app: "Mail" + - Select device: "Other (Custom name)" → Enter "Auctiora Monitor" + - Click "Generate" + - Google will display a 16-character password (e.g., `abcd efgh ijkl mnop`) + - **Copy this password immediately** (you won't see it again) + +3. **Use the App Password** + - Use this 16-character password (without spaces) in your configuration + - Format: `abcdefghijklmnop` + +## Configuration + +### Method 1: Environment Variable (Recommended for Production) + +Set the `auction.notification.config` property in your `application.properties` or via environment variable: + +```properties +# Format: smtp:username:password:recipient_email +auction.notification.config=smtp:michael@appmodel.nl:YOUR_APP_PASSWORD:michael@appmodel.nl +``` + +**Example with Docker:** +```bash +docker run -e AUCTION_NOTIFICATION_CONFIG="smtp:michael@appmodel.nl:abcdefghijklmnop:michael@appmodel.nl" ... +``` + +### Method 2: application.properties (Development) + +Edit `src/main/resources/application.properties`: + +```properties +# BEFORE (desktop only): +auction.notification.config=desktop + +# AFTER (desktop + email): +auction.notification.config=smtp:michael@appmodel.nl:YOUR_APP_PASSWORD_HERE:michael@appmodel.nl +``` + +### Format Breakdown + +The configuration string format is: +``` +smtp::: +``` + +Where: +- `SMTP_USERNAME`: Your Gmail address (michael@appmodel.nl) +- `APP_PASSWORD`: The 16-character app password from Google (no spaces) +- `RECIPIENT_EMAIL`: Email address to receive notifications (can be same as sender) + +## Configuration Examples + +### Desktop Notifications Only +```properties +auction.notification.config=desktop +``` + +### Email Notifications Only +```properties +auction.notification.config=smtp:michael@appmodel.nl:abcdefghijklmnop:michael@appmodel.nl +``` + +### Both Desktop and Email (Recommended) +The SMTP configuration automatically enables both: +```properties +auction.notification.config=smtp:michael@appmodel.nl:abcdefghijklmnop:michael@appmodel.nl +``` + +### Send to Multiple Recipients +To send to multiple recipients, you can modify the code or set up Gmail forwarding rules. + +## SMTP Configuration Details + +The application uses these Gmail SMTP settings (hardcoded): +- **Host**: smtp.gmail.com +- **Port**: 587 +- **Security**: STARTTLS +- **Authentication**: Required + +## Testing Configuration + +After configuration, restart the application and check logs: + +**Success:** +``` +✓ OpenCV loaded successfully +Email notification: Test Alert +``` + +**Failure (wrong password):** +``` +WARN NotificationService - Email failed: 535-5.7.8 Username and Password not accepted +``` + +## Troubleshooting + +### Error: "Username and Password not accepted" +- **Cause**: Invalid App Password or 2FA not enabled +- **Solution**: + 1. Verify 2-Factor Authentication is enabled + 2. Generate a new App Password + 3. Ensure no spaces in the password + 4. Check for typos in email address + +### Error: "AuthenticationFailedException" +- **Cause**: Incorrect credentials format +- **Solution**: Verify the format: `smtp:user:pass:recipient` + +### Gmail Blocks Sign-in +- **Cause**: "Less secure app access" is disabled (deprecated by Google) +- **Solution**: Use App Passwords (as described above) + +### Configuration Not Taking Effect +- **Cause**: Application not restarted or environment variable not set +- **Solution**: + 1. Restart the application/container + 2. Verify with: `docker logs auctiora | grep notification` + +### SMTP Connection Timeout +- **Error**: `Couldn't connect to host, port: smtp.gmail.com, 587; timeout -1` +- **Causes**: + 1. **Firewall/Network blocking port 587** + 2. **Corporate network blocking SMTP** + 3. **Antivirus/security software blocking connections** + 4. **No internet access in test/container environment** +- **Solutions**: + 1. **Test connectivity**: + ```bash + # On Linux/Mac + telnet smtp.gmail.com 587 + # On Windows + Test-NetConnection -ComputerName smtp.gmail.com -Port 587 + ``` + 2. **Check firewall rules**: Allow outbound connections to port 587 + 3. **Docker network**: Ensure container has internet access + ```bash + docker exec auctiora ping -c 3 smtp.gmail.com + ``` + 4. **Try alternative port 465** (SSL/TLS): + - Requires code change to use `mail.smtp.socketFactory` + 5. **Corporate networks**: May require VPN or proxy configuration + 6. **Windows Firewall**: Add Java/application to allowed programs + +### Connection Succeeds but Authentication Fails +- **Error**: `Email authentication failed - check Gmail App Password` +- **Solution**: Verify App Password is correct and has no spaces + +## Security Best Practices + +1. **Never commit passwords to git** + - Use environment variables in production + - Add `application-local.properties` to `.gitignore` + +2. **Rotate App Passwords periodically** + - Generate new App Password every 90 days + - Revoke old passwords at https://myaccount.google.com/apppasswords + +3. **Use separate App Passwords per application** + - Creates "Auctiora Monitor" specific password + - Easy to revoke if compromised + +4. **Monitor Gmail Activity** + - Check https://myaccount.google.com/notifications + - Review "Recent security activity" + +## Example Docker Compose Configuration + +```yaml +services: + auctiora: + image: auctiora:latest + environment: + - AUCTION_NOTIFICATION_CONFIG=smtp:michael@appmodel.nl:${GMAIL_APP_PASSWORD}:michael@appmodel.nl + - AUCTION_DATABASE_PATH=/mnt/okcomputer/output/cache.db + volumes: + - shared-auction-data:/mnt/okcomputer/output +``` + +Then set the password in `.env` file (not committed): +```bash +GMAIL_APP_PASSWORD=abcdefghijklmnop +``` + +## Notification Types + +The application sends these email notifications: + +1. **Lot Closing Soon** (Priority: High) + - Sent when a lot closes within 5 minutes + - Subject: `[Troostwijk] Lot nearing closure` + +2. **Bid Updated** (Priority: Normal) + - Sent when current bid increases + - Subject: `[Troostwijk] Bid update` + +3. **Critical Alerts** (Priority: High) + - System errors or important events + - Subject: `[Troostwijk] Critical Alert` + +## Alternative: Desktop Notifications Only + +If you don't want email notifications, use: +```properties +auction.notification.config=desktop +``` + +This will only show system tray notifications (Linux/Windows/Mac). diff --git a/src/main/java/auctiora/ImageProcessingService.java b/src/main/java/auctiora/ImageProcessingService.java index 5ffe76a..5552829 100644 --- a/src/main/java/auctiora/ImageProcessingService.java +++ b/src/main/java/auctiora/ImageProcessingService.java @@ -26,7 +26,7 @@ public record ImageProcessingService(DatabaseService db, ObjectDetectionService return true; } catch (Exception e) { - log.error("Process fail {}: {}", id, e.getMessage()); + log.warn("Process fail {}: {}", id, e.getMessage()); return false; } } @@ -49,7 +49,7 @@ public record ImageProcessingService(DatabaseService db, ObjectDetectionService log.info("Processed {}, detected {}", processed, detected); } catch (Exception e) { - log.error("Batch fail: {}", e.getMessage()); + log.warn("Batch fail: {}", e.getMessage()); } } } diff --git a/src/main/java/auctiora/db/AuctionRepository.java b/src/main/java/auctiora/db/AuctionRepository.java index 5d9b7df..bc8e613 100644 --- a/src/main/java/auctiora/db/AuctionRepository.java +++ b/src/main/java/auctiora/db/AuctionRepository.java @@ -56,12 +56,12 @@ public class AuctionRepository { } catch (Exception e) { // If UNIQUE constraint on url fails, try updating by url - String errMsg = e.getMessage(); + var errMsg = e.getMessage(); if (errMsg != null && (errMsg.contains("UNIQUE constraint failed") || errMsg.contains("PRIMARY KEY constraint failed"))) { log.debug("Auction conflict detected, attempting update by URL: {}", auction.url()); - int updated = handle.createUpdate(""" + var updated = handle.createUpdate(""" UPDATE auctions SET auction_id = :auctionId, title = :title, @@ -102,7 +102,7 @@ public class AuctionRepository { return jdbi.withHandle(handle -> handle.createQuery("SELECT * FROM auctions") .map((rs, ctx) -> { - String closingStr = rs.getString("closing_time"); + var closingStr = rs.getString("closing_time"); LocalDateTime closingTime = null; if (closingStr != null && !closingStr.isBlank()) { try { @@ -136,7 +136,7 @@ public class AuctionRepository { handle.createQuery("SELECT * FROM auctions WHERE country = :country") .bind("country", countryCode) .map((rs, ctx) -> { - String closingStr = rs.getString("closing_time"); + var closingStr = rs.getString("closing_time"); LocalDateTime closingTime = null; if (closingStr != null && !closingStr.isBlank()) { try { diff --git a/src/test/java/auctiora/NotificationServiceTest.java b/src/test/java/auctiora/NotificationServiceTest.java index e1dcea7..1e9eea6 100644 --- a/src/test/java/auctiora/NotificationServiceTest.java +++ b/src/test/java/auctiora/NotificationServiceTest.java @@ -14,14 +14,14 @@ class NotificationServiceTest { @Test @DisplayName("Should initialize with desktop-only configuration") void testDesktopOnlyConfiguration() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertNotNull(service); } @Test @DisplayName("Should initialize with SMTP configuration") void testSMTPConfiguration() { - NotificationService service = new NotificationService( + var service = new NotificationService( "smtp:test@gmail.com:app_password:recipient@example.com" ); assertNotNull(service); @@ -52,7 +52,7 @@ class NotificationServiceTest { @Test @DisplayName("Should send desktop notification without error") void testDesktopNotification() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); // Should not throw exception even if system tray not available assertDoesNotThrow(() -> @@ -63,7 +63,7 @@ class NotificationServiceTest { @Test @DisplayName("Should send high priority notification") void testHighPriorityNotification() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertDoesNotThrow(() -> service.sendNotification("Urgent message", "High Priority", 1) @@ -73,7 +73,7 @@ class NotificationServiceTest { @Test @DisplayName("Should send normal priority notification") void testNormalPriorityNotification() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertDoesNotThrow(() -> service.sendNotification("Regular message", "Normal Priority", 0) @@ -83,7 +83,7 @@ class NotificationServiceTest { @Test @DisplayName("Should handle notification when system tray not supported") void testNoSystemTraySupport() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); // Should gracefully handle missing system tray assertDoesNotThrow(() -> @@ -96,7 +96,7 @@ class NotificationServiceTest { void testEmailNotificationWithValidConfig() { // Note: This won't actually send email without valid credentials // But it should initialize properly - NotificationService service = new NotificationService( + var service = new NotificationService( "smtp:test@gmail.com:fake_password:test@example.com" ); @@ -112,7 +112,7 @@ class NotificationServiceTest { @Test @DisplayName("Should include both desktop and email when SMTP configured") void testBothNotificationChannels() { - NotificationService service = new NotificationService( + var service = new NotificationService( "smtp:user@gmail.com:password:recipient@example.com" ); @@ -125,7 +125,7 @@ class NotificationServiceTest { @Test @DisplayName("Should handle empty message gracefully") void testEmptyMessage() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertDoesNotThrow(() -> service.sendNotification("", "", 0) @@ -135,9 +135,9 @@ class NotificationServiceTest { @Test @DisplayName("Should handle very long message") void testLongMessage() { - NotificationService service = new NotificationService("desktop"); - - String longMessage = "A".repeat(1000); + var service = new NotificationService("desktop"); + + var longMessage = "A".repeat(1000); assertDoesNotThrow(() -> service.sendNotification(longMessage, "Long Message Test", 0) ); @@ -146,7 +146,7 @@ class NotificationServiceTest { @Test @DisplayName("Should handle special characters in message") void testSpecialCharactersInMessage() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertDoesNotThrow(() -> service.sendNotification( @@ -184,10 +184,10 @@ class NotificationServiceTest { @Test @DisplayName("Should handle multiple rapid notifications") void testRapidNotifications() { - NotificationService service = new NotificationService("desktop"); + var service = new NotificationService("desktop"); assertDoesNotThrow(() -> { - for (int i = 0; i < 5; i++) { + for (var i = 0; i < 5; i++) { service.sendNotification("Notification " + i, "Rapid Test", 0); } }); @@ -205,10 +205,10 @@ class NotificationServiceTest { @Test @DisplayName("Should send bid change notification format") void testBidChangeNotificationFormat() { - NotificationService service = new NotificationService("desktop"); - - String message = "Nieuw bod op kavel 12345: €150.00 (was €125.00)"; - String title = "Kavel bieding update"; + var service = new NotificationService("desktop"); + + var message = "Nieuw bod op kavel 12345: €150.00 (was €125.00)"; + var title = "Kavel bieding update"; assertDoesNotThrow(() -> service.sendNotification(message, title, 0) @@ -218,10 +218,10 @@ class NotificationServiceTest { @Test @DisplayName("Should send closing alert notification format") void testClosingAlertNotificationFormat() { - NotificationService service = new NotificationService("desktop"); - - String message = "Kavel 12345 sluit binnen 5 min."; - String title = "Lot nearing closure"; + var service = new NotificationService("desktop"); + + var message = "Kavel 12345 sluit binnen 5 min."; + var title = "Lot nearing closure"; assertDoesNotThrow(() -> service.sendNotification(message, title, 1) @@ -231,10 +231,10 @@ class NotificationServiceTest { @Test @DisplayName("Should send object detection notification format") void testObjectDetectionNotificationFormat() { - NotificationService service = new NotificationService("desktop"); - - String message = "Lot contains: car, truck, machinery\nEstimated value: €5000"; - String title = "Object Detected"; + var service = new NotificationService("desktop"); + + var message = "Lot contains: car, truck, machinery\nEstimated value: €5000"; + var title = "Object Detected"; assertDoesNotThrow(() -> service.sendNotification(message, title, 0)