diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c6886f0..c2bdc16 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -21,7 +21,8 @@ "Bash(tree:*)", "Bash(docker-compose:*)", "Bash(chmod:*)", - "Bash(ls:*)" + "Bash(ls:*)", + "Bash(cat:*)" ], "deny": [], "ask": [] diff --git a/pom.xml b/pom.xml index 6afdb4d..0c8a929 100644 --- a/pom.xml +++ b/pom.xml @@ -67,13 +67,13 @@ org.mockito mockito-core - 5.3.1 + 3.12.4 test org.mockito mockito-junit-jupiter - 5.3.1 + 3.12.4 test diff --git a/src/test/java/com/example/listener/DatabaseInitializationListenerTest.java b/src/test/java/com/example/listener/DatabaseInitializationListenerTest.java new file mode 100644 index 0000000..9ff2ce6 --- /dev/null +++ b/src/test/java/com/example/listener/DatabaseInitializationListenerTest.java @@ -0,0 +1,313 @@ +package com.example.listener; + +import com.example.util.DatabaseManager; +import org.junit.jupiter.api.*; +import org.mockito.Mockito; + +import javax.servlet.ServletContextEvent; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for DatabaseInitializationListener + */ +@DisplayName("DatabaseInitializationListener Tests") +class DatabaseInitializationListenerTest { + + private DatabaseInitializationListener listener; + private ServletContextEvent mockEvent; + private DatabaseManager dbManager; + + @BeforeEach + void setUp() { + listener = new DatabaseInitializationListener(); + mockEvent = mock(ServletContextEvent.class); + dbManager = DatabaseManager.getInstance(); + // Reset database before each test + dbManager.resetDatabase(); + } + + @Test + @DisplayName("Should initialize database on context initialization") + void testContextInitialized() throws SQLException { + // Call the listener + assertDoesNotThrow(() -> listener.contextInitialized(mockEvent)); + + // Verify database was initialized + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='customers'")) { + + assertTrue(rs.next(), "Customers table should exist after initialization"); + } + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='loan_history'")) { + + assertTrue(rs.next(), "Loan history table should exist after initialization"); + } + } + + @Test + @DisplayName("Should not throw exception on successful initialization") + void testSuccessfulInitialization() { + assertDoesNotThrow(() -> listener.contextInitialized(mockEvent)); + } + + @Test + @DisplayName("Should handle context destroyed event") + void testContextDestroyed() { + assertDoesNotThrow(() -> listener.contextDestroyed(mockEvent)); + } + + @Test + @DisplayName("Should initialize database even if already initialized") + void testReinitializeDatabase() throws SQLException { + // First initialization + listener.contextInitialized(mockEvent); + + // Add some data + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Test User', 0, '2025-01-01 10:00:00')"); + } + + // Second initialization should not fail + assertDoesNotThrow(() -> listener.contextInitialized(mockEvent)); + + // Data should still exist (tables are IF NOT EXISTS) + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers")) { + + assertTrue(rs.next()); + assertEquals(1, rs.getInt("count")); + } + } + + @Test + @DisplayName("Should test database connection before initialization") + void testConnectionTestBeforeInitialization() { + // The listener tests connection first + listener.contextInitialized(mockEvent); + + // Verify database is accessible + assertTrue(dbManager.testConnection()); + } + + @Test + @DisplayName("Should handle multiple context initialization calls") + void testMultipleContextInitializations() { + assertDoesNotThrow(() -> { + listener.contextInitialized(mockEvent); + listener.contextInitialized(mockEvent); + listener.contextInitialized(mockEvent); + }); + } + + @Test + @DisplayName("Should handle multiple context destroyed calls") + void testMultipleContextDestroyedCalls() { + assertDoesNotThrow(() -> { + listener.contextDestroyed(mockEvent); + listener.contextDestroyed(mockEvent); + listener.contextDestroyed(mockEvent); + }); + } + + @Test + @DisplayName("Should initialize and destroy in sequence") + void testInitializeAndDestroySequence() { + assertDoesNotThrow(() -> { + listener.contextInitialized(mockEvent); + listener.contextDestroyed(mockEvent); + listener.contextInitialized(mockEvent); + listener.contextDestroyed(mockEvent); + }); + } + + @Test + @DisplayName("Should verify ServletContextEvent is used") + void testServletContextEventUsage() { + ServletContextEvent event = mock(ServletContextEvent.class); + + assertDoesNotThrow(() -> { + listener.contextInitialized(event); + listener.contextDestroyed(event); + }); + + // Verify the event object is passed (though not used in implementation) + assertNotNull(event); + } + + @Test + @DisplayName("Should handle null ServletContextEvent gracefully in contextDestroyed") + void testNullEventInContextDestroyed() { + // contextDestroyed doesn't use the event, so null should be fine + assertDoesNotThrow(() -> listener.contextDestroyed(null)); + } + + @Test + @DisplayName("Should initialize database tables with correct schema") + void testDatabaseSchemaAfterInitialization() throws SQLException { + listener.contextInitialized(mockEvent); + + // Test customers table schema + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Schema Test', 1, '2025-01-01 10:00:00')"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM customers WHERE customer_name = 'Schema Test'")) { + assertTrue(rs.next()); + assertEquals("Schema Test", rs.getString("customer_name")); + assertEquals(1, rs.getInt("is_blacklisted")); + assertEquals("2025-01-01 10:00:00", rs.getString("registered_at")); + } + } + + // Test loan_history table schema + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.execute("INSERT INTO loan_history " + + "(applicant_name, requested_amount, approved, approved_rate, rejection_reason, processed_at) " + + "VALUES ('Schema Test', 50000, 1, 5.5, NULL, '2025-01-01 10:00:00')"); + + try (ResultSet rs = stmt.executeQuery("SELECT * FROM loan_history WHERE applicant_name = 'Schema Test'")) { + assertTrue(rs.next()); + assertEquals("Schema Test", rs.getString("applicant_name")); + assertEquals(50000, rs.getInt("requested_amount")); + assertEquals(1, rs.getInt("approved")); + assertEquals(5.5, rs.getDouble("approved_rate"), 0.001); + } + } + } + + @Test + @DisplayName("Should complete initialization within reasonable time") + void testInitializationPerformance() { + long startTime = System.currentTimeMillis(); + + listener.contextInitialized(mockEvent); + + long endTime = System.currentTimeMillis(); + long duration = endTime - startTime; + + assertTrue(duration < 5000, "Initialization should complete within 5 seconds, took: " + duration + "ms"); + } + + @Test + @DisplayName("Should handle rapid initialization and destruction") + void testRapidInitAndDestroy() { + assertDoesNotThrow(() -> { + for (int i = 0; i < 10; i++) { + listener.contextInitialized(mockEvent); + listener.contextDestroyed(mockEvent); + } + }); + } + + @Test + @DisplayName("Should maintain database state across listener calls") + void testDatabaseStatePersistence() throws SQLException { + listener.contextInitialized(mockEvent); + + // Add data + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Persistent User', 0, '2025-01-01 10:00:00')"); + } + + listener.contextDestroyed(mockEvent); + listener.contextInitialized(mockEvent); + + // Verify data persists + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM customers WHERE customer_name = 'Persistent User'")) { + + assertTrue(rs.next(), "Data should persist across listener lifecycle"); + } + } + + @Test + @DisplayName("Should create new listener instance successfully") + void testListenerInstantiation() { + DatabaseInitializationListener newListener = new DatabaseInitializationListener(); + + assertNotNull(newListener); + assertDoesNotThrow(() -> newListener.contextInitialized(mockEvent)); + } + + @Test + @DisplayName("Should handle concurrent context initialization") + void testConcurrentInitialization() throws InterruptedException { + int threadCount = 5; + Thread[] threads = new Thread[threadCount]; + + for (int i = 0; i < threadCount; i++) { + threads[i] = new Thread(() -> { + assertDoesNotThrow(() -> listener.contextInitialized(mockEvent)); + }); + threads[i].start(); + } + + for (Thread thread : threads) { + thread.join(); + } + + // Verify database is still in good state + assertTrue(dbManager.testConnection()); + } + + @Test + @DisplayName("Should verify database connection is working after initialization") + void testDatabaseConnectionAfterInit() { + listener.contextInitialized(mockEvent); + + assertTrue(dbManager.testConnection(), "Database connection should be working after initialization"); + } + + @Test + @DisplayName("Should initialize database with empty tables") + void testEmptyTablesAfterInitialization() throws SQLException { + listener.contextInitialized(mockEvent); + + // Verify customers table is empty + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("count"), "Customers table should be empty after initialization"); + } + + // Verify loan_history table is empty + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM loan_history")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("count"), "Loan history table should be empty after initialization"); + } + } + + @Test + @DisplayName("Should not throw exception when destroying uninitialized context") + void testDestroyUninitializedContext() { + // Call destroy without init + assertDoesNotThrow(() -> listener.contextDestroyed(mockEvent)); + } +} diff --git a/src/test/java/com/example/model/CustomerRegistrationRequestTest.java b/src/test/java/com/example/model/CustomerRegistrationRequestTest.java new file mode 100644 index 0000000..20fe46d --- /dev/null +++ b/src/test/java/com/example/model/CustomerRegistrationRequestTest.java @@ -0,0 +1,172 @@ +package com.example.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for CustomerRegistrationRequest model class + */ +@DisplayName("CustomerRegistrationRequest Model Tests") +class CustomerRegistrationRequestTest { + + @Test + @DisplayName("Should create CustomerRegistrationRequest with default constructor") + void testDefaultConstructor() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + + assertNotNull(request); + assertNull(request.getCustomerName()); + assertFalse(request.isBlacklisted()); + } + + @Test + @DisplayName("Should create CustomerRegistrationRequest with parameterized constructor") + void testParameterizedConstructor() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("John Doe", true); + + assertEquals("John Doe", request.getCustomerName()); + assertTrue(request.isBlacklisted()); + } + + @Test + @DisplayName("Should set and get customerName") + void testSetAndGetCustomerName() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName("Jane Smith"); + + assertEquals("Jane Smith", request.getCustomerName()); + } + + @Test + @DisplayName("Should set and get blacklisted status") + void testSetAndGetBlacklisted() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setBlacklisted(true); + + assertTrue(request.isBlacklisted()); + } + + @Test + @DisplayName("Should handle null customerName") + void testNullCustomerName() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest(null, false); + + assertNull(request.getCustomerName()); + assertFalse(request.isBlacklisted()); + } + + @Test + @DisplayName("Should handle empty string customerName") + void testEmptyCustomerName() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("", true); + + assertEquals("", request.getCustomerName()); + assertTrue(request.isBlacklisted()); + } + + @Test + @DisplayName("Should create non-blacklisted customer by default") + void testNonBlacklistedByDefault() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("Test User", false); + + assertFalse(request.isBlacklisted()); + } + + @Test + @DisplayName("Should create blacklisted customer") + void testBlacklistedCustomer() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("Blacklisted User", true); + + assertTrue(request.isBlacklisted()); + } + + @Test + @DisplayName("Should generate correct toString output") + void testToString() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("Alice Brown", true); + String result = request.toString(); + + assertNotNull(result); + assertTrue(result.contains("CustomerRegistrationRequest{")); + assertTrue(result.contains("customerName='Alice Brown'")); + assertTrue(result.contains("blacklisted=true")); + } + + @Test + @DisplayName("Should generate toString with null customerName") + void testToStringWithNull() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest(null, false); + String result = request.toString(); + + assertNotNull(result); + assertTrue(result.contains("customerName='null'") || result.contains("customerName=null")); + assertTrue(result.contains("blacklisted=false")); + } + + @Test + @DisplayName("Should handle special characters in customerName") + void testSpecialCharactersInCustomerName() { + String specialName = "José O'Reilly-Smith"; + CustomerRegistrationRequest request = new CustomerRegistrationRequest(specialName, false); + + assertEquals(specialName, request.getCustomerName()); + } + + @Test + @DisplayName("Should update values multiple times") + void testMultipleUpdates() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("Initial Name", false); + + request.setCustomerName("Updated Name"); + request.setBlacklisted(true); + + assertEquals("Updated Name", request.getCustomerName()); + assertTrue(request.isBlacklisted()); + + request.setCustomerName("Final Name"); + request.setBlacklisted(false); + + assertEquals("Final Name", request.getCustomerName()); + assertFalse(request.isBlacklisted()); + } + + @Test + @DisplayName("Should toggle blacklisted status") + void testToggleBlacklistedStatus() { + CustomerRegistrationRequest request = new CustomerRegistrationRequest("Toggle User", false); + + assertFalse(request.isBlacklisted()); + + request.setBlacklisted(true); + assertTrue(request.isBlacklisted()); + + request.setBlacklisted(false); + assertFalse(request.isBlacklisted()); + } + + @Test + @DisplayName("Should handle very long customerName") + void testLongCustomerName() { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + builder.append("A"); + } + String longName = builder.toString(); + CustomerRegistrationRequest request = new CustomerRegistrationRequest(longName, true); + + assertEquals(longName, request.getCustomerName()); + assertEquals(1000, request.getCustomerName().length()); + } + + @Test + @DisplayName("Should handle whitespace in customerName") + void testWhitespaceInCustomerName() { + CustomerRegistrationRequest request1 = new CustomerRegistrationRequest(" ", false); + assertEquals(" ", request1.getCustomerName()); + + CustomerRegistrationRequest request2 = new CustomerRegistrationRequest("John Doe", false); + assertEquals("John Doe", request2.getCustomerName()); + } +} diff --git a/src/test/java/com/example/model/LoanRequestTest.java b/src/test/java/com/example/model/LoanRequestTest.java new file mode 100644 index 0000000..94e82dc --- /dev/null +++ b/src/test/java/com/example/model/LoanRequestTest.java @@ -0,0 +1,169 @@ +package com.example.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for LoanRequest model class + */ +@DisplayName("LoanRequest Model Tests") +class LoanRequestTest { + + @Test + @DisplayName("Should create LoanRequest with default constructor") + void testDefaultConstructor() { + LoanRequest request = new LoanRequest(); + + assertNotNull(request); + assertNull(request.getApplicantName()); + assertEquals(0, request.getRequestedAmount()); + assertEquals(0, request.getCreditScore()); + } + + @Test + @DisplayName("Should create LoanRequest with parameterized constructor") + void testParameterizedConstructor() { + LoanRequest request = new LoanRequest("John Doe", 50000, 750); + + assertEquals("John Doe", request.getApplicantName()); + assertEquals(50000, request.getRequestedAmount()); + assertEquals(750, request.getCreditScore()); + } + + @Test + @DisplayName("Should set and get applicantName") + void testSetAndGetApplicantName() { + LoanRequest request = new LoanRequest(); + request.setApplicantName("Jane Smith"); + + assertEquals("Jane Smith", request.getApplicantName()); + } + + @Test + @DisplayName("Should set and get requestedAmount") + void testSetAndGetRequestedAmount() { + LoanRequest request = new LoanRequest(); + request.setRequestedAmount(100000); + + assertEquals(100000, request.getRequestedAmount()); + } + + @Test + @DisplayName("Should set and get creditScore") + void testSetAndGetCreditScore() { + LoanRequest request = new LoanRequest(); + request.setCreditScore(800); + + assertEquals(800, request.getCreditScore()); + } + + @Test + @DisplayName("Should handle null applicantName") + void testNullApplicantName() { + LoanRequest request = new LoanRequest(null, 50000, 700); + + assertNull(request.getApplicantName()); + } + + @Test + @DisplayName("Should handle negative requestedAmount") + void testNegativeRequestedAmount() { + LoanRequest request = new LoanRequest(); + request.setRequestedAmount(-1000); + + assertEquals(-1000, request.getRequestedAmount()); + } + + @Test + @DisplayName("Should handle negative creditScore") + void testNegativeCreditScore() { + LoanRequest request = new LoanRequest(); + request.setCreditScore(-100); + + assertEquals(-100, request.getCreditScore()); + } + + @Test + @DisplayName("Should handle zero values") + void testZeroValues() { + LoanRequest request = new LoanRequest("Test User", 0, 0); + + assertEquals("Test User", request.getApplicantName()); + assertEquals(0, request.getRequestedAmount()); + assertEquals(0, request.getCreditScore()); + } + + @Test + @DisplayName("Should handle maximum integer values") + void testMaximumValues() { + LoanRequest request = new LoanRequest("Test User", Integer.MAX_VALUE, Integer.MAX_VALUE); + + assertEquals(Integer.MAX_VALUE, request.getRequestedAmount()); + assertEquals(Integer.MAX_VALUE, request.getCreditScore()); + } + + @Test + @DisplayName("Should generate correct toString output") + void testToString() { + LoanRequest request = new LoanRequest("Alice Brown", 75000, 720); + String result = request.toString(); + + assertNotNull(result); + assertTrue(result.contains("LoanRequest{")); + assertTrue(result.contains("applicantName='Alice Brown'")); + assertTrue(result.contains("requestedAmount=75000")); + assertTrue(result.contains("creditScore=720")); + } + + @Test + @DisplayName("Should generate toString with null applicantName") + void testToStringWithNull() { + LoanRequest request = new LoanRequest(null, 50000, 700); + String result = request.toString(); + + assertNotNull(result); + assertTrue(result.contains("applicantName='null'") || result.contains("applicantName=null")); + } + + @Test + @DisplayName("Should handle empty string applicantName") + void testEmptyApplicantName() { + LoanRequest request = new LoanRequest("", 50000, 700); + + assertEquals("", request.getApplicantName()); + assertTrue(request.toString().contains("applicantName=''")); + } + + @Test + @DisplayName("Should handle special characters in applicantName") + void testSpecialCharactersInApplicantName() { + String specialName = "José O'Reilly-Smith"; + LoanRequest request = new LoanRequest(specialName, 50000, 700); + + assertEquals(specialName, request.getApplicantName()); + } + + @Test + @DisplayName("Should update values multiple times") + void testMultipleUpdates() { + LoanRequest request = new LoanRequest("Initial Name", 10000, 600); + + request.setApplicantName("Updated Name"); + request.setRequestedAmount(20000); + request.setCreditScore(650); + + assertEquals("Updated Name", request.getApplicantName()); + assertEquals(20000, request.getRequestedAmount()); + assertEquals(650, request.getCreditScore()); + + request.setApplicantName("Final Name"); + request.setRequestedAmount(30000); + request.setCreditScore(700); + + assertEquals("Final Name", request.getApplicantName()); + assertEquals(30000, request.getRequestedAmount()); + assertEquals(700, request.getCreditScore()); + } +} diff --git a/src/test/java/com/example/model/LoanResponseTest.java b/src/test/java/com/example/model/LoanResponseTest.java new file mode 100644 index 0000000..2ecf709 --- /dev/null +++ b/src/test/java/com/example/model/LoanResponseTest.java @@ -0,0 +1,274 @@ +package com.example.model; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for LoanResponse model class + */ +@DisplayName("LoanResponse Model Tests") +class LoanResponseTest { + + @Test + @DisplayName("Should create LoanResponse with default constructor") + void testDefaultConstructor() { + LoanResponse response = new LoanResponse(); + + assertNotNull(response); + assertFalse(response.isApproved()); + assertEquals(0.0, response.getApprovedRate(), 0.001); + assertNull(response.getRejectionReason()); + assertNull(response.getMessage()); + } + + @Test + @DisplayName("Should create LoanResponse with parameterized constructor") + void testParameterizedConstructor() { + LoanResponse response = new LoanResponse(true, 5.5, null, "Loan approved"); + + assertTrue(response.isApproved()); + assertEquals(5.5, response.getApprovedRate(), 0.001); + assertNull(response.getRejectionReason()); + assertEquals("Loan approved", response.getMessage()); + } + + @Test + @DisplayName("Should create approved loan response") + void testApprovedLoanResponse() { + LoanResponse response = new LoanResponse(true, 4.5, null, "Congratulations! Your loan has been approved."); + + assertTrue(response.isApproved()); + assertEquals(4.5, response.getApprovedRate(), 0.001); + assertNull(response.getRejectionReason()); + assertNotNull(response.getMessage()); + } + + @Test + @DisplayName("Should create rejected loan response") + void testRejectedLoanResponse() { + LoanResponse response = new LoanResponse(false, 0.0, "Poor credit score", "Loan application rejected"); + + assertFalse(response.isApproved()); + assertEquals(0.0, response.getApprovedRate(), 0.001); + assertEquals("Poor credit score", response.getRejectionReason()); + assertEquals("Loan application rejected", response.getMessage()); + } + + @Test + @DisplayName("Should set and get approved status") + void testSetAndGetApproved() { + LoanResponse response = new LoanResponse(); + response.setApproved(true); + + assertTrue(response.isApproved()); + } + + @Test + @DisplayName("Should set and get approvedRate") + void testSetAndGetApprovedRate() { + LoanResponse response = new LoanResponse(); + response.setApprovedRate(6.25); + + assertEquals(6.25, response.getApprovedRate(), 0.001); + } + + @Test + @DisplayName("Should set and get rejectionReason") + void testSetAndGetRejectionReason() { + LoanResponse response = new LoanResponse(); + response.setRejectionReason("Insufficient income"); + + assertEquals("Insufficient income", response.getRejectionReason()); + } + + @Test + @DisplayName("Should set and get message") + void testSetAndGetMessage() { + LoanResponse response = new LoanResponse(); + response.setMessage("Processing complete"); + + assertEquals("Processing complete", response.getMessage()); + } + + @Test + @DisplayName("Should handle null rejectionReason") + void testNullRejectionReason() { + LoanResponse response = new LoanResponse(true, 5.0, null, "Approved"); + + assertNull(response.getRejectionReason()); + } + + @Test + @DisplayName("Should handle null message") + void testNullMessage() { + LoanResponse response = new LoanResponse(false, 0.0, "Rejected", null); + + assertNull(response.getMessage()); + } + + @Test + @DisplayName("Should handle zero approvedRate") + void testZeroApprovedRate() { + LoanResponse response = new LoanResponse(false, 0.0, "Not approved", "Rejected"); + + assertEquals(0.0, response.getApprovedRate(), 0.001); + } + + @Test + @DisplayName("Should handle negative approvedRate") + void testNegativeApprovedRate() { + LoanResponse response = new LoanResponse(); + response.setApprovedRate(-1.5); + + assertEquals(-1.5, response.getApprovedRate(), 0.001); + } + + @Test + @DisplayName("Should handle very high approvedRate") + void testHighApprovedRate() { + LoanResponse response = new LoanResponse(); + response.setApprovedRate(99.99); + + assertEquals(99.99, response.getApprovedRate(), 0.001); + } + + @Test + @DisplayName("Should handle decimal precision in approvedRate") + void testDecimalPrecision() { + LoanResponse response = new LoanResponse(true, 5.125, null, "Approved"); + + assertEquals(5.125, response.getApprovedRate(), 0.0001); + } + + @Test + @DisplayName("Should generate correct toString output") + void testToString() { + LoanResponse response = new LoanResponse(true, 4.75, null, "Loan approved successfully"); + String result = response.toString(); + + assertNotNull(result); + assertTrue(result.contains("LoanResponse{")); + assertTrue(result.contains("approved=true")); + assertTrue(result.contains("approvedRate=4.75")); + assertTrue(result.contains("rejectionReason='null'") || result.contains("rejectionReason=null")); + assertTrue(result.contains("message='Loan approved successfully'")); + } + + @Test + @DisplayName("Should generate toString with rejection") + void testToStringWithRejection() { + LoanResponse response = new LoanResponse(false, 0.0, "Credit too low", "Application denied"); + String result = response.toString(); + + assertNotNull(result); + assertTrue(result.contains("approved=false")); + assertTrue(result.contains("approvedRate=0.0")); + assertTrue(result.contains("rejectionReason='Credit too low'")); + assertTrue(result.contains("message='Application denied'")); + } + + @Test + @DisplayName("Should update values multiple times") + void testMultipleUpdates() { + LoanResponse response = new LoanResponse(false, 0.0, "Initial rejection", "Initial message"); + + response.setApproved(true); + response.setApprovedRate(5.5); + response.setRejectionReason(null); + response.setMessage("Updated to approved"); + + assertTrue(response.isApproved()); + assertEquals(5.5, response.getApprovedRate(), 0.001); + assertNull(response.getRejectionReason()); + assertEquals("Updated to approved", response.getMessage()); + + response.setApproved(false); + response.setApprovedRate(0.0); + response.setRejectionReason("Final rejection"); + response.setMessage("Final message"); + + assertFalse(response.isApproved()); + assertEquals(0.0, response.getApprovedRate(), 0.001); + assertEquals("Final rejection", response.getRejectionReason()); + assertEquals("Final message", response.getMessage()); + } + + @Test + @DisplayName("Should handle empty strings") + void testEmptyStrings() { + LoanResponse response = new LoanResponse(false, 0.0, "", ""); + + assertEquals("", response.getRejectionReason()); + assertEquals("", response.getMessage()); + } + + @Test + @DisplayName("Should handle special characters in strings") + void testSpecialCharacters() { + LoanResponse response = new LoanResponse( + false, + 0.0, + "Reason: Credit < 500 & income > $50k", + "Message: Contact us @ info@bank.com" + ); + + assertTrue(response.getRejectionReason().contains("<")); + assertTrue(response.getRejectionReason().contains("&")); + assertTrue(response.getMessage().contains("@")); + } + + @Test + @DisplayName("Should handle very long strings") + void testLongStrings() { + StringBuilder reasonBuilder = new StringBuilder(); + StringBuilder messageBuilder = new StringBuilder(); + for (int i = 0; i < 500; i++) { + reasonBuilder.append("A"); + messageBuilder.append("B"); + } + String longReason = reasonBuilder.toString(); + String longMessage = messageBuilder.toString(); + + LoanResponse response = new LoanResponse(false, 0.0, longReason, longMessage); + + assertEquals(500, response.getRejectionReason().length()); + assertEquals(500, response.getMessage().length()); + } + + @Test + @DisplayName("Should handle approved loan with various rates") + void testVariousApprovedRates() { + double[] rates = {3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 7.0, 8.0, 9.0, 10.0}; + + for (double rate : rates) { + LoanResponse response = new LoanResponse(true, rate, null, "Approved at " + rate + "%"); + assertTrue(response.isApproved()); + assertEquals(rate, response.getApprovedRate(), 0.001); + } + } + + @Test + @DisplayName("Should toggle approved status") + void testToggleApprovedStatus() { + LoanResponse response = new LoanResponse(false, 0.0, "Rejected", "Not approved"); + + assertFalse(response.isApproved()); + + response.setApproved(true); + assertTrue(response.isApproved()); + + response.setApproved(false); + assertFalse(response.isApproved()); + } + + @Test + @DisplayName("Should handle Double.MAX_VALUE as approvedRate") + void testMaxDoubleValue() { + LoanResponse response = new LoanResponse(); + response.setApprovedRate(Double.MAX_VALUE); + + assertEquals(Double.MAX_VALUE, response.getApprovedRate(), 0.001); + } +} diff --git a/src/test/java/com/example/repository/impl/CustomerRepositoryImplTest.java b/src/test/java/com/example/repository/impl/CustomerRepositoryImplTest.java new file mode 100644 index 0000000..4765c58 --- /dev/null +++ b/src/test/java/com/example/repository/impl/CustomerRepositoryImplTest.java @@ -0,0 +1,268 @@ +package com.example.repository.impl; + +import com.example.util.DatabaseManager; +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for CustomerRepositoryImpl + */ +@DisplayName("CustomerRepositoryImpl Tests") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class CustomerRepositoryImplTest { + + private CustomerRepositoryImpl repository; + private DatabaseManager dbManager; + + @BeforeEach + void setUp() { + dbManager = DatabaseManager.getInstance(); + dbManager.resetDatabase(); + repository = new CustomerRepositoryImpl(); + } + + @Test + @Order(1) + @DisplayName("Should register new customer successfully") + void testRegisterCustomer() { + boolean result = repository.registerCustomer("John Doe", false); + + assertTrue(result, "Should successfully register new customer"); + } + + @Test + @Order(2) + @DisplayName("Should register blacklisted customer") + void testRegisterBlacklistedCustomer() { + boolean result = repository.registerCustomer("Blacklisted User", true); + + assertTrue(result); + assertTrue(repository.isBlacklisted("Blacklisted User")); + } + + @Test + @Order(3) + @DisplayName("Should not register duplicate customer") + void testRegisterDuplicateCustomer() { + repository.registerCustomer("John Doe", false); + boolean result = repository.registerCustomer("John Doe", false); + + assertFalse(result, "Should not register duplicate customer"); + } + + @Test + @Order(4) + @DisplayName("Should throw exception for null customer name") + void testRegisterCustomerWithNullName() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + repository.registerCustomer(null, false); + }); + + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + @Order(5) + @DisplayName("Should throw exception for empty customer name") + void testRegisterCustomerWithEmptyName() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + repository.registerCustomer("", false); + }); + + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + @Order(6) + @DisplayName("Should throw exception for whitespace-only customer name") + void testRegisterCustomerWithWhitespaceName() { + Exception exception = assertThrows(IllegalArgumentException.class, () -> { + repository.registerCustomer(" ", false); + }); + + assertTrue(exception.getMessage().contains("cannot be null or empty")); + } + + @Test + @Order(7) + @DisplayName("Should check blacklist status for existing customer") + void testIsBlacklistedForExistingCustomer() { + repository.registerCustomer("Normal User", false); + repository.registerCustomer("Bad User", true); + + assertFalse(repository.isBlacklisted("Normal User")); + assertTrue(repository.isBlacklisted("Bad User")); + } + + @Test + @Order(8) + @DisplayName("Should return false for non-existent customer blacklist check") + void testIsBlacklistedForNonExistentCustomer() { + boolean result = repository.isBlacklisted("Non Existent User"); + + assertFalse(result, "Non-existent customer should not be blacklisted"); + } + + @Test + @Order(9) + @DisplayName("Should handle multiple customer registrations") + void testMultipleCustomerRegistrations() { + assertTrue(repository.registerCustomer("User1", false)); + assertTrue(repository.registerCustomer("User2", true)); + assertTrue(repository.registerCustomer("User3", false)); + assertTrue(repository.registerCustomer("User4", true)); + + assertFalse(repository.isBlacklisted("User1")); + assertTrue(repository.isBlacklisted("User2")); + assertFalse(repository.isBlacklisted("User3")); + assertTrue(repository.isBlacklisted("User4")); + } + + @Test + @Order(10) + @DisplayName("Should handle customer names with special characters") + void testCustomerNamesWithSpecialCharacters() { + assertTrue(repository.registerCustomer("O'Brien", false)); + assertTrue(repository.registerCustomer("Jean-Claude", false)); + assertTrue(repository.registerCustomer("José García", false)); + + assertFalse(repository.isBlacklisted("O'Brien")); + assertFalse(repository.isBlacklisted("Jean-Claude")); + assertFalse(repository.isBlacklisted("José García")); + } + + @Test + @Order(11) + @DisplayName("Should handle very long customer names") + void testVeryLongCustomerName() { + StringBuilder longName = new StringBuilder(); + for (int i = 0; i < 200; i++) { + longName.append("A"); + } + String customerName = longName.toString(); + + assertTrue(repository.registerCustomer(customerName, false)); + assertFalse(repository.isBlacklisted(customerName)); + } + + @Test + @Order(12) + @DisplayName("Should handle case-sensitive customer names") + void testCaseSensitiveCustomerNames() { + assertTrue(repository.registerCustomer("john doe", false)); + assertTrue(repository.registerCustomer("John Doe", false)); + assertTrue(repository.registerCustomer("JOHN DOE", false)); + + // All should be registered as separate customers + assertFalse(repository.isBlacklisted("john doe")); + assertFalse(repository.isBlacklisted("John Doe")); + assertFalse(repository.isBlacklisted("JOHN DOE")); + } + + @Test + @Order(13) + @DisplayName("Should handle customer names with numbers") + void testCustomerNamesWithNumbers() { + assertTrue(repository.registerCustomer("User123", false)); + assertTrue(repository.registerCustomer("123User", false)); + assertTrue(repository.registerCustomer("User 456", false)); + + assertFalse(repository.isBlacklisted("User123")); + assertFalse(repository.isBlacklisted("123User")); + assertFalse(repository.isBlacklisted("User 456")); + } + + @Test + @Order(14) + @DisplayName("Should handle rapid successive registrations") + void testRapidSuccessiveRegistrations() { + for (int i = 0; i < 50; i++) { + assertTrue(repository.registerCustomer("RapidUser" + i, i % 2 == 0)); + } + + // Verify some registrations (i%2==0 means blacklisted=true) + assertTrue(repository.isBlacklisted("RapidUser0")); // i=0: even, blacklisted + assertFalse(repository.isBlacklisted("RapidUser1")); // i=1: odd, not blacklisted + assertTrue(repository.isBlacklisted("RapidUser10")); // i=10: even, blacklisted + assertFalse(repository.isBlacklisted("RapidUser11")); // i=11: odd, not blacklisted + } + + @Test + @Order(15) + @DisplayName("Should maintain blacklist status consistency") + void testBlacklistStatusConsistency() { + repository.registerCustomer("Test User", true); + + // Check multiple times + for (int i = 0; i < 10; i++) { + assertTrue(repository.isBlacklisted("Test User"), + "Blacklist status should be consistent across multiple checks"); + } + } + + @Test + @Order(16) + @DisplayName("Should handle customer names with unicode characters") + void testCustomerNamesWithUnicodeCharacters() { + assertTrue(repository.registerCustomer("用户名", false)); + assertTrue(repository.registerCustomer("Имя пользователя", false)); + assertTrue(repository.registerCustomer("اسم المستخدم", false)); + + assertFalse(repository.isBlacklisted("用户名")); + assertFalse(repository.isBlacklisted("Имя пользователя")); + assertFalse(repository.isBlacklisted("اسم المستخدم")); + } + + @Test + @Order(17) + @DisplayName("Should handle customer registration with leading/trailing spaces") + void testCustomerNamesWithSpaces() { + // The repository trims spaces in validation, so these should work + assertTrue(repository.registerCustomer("User With Spaces", false)); + assertFalse(repository.isBlacklisted("User With Spaces")); + } + + @Test + @Order(18) + @DisplayName("Should handle alternating blacklist registrations") + void testAlternatingBlacklistRegistrations() { + for (int i = 0; i < 20; i++) { + boolean isBlacklisted = (i % 3 == 0); + repository.registerCustomer("AlternateUser" + i, isBlacklisted); + } + + assertTrue(repository.isBlacklisted("AlternateUser0")); + assertFalse(repository.isBlacklisted("AlternateUser1")); + assertFalse(repository.isBlacklisted("AlternateUser2")); + assertTrue(repository.isBlacklisted("AlternateUser3")); + assertFalse(repository.isBlacklisted("AlternateUser4")); + } + + @Test + @Order(19) + @DisplayName("Should handle registration attempt after failed registration") + void testRegistrationAfterFailedAttempt() { + repository.registerCustomer("TestUser", false); + + // First duplicate attempt fails + assertFalse(repository.registerCustomer("TestUser", true)); + + // Second duplicate attempt also fails + assertFalse(repository.registerCustomer("TestUser", false)); + + // Original registration should still be valid + assertFalse(repository.isBlacklisted("TestUser")); + } + + @Test + @Order(20) + @DisplayName("Should correctly identify non-blacklisted status") + void testNonBlacklistedStatus() { + repository.registerCustomer("Good Customer", false); + + boolean isBlacklisted = repository.isBlacklisted("Good Customer"); + + assertFalse(isBlacklisted, "Good customer should not be blacklisted"); + } +} diff --git a/src/test/java/com/example/repository/impl/LoanHistoryRepositoryImplTest.java b/src/test/java/com/example/repository/impl/LoanHistoryRepositoryImplTest.java new file mode 100644 index 0000000..b0dfa6e --- /dev/null +++ b/src/test/java/com/example/repository/impl/LoanHistoryRepositoryImplTest.java @@ -0,0 +1,410 @@ +package com.example.repository.impl; + +import com.example.util.DatabaseManager; +import org.junit.jupiter.api.*; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for LoanHistoryRepositoryImpl + */ +@DisplayName("LoanHistoryRepositoryImpl Tests") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class LoanHistoryRepositoryImplTest { + + private LoanHistoryRepositoryImpl repository; + private DatabaseManager dbManager; + + @BeforeEach + void setUp() { + dbManager = DatabaseManager.getInstance(); + dbManager.resetDatabase(); + repository = new LoanHistoryRepositoryImpl(); + } + + @Test + @Order(1) + @DisplayName("Should save approved loan history") + void testSaveApprovedLoanHistory() throws SQLException { + repository.saveHistory("John Doe", 50000, true, 5.5, null); + + // Verify data was saved + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT * FROM loan_history WHERE applicant_name = 'John Doe'")) { + + assertTrue(rs.next()); + assertEquals("John Doe", rs.getString("applicant_name")); + assertEquals(50000, rs.getInt("requested_amount")); + assertEquals(1, rs.getInt("approved")); + assertEquals(5.5, rs.getDouble("approved_rate"), 0.001); + assertNull(rs.getString("rejection_reason")); + assertNotNull(rs.getString("processed_at")); + } + } + + @Test + @Order(2) + @DisplayName("Should save rejected loan history") + void testSaveRejectedLoanHistory() throws SQLException { + repository.saveHistory("Jane Smith", 100000, false, 0.0, "Credit score too low"); + + // Verify data was saved + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT * FROM loan_history WHERE applicant_name = 'Jane Smith'")) { + + assertTrue(rs.next()); + assertEquals("Jane Smith", rs.getString("applicant_name")); + assertEquals(100000, rs.getInt("requested_amount")); + assertEquals(0, rs.getInt("approved")); + assertEquals(0.0, rs.getDouble("approved_rate"), 0.001); + assertEquals("Credit score too low", rs.getString("rejection_reason")); + assertNotNull(rs.getString("processed_at")); + } + } + + @Test + @Order(3) + @DisplayName("Should save multiple loan histories") + void testSaveMultipleLoanHistories() throws SQLException { + repository.saveHistory("User1", 30000, true, 4.5, null); + repository.saveHistory("User2", 60000, false, 0.0, "Insufficient income"); + repository.saveHistory("User3", 45000, true, 6.0, null); + + // Verify count + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM loan_history")) { + + assertTrue(rs.next()); + assertEquals(3, rs.getInt("count")); + } + } + + @Test + @Order(4) + @DisplayName("Should save loan history with zero amount") + void testSaveLoanHistoryWithZeroAmount() throws SQLException { + repository.saveHistory("Zero User", 0, false, 0.0, "Invalid amount"); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT * FROM loan_history WHERE applicant_name = 'Zero User'")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("requested_amount")); + assertFalse(rs.getBoolean("approved")); + } + } + + @Test + @Order(5) + @DisplayName("Should save loan history with very large amount") + void testSaveLoanHistoryWithLargeAmount() throws SQLException { + repository.saveHistory("Rich User", 10000000, true, 3.5, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT * FROM loan_history WHERE applicant_name = 'Rich User'")) { + + assertTrue(rs.next()); + assertEquals(10000000, rs.getInt("requested_amount")); + assertEquals(3.5, rs.getDouble("approved_rate"), 0.001); + } + } + + @Test + @Order(6) + @DisplayName("Should save loan history with special characters in name") + void testSaveLoanHistoryWithSpecialCharacters() throws SQLException { + repository.saveHistory("O'Brien", 50000, true, 5.0, null); + repository.saveHistory("Jean-Claude", 60000, false, 0.0, "Reason: Bad history"); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) as count FROM loan_history WHERE applicant_name LIKE '%-%' OR applicant_name LIKE '%''%'")) { + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("count")); + } + } + + @Test + @Order(7) + @DisplayName("Should save loan history with different approval rates") + void testSaveLoanHistoryWithDifferentRates() throws SQLException { + double[] rates = {3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 7.0, 8.0}; + + for (int i = 0; i < rates.length; i++) { + repository.saveHistory("User" + i, 50000, true, rates[i], null); + } + + // Verify all rates were saved correctly + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT approved_rate FROM loan_history WHERE approved = 1 ORDER BY approved_rate")) { + + for (double rate : rates) { + assertTrue(rs.next()); + assertEquals(rate, rs.getDouble("approved_rate"), 0.001); + } + } + } + + @Test + @Order(8) + @DisplayName("Should save loan history with long rejection reason") + void testSaveLoanHistoryWithLongRejectionReason() throws SQLException { + StringBuilder longReason = new StringBuilder(); + for (int i = 0; i < 100; i++) { + longReason.append("Reason part ").append(i).append(". "); + } + + repository.saveHistory("Test User", 50000, false, 0.0, longReason.toString()); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT rejection_reason FROM loan_history WHERE applicant_name = 'Test User'")) { + + assertTrue(rs.next()); + String savedReason = rs.getString("rejection_reason"); + assertNotNull(savedReason); + assertTrue(savedReason.length() > 1000); + } + } + + @Test + @Order(9) + @DisplayName("Should save multiple histories for same applicant") + void testSaveMultipleHistoriesForSameApplicant() throws SQLException { + repository.saveHistory("Repeat User", 30000, false, 0.0, "First rejection"); + repository.saveHistory("Repeat User", 40000, false, 0.0, "Second rejection"); + repository.saveHistory("Repeat User", 50000, true, 5.5, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(*) as count FROM loan_history WHERE applicant_name = 'Repeat User'")) { + + assertTrue(rs.next()); + assertEquals(3, rs.getInt("count")); + } + } + + @Test + @Order(10) + @DisplayName("Should save loan history with null rejection reason for approved loan") + void testSaveLoanHistoryWithNullRejectionReason() throws SQLException { + repository.saveHistory("Approved User", 50000, true, 5.5, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT rejection_reason FROM loan_history WHERE applicant_name = 'Approved User'")) { + + assertTrue(rs.next()); + assertNull(rs.getString("rejection_reason")); + } + } + + @Test + @Order(11) + @DisplayName("Should handle rapid successive history saves") + void testRapidSuccessiveHistorySaves() throws SQLException { + for (int i = 0; i < 100; i++) { + repository.saveHistory("Rapid" + i, 50000, i % 2 == 0, 5.5, i % 2 == 0 ? null : "Rejected"); + } + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM loan_history")) { + + assertTrue(rs.next()); + assertEquals(100, rs.getInt("count")); + } + } + + @Test + @Order(12) + @DisplayName("Should save loan history with unicode characters in name") + void testSaveLoanHistoryWithUnicodeCharacters() throws SQLException { + repository.saveHistory("用户", 50000, true, 5.0, null); + repository.saveHistory("Пользователь", 60000, false, 0.0, "Отклонено"); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM loan_history")) { + + assertTrue(rs.next()); + assertEquals(2, rs.getInt("count")); + } + } + + @Test + @Order(13) + @DisplayName("Should save loan history with decimal approval rates") + void testSaveLoanHistoryWithDecimalRates() throws SQLException { + repository.saveHistory("Decimal User", 50000, true, 5.125, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT approved_rate FROM loan_history WHERE applicant_name = 'Decimal User'")) { + + assertTrue(rs.next()); + assertEquals(5.125, rs.getDouble("approved_rate"), 0.0001); + } + } + + @Test + @Order(14) + @DisplayName("Should save loan history with empty rejection reason") + void testSaveLoanHistoryWithEmptyRejectionReason() throws SQLException { + repository.saveHistory("Empty Reason", 50000, false, 0.0, ""); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT rejection_reason FROM loan_history WHERE applicant_name = 'Empty Reason'")) { + + assertTrue(rs.next()); + assertEquals("", rs.getString("rejection_reason")); + } + } + + @Test + @Order(15) + @DisplayName("Should auto-generate ID for loan history records") + void testAutoGenerateId() throws SQLException { + repository.saveHistory("User1", 50000, true, 5.5, null); + repository.saveHistory("User2", 60000, true, 6.0, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT id FROM loan_history ORDER BY id")) { + + assertTrue(rs.next()); + int id1 = rs.getInt("id"); + assertTrue(rs.next()); + int id2 = rs.getInt("id"); + + assertTrue(id2 > id1, "IDs should be auto-incrementing"); + } + } + + @Test + @Order(16) + @DisplayName("Should save processed_at timestamp") + void testSaveProcessedAtTimestamp() throws SQLException { + repository.saveHistory("Timestamp User", 50000, true, 5.5, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT processed_at FROM loan_history WHERE applicant_name = 'Timestamp User'")) { + + assertTrue(rs.next()); + String processedAt = rs.getString("processed_at"); + assertNotNull(processedAt); + // Verify format: yyyy-MM-dd HH:mm:ss + assertTrue(processedAt.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")); + } + } + + @Test + @Order(17) + @DisplayName("Should handle different rejection reasons") + void testDifferentRejectionReasons() throws SQLException { + String[] reasons = { + "Credit score too low", + "Insufficient income", + "Poor credit history", + "High debt-to-income ratio", + "Blacklisted customer", + "Invalid application" + }; + + for (int i = 0; i < reasons.length; i++) { + repository.saveHistory("RejectedUser" + i, 50000, false, 0.0, reasons[i]); + } + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT COUNT(DISTINCT rejection_reason) as count FROM loan_history WHERE approved = 0")) { + + assertTrue(rs.next()); + assertEquals(reasons.length, rs.getInt("count")); + } + } + + @Test + @Order(18) + @DisplayName("Should save loan history with negative amount") + void testSaveLoanHistoryWithNegativeAmount() throws SQLException { + repository.saveHistory("Negative User", -1000, false, 0.0, "Invalid amount"); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT requested_amount FROM loan_history WHERE applicant_name = 'Negative User'")) { + + assertTrue(rs.next()); + assertEquals(-1000, rs.getInt("requested_amount")); + } + } + + @Test + @Order(19) + @DisplayName("Should maintain history order by processed_at") + void testHistoryOrder() throws SQLException { + // Save multiple histories with slight delay + repository.saveHistory("First", 50000, true, 5.5, null); + try { Thread.sleep(10); } catch (InterruptedException e) { } + repository.saveHistory("Second", 60000, true, 6.0, null); + try { Thread.sleep(10); } catch (InterruptedException e) { } + repository.saveHistory("Third", 70000, true, 7.0, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT applicant_name FROM loan_history ORDER BY processed_at")) { + + assertTrue(rs.next()); + assertEquals("First", rs.getString("applicant_name")); + assertTrue(rs.next()); + assertEquals("Second", rs.getString("applicant_name")); + assertTrue(rs.next()); + assertEquals("Third", rs.getString("applicant_name")); + } + } + + @Test + @Order(20) + @DisplayName("Should save loan history with maximum double value for rate") + void testSaveLoanHistoryWithMaxRate() throws SQLException { + repository.saveHistory("Max Rate User", 50000, true, Double.MAX_VALUE, null); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT approved_rate FROM loan_history WHERE applicant_name = 'Max Rate User'")) { + + assertTrue(rs.next()); + assertEquals(Double.MAX_VALUE, rs.getDouble("approved_rate"), 0.001); + } + } +} diff --git a/src/test/java/com/example/service/LoanApprovalServiceTest.java b/src/test/java/com/example/service/LoanApprovalServiceTest.java new file mode 100644 index 0000000..2186b16 --- /dev/null +++ b/src/test/java/com/example/service/LoanApprovalServiceTest.java @@ -0,0 +1,561 @@ +package com.example.service; + +import com.example.model.CustomerRegistrationRequest; +import com.example.model.LoanRequest; +import com.example.model.LoanResponse; +import com.example.repository.CustomerRepository; +import com.example.repository.LoanHistoryRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import javax.xml.ws.soap.SOAPFaultException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** + * Unit tests for LoanApprovalService + * Tests customer registration and loan approval logic + */ +@DisplayName("LoanApprovalService Tests") +class LoanApprovalServiceTest { + + private LoanApprovalService service; + + @Mock + private CustomerRepository customerRepository; + + @Mock + private LoanHistoryRepository loanHistoryRepository; + + @Mock + private CreditScoreService creditScoreService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new LoanApprovalService(); + service.setCustomerRepository(customerRepository); + service.setLoanHistoryRepository(loanHistoryRepository); + service.setCreditScoreService(creditScoreService); + } + + // ============================================================ + // Customer Registration Tests + // ============================================================ + + @Test + @DisplayName("Should register new customer successfully") + void testRegisterNewCustomer_Success() { + // Arrange + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName("John Doe"); + request.setBlacklisted(false); + + when(customerRepository.registerCustomer("John Doe", false)).thenReturn(true); + + // Act + String result = service.registerNewCustomer(request); + + // Assert + assertEquals("Registration Successful", result); + verify(customerRepository).registerCustomer("John Doe", false); + } + + @Test + @DisplayName("Should register blacklisted customer successfully") + void testRegisterNewCustomer_Blacklisted() { + // Arrange + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName("Bad Actor"); + request.setBlacklisted(true); + + when(customerRepository.registerCustomer("Bad Actor", true)).thenReturn(true); + + // Act + String result = service.registerNewCustomer(request); + + // Assert + assertEquals("Registration Successful", result); + verify(customerRepository).registerCustomer("Bad Actor", true); + } + + @Test + @DisplayName("Should fail registration for duplicate customer") + void testRegisterNewCustomer_Duplicate() { + // Arrange + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName("Existing Customer"); + request.setBlacklisted(false); + + when(customerRepository.registerCustomer("Existing Customer", false)).thenReturn(false); + + // Act + String result = service.registerNewCustomer(request); + + // Assert + assertEquals("Error: Customer already exists", result); + verify(customerRepository).registerCustomer("Existing Customer", false); + } + + @Test + @DisplayName("Should throw SOAP fault for null registration request") + void testRegisterNewCustomer_NullRequest() { + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.registerNewCustomer(null); + }); + + verify(customerRepository, never()).registerCustomer(anyString(), anyBoolean()); + } + + @Test + @DisplayName("Should throw SOAP fault for null customer name") + void testRegisterNewCustomer_NullName() { + // Arrange + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName(null); + request.setBlacklisted(false); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.registerNewCustomer(request); + }); + + verify(customerRepository, never()).registerCustomer(anyString(), anyBoolean()); + } + + @Test + @DisplayName("Should throw SOAP fault for empty customer name") + void testRegisterNewCustomer_EmptyName() { + // Arrange + CustomerRegistrationRequest request = new CustomerRegistrationRequest(); + request.setCustomerName(" "); + request.setBlacklisted(false); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.registerNewCustomer(request); + }); + + verify(customerRepository, never()).registerCustomer(anyString(), anyBoolean()); + } + + // ============================================================ + // Loan Approval Tests - Excellent Credit (>= 700) + // ============================================================ + + @Test + @DisplayName("Should approve loan with 3.5% rate for credit score 750") + void testLoanApproval_ExcellentCredit() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Alice"); + request.setRequestedAmount(50000); + request.setCreditScore(750); + + when(customerRepository.isBlacklisted("Alice")).thenReturn(false); + when(creditScoreService.getCreditScore("Alice")).thenReturn(750); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(3.5, response.getApprovedRate()); + assertNull(response.getRejectionReason()); + assertEquals("Loan approved with excellent rate", response.getMessage()); + + verify(loanHistoryRepository).saveHistory("Alice", 50000, true, 3.5, null); + } + + @Test + @DisplayName("Should approve loan with 3.5% rate for credit score exactly 700") + void testLoanApproval_CreditScore700() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Bob"); + request.setRequestedAmount(30000); + request.setCreditScore(700); + + when(customerRepository.isBlacklisted("Bob")).thenReturn(false); + when(creditScoreService.getCreditScore("Bob")).thenReturn(700); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(3.5, response.getApprovedRate()); + assertEquals("Loan approved with excellent rate", response.getMessage()); + } + + // ============================================================ + // Loan Approval Tests - Good Credit (600-699) + // ============================================================ + + @Test + @DisplayName("Should approve loan with 5.5% rate for credit score 650") + void testLoanApproval_GoodCredit() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Charlie"); + request.setRequestedAmount(25000); + request.setCreditScore(650); + + when(customerRepository.isBlacklisted("Charlie")).thenReturn(false); + when(creditScoreService.getCreditScore("Charlie")).thenReturn(650); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(5.5, response.getApprovedRate()); + assertNull(response.getRejectionReason()); + assertEquals("Loan approved with standard rate", response.getMessage()); + + verify(loanHistoryRepository).saveHistory("Charlie", 25000, true, 5.5, null); + } + + @Test + @DisplayName("Should approve loan with 5.5% rate for credit score exactly 600") + void testLoanApproval_CreditScore600() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Diana"); + request.setRequestedAmount(20000); + request.setCreditScore(600); + + when(customerRepository.isBlacklisted("Diana")).thenReturn(false); + when(creditScoreService.getCreditScore("Diana")).thenReturn(600); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(5.5, response.getApprovedRate()); + assertEquals("Loan approved with standard rate", response.getMessage()); + } + + @Test + @DisplayName("Should approve loan with 5.5% rate for credit score 699") + void testLoanApproval_CreditScore699() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Eve"); + request.setRequestedAmount(15000); + request.setCreditScore(699); + + when(customerRepository.isBlacklisted("Eve")).thenReturn(false); + when(creditScoreService.getCreditScore("Eve")).thenReturn(699); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(5.5, response.getApprovedRate()); + assertEquals("Loan approved with standard rate", response.getMessage()); + } + + // ============================================================ + // Loan Approval Tests - Fair Credit (500-599) + // ============================================================ + + @Test + @DisplayName("Should approve loan with 8.5% rate for credit score 550") + void testLoanApproval_FairCredit() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Frank"); + request.setRequestedAmount(10000); + request.setCreditScore(550); + + when(customerRepository.isBlacklisted("Frank")).thenReturn(false); + when(creditScoreService.getCreditScore("Frank")).thenReturn(550); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(8.5, response.getApprovedRate()); + assertNull(response.getRejectionReason()); + assertEquals("Loan approved with high risk rate", response.getMessage()); + + verify(loanHistoryRepository).saveHistory("Frank", 10000, true, 8.5, null); + } + + @Test + @DisplayName("Should approve loan with 8.5% rate for credit score exactly 500") + void testLoanApproval_CreditScore500() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Grace"); + request.setRequestedAmount(5000); + request.setCreditScore(500); + + when(customerRepository.isBlacklisted("Grace")).thenReturn(false); + when(creditScoreService.getCreditScore("Grace")).thenReturn(500); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(8.5, response.getApprovedRate()); + assertEquals("Loan approved with high risk rate", response.getMessage()); + } + + @Test + @DisplayName("Should approve loan with 8.5% rate for credit score 599") + void testLoanApproval_CreditScore599() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Henry"); + request.setRequestedAmount(8000); + request.setCreditScore(599); + + when(customerRepository.isBlacklisted("Henry")).thenReturn(false); + when(creditScoreService.getCreditScore("Henry")).thenReturn(599); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(8.5, response.getApprovedRate()); + assertEquals("Loan approved with high risk rate", response.getMessage()); + } + + // ============================================================ + // Loan Rejection Tests - Poor Credit (< 500) + // ============================================================ + + @Test + @DisplayName("Should reject loan for credit score below 500") + void testLoanRejection_PoorCredit() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Ivan"); + request.setRequestedAmount(5000); + request.setCreditScore(450); + + when(customerRepository.isBlacklisted("Ivan")).thenReturn(false); + when(creditScoreService.getCreditScore("Ivan")).thenReturn(450); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertFalse(response.isApproved()); + assertEquals(0.0, response.getApprovedRate()); + assertEquals("Credit score too low", response.getRejectionReason()); + assertEquals("Loan application rejected", response.getMessage()); + + verify(loanHistoryRepository).saveHistory("Ivan", 5000, false, 0.0, "Credit score too low"); + } + + @Test + @DisplayName("Should reject loan for credit score 499") + void testLoanRejection_CreditScore499() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Jane"); + request.setRequestedAmount(3000); + request.setCreditScore(499); + + when(customerRepository.isBlacklisted("Jane")).thenReturn(false); + when(creditScoreService.getCreditScore("Jane")).thenReturn(499); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertFalse(response.isApproved()); + assertEquals("Credit score too low", response.getRejectionReason()); + } + + // ============================================================ + // Blacklist Tests + // ============================================================ + + @Test + @DisplayName("Should reject loan for blacklisted applicant") + void testLoanRejection_Blacklisted() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Blacklisted Person"); + request.setRequestedAmount(10000); + request.setCreditScore(750); + + when(customerRepository.isBlacklisted("Blacklisted Person")).thenReturn(true); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertFalse(response.isApproved()); + assertEquals(0.0, response.getApprovedRate()); + assertEquals("Applicant is blacklisted", response.getRejectionReason()); + assertEquals("Loan application rejected", response.getMessage()); + + verify(loanHistoryRepository).saveHistory("Blacklisted Person", 10000, false, 0.0, "Applicant is blacklisted"); + verify(creditScoreService, never()).getCreditScore(anyString()); + } + + // ============================================================ + // Input Validation Tests + // ============================================================ + + @Test + @DisplayName("Should throw SOAP fault for null loan request") + void testLoanApplication_NullRequest() { + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.processLoanApplication(null); + }); + + verify(customerRepository, never()).isBlacklisted(anyString()); + verify(loanHistoryRepository, never()).saveHistory(anyString(), anyInt(), anyBoolean(), anyDouble(), anyString()); + } + + @Test + @DisplayName("Should throw SOAP fault for null applicant name") + void testLoanApplication_NullApplicantName() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName(null); + request.setRequestedAmount(10000); + request.setCreditScore(700); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.processLoanApplication(request); + }); + + verify(customerRepository, never()).isBlacklisted(anyString()); + } + + @Test + @DisplayName("Should throw SOAP fault for empty applicant name") + void testLoanApplication_EmptyApplicantName() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName(" "); + request.setRequestedAmount(10000); + request.setCreditScore(700); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.processLoanApplication(request); + }); + + verify(customerRepository, never()).isBlacklisted(anyString()); + } + + @Test + @DisplayName("Should throw SOAP fault for zero loan amount") + void testLoanApplication_ZeroAmount() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Test User"); + request.setRequestedAmount(0); + request.setCreditScore(700); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.processLoanApplication(request); + }); + + verify(customerRepository, never()).isBlacklisted(anyString()); + } + + @Test + @DisplayName("Should throw SOAP fault for negative loan amount") + void testLoanApplication_NegativeAmount() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Test User"); + request.setRequestedAmount(-1000); + request.setCreditScore(700); + + // Act & Assert + assertThrows(SOAPFaultException.class, () -> { + service.processLoanApplication(request); + }); + + verify(customerRepository, never()).isBlacklisted(anyString()); + } + + // ============================================================ + // Edge Case Tests + // ============================================================ + + @Test + @DisplayName("Should handle very large loan amount") + void testLoanApplication_LargeLoanAmount() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Rich Person"); + request.setRequestedAmount(Integer.MAX_VALUE); + request.setCreditScore(800); + + when(customerRepository.isBlacklisted("Rich Person")).thenReturn(false); + when(creditScoreService.getCreditScore("Rich Person")).thenReturn(800); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(3.5, response.getApprovedRate()); + + verify(loanHistoryRepository).saveHistory("Rich Person", Integer.MAX_VALUE, true, 3.5, null); + } + + @Test + @DisplayName("Should handle applicant names with special characters") + void testLoanApplication_SpecialCharactersInName() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("O'Brien-Smith"); + request.setRequestedAmount(20000); + request.setCreditScore(720); + + when(customerRepository.isBlacklisted("O'Brien-Smith")).thenReturn(false); + when(creditScoreService.getCreditScore("O'Brien-Smith")).thenReturn(720); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + assertEquals(3.5, response.getApprovedRate()); + } + + @Test + @DisplayName("Should handle minimum valid loan amount") + void testLoanApplication_MinimumAmount() { + // Arrange + LoanRequest request = new LoanRequest(); + request.setApplicantName("Small Borrower"); + request.setRequestedAmount(1); + request.setCreditScore(700); + + when(customerRepository.isBlacklisted("Small Borrower")).thenReturn(false); + when(creditScoreService.getCreditScore("Small Borrower")).thenReturn(700); + + // Act + LoanResponse response = service.processLoanApplication(request); + + // Assert + assertTrue(response.isApproved()); + verify(loanHistoryRepository).saveHistory("Small Borrower", 1, true, 3.5, null); + } +} diff --git a/src/test/java/com/example/util/DatabaseManagerTest.java b/src/test/java/com/example/util/DatabaseManagerTest.java new file mode 100644 index 0000000..6ee8bff --- /dev/null +++ b/src/test/java/com/example/util/DatabaseManagerTest.java @@ -0,0 +1,315 @@ +package com.example.util; + +import org.junit.jupiter.api.*; + +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for DatabaseManager utility class + */ +@DisplayName("DatabaseManager Tests") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class DatabaseManagerTest { + + private DatabaseManager dbManager; + + @BeforeEach + void setUp() { + dbManager = DatabaseManager.getInstance(); + // Reset database before each test + dbManager.resetDatabase(); + } + + @Test + @Order(1) + @DisplayName("Should return singleton instance") + void testGetInstance() { + DatabaseManager instance1 = DatabaseManager.getInstance(); + DatabaseManager instance2 = DatabaseManager.getInstance(); + + assertNotNull(instance1); + assertNotNull(instance2); + assertSame(instance1, instance2, "Should return same instance"); + } + + @Test + @Order(2) + @DisplayName("Should get database connection") + void testGetConnection() throws SQLException { + Connection conn = dbManager.getConnection(); + + assertNotNull(conn); + assertFalse(conn.isClosed()); + + conn.close(); + assertTrue(conn.isClosed()); + } + + @Test + @Order(3) + @DisplayName("Should initialize database schema") + void testInitializeSchema() throws SQLException { + dbManager.initializeSchema(); + + // Verify customers table exists + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='customers'")) { + + assertTrue(rs.next(), "Customers table should exist"); + } + + // Verify loan_history table exists + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery( + "SELECT name FROM sqlite_master WHERE type='table' AND name='loan_history'")) { + + assertTrue(rs.next(), "Loan history table should exist"); + } + } + + @Test + @Order(4) + @DisplayName("Should create customers table with correct schema") + void testCustomersTableSchema() throws SQLException { + dbManager.initializeSchema(); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + // Insert test data + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Test User', 0, '2025-01-01 10:00:00')"); + + // Query test data + try (ResultSet rs = stmt.executeQuery("SELECT * FROM customers WHERE customer_name = 'Test User'")) { + assertTrue(rs.next()); + assertEquals("Test User", rs.getString("customer_name")); + assertEquals(0, rs.getInt("is_blacklisted")); + assertEquals("2025-01-01 10:00:00", rs.getString("registered_at")); + } + } + } + + @Test + @Order(5) + @DisplayName("Should create loan_history table with correct schema") + void testLoanHistoryTableSchema() throws SQLException { + dbManager.initializeSchema(); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + // Insert test data + stmt.execute("INSERT INTO loan_history " + + "(applicant_name, requested_amount, approved, approved_rate, rejection_reason, processed_at) " + + "VALUES ('Test Applicant', 50000, 1, 5.5, NULL, '2025-01-01 10:00:00')"); + + // Query test data + try (ResultSet rs = stmt.executeQuery("SELECT * FROM loan_history WHERE applicant_name = 'Test Applicant'")) { + assertTrue(rs.next()); + assertEquals("Test Applicant", rs.getString("applicant_name")); + assertEquals(50000, rs.getInt("requested_amount")); + assertEquals(1, rs.getInt("approved")); + assertEquals(5.5, rs.getDouble("approved_rate"), 0.001); + assertNull(rs.getString("rejection_reason")); + } + } + } + + @Test + @Order(6) + @DisplayName("Should reset database successfully") + void testResetDatabase() throws SQLException { + // First, initialize and add some data + dbManager.initializeSchema(); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('User1', 0, '2025-01-01 10:00:00')"); + + // Verify data exists + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers")) { + assertTrue(rs.next()); + assertEquals(1, rs.getInt("count")); + } + } + + // Reset database + dbManager.resetDatabase(); + + // Verify tables are empty + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("count"), "Customers table should be empty after reset"); + } + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM loan_history")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("count"), "Loan history table should be empty after reset"); + } + } + + @Test + @Order(7) + @DisplayName("Should test database connection successfully") + void testTestConnection() { + boolean result = dbManager.testConnection(); + + assertTrue(result, "Test connection should return true"); + } + + @Test + @Order(8) + @DisplayName("Should handle multiple connections") + void testMultipleConnections() throws SQLException { + Connection conn1 = dbManager.getConnection(); + Connection conn2 = dbManager.getConnection(); + Connection conn3 = dbManager.getConnection(); + + assertNotNull(conn1); + assertNotNull(conn2); + assertNotNull(conn3); + + assertNotSame(conn1, conn2, "Should create different connection objects"); + assertNotSame(conn2, conn3, "Should create different connection objects"); + + conn1.close(); + conn2.close(); + conn3.close(); + } + + @Test + @Order(9) + @DisplayName("Should close connection safely with non-null connection") + void testCloseConnectionWithValidConnection() throws SQLException { + Connection conn = dbManager.getConnection(); + assertFalse(conn.isClosed()); + + DatabaseManager.closeConnection(conn); + + assertTrue(conn.isClosed()); + } + + @Test + @Order(10) + @DisplayName("Should handle closing null connection gracefully") + void testCloseConnectionWithNullConnection() { + // Should not throw exception + assertDoesNotThrow(() -> DatabaseManager.closeConnection(null)); + } + + @Test + @Order(11) + @DisplayName("Should handle closing already closed connection") + void testCloseConnectionWithAlreadyClosedConnection() throws SQLException { + Connection conn = dbManager.getConnection(); + conn.close(); + + // Should not throw exception + assertDoesNotThrow(() -> DatabaseManager.closeConnection(conn)); + } + + @Test + @Order(12) + @DisplayName("Should handle schema initialization multiple times") + void testInitializeSchemMultipleTimes() { + // Initialize multiple times - should not fail + assertDoesNotThrow(() -> { + dbManager.initializeSchema(); + dbManager.initializeSchema(); + dbManager.initializeSchema(); + }); + } + + @Test + @Order(13) + @DisplayName("Should maintain data consistency across connections") + void testDataConsistencyAcrossConnections() throws SQLException { + dbManager.initializeSchema(); + + // Insert data with one connection + try (Connection conn1 = dbManager.getConnection(); + Statement stmt = conn1.createStatement()) { + + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Consistency Test', 1, '2025-01-01 10:00:00')"); + } + + // Read data with different connection + try (Connection conn2 = dbManager.getConnection(); + Statement stmt = conn2.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT * FROM customers WHERE customer_name = 'Consistency Test'")) { + + assertTrue(rs.next()); + assertEquals("Consistency Test", rs.getString("customer_name")); + assertEquals(1, rs.getInt("is_blacklisted")); + } + } + + @Test + @Order(14) + @DisplayName("Should support transactions") + void testTransactionSupport() throws SQLException { + dbManager.initializeSchema(); + + try (Connection conn = dbManager.getConnection()) { + conn.setAutoCommit(false); + + try (Statement stmt = conn.createStatement()) { + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Transaction Test', 0, '2025-01-01 10:00:00')"); + + // Rollback + conn.rollback(); + } + } + + // Verify data was not committed + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement(); + ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers WHERE customer_name = 'Transaction Test'")) { + + assertTrue(rs.next()); + assertEquals(0, rs.getInt("count"), "Data should not exist after rollback"); + } + } + + @Test + @Order(15) + @DisplayName("Should handle large data insertions") + void testLargeDataInsertion() throws SQLException { + dbManager.initializeSchema(); + + try (Connection conn = dbManager.getConnection(); + Statement stmt = conn.createStatement()) { + + // Insert 100 customers + for (int i = 0; i < 100; i++) { + stmt.execute("INSERT INTO customers (customer_name, is_blacklisted, registered_at) " + + "VALUES ('Customer" + i + "', " + (i % 2) + ", '2025-01-01 10:00:00')"); + } + + // Verify count + try (ResultSet rs = stmt.executeQuery("SELECT COUNT(*) as count FROM customers")) { + assertTrue(rs.next()); + assertEquals(100, rs.getInt("count")); + } + } + } +}