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"));
+ }
+ }
+ }
+}