diff --git a/src/test/java/de/rub/nds/crawler/data/BulkScanInfoTest.java b/src/test/java/de/rub/nds/crawler/data/BulkScanInfoTest.java new file mode 100644 index 0000000..f029f6c --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/BulkScanInfoTest.java @@ -0,0 +1,323 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2023 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.config.ScannerDetail; +import java.io.Serializable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BulkScanInfoTest { + + private BulkScan testBulkScan; + + private ScanConfig testScanConfig; + + private BulkScanInfo bulkScanInfo; + + // Test implementation of ScanConfig for type-safe testing + private static class TestScanConfig extends ScanConfig { + private String testField; + + public TestScanConfig(String testField) { + super(ScannerDetail.NORMAL, 1, 1000); + this.testField = testField; + } + + public String getTestField() { + return testField; + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + // Another test implementation to test wrong cast + private static class AnotherTestScanConfig extends ScanConfig { + public AnotherTestScanConfig() { + super(ScannerDetail.QUICK, 0, 500); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + // Basic test implementation of ScanConfig + private static class BasicTestScanConfig extends ScanConfig { + public BasicTestScanConfig() { + super(ScannerDetail.NORMAL, 1, 1000); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + @BeforeEach + void setUp() { + testScanConfig = new BasicTestScanConfig(); + + testBulkScan = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "TestScan", + testScanConfig, + System.currentTimeMillis(), + true, + null); + testBulkScan.set_id("bulk-scan-123"); + + bulkScanInfo = new BulkScanInfo(testBulkScan); + } + + @Test + void testConstructor() { + assertNotNull(bulkScanInfo); + assertEquals("bulk-scan-123", bulkScanInfo.getBulkScanId()); + assertEquals(testScanConfig, bulkScanInfo.getScanConfig()); + assertTrue(bulkScanInfo.isMonitored()); + } + + @Test + void testConstructorWithDifferentValues() { + BulkScan anotherBulkScan = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "AnotherTestScan", + testScanConfig, + System.currentTimeMillis(), + false, + null); + anotherBulkScan.set_id("another-scan-456"); + + BulkScanInfo anotherInfo = new BulkScanInfo(anotherBulkScan); + + assertEquals("another-scan-456", anotherInfo.getBulkScanId()); + assertEquals(testScanConfig, anotherInfo.getScanConfig()); + assertFalse(anotherInfo.isMonitored()); + } + + @Test + void testGetBulkScanId() { + assertEquals("bulk-scan-123", bulkScanInfo.getBulkScanId()); + } + + @Test + void testGetScanConfig() { + assertEquals(testScanConfig, bulkScanInfo.getScanConfig()); + } + + @Test + void testGetScanConfigWithClass() { + TestScanConfig testConfig = new TestScanConfig("test-value"); + BulkScan bulkScanWithTestConfig = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "TestScan", + testConfig, + System.currentTimeMillis(), + true, + null); + bulkScanWithTestConfig.set_id("test-scan-id"); + + BulkScanInfo infoWithTestConfig = new BulkScanInfo(bulkScanWithTestConfig); + + // Test type-safe getter + TestScanConfig retrievedConfig = infoWithTestConfig.getScanConfig(TestScanConfig.class); + assertNotNull(retrievedConfig); + assertEquals(testConfig, retrievedConfig); + assertEquals("test-value", retrievedConfig.getTestField()); + } + + @Test + void testGetScanConfigWithWrongClass() { + TestScanConfig testConfig = new TestScanConfig("test-value"); + BulkScan bulkScanWithTestConfig = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "TestScan", + testConfig, + System.currentTimeMillis(), + true, + null); + bulkScanWithTestConfig.set_id("test-scan-id"); + + BulkScanInfo infoWithTestConfig = new BulkScanInfo(bulkScanWithTestConfig); + + // Test ClassCastException when casting to wrong type + assertThrows( + ClassCastException.class, + () -> { + infoWithTestConfig.getScanConfig(AnotherTestScanConfig.class); + }); + } + + @Test + void testIsMonitored() { + assertTrue(bulkScanInfo.isMonitored()); + } + + @Test + void testIsMonitoredFalse() { + BulkScan unmonitoredBulkScan = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "UnmonitoredScan", + testScanConfig, + System.currentTimeMillis(), + false, + null); + unmonitoredBulkScan.set_id("unmonitored-scan-id"); + + BulkScanInfo unmonitoredInfo = new BulkScanInfo(unmonitoredBulkScan); + assertFalse(unmonitoredInfo.isMonitored()); + } + + @Test + void testSerializable() { + assertTrue(Serializable.class.isAssignableFrom(bulkScanInfo.getClass())); + } + + @Test + void testFieldsAreFinal() throws NoSuchFieldException { + // Verify fields are final (immutable) + assertTrue( + java.lang.reflect.Modifier.isFinal( + BulkScanInfo.class.getDeclaredField("bulkScanId").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + BulkScanInfo.class.getDeclaredField("scanConfig").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + BulkScanInfo.class.getDeclaredField("isMonitored").getModifiers())); + } + + @Test + void testNullBulkScanId() { + BulkScan bulkScanWithNullId = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "NullIdScan", + testScanConfig, + System.currentTimeMillis(), + true, + null); + // Don't set ID, leaving it null + + BulkScanInfo infoWithNullId = new BulkScanInfo(bulkScanWithNullId); + assertNull(infoWithNullId.getBulkScanId()); + } + + @Test + void testNullScanConfig() { + BulkScan bulkScanWithNullConfig = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "NullConfigScan", + null, + System.currentTimeMillis(), + true, + null); + bulkScanWithNullConfig.set_id("null-config-scan-id"); + + BulkScanInfo infoWithNullConfig = new BulkScanInfo(bulkScanWithNullConfig); + assertNull(infoWithNullConfig.getScanConfig()); + } + + @Test + void testGetScanConfigWithClassOnNull() { + BulkScan bulkScanWithNullConfig = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "NullConfigScan", + null, + System.currentTimeMillis(), + true, + null); + bulkScanWithNullConfig.set_id("null-config-scan-id"); + + BulkScanInfo infoWithNullConfig = new BulkScanInfo(bulkScanWithNullConfig); + + // The cast method will return null when called on null + TestScanConfig result = infoWithNullConfig.getScanConfig(TestScanConfig.class); + assertNull(result); + } + + @Test + void testMultipleCallsReturnSameValues() { + // Test that the values don't change (immutability) + String id1 = bulkScanInfo.getBulkScanId(); + String id2 = bulkScanInfo.getBulkScanId(); + ScanConfig config1 = bulkScanInfo.getScanConfig(); + ScanConfig config2 = bulkScanInfo.getScanConfig(); + boolean monitored1 = bulkScanInfo.isMonitored(); + boolean monitored2 = bulkScanInfo.isMonitored(); + + assertEquals(id1, id2); + assertSame(config1, config2); + assertEquals(monitored1, monitored2); + } + + @Test + void testEmptyBulkScanId() { + BulkScan bulkScanWithEmptyId = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "EmptyIdScan", + testScanConfig, + System.currentTimeMillis(), + true, + null); + bulkScanWithEmptyId.set_id(""); + + BulkScanInfo infoWithEmptyId = new BulkScanInfo(bulkScanWithEmptyId); + assertEquals("", infoWithEmptyId.getBulkScanId()); + } + + @Test + void testGetScanConfigGenericCast() { + // Test that the generic method properly preserves type + TestScanConfig testConfig = new TestScanConfig("generic-test"); + BulkScan bulkScanWithTestConfig = + new BulkScan( + BulkScanInfoTest.class, + BulkScanInfoTest.class, + "GenericTestScan", + testConfig, + System.currentTimeMillis(), + true, + null); + bulkScanWithTestConfig.set_id("generic-test-scan-id"); + + BulkScanInfo info = new BulkScanInfo(bulkScanWithTestConfig); + + // This should compile and work without explicit cast + TestScanConfig retrieved = info.getScanConfig(TestScanConfig.class); + assertEquals("generic-test", retrieved.getTestField()); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/BulkScanJobCountersTest.java b/src/test/java/de/rub/nds/crawler/data/BulkScanJobCountersTest.java new file mode 100644 index 0000000..625db3b --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/BulkScanJobCountersTest.java @@ -0,0 +1,297 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.crawler.constant.JobStatus; +import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BulkScanJobCountersTest { + + private BulkScan testBulkScan; + + private BulkScanJobCounters counters; + + // Test implementation of ScanConfig for BulkScan + private static class TestScanConfig extends ScanConfig { + public TestScanConfig() { + super(de.rub.nds.scanner.core.config.ScannerDetail.NORMAL, 1, 1000); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + @BeforeEach + void setUp() { + testBulkScan = + new BulkScan( + BulkScanJobCountersTest.class, + BulkScanJobCountersTest.class, + "TestScan", + new TestScanConfig(), + System.currentTimeMillis(), + true, + null); + testBulkScan.set_id("test-bulk-scan-id"); + + counters = new BulkScanJobCounters(testBulkScan); + } + + @Test + void testConstructor() { + assertNotNull(counters); + assertEquals(testBulkScan, counters.getBulkScan()); + + // Verify all JobStatus values except TO_BE_EXECUTED are initialized with 0 + Map statusCounts = counters.getJobStatusCountersCopy(); + assertEquals(JobStatus.values().length - 1, statusCounts.size()); + + for (JobStatus status : JobStatus.values()) { + if (status != JobStatus.TO_BE_EXECUTED) { + assertTrue(statusCounts.containsKey(status)); + assertEquals(0, statusCounts.get(status)); + } else { + assertFalse(statusCounts.containsKey(status)); + } + } + } + + @Test + void testGetBulkScan() { + assertEquals(testBulkScan, counters.getBulkScan()); + } + + @Test + void testGetJobStatusCountersCopy() { + // Increase some counters + counters.increaseJobStatusCount(JobStatus.SUCCESS); + counters.increaseJobStatusCount(JobStatus.SUCCESS); + counters.increaseJobStatusCount(JobStatus.ERROR); + + Map copy1 = counters.getJobStatusCountersCopy(); + Map copy2 = counters.getJobStatusCountersCopy(); + + // Verify copies are independent + assertNotSame(copy1, copy2); + assertEquals(copy1, copy2); + + // Verify counts + assertEquals(2, copy1.get(JobStatus.SUCCESS)); + assertEquals(1, copy1.get(JobStatus.ERROR)); + assertEquals(0, copy1.get(JobStatus.EMPTY)); + + // Verify TO_BE_EXECUTED is not in the map + assertFalse(copy1.containsKey(JobStatus.TO_BE_EXECUTED)); + } + + @Test + void testGetJobStatusCount() { + assertEquals(0, counters.getJobStatusCount(JobStatus.SUCCESS)); + + counters.increaseJobStatusCount(JobStatus.SUCCESS); + assertEquals(1, counters.getJobStatusCount(JobStatus.SUCCESS)); + + counters.increaseJobStatusCount(JobStatus.SUCCESS); + assertEquals(2, counters.getJobStatusCount(JobStatus.SUCCESS)); + + // Test other statuses remain at 0 + assertEquals(0, counters.getJobStatusCount(JobStatus.ERROR)); + assertEquals(0, counters.getJobStatusCount(JobStatus.DENYLISTED)); + } + + @Test + void testIncreaseJobStatusCount() { + // Test that increaseJobStatusCount returns the total count + assertEquals(1, counters.increaseJobStatusCount(JobStatus.SUCCESS)); + assertEquals(2, counters.increaseJobStatusCount(JobStatus.ERROR)); + assertEquals(3, counters.increaseJobStatusCount(JobStatus.SUCCESS)); + assertEquals(4, counters.increaseJobStatusCount(JobStatus.EMPTY)); + + // Verify individual counts + assertEquals(2, counters.getJobStatusCount(JobStatus.SUCCESS)); + assertEquals(1, counters.getJobStatusCount(JobStatus.ERROR)); + assertEquals(1, counters.getJobStatusCount(JobStatus.EMPTY)); + } + + @Test + void testAllJobStatusValues() { + // Test incrementing all valid job statuses + int expectedTotal = 0; + for (JobStatus status : JobStatus.values()) { + if (status != JobStatus.TO_BE_EXECUTED) { + expectedTotal++; + assertEquals(expectedTotal, counters.increaseJobStatusCount(status)); + assertEquals(1, counters.getJobStatusCount(status)); + } + } + } + + @Test + void testConcurrentIncrement() throws InterruptedException { + int threadCount = 10; + int incrementsPerThread = 100; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + // Create threads that will increment SUCCESS counter + for (int i = 0; i < threadCount; i++) { + executor.submit( + () -> { + try { + startLatch.await(); + for (int j = 0; j < incrementsPerThread; j++) { + counters.increaseJobStatusCount(JobStatus.SUCCESS); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }); + } + + // Start all threads at once + startLatch.countDown(); + + // Wait for all threads to complete + assertTrue(endLatch.await(5, TimeUnit.SECONDS)); + executor.shutdown(); + + // Verify the count is correct + assertEquals( + threadCount * incrementsPerThread, counters.getJobStatusCount(JobStatus.SUCCESS)); + } + + @Test + void testConcurrentMixedOperations() throws InterruptedException { + int threadCount = 10; + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + CountDownLatch startLatch = new CountDownLatch(1); + CountDownLatch endLatch = new CountDownLatch(threadCount); + + // Some threads increment, others read + for (int i = 0; i < threadCount; i++) { + final int threadId = i; + executor.submit( + () -> { + try { + startLatch.await(); + if (threadId % 2 == 0) { + // Even threads increment + for (int j = 0; j < 50; j++) { + counters.increaseJobStatusCount(JobStatus.SUCCESS); + counters.increaseJobStatusCount(JobStatus.ERROR); + } + } else { + // Odd threads read + for (int j = 0; j < 50; j++) { + counters.getJobStatusCount(JobStatus.SUCCESS); + counters.getJobStatusCountersCopy(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } finally { + endLatch.countDown(); + } + }); + } + + // Start all threads at once + startLatch.countDown(); + + // Wait for all threads to complete + assertTrue(endLatch.await(5, TimeUnit.SECONDS)); + executor.shutdown(); + + // Verify counts + assertEquals(250, counters.getJobStatusCount(JobStatus.SUCCESS)); // 5 even threads * 50 + assertEquals(250, counters.getJobStatusCount(JobStatus.ERROR)); // 5 even threads * 50 + } + + @Test + void testNullPointerExceptionForToBeExecuted() { + // This should throw NPE because TO_BE_EXECUTED is not in the map + assertThrows( + NullPointerException.class, + () -> { + counters.getJobStatusCount(JobStatus.TO_BE_EXECUTED); + }); + + assertThrows( + NullPointerException.class, + () -> { + counters.increaseJobStatusCount(JobStatus.TO_BE_EXECUTED); + }); + } + + @Test + void testGetJobStatusCountersCopyIndependence() { + counters.increaseJobStatusCount(JobStatus.SUCCESS); + + Map copy = counters.getJobStatusCountersCopy(); + assertEquals(1, copy.get(JobStatus.SUCCESS)); + + // Modify the original + counters.increaseJobStatusCount(JobStatus.SUCCESS); + + // Copy should not change + assertEquals(1, copy.get(JobStatus.SUCCESS)); + + // New copy should have updated value + Map newCopy = counters.getJobStatusCountersCopy(); + assertEquals(2, newCopy.get(JobStatus.SUCCESS)); + } + + @Test + void testTotalCountAcrossAllStatuses() { + AtomicInteger expectedTotal = new AtomicInteger(0); + + // Increment various statuses + assertEquals( + expectedTotal.incrementAndGet(), + counters.increaseJobStatusCount(JobStatus.SUCCESS)); + assertEquals( + expectedTotal.incrementAndGet(), counters.increaseJobStatusCount(JobStatus.ERROR)); + assertEquals( + expectedTotal.incrementAndGet(), counters.increaseJobStatusCount(JobStatus.EMPTY)); + assertEquals( + expectedTotal.incrementAndGet(), + counters.increaseJobStatusCount(JobStatus.SUCCESS)); + assertEquals( + expectedTotal.incrementAndGet(), + counters.increaseJobStatusCount(JobStatus.DENYLISTED)); + assertEquals( + expectedTotal.incrementAndGet(), + counters.increaseJobStatusCount(JobStatus.UNRESOLVABLE)); + + // Verify individual counts + assertEquals(2, counters.getJobStatusCount(JobStatus.SUCCESS)); + assertEquals(1, counters.getJobStatusCount(JobStatus.ERROR)); + assertEquals(1, counters.getJobStatusCount(JobStatus.EMPTY)); + assertEquals(1, counters.getJobStatusCount(JobStatus.DENYLISTED)); + assertEquals(1, counters.getJobStatusCount(JobStatus.UNRESOLVABLE)); + + // Verify total + assertEquals(6, expectedTotal.get()); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/BulkScanTest.java b/src/test/java/de/rub/nds/crawler/data/BulkScanTest.java new file mode 100644 index 0000000..f59665f --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/BulkScanTest.java @@ -0,0 +1,382 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.crawler.constant.JobStatus; +import java.io.Serializable; +import java.text.SimpleDateFormat; +import java.time.Instant; +import java.util.Date; +import java.util.EnumMap; +import java.util.Map; +import javax.persistence.Id; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BulkScanTest { + + private ScanConfig testScanConfig; + + private BulkScan bulkScan; + private static final long TEST_START_TIME = 1640995200000L; // 2022-01-01 00:00:00 UTC + private static final String TEST_NAME = "TestScan"; + private static final String TEST_NOTIFY_URL = "https://example.com/notify"; + + // Test classes for version extraction + static class TestScannerClass { + // Mock scanner class + } + + static class TestCrawlerClass { + // Mock crawler class + } + + // Test implementation of ScanConfig + private static class TestScanConfig extends ScanConfig { + public TestScanConfig() { + super(de.rub.nds.scanner.core.config.ScannerDetail.NORMAL, 1, 1000); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + @BeforeEach + void setUp() { + testScanConfig = new TestScanConfig(); + + // Create a bulkScan with the main constructor + bulkScan = + new BulkScan( + TestScannerClass.class, + TestCrawlerClass.class, + TEST_NAME, + testScanConfig, + TEST_START_TIME, + true, + TEST_NOTIFY_URL); + } + + @Test + void testMainConstructor() { + assertNotNull(bulkScan); + assertEquals(TEST_NAME, bulkScan.getName()); + assertEquals(testScanConfig, bulkScan.getScanConfig()); + assertEquals(TEST_START_TIME, bulkScan.getStartTime()); + assertTrue(bulkScan.isMonitored()); + assertFalse(bulkScan.isFinished()); + assertEquals(TEST_NOTIFY_URL, bulkScan.getNotifyUrl()); + + // Test collection name generation + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd_HH-mm"); + String expectedCollectionName = + TEST_NAME + + "_" + + dateFormat.format(Date.from(Instant.ofEpochMilli(TEST_START_TIME))); + assertEquals(expectedCollectionName, bulkScan.getCollectionName()); + + // Version extraction from packages (will be null in test environment) + assertNull(bulkScan.getScannerVersion()); + assertNull(bulkScan.getCrawlerVersion()); + } + + @Test + void testCollectionNameFormat() { + // Test with different timestamps + long timestamp1 = 1640995200000L; // 2022-01-01 00:00:00 UTC + long timestamp2 = 1672531200000L; // 2023-01-01 00:00:00 UTC + + BulkScan scan1 = + new BulkScan( + TestScannerClass.class, + TestCrawlerClass.class, + "Scan1", + testScanConfig, + timestamp1, + false, + null); + + BulkScan scan2 = + new BulkScan( + TestScannerClass.class, + TestCrawlerClass.class, + "Scan2", + testScanConfig, + timestamp2, + false, + null); + + assertTrue(scan1.getCollectionName().contains("Scan1")); + assertTrue(scan2.getCollectionName().contains("Scan2")); + assertTrue(scan1.getCollectionName().contains("2022-01-01")); + assertTrue(scan2.getCollectionName().contains("2023-01-01")); + } + + @Test + void testGetAndSetId() { + assertNull(bulkScan.get_id()); + + bulkScan.set_id("bulk-scan-12345"); + assertEquals("bulk-scan-12345", bulkScan.get_id()); + } + + @Test + void testGetAndSetName() { + assertEquals(TEST_NAME, bulkScan.getName()); + + bulkScan.setName("NewName"); + assertEquals("NewName", bulkScan.getName()); + } + + @Test + void testGetAndSetCollectionName() { + String originalName = bulkScan.getCollectionName(); + assertNotNull(originalName); + + bulkScan.setCollectionName("custom_collection"); + assertEquals("custom_collection", bulkScan.getCollectionName()); + } + + @Test + void testGetAndSetScanConfig() { + assertEquals(testScanConfig, bulkScan.getScanConfig()); + + ScanConfig newConfig = new TestScanConfig(); + bulkScan.setScanConfig(newConfig); + assertEquals(newConfig, bulkScan.getScanConfig()); + } + + @Test + void testGetAndSetMonitored() { + assertTrue(bulkScan.isMonitored()); + + bulkScan.setMonitored(false); + assertFalse(bulkScan.isMonitored()); + } + + @Test + void testGetAndSetFinished() { + assertFalse(bulkScan.isFinished()); + + bulkScan.setFinished(true); + assertTrue(bulkScan.isFinished()); + } + + @Test + void testGetAndSetStartTime() { + assertEquals(TEST_START_TIME, bulkScan.getStartTime()); + + long newTime = 1672531200000L; + bulkScan.setStartTime(newTime); + assertEquals(newTime, bulkScan.getStartTime()); + } + + @Test + void testGetAndSetEndTime() { + assertEquals(0, bulkScan.getEndTime()); + + long endTime = 1640998800000L; + bulkScan.setEndTime(endTime); + assertEquals(endTime, bulkScan.getEndTime()); + } + + @Test + void testGetAndSetTargetsGiven() { + assertEquals(0, bulkScan.getTargetsGiven()); + + bulkScan.setTargetsGiven(1000); + assertEquals(1000, bulkScan.getTargetsGiven()); + } + + @Test + void testGetAndSetScanJobsPublished() { + assertEquals(0, bulkScan.getScanJobsPublished()); + + bulkScan.setScanJobsPublished(500L); + assertEquals(500L, bulkScan.getScanJobsPublished()); + } + + @Test + void testGetAndSetSuccessfulScans() { + assertEquals(0, bulkScan.getSuccessfulScans()); + + bulkScan.setSuccessfulScans(250); + assertEquals(250, bulkScan.getSuccessfulScans()); + } + + @Test + void testGetAndSetNotifyUrl() { + assertEquals(TEST_NOTIFY_URL, bulkScan.getNotifyUrl()); + + bulkScan.setNotifyUrl("https://new.example.com"); + assertEquals("https://new.example.com", bulkScan.getNotifyUrl()); + } + + @Test + void testGetAndSetScannerVersion() { + assertNull(bulkScan.getScannerVersion()); + + bulkScan.setScannerVersion("1.2.3"); + assertEquals("1.2.3", bulkScan.getScannerVersion()); + } + + @Test + void testGetAndSetCrawlerVersion() { + assertNull(bulkScan.getCrawlerVersion()); + + bulkScan.setCrawlerVersion("2.3.4"); + assertEquals("2.3.4", bulkScan.getCrawlerVersion()); + } + + @Test + void testGetAndSetJobStatusCounters() { + Map counters = bulkScan.getJobStatusCounters(); + assertNotNull(counters); + assertTrue(counters instanceof EnumMap); + assertTrue(counters.isEmpty()); + + Map newCounters = new EnumMap<>(JobStatus.class); + newCounters.put(JobStatus.SUCCESS, 100); + newCounters.put(JobStatus.ERROR, 10); + + bulkScan.setJobStatusCounters(newCounters); + assertEquals(newCounters, bulkScan.getJobStatusCounters()); + assertEquals(100, bulkScan.getJobStatusCounters().get(JobStatus.SUCCESS)); + assertEquals(10, bulkScan.getJobStatusCounters().get(JobStatus.ERROR)); + } + + @Test + void testGetAndSetScanJobsResolutionErrors() { + assertEquals(0, bulkScan.getScanJobsResolutionErrors()); + + bulkScan.setScanJobsResolutionErrors(25L); + assertEquals(25L, bulkScan.getScanJobsResolutionErrors()); + } + + @Test + void testGetAndSetScanJobsDenylisted() { + assertEquals(0, bulkScan.getScanJobsDenylisted()); + + bulkScan.setScanJobsDenylisted(15L); + assertEquals(15L, bulkScan.getScanJobsDenylisted()); + } + + @Test + void testSerializable() { + assertTrue(Serializable.class.isAssignableFrom(bulkScan.getClass())); + } + + @Test + void testIdAnnotation() throws NoSuchFieldException { + assertTrue(BulkScan.class.getDeclaredField("_id").isAnnotationPresent(Id.class)); + } + + @Test + void testConstructorWithNullValues() { + BulkScan scanWithNulls = + new BulkScan( + TestScannerClass.class, + TestCrawlerClass.class, + null, + null, + TEST_START_TIME, + false, + null); + + assertNull(scanWithNulls.getName()); + assertNull(scanWithNulls.getScanConfig()); + assertNull(scanWithNulls.getNotifyUrl()); + assertFalse(scanWithNulls.isMonitored()); + + // Collection name should still be generated + assertNotNull(scanWithNulls.getCollectionName()); + assertTrue(scanWithNulls.getCollectionName().startsWith("null_")); + } + + @Test + void testBoundaryValues() { + bulkScan.setStartTime(0L); + assertEquals(0L, bulkScan.getStartTime()); + + bulkScan.setStartTime(Long.MAX_VALUE); + assertEquals(Long.MAX_VALUE, bulkScan.getStartTime()); + + bulkScan.setTargetsGiven(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, bulkScan.getTargetsGiven()); + + bulkScan.setScanJobsPublished(Long.MAX_VALUE); + assertEquals(Long.MAX_VALUE, bulkScan.getScanJobsPublished()); + } + + @Test + void testEmptyStrings() { + bulkScan.set_id(""); + assertEquals("", bulkScan.get_id()); + + bulkScan.setName(""); + assertEquals("", bulkScan.getName()); + + bulkScan.setCollectionName(""); + assertEquals("", bulkScan.getCollectionName()); + + bulkScan.setNotifyUrl(""); + assertEquals("", bulkScan.getNotifyUrl()); + } + + @Test + void testStaticDateFormatField() throws NoSuchFieldException { + assertTrue( + java.lang.reflect.Modifier.isStatic( + BulkScan.class.getDeclaredField("dateFormat").getModifiers())); + } + + @Test + void testPrivateConstructor() { + // Verify the private constructor exists + java.lang.reflect.Constructor[] constructors = BulkScan.class.getDeclaredConstructors(); + boolean hasPrivateConstructor = false; + + for (java.lang.reflect.Constructor constructor : constructors) { + if (java.lang.reflect.Modifier.isPrivate(constructor.getModifiers()) + && constructor.getParameterCount() == 0) { + hasPrivateConstructor = true; + break; + } + } + + assertTrue(hasPrivateConstructor); + } + + @Test + void testAllFieldsInitialization() { + // Test that all numeric fields are initialized to 0 + BulkScan newScan = + new BulkScan( + TestScannerClass.class, + TestCrawlerClass.class, + "Test", + testScanConfig, + TEST_START_TIME, + false, + null); + + assertEquals(0, newScan.getEndTime()); + assertEquals(0, newScan.getTargetsGiven()); + assertEquals(0, newScan.getScanJobsPublished()); + assertEquals(0, newScan.getSuccessfulScans()); + assertEquals(0, newScan.getScanJobsResolutionErrors()); + assertEquals(0, newScan.getScanJobsDenylisted()); + assertNull(newScan.get_id()); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/ScanConfigTest.java b/src/test/java/de/rub/nds/crawler/data/ScanConfigTest.java new file mode 100644 index 0000000..0e4eb35 --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/ScanConfigTest.java @@ -0,0 +1,179 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.scanner.core.config.ScannerDetail; +import java.io.Serializable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScanConfigTest { + + private TestScanConfig scanConfig; + private static final ScannerDetail DEFAULT_DETAIL = ScannerDetail.NORMAL; + private static final int DEFAULT_REEXECUTIONS = 3; + private static final int DEFAULT_TIMEOUT = 30000; + + // Concrete implementation for testing + private static class TestScanConfig extends ScanConfig { + public TestScanConfig(ScannerDetail scannerDetail, int reexecutions, int timeout) { + super(scannerDetail, reexecutions, timeout); + } + + public TestScanConfig() { + // Using private constructor via reflection would be tested separately + super(ScannerDetail.NORMAL, 0, 0); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + // Return null for testing - we're not testing actual worker creation + return null; + } + } + + @BeforeEach + void setUp() { + scanConfig = new TestScanConfig(DEFAULT_DETAIL, DEFAULT_REEXECUTIONS, DEFAULT_TIMEOUT); + } + + @Test + void testProtectedConstructor() { + TestScanConfig config = new TestScanConfig(ScannerDetail.DETAILED, 5, 60000); + assertEquals(ScannerDetail.DETAILED, config.getScannerDetail()); + assertEquals(5, config.getReexecutions()); + assertEquals(60000, config.getTimeout()); + } + + @Test + void testGetScannerDetail() { + assertEquals(DEFAULT_DETAIL, scanConfig.getScannerDetail()); + } + + @Test + void testSetScannerDetail() { + scanConfig.setScannerDetail(ScannerDetail.ALL); + assertEquals(ScannerDetail.ALL, scanConfig.getScannerDetail()); + + scanConfig.setScannerDetail(ScannerDetail.QUICK); + assertEquals(ScannerDetail.QUICK, scanConfig.getScannerDetail()); + } + + @Test + void testGetReexecutions() { + assertEquals(DEFAULT_REEXECUTIONS, scanConfig.getReexecutions()); + } + + @Test + void testSetReexecutions() { + scanConfig.setReexecutions(10); + assertEquals(10, scanConfig.getReexecutions()); + + scanConfig.setReexecutions(0); + assertEquals(0, scanConfig.getReexecutions()); + + scanConfig.setReexecutions(-1); + assertEquals(-1, scanConfig.getReexecutions()); + } + + @Test + void testGetTimeout() { + assertEquals(DEFAULT_TIMEOUT, scanConfig.getTimeout()); + } + + @Test + void testSetTimeout() { + scanConfig.setTimeout(120000); + assertEquals(120000, scanConfig.getTimeout()); + + scanConfig.setTimeout(0); + assertEquals(0, scanConfig.getTimeout()); + + scanConfig.setTimeout(-1000); + assertEquals(-1000, scanConfig.getTimeout()); + } + + @Test + void testCreateWorker() { + // Just test that createWorker can be called - we return null in the test implementation + de.rub.nds.crawler.core.BulkScanWorker worker = + scanConfig.createWorker("bulkScan123", 4, 8); + assertNull(worker); + } + + @Test + void testCreateWorkerWithDifferentParameters() { + de.rub.nds.crawler.core.BulkScanWorker worker1 = + scanConfig.createWorker("scan1", 1, 1); + de.rub.nds.crawler.core.BulkScanWorker worker2 = + scanConfig.createWorker("scan2", 10, 20); + + // We return null in test implementation + assertNull(worker1); + assertNull(worker2); + } + + @Test + void testSerializable() { + assertTrue(Serializable.class.isAssignableFrom(scanConfig.getClass())); + } + + @Test + void testPrivateConstructorAccess() throws Exception { + // Test that the private constructor exists and can be accessed via reflection + java.lang.reflect.Constructor constructor = + TestScanConfig.class.getDeclaredConstructor(); + assertNotNull(constructor); + assertTrue(java.lang.reflect.Modifier.isPublic(constructor.getModifiers())); + } + + @Test + void testAllScannerDetailValues() { + // Test setting all possible ScannerDetail values + for (ScannerDetail detail : ScannerDetail.values()) { + scanConfig.setScannerDetail(detail); + assertEquals(detail, scanConfig.getScannerDetail()); + } + } + + @Test + void testNullScannerDetail() { + scanConfig.setScannerDetail(null); + assertNull(scanConfig.getScannerDetail()); + } + + @Test + void testBoundaryTimeoutValues() { + scanConfig.setTimeout(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, scanConfig.getTimeout()); + + scanConfig.setTimeout(Integer.MIN_VALUE); + assertEquals(Integer.MIN_VALUE, scanConfig.getTimeout()); + } + + @Test + void testBoundaryReexecutionValues() { + scanConfig.setReexecutions(Integer.MAX_VALUE); + assertEquals(Integer.MAX_VALUE, scanConfig.getReexecutions()); + + scanConfig.setReexecutions(Integer.MIN_VALUE); + assertEquals(Integer.MIN_VALUE, scanConfig.getReexecutions()); + } + + @Test + void testDefaultConstructorInitialization() { + TestScanConfig defaultConfig = new TestScanConfig(); + assertEquals(ScannerDetail.NORMAL, defaultConfig.getScannerDetail()); + assertEquals(0, defaultConfig.getReexecutions()); + assertEquals(0, defaultConfig.getTimeout()); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/ScanJobDescriptionTest.java b/src/test/java/de/rub/nds/crawler/data/ScanJobDescriptionTest.java new file mode 100644 index 0000000..fd8a537 --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/ScanJobDescriptionTest.java @@ -0,0 +1,385 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.crawler.constant.JobStatus; +import java.io.*; +import java.util.NoSuchElementException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScanJobDescriptionTest { + + private ScanTarget testScanTarget; + + private BulkScanInfo testBulkScanInfo; + + private BulkScan testBulkScan; + + private ScanJobDescription scanJobDescription; + + // Test implementation of ScanConfig + private static class TestScanConfig extends ScanConfig { + public TestScanConfig() { + super(de.rub.nds.scanner.core.config.ScannerDetail.NORMAL, 1, 1000); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + @BeforeEach + void setUp() { + // Create test ScanTarget + testScanTarget = new ScanTarget(); + testScanTarget.setIp("192.168.1.100"); + testScanTarget.setPort(443); + + // Create test BulkScan + testBulkScan = + new BulkScan( + ScanJobDescriptionTest.class, + ScanJobDescriptionTest.class, + "TestScan", + new TestScanConfig(), + System.currentTimeMillis(), + true, + null); + testBulkScan.set_id("bulk-scan-id"); + testBulkScan.setCollectionName("test_collection"); + + // Create test BulkScanInfo + testBulkScanInfo = new BulkScanInfo(testBulkScan); + } + + @Test + void testConstructorWithBulkScanInfo() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.TO_BE_EXECUTED); + + assertNotNull(scanJobDescription); + assertEquals(testScanTarget, scanJobDescription.getScanTarget()); + assertEquals(testBulkScanInfo, scanJobDescription.getBulkScanInfo()); + assertEquals("test_db", scanJobDescription.getDbName()); + assertEquals("test_collection", scanJobDescription.getCollectionName()); + assertEquals(JobStatus.TO_BE_EXECUTED, scanJobDescription.getStatus()); + } + + @Test + void testConstructorWithBulkScan() { + scanJobDescription = + new ScanJobDescription(testScanTarget, testBulkScan, JobStatus.SUCCESS); + + assertNotNull(scanJobDescription); + assertEquals(testScanTarget, scanJobDescription.getScanTarget()); + assertNotNull(scanJobDescription.getBulkScanInfo()); + assertEquals("TestScan", scanJobDescription.getDbName()); + assertEquals("test_collection", scanJobDescription.getCollectionName()); + assertEquals(JobStatus.SUCCESS, scanJobDescription.getStatus()); + + // Verify BulkScanInfo was created properly + assertEquals("bulk-scan-id", scanJobDescription.getBulkScanInfo().getBulkScanId()); + assertTrue(scanJobDescription.getBulkScanInfo().isMonitored()); + } + + @Test + void testGetScanTarget() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + assertEquals(testScanTarget, scanJobDescription.getScanTarget()); + } + + @Test + void testGetDbName() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "production_db", + "collection", + JobStatus.TO_BE_EXECUTED); + + assertEquals("production_db", scanJobDescription.getDbName()); + } + + @Test + void testGetCollectionName() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "scan_results", + JobStatus.TO_BE_EXECUTED); + + assertEquals("scan_results", scanJobDescription.getCollectionName()); + } + + @Test + void testGetAndSetStatus() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + assertEquals(JobStatus.TO_BE_EXECUTED, scanJobDescription.getStatus()); + + scanJobDescription.setStatus(JobStatus.SUCCESS); + assertEquals(JobStatus.SUCCESS, scanJobDescription.getStatus()); + + scanJobDescription.setStatus(JobStatus.ERROR); + assertEquals(JobStatus.ERROR, scanJobDescription.getStatus()); + } + + @Test + void testDeliveryTagInitiallyEmpty() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + // Should throw NoSuchElementException as deliveryTag is empty + assertThrows( + NoSuchElementException.class, + () -> { + scanJobDescription.getDeliveryTag(); + }); + } + + @Test + void testSetAndGetDeliveryTag() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + scanJobDescription.setDeliveryTag(12345L); + assertEquals(12345L, scanJobDescription.getDeliveryTag()); + } + + @Test + void testSetDeliveryTagTwiceThrowsException() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + scanJobDescription.setDeliveryTag(12345L); + + // Second set should throw IllegalStateException + assertThrows( + IllegalStateException.class, + () -> { + scanJobDescription.setDeliveryTag(67890L); + }); + + // Original value should remain + assertEquals(12345L, scanJobDescription.getDeliveryTag()); + } + + @Test + void testGetBulkScanInfo() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + assertEquals(testBulkScanInfo, scanJobDescription.getBulkScanInfo()); + } + + @Test + void testSerializable() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + assertTrue(Serializable.class.isAssignableFrom(scanJobDescription.getClass())); + } + + @Test + void testSerializationDeserialization() throws IOException, ClassNotFoundException { + // Create a real ScanTarget for serialization + ScanTarget realTarget = new ScanTarget(); + realTarget.setIp("192.168.1.1"); + realTarget.setPort(443); + + // Create a real BulkScan and BulkScanInfo + BulkScan realBulkScan = + new BulkScan( + ScanJobDescriptionTest.class, + ScanJobDescriptionTest.class, + "RealScan", + new TestScanConfig(), + System.currentTimeMillis(), + false, + null); + realBulkScan.set_id("real-id"); + BulkScanInfo realBulkScanInfo = new BulkScanInfo(realBulkScan); + + scanJobDescription = + new ScanJobDescription( + realTarget, + realBulkScanInfo, + "test_db", + "test_collection", + JobStatus.SUCCESS); + + // Set delivery tag before serialization + scanJobDescription.setDeliveryTag(999L); + + // Serialize + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(scanJobDescription); + oos.close(); + + // Deserialize + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ObjectInputStream ois = new ObjectInputStream(bais); + ScanJobDescription deserialized = (ScanJobDescription) ois.readObject(); + ois.close(); + + // Verify deserialized object + assertNotNull(deserialized); + assertEquals("192.168.1.1", deserialized.getScanTarget().getIp()); + assertEquals(443, deserialized.getScanTarget().getPort()); + assertEquals("test_db", deserialized.getDbName()); + assertEquals("test_collection", deserialized.getCollectionName()); + assertEquals(JobStatus.SUCCESS, deserialized.getStatus()); + assertEquals("real-id", deserialized.getBulkScanInfo().getBulkScanId()); + + // Delivery tag should be reset after deserialization + assertThrows( + NoSuchElementException.class, + () -> { + deserialized.getDeliveryTag(); + }); + + // Should be able to set new delivery tag + deserialized.setDeliveryTag(111L); + assertEquals(111L, deserialized.getDeliveryTag()); + } + + @Test + void testAllJobStatusValues() { + for (JobStatus status : JobStatus.values()) { + ScanJobDescription jobDesc = + new ScanJobDescription( + testScanTarget, testBulkScanInfo, "db", "collection", status); + assertEquals(status, jobDesc.getStatus()); + } + } + + @Test + void testNullValues() { + // Test with null values + scanJobDescription = new ScanJobDescription(null, null, null, null, null); + + assertNull(scanJobDescription.getScanTarget()); + assertNull(scanJobDescription.getBulkScanInfo()); + assertNull(scanJobDescription.getDbName()); + assertNull(scanJobDescription.getCollectionName()); + assertNull(scanJobDescription.getStatus()); + } + + @Test + void testEmptyStrings() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, testBulkScanInfo, "", "", JobStatus.TO_BE_EXECUTED); + + assertEquals("", scanJobDescription.getDbName()); + assertEquals("", scanJobDescription.getCollectionName()); + } + + @Test + void testSetDeliveryTagWithNull() { + scanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "db", + "collection", + JobStatus.TO_BE_EXECUTED); + + // This should work fine and create Optional.of(null) which will throw NPE later + assertThrows( + NullPointerException.class, + () -> { + scanJobDescription.setDeliveryTag(null); + }); + } + + @Test + void testFinalFields() throws NoSuchFieldException { + // Verify that certain fields are final + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanJobDescription.class.getDeclaredField("scanTarget").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanJobDescription.class.getDeclaredField("bulkScanInfo").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanJobDescription.class.getDeclaredField("dbName").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanJobDescription.class + .getDeclaredField("collectionName") + .getModifiers())); + + // Status should not be final as it has a setter + assertFalse( + java.lang.reflect.Modifier.isFinal( + ScanJobDescription.class.getDeclaredField("status").getModifiers())); + } + + @Test + void testTransientDeliveryTag() throws NoSuchFieldException { + // Verify deliveryTag is transient + assertTrue( + java.lang.reflect.Modifier.isTransient( + ScanJobDescription.class.getDeclaredField("deliveryTag").getModifiers())); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/ScanResultTest.java b/src/test/java/de/rub/nds/crawler/data/ScanResultTest.java new file mode 100644 index 0000000..8350c4d --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/ScanResultTest.java @@ -0,0 +1,360 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.crawler.constant.JobStatus; +import java.io.Serializable; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScanResultTest { + + private ScanJobDescription testScanJobDescription; + + private BulkScanInfo testBulkScanInfo; + + private ScanTarget testScanTarget; + + private BulkScan testBulkScan; + + private Document testDocument; + + // Test implementation of ScanConfig + private static class TestScanConfig extends ScanConfig { + public TestScanConfig() { + super(de.rub.nds.scanner.core.config.ScannerDetail.NORMAL, 1, 1000); + } + + @Override + public de.rub.nds.crawler.core.BulkScanWorker createWorker( + String bulkScanID, int parallelConnectionThreads, int parallelScanThreads) { + return null; + } + } + + @BeforeEach + void setUp() { + // Create test ScanTarget + testScanTarget = new ScanTarget(); + testScanTarget.setIp("192.168.1.100"); + testScanTarget.setPort(443); + + // Create test BulkScan + testBulkScan = + new BulkScan( + ScanResultTest.class, + ScanResultTest.class, + "TestScan", + new TestScanConfig(), + System.currentTimeMillis(), + true, + null); + testBulkScan.set_id("bulk-scan-123"); + + // Create test BulkScanInfo + testBulkScanInfo = new BulkScanInfo(testBulkScan); + + // Create test ScanJobDescription + testScanJobDescription = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.SUCCESS); + + testDocument = new Document(); + testDocument.put("key", "value"); + testDocument.put("number", 42); + } + + @Test + void testConstructorWithScanJobDescription() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + + assertNotNull(scanResult); + assertNotNull(scanResult.getId()); + assertTrue( + scanResult + .getId() + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}")); + assertEquals("bulk-scan-123", scanResult.getBulkScan()); + assertEquals(testScanTarget, scanResult.getScanTarget()); + assertEquals(JobStatus.SUCCESS, scanResult.getResultStatus()); + assertEquals(testDocument, scanResult.getResult()); + } + + @Test + void testConstructorWithToBeExecutedStatusThrowsException() { + // Create a new ScanJobDescription with TO_BE_EXECUTED status + ScanJobDescription toBeExecutedJob = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.TO_BE_EXECUTED); + + assertThrows( + IllegalArgumentException.class, + () -> { + new ScanResult(toBeExecutedJob, testDocument); + }); + } + + @Test + void testConstructorWithDifferentStatuses() { + // Test all valid statuses (except TO_BE_EXECUTED) + JobStatus[] validStatuses = { + JobStatus.SUCCESS, + JobStatus.ERROR, + JobStatus.EMPTY, + JobStatus.DENYLISTED, + JobStatus.UNRESOLVABLE, + JobStatus.CANCELLED, + JobStatus.SERIALIZATION_ERROR, + JobStatus.INTERNAL_ERROR, + JobStatus.RESOLUTION_ERROR, + JobStatus.CRAWLER_ERROR + }; + + for (JobStatus status : validStatuses) { + ScanJobDescription jobWithStatus = + new ScanJobDescription( + testScanTarget, testBulkScanInfo, "test_db", "test_collection", status); + ScanResult result = new ScanResult(jobWithStatus, testDocument); + assertEquals(status, result.getResultStatus()); + } + } + + @Test + void testFromExceptionWithErrorStatus() { + ScanJobDescription errorJob = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.ERROR); + Exception testException = new RuntimeException("Test error message"); + + ScanResult errorResult = ScanResult.fromException(errorJob, testException); + + assertNotNull(errorResult); + assertNotNull(errorResult.getId()); + assertEquals("bulk-scan-123", errorResult.getBulkScan()); + assertEquals(testScanTarget, errorResult.getScanTarget()); + assertEquals(JobStatus.ERROR, errorResult.getResultStatus()); + + Document resultDoc = errorResult.getResult(); + assertNotNull(resultDoc); + assertEquals(testException, resultDoc.get("exception")); + } + + @Test + void testFromExceptionWithNonErrorStatusThrowsException() { + // testScanJobDescription already has SUCCESS status from setUp + Exception testException = new RuntimeException("Test error"); + + assertThrows( + IllegalArgumentException.class, + () -> { + ScanResult.fromException(testScanJobDescription, testException); + }); + } + + @Test + void testFromExceptionWithAllErrorStatuses() { + Exception testException = new RuntimeException("Test error"); + + for (JobStatus status : JobStatus.values()) { + if (status.isError()) { + ScanJobDescription errorJob = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + status); + ScanResult result = ScanResult.fromException(errorJob, testException); + assertEquals(status, result.getResultStatus()); + assertEquals(testException, result.getResult().get("exception")); + } + } + } + + @Test + void testGetAndSetId() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + + String originalId = scanResult.getId(); + assertNotNull(originalId); + + String newId = "custom-id-12345"; + scanResult.setId(newId); + assertEquals(newId, scanResult.getId()); + } + + @Test + void testGetBulkScan() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + assertEquals("bulk-scan-123", scanResult.getBulkScan()); + } + + @Test + void testGetScanTarget() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + assertEquals(testScanTarget, scanResult.getScanTarget()); + } + + @Test + void testGetResult() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + assertEquals(testDocument, scanResult.getResult()); + assertEquals("value", scanResult.getResult().get("key")); + assertEquals(42, scanResult.getResult().get("number")); + } + + @Test + void testGetResultStatus() { + ScanJobDescription emptyJob = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.EMPTY); + ScanResult scanResult = new ScanResult(emptyJob, testDocument); + assertEquals(JobStatus.EMPTY, scanResult.getResultStatus()); + } + + @Test + void testSerializable() { + ScanResult scanResult = new ScanResult(testScanJobDescription, testDocument); + assertTrue(Serializable.class.isAssignableFrom(scanResult.getClass())); + } + + @Test + void testFinalFields() throws NoSuchFieldException { + // Verify that certain fields are final + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanResult.class.getDeclaredField("bulkScan").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanResult.class.getDeclaredField("scanTarget").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanResult.class.getDeclaredField("jobStatus").getModifiers())); + assertTrue( + java.lang.reflect.Modifier.isFinal( + ScanResult.class.getDeclaredField("result").getModifiers())); + + // id should not be final as it has a setter + assertFalse( + java.lang.reflect.Modifier.isFinal( + ScanResult.class.getDeclaredField("id").getModifiers())); + } + + @Test + void testNullDocument() { + ScanResult scanResult = new ScanResult(testScanJobDescription, null); + assertNull(scanResult.getResult()); + } + + @Test + void testEmptyDocument() { + Document emptyDoc = new Document(); + ScanResult scanResult = new ScanResult(testScanJobDescription, emptyDoc); + assertNotNull(scanResult.getResult()); + assertTrue(scanResult.getResult().isEmpty()); + } + + @Test + void testLargeDocument() { + Document largeDoc = new Document(); + for (int i = 0; i < 1000; i++) { + largeDoc.put("key" + i, "value" + i); + } + + ScanResult scanResult = new ScanResult(testScanJobDescription, largeDoc); + assertEquals(largeDoc, scanResult.getResult()); + assertEquals(1000, scanResult.getResult().size()); + } + + @Test + void testUniqueIdGeneration() { + // Create multiple ScanResults and verify unique IDs + ScanResult result1 = new ScanResult(testScanJobDescription, testDocument); + ScanResult result2 = new ScanResult(testScanJobDescription, testDocument); + ScanResult result3 = new ScanResult(testScanJobDescription, testDocument); + + assertNotEquals(result1.getId(), result2.getId()); + assertNotEquals(result1.getId(), result3.getId()); + assertNotEquals(result2.getId(), result3.getId()); + } + + @Test + void testFromExceptionWithNullException() { + ScanJobDescription errorJob = + new ScanJobDescription( + testScanTarget, + testBulkScanInfo, + "test_db", + "test_collection", + JobStatus.ERROR); + + ScanResult errorResult = ScanResult.fromException(errorJob, null); + + assertNotNull(errorResult); + assertNull(errorResult.getResult().get("exception")); + } + + @Test + void testJsonPropertyAnnotations() throws NoSuchMethodException { + // Verify @JsonProperty annotations + assertTrue( + ScanResult.class + .getMethod("getId") + .isAnnotationPresent(com.fasterxml.jackson.annotation.JsonProperty.class)); + assertTrue( + ScanResult.class + .getMethod("setId", String.class) + .isAnnotationPresent(com.fasterxml.jackson.annotation.JsonProperty.class)); + + // Check annotation value + assertEquals( + "_id", + ScanResult.class + .getMethod("getId") + .getAnnotation(com.fasterxml.jackson.annotation.JsonProperty.class) + .value()); + } + + @Test + void testPrivateConstructor() throws NoSuchMethodException { + // Verify the private constructor exists + java.lang.reflect.Constructor[] constructors = + ScanResult.class.getDeclaredConstructors(); + boolean hasPrivateConstructor = false; + + for (java.lang.reflect.Constructor constructor : constructors) { + if (java.lang.reflect.Modifier.isPrivate(constructor.getModifiers()) + && constructor.getParameterCount() == 4) { + hasPrivateConstructor = true; + break; + } + } + + assertTrue(hasPrivateConstructor); + } +} diff --git a/src/test/java/de/rub/nds/crawler/data/ScanTargetTest.java b/src/test/java/de/rub/nds/crawler/data/ScanTargetTest.java new file mode 100644 index 0000000..b7e2ac9 --- /dev/null +++ b/src/test/java/de/rub/nds/crawler/data/ScanTargetTest.java @@ -0,0 +1,378 @@ +/* + * TLS-Crawler - A TLS scanning tool to perform large scale scans with the TLS-Scanner + * + * Copyright 2018-2022 Ruhr University Bochum, Paderborn University, and Hackmanit GmbH + * + * Licensed under Apache License, Version 2.0 + * http://www.apache.org/licenses/LICENSE-2.0.txt + */ +package de.rub.nds.crawler.data; + +import static org.junit.jupiter.api.Assertions.*; + +import de.rub.nds.crawler.constant.JobStatus; +import de.rub.nds.crawler.denylist.IDenylistProvider; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ScanTargetTest { + + private IDenylistProvider testDenylistProvider; + + private static final int DEFAULT_PORT = 443; + + // Test implementation of IDenylistProvider + private static class TestDenylistProvider implements IDenylistProvider { + private boolean shouldDenylist; + + public TestDenylistProvider(boolean shouldDenylist) { + this.shouldDenylist = shouldDenylist; + } + + @Override + public boolean isDenylisted(ScanTarget target) { + return shouldDenylist; + } + + public void setShouldDenylist(boolean shouldDenylist) { + this.shouldDenylist = shouldDenylist; + } + } + + @BeforeEach + void setUp() { + testDenylistProvider = new TestDenylistProvider(false); + } + + @Test + void testConstructor() { + ScanTarget target = new ScanTarget(); + assertNotNull(target); + assertNull(target.getIp()); + assertNull(target.getHostname()); + assertEquals(0, target.getPort()); + assertEquals(0, target.getTrancoRank()); + } + + @Test + void testGettersAndSetters() { + ScanTarget target = new ScanTarget(); + + target.setIp("192.168.1.1"); + assertEquals("192.168.1.1", target.getIp()); + + target.setHostname("example.com"); + assertEquals("example.com", target.getHostname()); + + target.setPort(8080); + assertEquals(8080, target.getPort()); + + target.setTrancoRank(100); + assertEquals(100, target.getTrancoRank()); + } + + @Test + void testToStringWithHostname() { + ScanTarget target = new ScanTarget(); + target.setHostname("example.com"); + target.setIp("192.168.1.1"); + assertEquals("example.com", target.toString()); + } + + @Test + void testToStringWithoutHostname() { + ScanTarget target = new ScanTarget(); + target.setIp("192.168.1.1"); + assertEquals("192.168.1.1", target.toString()); + } + + @Test + void testFromTargetStringWithValidIp() { + String targetString = "192.168.1.1"; + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("192.168.1.1", target.getIp()); + assertNull(target.getHostname()); + assertEquals(DEFAULT_PORT, target.getPort()); + assertEquals(0, target.getTrancoRank()); + } + + @Test + void testFromTargetStringWithValidIpAndPort() { + String targetString = "192.168.1.1:8080"; + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("192.168.1.1", target.getIp()); + assertNull(target.getHostname()); + assertEquals(8080, target.getPort()); + assertEquals(0, target.getTrancoRank()); + } + + @Test + void testFromTargetStringWithInvalidPort() { + String targetString = "192.168.1.1:0"; + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("192.168.1.1", target.getIp()); + // Port 0 fails validation (not > 1), so setPort is never called + // The port field remains at its default value of 0 + assertEquals(0, target.getPort()); + } + + @Test + void testFromTargetStringWithPortOutOfRange() { + // Port validation happens during parsing: if (port > 1 && port < 65535) + String targetString = "192.168.1.1:70000"; + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("192.168.1.1", target.getIp()); + // Port 70000 fails validation (not < 65535), so setPort is never called + // The port field remains at its default value of 0 + assertEquals(0, target.getPort()); + } + + @Test + void testFromTargetStringWithRankAndHostname() { + // This test needs to use a real hostname that resolves + // Using localhost as it should always resolve + String targetString = "1,localhost"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("localhost", target.getHostname()); + assertNotNull(target.getIp()); + assertTrue(target.getIp().equals("127.0.0.1") || target.getIp().equals("::1")); + assertEquals(DEFAULT_PORT, target.getPort()); + assertEquals(1, target.getTrancoRank()); + } + + @Test + void testFromTargetStringWithRankAndHostnameAndPort() { + String targetString = "100,localhost:8443"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("localhost", target.getHostname()); + assertNotNull(target.getIp()); + assertTrue(target.getIp().equals("127.0.0.1") || target.getIp().equals("::1")); + assertEquals(8443, target.getPort()); + assertEquals(100, target.getTrancoRank()); + } + + @Test + void testFromTargetStringWithMxFormat() { + String targetString = "//localhost"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("localhost", target.getHostname()); + assertNotNull(target.getIp()); + assertTrue(target.getIp().equals("127.0.0.1") || target.getIp().equals("::1")); + } + + @Test + void testFromTargetStringWithQuotes() { + String targetString = "\"localhost\""; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("localhost", target.getHostname()); + assertNotNull(target.getIp()); + assertTrue(target.getIp().equals("127.0.0.1") || target.getIp().equals("::1")); + } + + @Test + void testFromTargetStringWithUnresolvableHost() { + // Use a domain that is guaranteed not to resolve + String targetString = "non.existent.domain.invalid"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.UNRESOLVABLE, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("non.existent.domain.invalid", target.getHostname()); + assertNull(target.getIp()); + } + + @Test + void testFromTargetStringWithDenylistedHost() { + String targetString = "localhost"; + + // Set the denylist provider to return true + ((TestDenylistProvider) testDenylistProvider).setShouldDenylist(true); + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + assertEquals(JobStatus.DENYLISTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("localhost", target.getHostname()); + assertNotNull(target.getIp()); + assertTrue(target.getIp().equals("127.0.0.1") || target.getIp().equals("::1")); + } + + @Test + void testFromTargetStringWithNullDenylistProvider() { + String targetString = "192.168.1.1"; + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, null); + + assertNotNull(result); + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + assertEquals("192.168.1.1", target.getIp()); + } + + @Test + void testFromTargetStringWithInvalidCommaFormat() { + String targetString = "abc,def,ghi"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + // When the first part is not all digits, targetString becomes empty string + // Empty string is not a valid IP, so it's treated as hostname and resolved + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + // Empty hostname gets resolved to localhost/127.0.0.1 in test environment + assertEquals("127.0.0.1", target.getIp()); + assertEquals("", target.getHostname()); + } + + @Test + void testFromTargetStringWithIpv6() { + // The code has a FIXME comment that IPv6 parsing is broken due to : handling + // The current code will try to parse "0db8" as a port number, which fails + String targetString = "2001:0db8:85a3:0000:0000:8a2e:0370:7334"; + + try { + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + // If parsing succeeds, the behavior is likely incorrect due to the FIXME + fail("Expected NumberFormatException due to broken IPv6 parsing"); + } catch (NumberFormatException e) { + // Expected due to the FIXME comment in the code + assertTrue(e.getMessage().contains("0db8")); + } + } + + @Test + void testFromTargetStringWithCompressedIpv6() { + // The code has a FIXME comment that IPv6 parsing is broken due to : handling + String targetString = "2001:db8::8a2e:370:7334"; + + try { + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + // If parsing succeeds, the behavior is likely incorrect due to the FIXME + fail("Expected NumberFormatException due to broken IPv6 parsing"); + } catch (NumberFormatException e) { + // Expected due to the FIXME comment in the code + assertTrue(e.getMessage().contains("db8")); + } + } + + @Test + void testSerializable() { + ScanTarget target = new ScanTarget(); + assertTrue(java.io.Serializable.class.isAssignableFrom(target.getClass())); + } + + @Test + void testFromTargetStringWithComplexMxFormat() { + String targetString = "100,//\"localhost\":25"; + + Pair result = + ScanTarget.fromTargetString(targetString, DEFAULT_PORT, testDenylistProvider); + + assertNotNull(result); + // localhost may resolve to ::1 which may be denylisted or unresolvable in some environments + if (result.getRight() == JobStatus.UNRESOLVABLE + || result.getRight() == JobStatus.DENYLISTED) { + // Expected in some test environments + ScanTarget target = result.getLeft(); + // Quotes are not removed because string doesn't end with quote (ends with :25) + assertEquals("\"localhost\"", target.getHostname()); + assertEquals(25, target.getPort()); + assertEquals(100, target.getTrancoRank()); + } else { + assertEquals(JobStatus.TO_BE_EXECUTED, result.getRight()); + ScanTarget target = result.getLeft(); + // Quotes are not removed because string doesn't end with quote (ends with :25) + assertEquals("\"localhost\"", target.getHostname()); + assertNotNull(target.getIp()); + // IP resolution depends on how the system resolves "localhost" (with quotes) + assertNotNull(target.getIp()); + assertEquals(25, target.getPort()); + assertEquals(100, target.getTrancoRank()); + } + } + + @Test + void testFromTargetStringPortBoundaryValues() { + // Test port = 1 (parsed but not valid since not > 1) + String targetString1 = "192.168.1.1:1"; + Pair result1 = + ScanTarget.fromTargetString(targetString1, DEFAULT_PORT, testDenylistProvider); + // Port 1 fails validation (not > 1), so setPort is never called + // The port field remains at its default value of 0 + assertEquals(0, result1.getLeft().getPort()); + + // Test port = 2 (first valid) + String targetString2 = "192.168.1.1:2"; + Pair result2 = + ScanTarget.fromTargetString(targetString2, DEFAULT_PORT, testDenylistProvider); + assertEquals(2, result2.getLeft().getPort()); + + // Test port = 65534 (last valid) + String targetString3 = "192.168.1.1:65534"; + Pair result3 = + ScanTarget.fromTargetString(targetString3, DEFAULT_PORT, testDenylistProvider); + assertEquals(65534, result3.getLeft().getPort()); + + // Test port = 65535 (parsed but not valid since not < 65535) + String targetString4 = "192.168.1.1:65535"; + Pair result4 = + ScanTarget.fromTargetString(targetString4, DEFAULT_PORT, testDenylistProvider); + assertEquals(0, result4.getLeft().getPort()); // Port fails validation, remains at default 0 + } +}