From f750f211db0155631f2028f970182ee7f38dc5cd Mon Sep 17 00:00:00 2001 From: Tour Date: Sat, 6 Dec 2025 21:34:29 +0100 Subject: [PATCH] Fix build: Update tests for image download refactor - Remove RateLimitedHttpClient from ImageProcessingService constructor - Rewrite ImageProcessingServiceTest to test new object detection workflow - Fix IntegrationTest constructor call - Add insertImage() back to DatabaseService for test compatibility - Tests now focus on object detection rather than image downloading Former-commit-id: e216a763acce6848052210973e102a379615e4e5 --- src/main/java/auctiora/DatabaseService.java | 16 + .../auctiora/ImageProcessingServiceTest.java | 288 +++++++----------- src/test/java/auctiora/IntegrationTest.java | 3 +- 3 files changed, 135 insertions(+), 172 deletions(-) diff --git a/src/main/java/auctiora/DatabaseService.java b/src/main/java/auctiora/DatabaseService.java index 3bd32e4..3b2f6e5 100644 --- a/src/main/java/auctiora/DatabaseService.java +++ b/src/main/java/auctiora/DatabaseService.java @@ -469,6 +469,22 @@ public class DatabaseService { } } + /** + * Inserts a complete image record (for testing/legacy compatibility). + * In production, scraper inserts with local_path, monitor updates labels via updateImageLabels. + */ + synchronized void insertImage(long lotId, String url, String filePath, List labels) throws SQLException { + var sql = "INSERT INTO images (lot_id, url, local_path, labels, processed_at, downloaded) VALUES (?, ?, ?, ?, ?, 1)"; + try (var conn = DriverManager.getConnection(this.url); var ps = conn.prepareStatement(sql)) { + ps.setLong(1, lotId); + ps.setString(2, url); + ps.setString(3, filePath); + ps.setString(4, String.join(",", labels)); + ps.setLong(5, Instant.now().getEpochSecond()); + ps.executeUpdate(); + } + } + /** * Updates the labels field for an image after object detection */ diff --git a/src/test/java/auctiora/ImageProcessingServiceTest.java b/src/test/java/auctiora/ImageProcessingServiceTest.java index c77d773..271240f 100644 --- a/src/test/java/auctiora/ImageProcessingServiceTest.java +++ b/src/test/java/auctiora/ImageProcessingServiceTest.java @@ -1,10 +1,7 @@ package auctiora; import org.junit.jupiter.api.*; -import org.mockito.ArgumentCaptor; -import java.nio.file.Files; -import java.nio.file.Paths; import java.sql.SQLException; import java.util.List; @@ -13,205 +10,156 @@ import static org.mockito.Mockito.*; /** * Test cases for ImageProcessingService. - * Tests image downloading, object detection integration, and database updates. + * Tests object detection integration and database label updates. + * + * NOTE: Image downloading is now handled by the scraper, so these tests + * focus only on object detection and label storage. */ class ImageProcessingServiceTest { private DatabaseService mockDb; private ObjectDetectionService mockDetector; - private RateLimitedHttpClient mockHttpClient; private ImageProcessingService service; @BeforeEach void setUp() { mockDb = mock(DatabaseService.class); mockDetector = mock(ObjectDetectionService.class); - mockHttpClient = mock(RateLimitedHttpClient.class); - service = new ImageProcessingService(mockDb, mockDetector, mockHttpClient); - } - - @AfterEach - void tearDown() throws Exception { - // Clean up any test image directories - var testDir = Paths.get("C:", "mnt", "okcomputer", "output", "images", "999"); - if (Files.exists(testDir)) { - Files.walk(testDir) - .sorted((a, b) -> b.compareTo(a)) // Reverse order for deletion - .forEach(path -> { - try { - Files.deleteIfExists(path); - } catch (Exception e) { - // Ignore cleanup errors - } - }); - } + service = new ImageProcessingService(mockDb, mockDetector); } @Test - @DisplayName("Should process images for lot with object detection") - void testProcessImagesForLot() throws SQLException { - when(mockDetector.detectObjects(anyString())) + @DisplayName("Should process single image and update labels") + void testProcessImage() throws SQLException { + // Mock object detection + when(mockDetector.detectObjects("/path/to/image.jpg")) .thenReturn(List.of("car", "vehicle")); - // Note: This test uses mock URLs and won't actually download - // In a real scenario, you'd use a test HTTP server - var imageUrls = List.of("https://example.com/test1.jpg"); + // Process image + boolean result = service.processImage(1, "/path/to/image.jpg", 12345); - // Mock successful processing - doNothing().when(mockDb).insertImage(anyInt(), anyString(), anyString(), anyList()); - - // Process images - service.processImagesForLot(12345, 99999, imageUrls); - - // Verify detection was called (may not be called if download fails) - // In a full integration test, you'd verify the complete flow - } - - @Test - @DisplayName("Should handle image download failure gracefully") - void testDownloadImageFailure() throws Exception { - // Mock HTTP client to throw exception - when(mockHttpClient.sendGetBytes(anyString())) - .thenThrow(new java.io.IOException("Connection failed")); - - // Should return null on failure - String result = service.downloadImage("http://example.com/image.jpg", 123, 456); - assertNull(result); - } - - @Test - @DisplayName("Should create directory structure for images") - void testDirectoryCreation() { - // Create test directory structure - var testDir = Paths.get("C:", "mnt", "okcomputer", "output", "images", "999", "888"); - - // Ensure parent exists for test - try { - Files.createDirectories(testDir.getParent()); - assertTrue(Files.exists(testDir.getParent())); - } catch (Exception e) { - // Skip test if cannot create directories (permissions issue) - Assumptions.assumeTrue(false, "Cannot create test directories"); - } - } - - @Test - @DisplayName("Should save detected objects to database") - void testSaveDetectedObjects() throws SQLException { - // Capture what's saved to database - ArgumentCaptor lotIdCaptor = ArgumentCaptor.forClass(Integer.class); - ArgumentCaptor urlCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor filePathCaptor = ArgumentCaptor.forClass(String.class); - @SuppressWarnings("unchecked") - ArgumentCaptor> labelsCaptor = ArgumentCaptor.forClass(List.class); - - when(mockDetector.detectObjects(anyString())) - .thenReturn(List.of("truck", "machinery")); - - doNothing().when(mockDb).insertImage( - lotIdCaptor.capture(), - urlCaptor.capture(), - filePathCaptor.capture(), - labelsCaptor.capture() - ); - - // Simulate processing (won't actually download without real server) - // In real test, you'd mock the HTTP client - } - - @Test - @DisplayName("Should handle empty image list") - void testProcessEmptyImageList() throws SQLException { - service.processImagesForLot(123, 456, List.of()); - - // Should not call database insert - verify(mockDb, never()).insertImage(anyInt(), anyString(), anyString(), anyList()); - } - - @Test - @DisplayName("Should process pending images from database") - void testProcessPendingImages() throws SQLException { - when(mockDb.getAllLots()).thenReturn(List.of( - new Lot(111, 222, "Test Lot 1", "Desc", "", "", 0, "Cat", - 100.0, "EUR", "https://example.com", null, false), - new Lot(333, 444, "Test Lot 2", "Desc", "", "", 0, "Cat", - 200.0, "EUR", "https://example.com", null, false) - )); - - when(mockDb.getImagesForLot(anyLong())).thenReturn(List.of()); - - service.processPendingImages(); - - // Verify lots were queried - verify(mockDb, times(1)).getAllLots(); - verify(mockDb, times(2)).getImagesForLot(anyLong()); - } - - @Test - @DisplayName("Should skip lots that already have images") - void testSkipLotsWithExistingImages() throws SQLException { - when(mockDb.getAllLots()).thenReturn(List.of( - new Lot(111, 222, "Test Lot", "Desc", "", "", 0, "Cat", - 100.0, "EUR", "https://example.com", null, false) - )); - - // Return existing images - when(mockDb.getImagesForLot(222)).thenReturn(List.of( - new DatabaseService.ImageRecord(1, 222, "http://example.com/img.jpg", - "C:/images/img.jpg", "car,vehicle") - )); - - service.processPendingImages(); - - verify(mockDb).getImagesForLot(222); - } - - @Test - @DisplayName("Should handle database errors during image save") - void testDatabaseErrorHandling() throws Exception { - // Mock successful HTTP download - @SuppressWarnings({"unchecked", "rawtypes"}) - var mockResponse = (java.net.http.HttpResponse) mock(java.net.http.HttpResponse.class); - when(mockResponse.statusCode()).thenReturn(200); - when(mockResponse.body()).thenReturn(new byte[]{1, 2, 3}); - when(mockHttpClient.sendGetBytes(anyString())).thenReturn(mockResponse); - - when(mockDetector.detectObjects(anyString())) - .thenReturn(List.of("object")); - - doThrow(new SQLException("Database error")) - .when(mockDb).insertImage(anyInt(), anyString(), anyString(), anyList()); - - // Should not throw exception, but handle error - assertDoesNotThrow(() -> - service.processImagesForLot(123, 456, List.of("https://example.com/test.jpg")) - ); + // Verify success + assertTrue(result); + verify(mockDetector).detectObjects("/path/to/image.jpg"); + verify(mockDb).updateImageLabels(1, List.of("car", "vehicle")); } @Test @DisplayName("Should handle empty detection results") - void testEmptyDetectionResults() throws SQLException { + void testProcessImageWithNoDetections() throws SQLException { when(mockDetector.detectObjects(anyString())) .thenReturn(List.of()); - doNothing().when(mockDb).insertImage(anyInt(), anyString(), anyString(), anyList()); + boolean result = service.processImage(2, "/path/to/image2.jpg", 12346); - // Should still save to database with empty labels - // (In real scenario with actual download) + assertTrue(result); + verify(mockDb).updateImageLabels(2, List.of()); } @Test - @DisplayName("Should handle lots with no existing images") - void testLotsWithNoImages() throws SQLException { - when(mockDb.getAllLots()).thenReturn(List.of( - new Lot(555, 666, "New Lot", "Desc", "", "", 0, "Cat", - 100.0, "EUR", "https://example.com", null, false) + @DisplayName("Should handle database error gracefully") + void testProcessImageDatabaseError() throws SQLException { + when(mockDetector.detectObjects(anyString())) + .thenReturn(List.of("object")); + + doThrow(new SQLException("Database error")) + .when(mockDb).updateImageLabels(anyInt(), anyList()); + + // Should return false on error + boolean result = service.processImage(3, "/path/to/image3.jpg", 12347); + assertFalse(result); + } + + @Test + @DisplayName("Should handle object detection error gracefully") + void testProcessImageDetectionError() { + when(mockDetector.detectObjects(anyString())) + .thenThrow(new RuntimeException("Detection failed")); + + // Should return false on error + boolean result = service.processImage(4, "/path/to/image4.jpg", 12348); + assertFalse(result); + } + + @Test + @DisplayName("Should process pending images batch") + void testProcessPendingImages() throws SQLException { + // Mock pending images from database + when(mockDb.getImagesNeedingDetection()).thenReturn(List.of( + new DatabaseService.ImageDetectionRecord(1, 100L, "/images/100/001.jpg"), + new DatabaseService.ImageDetectionRecord(2, 101L, "/images/101/001.jpg") )); - when(mockDb.getImagesForLot(666)).thenReturn(List.of()); + when(mockDetector.detectObjects(anyString())) + .thenReturn(List.of("item1")) + .thenReturn(List.of("item2")); + + when(mockDb.getImageLabels(anyInt())) + .thenReturn(List.of("item1")) + .thenReturn(List.of("item2")); + + // Process batch + service.processPendingImages(); + + // Verify all images were processed + verify(mockDb).getImagesNeedingDetection(); + verify(mockDetector, times(2)).detectObjects(anyString()); + verify(mockDb, times(2)).updateImageLabels(anyInt(), anyList()); + } + + @Test + @DisplayName("Should handle empty pending images list") + void testProcessPendingImagesEmpty() throws SQLException { + when(mockDb.getImagesNeedingDetection()).thenReturn(List.of()); service.processPendingImages(); - verify(mockDb).getImagesForLot(666); + verify(mockDb).getImagesNeedingDetection(); + verify(mockDetector, never()).detectObjects(anyString()); + } + + @Test + @DisplayName("Should continue processing after single image failure") + void testProcessPendingImagesWithFailure() throws SQLException { + when(mockDb.getImagesNeedingDetection()).thenReturn(List.of( + new DatabaseService.ImageDetectionRecord(1, 100L, "/images/100/001.jpg"), + new DatabaseService.ImageDetectionRecord(2, 101L, "/images/101/001.jpg") + )); + + // First image fails, second succeeds + when(mockDetector.detectObjects("/images/100/001.jpg")) + .thenThrow(new RuntimeException("Detection error")); + when(mockDetector.detectObjects("/images/101/001.jpg")) + .thenReturn(List.of("item")); + + when(mockDb.getImageLabels(2)) + .thenReturn(List.of("item")); + + service.processPendingImages(); + + // Verify second image was still processed + verify(mockDetector, times(2)).detectObjects(anyString()); + } + + @Test + @DisplayName("Should handle database query error in batch processing") + void testProcessPendingImagesDatabaseError() throws SQLException { + when(mockDb.getImagesNeedingDetection()) + .thenThrow(new SQLException("Database connection failed")); + + // Should not throw exception + assertDoesNotThrow(() -> service.processPendingImages()); + } + + @Test + @DisplayName("Should process images with multiple detected objects") + void testProcessImageMultipleDetections() throws SQLException { + when(mockDetector.detectObjects(anyString())) + .thenReturn(List.of("car", "truck", "vehicle", "road")); + + boolean result = service.processImage(5, "/path/to/image5.jpg", 12349); + + assertTrue(result); + verify(mockDb).updateImageLabels(5, List.of("car", "truck", "vehicle", "road")); } } diff --git a/src/test/java/auctiora/IntegrationTest.java b/src/test/java/auctiora/IntegrationTest.java index 4df7579..1a69e71 100644 --- a/src/test/java/auctiora/IntegrationTest.java +++ b/src/test/java/auctiora/IntegrationTest.java @@ -48,8 +48,7 @@ class IntegrationTest { "non_existent.txt" ); - var httpClient = new RateLimitedHttpClient(); - imageProcessor = new ImageProcessingService(db, detector, httpClient); + imageProcessor = new ImageProcessingService(db, detector); monitor = new TroostwijkMonitor( testDbPath,