diff --git a/Online-Banking-App-Spring-Boot/pom.xml b/Online-Banking-App-Spring-Boot/pom.xml index 2f14ebe..fdd1762 100644 --- a/Online-Banking-App-Spring-Boot/pom.xml +++ b/Online-Banking-App-Spring-Boot/pom.xml @@ -1,114 +1,171 @@ - - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 2.7.15 - - - com.beko - DemoBank_v1 - 0.0.1-SNAPSHOT - DemoBank_v1 - Demo project for Spring Boot - - 1.8 - - - - org.springframework.boot - spring-boot-starter-data-jpa - - - org.springframework.boot - spring-boot-starter-mail - - - org.springframework.boot - spring-boot-starter-web - - - - com.mysql - mysql-connector-j - runtime - - - - org.springframework.boot - spring-boot-starter-tomcat - provided - - - org.springframework.boot - spring-boot-starter-test - test - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - org.apache.tomcat.embed - tomcat-embed-jasper - provided - - - - org.springframework.boot - spring-boot-starter-validation - - - - javax.servlet.jsp - javax.servlet.jsp-api - 2.3.3 - provided - - - - javax.servlet - jstl - 1.2 - - - - org.springframework.security - spring-security-crypto - - - - io.jsonwebtoken - jjwt-api - 0.11.5 - - - - io.jsonwebtoken - jjwt-impl - 0.11.2 - runtime - - - - io.jsonwebtoken - jjwt-jackson - 0.11.2 - runtime - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - - + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.15 + + + + com.beko + DemoBank_v1 + 0.0.1-SNAPSHOT + DemoBank_v1 + Demo project for Spring Boot + + 1.8 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-web + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + javax.servlet + javax.servlet-api + 3.1.0 + + + org.apache.tomcat.embed + tomcat-embed-jasper + provided + + + org.springframework.boot + spring-boot-starter-validation + + + javax.servlet.jsp + javax.servlet.jsp-api + 2.3.3 + provided + + + javax.servlet + jstl + 1.2 + + + org.springframework.security + spring-security-crypto + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.2 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.2 + runtime + + + org.mockito + mockito-junit-jupiter + 2.23.4 + test + + + + io.spring.javaformat + spring-javaformat-formatter + 0.0.40 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + + + prepare-agent + + + + report + test + + report + + + coverageReport + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 3.2.5 + + testReport + + + + + org.apache.maven.plugins + maven-site-plugin + 2.1 + + testReport + + + + + io.spring.javaformat + spring-javaformat-maven-plugin + 0.0.40 + + + + + + + + \ No newline at end of file diff --git a/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer703Test.java.invalid b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer703Test.java.invalid new file mode 100644 index 0000000..8b4a4ea --- /dev/null +++ b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer703Test.java.invalid @@ -0,0 +1,974 @@ +//This test file is marked invalid as it contains compilation errors. Change the extension to of this file to .java, to manually edit its contents + + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test unit-java-springboot using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=transfer_05410c1574 +ROOST_METHOD_SIG_HASH=transfer_d754a3fac1 + +Scenario 1: Successful withdrawal with sufficient funds + +Details: + TestName: successfulWithdrawalReducesBalanceAndReturnsOk + Description: Verifies that when both withdrawal_amount and account_id are valid, and the account has sufficient funds, the method reduces the balance, logs a successful transaction, fetches updated accounts, and returns an HTTP 200 OK with the expected message and payload. + +Execution: + Arrange: Set up a TransactController with stubbed AccountRepository and TransactRepository. Prepare an HttpSession containing a valid User whose user_id is parseable as an integer. Configure accountRepository.getAccountBalance(user_id, account_id) to return a current balance greater than the withdrawal amount. Prepare requestMap with withdrawal_amount (e.g., "100.00") and account_id (e.g., "10"). + Act: Invoke transfer(requestMap, session). + Assert: Use JUnit assertions to check response.getStatusCode() is HttpStatus.OK, response.getBody() is a Map containing "message" = "Withdrawal Successfull!" and "accounts" as returned by accountRepository.getUserAccountsById(user_id). Assert that accountRepository.changeAccountsBalanceById(newBalance, account_id) was called with newBalance = currentBalance - withdrawal_amount, and that transactRepository.logTransaction(account_id, "Withdrawal", withdrawal_amount, "online", "success", "Withdrawal Transaction Successfull", currentDateTime) was invoked exactly once. + +Validation: + Confirms the happy path: the method correctly computes the new balance, applies the update, logs success with the exact case-sensitive type "Withdrawal", and returns a properly structured 200 OK response. This ensures core withdrawal functionality works as intended. + + +Scenario 2: Withdrawal equal to current balance reduces balance to zero and succeeds + +Details: + TestName: withdrawalEqualToBalanceSucceedsAndResultsInZeroBalance + Description: Ensures that when the withdrawal amount equals the current balance, the method permits the withdrawal (since the check is strictly less-than) and results in a zero balance with a successful response. + +Execution: + Arrange: Stub getAccountBalance to return 100.00 and use withdrawal_amount "100.00". Set session user with a valid numeric user_id and a valid account_id in the request. + Act: Call transfer(requestMap, session). + Assert: Assert status is HttpStatus.OK and response body contains "Withdrawal Successfull!" and updated "accounts". Assert changeAccountsBalanceById was called with newBalance = 0.0, and success logTransaction was called with type "Withdrawal" and status "success". + +Validation: + Verifies boundary behavior at equality. Confirms that the strict less-than comparison allows withdrawal equal to balance and ensures correct end state. + + +Scenario 3: Insufficient funds returns Bad Request and logs failure + +Details: + TestName: insufficientFundsReturnsBadRequestAndLogsFailure + Description: Validates that when the requested withdrawal exceeds the current balance, the method does not update the account balance, logs a failed transaction with "withdrawal" (lowercase), and returns HTTP 400 with the appropriate message. + +Execution: + Arrange: Stub getAccountBalance to return a value smaller than withdrawal_amount, e.g., currentBalance = 50.00 and withdrawal_amount = "75.00". Set up session with a valid User and numeric user_id. Provide a valid account_id. + Act: Invoke transfer(requestMap, session). + Assert: Assert status is HttpStatus.BAD_REQUEST and body equals "You have insufficient Funds to perform this transfer.". Assert that accountRepository.changeAccountsBalanceById was not called. Assert that transactRepository.logTransaction(account_id, "withdrawal", withdrawal_amount, "online", "failed", "Insufficient funds.", currentDateTime) was called once. + +Validation: + Confirms correct handling of insufficient funds, including not mutating state, logging a failed transaction with exact values, and returning the expected error response. + + +Scenario 4: Zero withdrawal amount returns Bad Request + +Details: + TestName: zeroAmountReturnsBadRequestWithProperMessage + Description: Ensures that a zero ("0" or "0.0") withdrawal amount triggers a validation failure with HTTP 400 and the precise error message. + +Execution: + Arrange: Build a requestMap with withdrawal_amount = "0" (or "0.0") and a non-empty valid account_id. Provide a session with a valid User only if needed (the method returns before hitting balance). + Act: Call transfer(requestMap, session). + Assert: Assert status is HttpStatus.BAD_REQUEST and body equals "Withdrawal amount cannot be 0 value.". + +Validation: + Verifies numeric boundary validation and that the method fails fast before any repository interaction. + + +Scenario 5: Empty strings for withdrawal amount and account id return Bad Request + +Details: + TestName: emptyFieldsReturnBadRequestWithExactMessage + Description: Checks that when either withdrawal_amount or account_id is an empty string, the method returns HTTP 400 with the exact validation message. + +Execution: + Arrange: Prepare requestMap with withdrawal_amount = "" and account_id = "" (also validate with one empty and the other non-empty). A session is not required as the method returns before needing it. + Act: Invoke transfer(requestMap, session). + Assert: Assert status is HttpStatus.BAD_REQUEST and body equals "Account withdrawing from and withdrawal amount cannot be empty!". + +Validation: + Confirms defensive input validation for empty strings and ensures no downstream calls occur. + + +Scenario 6: Missing withdrawal_amount key causes NullPointerException + +Details: + TestName: missingWithdrawalAmountKeyCausesNullPointerException + Description: Validates behavior when the requestMap does not contain the key "withdrawal_amount", leading to a NullPointerException due to calling isEmpty() on null. + +Execution: + Arrange: Create requestMap without the "withdrawal_amount" key but include a valid "account_id". Provide any session if needed (not reached). + Act: Invoke transfer(requestMap, session) and capture exceptions. + Assert: Assert that a NullPointerException is thrown. + +Validation: + Documents current method behavior for missing keys. Highlights the absence of null checks on request map values. + + +Scenario 7: Missing account_id key causes NullPointerException + +Details: + TestName: missingAccountIdKeyCausesNullPointerException + Description: Validates behavior when the requestMap does not contain the key "account_id", resulting in a NullPointerException on isEmpty(). + +Execution: + Arrange: Create requestMap with "withdrawal_amount" but without "account_id". + Act: Invoke transfer(requestMap, session). + Assert: Assert that a NullPointerException is thrown. + +Validation: + Further confirms missing key handling is not guarded and results in a runtime exception. + + +Scenario 8: Null requestMap causes NullPointerException + +Details: + TestName: nullRequestMapCausesNullPointerException + Description: Ensures the method throws a NullPointerException when requestMap is null and any attempt to call get() is made. + +Execution: + Arrange: Set requestMap to null and prepare a valid session (not used). + Act: Invoke transfer(null, session) and capture exceptions. + Assert: Assert a NullPointerException is thrown. + +Validation: + Captures behavior with null inputs, emphasizing the need for defensive checks in future improvements. + + +Scenario 9: Non-numeric account_id results in NumberFormatException + +Details: + TestName: nonNumericAccountIdThrowsNumberFormatException + Description: Validates that when account_id cannot be parsed as an integer, the method throws a NumberFormatException. + +Execution: + Arrange: requestMap with withdrawal_amount = "50.00" and account_id = "ABC". + Act: Call transfer(requestMap, session). + Assert: Assert that a NumberFormatException is thrown. + +Validation: + Ensures numeric parsing assumptions are enforced and invalid numeric strings are rejected. + + +Scenario 10: Non-numeric withdrawal_amount results in NumberFormatException + +Details: + TestName: nonNumericWithdrawalAmountThrowsNumberFormatException + Description: Validates that when withdrawal_amount cannot be parsed as a double, the method throws a NumberFormatException. + +Execution: + Arrange: requestMap with withdrawal_amount = "FIFTY" and a valid numeric account_id. + Act: Call transfer(requestMap, session). + Assert: Assert that a NumberFormatException is thrown. + +Validation: + Confirms numeric parsing behavior for withdrawal_amount. + + +Scenario 11: Whitespace-only values trigger NumberFormatException + +Details: + TestName: whitespaceOnlyValuesCauseNumberFormatException + Description: Ensures that inputs with only whitespace (e.g., " ") are not considered empty by isEmpty(), leading to NumberFormatException on parsing. + +Execution: + Arrange: requestMap with withdrawal_amount = " " and account_id = " ". + Act: Call transfer(requestMap, session). + Assert: Assert that a NumberFormatException is thrown. + +Validation: + Documents nuanced validation behavior: empty string check is insufficient for whitespace inputs. + + +Scenario 12: Null session causes NullPointerException + +Details: + TestName: nullSessionCausesNullPointerException + Description: Verifies that passing a null HttpSession leads to a NullPointerException when attempting to access session.getAttribute("user"). + +Execution: + Arrange: Prepare a valid requestMap with parseable values. Set session to null. + Act: Invoke transfer(requestMap, null). + Assert: Assert that a NullPointerException is thrown. + +Validation: + Confirms the method requires a non-null HttpSession and does not guard against null. + + +Scenario 13: Session without user attribute causes NullPointerException + +Details: + TestName: missingUserInSessionCausesNullPointerException + Description: Validates behavior when session.getAttribute("user") returns null, leading to a NullPointerException at user.getUser_id(). + +Execution: + Arrange: Use an HttpSession stub that returns null for "user". Provide a valid requestMap with parseable values. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a NullPointerException is thrown. + +Validation: + Ensures the method depends on a valid User object in session. + + +Scenario 14: Non-numeric user_id on session user causes NumberFormatException + +Details: + TestName: nonNumericUserIdOnUserCausesNumberFormatException + Description: Ensures that if user.getUser_id() returns a non-numeric string, parsing to int fails with NumberFormatException. + +Execution: + Arrange: Session contains a User whose getUser_id() returns "user-xyz". requestMap has valid numeric strings. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a NumberFormatException is thrown. + +Validation: + Confirms that the controller expects numeric user identifiers. + + +Scenario 15: Negative withdrawal amount increases balance and returns success (logical defect exposure) + +Details: + TestName: negativeWithdrawalAmountIncreasesBalanceAndReturnsOk + Description: Demonstrates that a negative withdrawal amount passes validation (since only zero is blocked), causing newBalance = currentBalance - (-amount) and therefore increasing the balance. The method returns success, logs a "Withdrawal" success, and updates the account. + +Execution: + Arrange: Set currentBalance = 500.00 and withdrawal_amount = "-100.00". Provide a valid User and account_id. Ensure getAccountBalance returns 500.00. + Act: Call transfer(requestMap, session). + Assert: Assert status is HttpStatus.OK, message is "Withdrawal Successfull!", and changeAccountsBalanceById was called with 600.00. Assert that transactRepository logged a success with type "Withdrawal". + +Validation: + Exposes a potential business logic bug (negative withdrawals treated as deposits). This test is important to highlight the need for validating positive amounts. + + +Scenario 16: Very small fractional withdrawal amount succeeds + +Details: + TestName: fractionalWithdrawalWithPrecisionSucceeds + Description: Ensures that small positive decimals (e.g., "0.01") are processed correctly with double arithmetic when sufficient funds exist. + +Execution: + Arrange: currentBalance = 100.00, withdrawal_amount = "0.01", valid user and account_id. + Act: Invoke transfer(requestMap, session). + Assert: Assert HttpStatus.OK and newBalance = 99.99 with a tolerance suitable for double comparisons. Assert success logging and accounts retrieval. + +Validation: + Confirms that decimal amounts are handled and precision is acceptable for typical use. + + +Scenario 17: Very large account_id within int range is accepted + +Details: + TestName: maxIntAccountIdIsParsedAndProcessed + Description: Verifies that the method correctly parses and uses a large account_id, e.g., "2147483647", provided sufficient funds. + +Execution: + Arrange: requestMap with account_id = "2147483647" and withdrawal_amount = "10.00". Stub getAccountBalance to return a sufficient value. Valid session user. + Act: Call transfer(requestMap, session). + Assert: Assert success status and that repository methods were called with account_id = 2147483647. + +Validation: + Confirms parsing and handling of large integer IDs. + + +Scenario 18: Leading zeros in numeric inputs are accepted + +Details: + TestName: leadingZerosInInputsAreParsedCorrectly + Description: Validates that account_id "000123" and withdrawal_amount "00050.00" are parsed correctly and processed. + +Execution: + Arrange: requestMap with account_id = "000123" and withdrawal_amount = "00050.00"; sufficient current balance; valid session user. + Act: Invoke transfer(requestMap, session). + Assert: Assert HttpStatus.OK and correct computed newBalance. Verify accountRepository.changeAccountsBalanceById was called with account_id = 123 and withdrawal_amount = 50.00. + +Validation: + Ensures robust parsing of numeric strings with leading zeros. + + +Scenario 19: Repository getAccountBalance throws an exception and it propagates + +Details: + TestName: repositoryGetBalanceExceptionPropagates + Description: Ensures that if accountRepository.getAccountBalance throws a runtime exception, the method does not catch it and it propagates to the caller. + +Execution: + Arrange: Valid requestMap and session user. Configure getAccountBalance to throw a RuntimeException. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a RuntimeException is thrown. Assert that changeAccountsBalanceById and logTransaction are not called. + +Validation: + Confirms transactional behavior is not attempted when prerequisites fail and that exceptions bubble up. + + +Scenario 20: Repository changeAccountsBalanceById throws and success log is not reached + +Details: + TestName: repositoryChangeBalanceExceptionStopsBeforeSuccessLog + Description: Validates that if changeAccountsBalanceById throws an exception in the success path, the method does not proceed to log a successful transaction, and the exception propagates. + +Execution: + Arrange: Sufficient funds path. Configure changeAccountsBalanceById to throw a RuntimeException. Valid requestMap and session. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a RuntimeException is thrown. Assert that transactRepository.logTransaction with "success" is not called. + +Validation: + Ensures partial failure prevents success logging and highlights potential need for transactional safeguards. + + +Scenario 21: transactRepository.logTransaction throws and exception propagates after balance update + +Details: + TestName: transactionLogFailureAfterBalanceUpdatePropagates + Description: Ensures that if logTransaction throws in the success path, the exception propagates and the account balance has already been updated. + +Execution: + Arrange: Sufficient funds scenario. Configure changeAccountsBalanceById to succeed and logTransaction to throw a RuntimeException. Valid requestMap and session. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a RuntimeException is thrown. Verify that changeAccountsBalanceById was called before the exception. No OK response is returned. + +Validation: + Demonstrates lack of atomicity and potential inconsistency if logging fails post-update. + + +Scenario 22: accountRepository.getUserAccountsById throws and exception propagates after logging and update + +Details: + TestName: getUserAccountsByIdExceptionPropagatesAfterSuccessPath + Description: Ensures that if fetching the accounts for the response throws an exception, the exception propagates after balance update and success logging have completed. + +Execution: + Arrange: Sufficient funds scenario. Configure getUserAccountsById to throw a RuntimeException. Other calls succeed. + Act: Invoke transfer(requestMap, session). + Assert: Assert that a RuntimeException is thrown. Assert that changeAccountsBalanceById and success logTransaction occurred before the exception. + +Validation: + Highlights that the final response assembly can fail after state changes, informing needs for error handling. + + +Scenario 23: Validates exact case sensitivity of transaction type between failure and success + +Details: + TestName: transactionTypeCaseSensitivityDiffersBetweenFailureAndSuccess + Description: Confirms that on insufficient funds the transaction type is "withdrawal" (lowercase) while on success it is "Withdrawal" (capitalized), matching the current implementation. + +Execution: + Arrange: Create two separate runs: one insufficient funds and one sufficient funds. Use the same account_id and different balances to trigger both paths. + Act: Invoke transfer in both scenarios. + Assert: For insufficient funds, assert logTransaction was invoked with type "withdrawal". For success, assert logTransaction was invoked with type "Withdrawal". + +Validation: + Ensures tests pin the current behavior, even if it appears inconsistent, to catch regressions or to inform future refactoring. + + +Scenario 24: No repository interactions when early validation fails (empty fields) + +Details: + TestName: noRepositoryCallsWhenFieldsAreEmpty + Description: Asserts that when the early empty string validation fails, no repository methods are invoked. + +Execution: + Arrange: Use requestMap with withdrawal_amount = "" or account_id = "". Provide mocked/stubbed repositories that can record calls. + Act: Invoke transfer(requestMap, session). + Assert: Assert HttpStatus.BAD_REQUEST and that neither accountRepository nor transactRepository methods were called. + +Validation: + Confirms fail-fast behavior avoids unnecessary operations and side effects. + + +Scenario 25: Response body contains accounts collection from repository on success + +Details: + TestName: successResponseContainsAccountsFromRepository + Description: Ensures that the "accounts" entry in the success response body is exactly what accountRepository.getUserAccountsById(user_id) returns. + +Execution: + Arrange: Sufficient funds scenario. Stub getUserAccountsById to return a known object/map/list reference. Prepare valid requestMap and session. + Act: Invoke transfer(requestMap, session). + Assert: Assert HttpStatus.OK and that ((Map)response.getBody()).get("accounts") is the exact same object returned by accountRepository.getUserAccountsById(user_id). + +Validation: + Confirms response assembly uses repository data as-is, which is important for consumers relying on the exact structure. + + +Scenario 26: Verifies method call order on success path + +Details: + TestName: callOrderOnSuccessBalanceThenLogThenFetchAccounts + Description: Verifies the sequence: getAccountBalance -> changeAccountsBalanceById -> logTransaction (success) -> getUserAccountsById, for the successful withdrawal. + +Execution: + Arrange: Sufficient funds setup with stubs/spies capable of recording invocation order. + Act: Invoke transfer(requestMap, session). + Assert: Assert that the recorded order matches: getAccountBalance first, changeAccountsBalanceById second, logTransaction third, getUserAccountsById last. + +Validation: + Ensures the operational order is consistent, which can be crucial for correctness and observability. + + +Scenario 27: Verifies no balance update or success log on insufficient funds + +Details: + TestName: insufficientFundsDoesNotUpdateBalanceOrLogSuccess + Description: Ensures that in the insufficient funds branch, the method neither updates the balance nor logs a success, only logs a failure and returns 400. + +Execution: + Arrange: Insufficient funds scenario with recording stubs. + Act: Invoke transfer(requestMap, session). + Assert: Assert status HttpStatus.BAD_REQUEST, one failure logTransaction call, zero calls to changeAccountsBalanceById, and zero calls to success logTransaction. + +Validation: + Confirms correct side-effect limitations when validation fails at the business rule (insufficient funds). + + +Scenario 28: Correct computation of newBalance on success + +Details: + TestName: newBalanceComputationIsCorrect + Description: Validates that newBalance is computed exactly as currentBalance - withdrawal_amount and passed to changeAccountsBalanceById. + +Execution: + Arrange: Set currentBalance = 1234.56 and withdrawal_amount = "234.56" with sufficient funds. Valid session. + Act: Call transfer(requestMap, session). + Assert: Assert that changeAccountsBalanceById was called with newBalance = 1000.00 (double comparison with small tolerance if needed). + +Validation: + Confirms the correctness of arithmetic logic used to adjust balances. + + +Scenario 29: Ensures message text is exact on success + +Details: + TestName: successMessageTextMatchesExactly + Description: Validates that the success response contains the exact message "Withdrawal Successfull!" (including capitalization and spelling). + +Execution: + Arrange: Sufficient funds setup with valid inputs and session. + Act: Invoke transfer(requestMap, session). + Assert: Assert HttpStatus.OK and exact match on message string: "Withdrawal Successfull!". + +Validation: + Ensures clients relying on exact messages receive the expected text; helps catch inadvertent message changes. + + +Scenario 30: Ensures error message text is exact on insufficient funds + +Details: + TestName: insufficientFundsMessageTextMatchesExactly + Description: Validates that the error response message for insufficient funds matches exactly "You have insufficient Funds to perform this transfer." + +Execution: + Arrange: Insufficient funds setup. + Act: Call transfer(requestMap, session). + Assert: Assert HttpStatus.BAD_REQUEST and exact body string match. + +Validation: + Locks down error contract and prevents unintentional message changes that could affect clients. + +*/ + +// ********RoostGPT******** +package com.beko.DemoBank_v1.controllers;import com.beko.DemoBank_v1.models.User; +import com.beko.DemoBank_v1.repository.AccountRepository; +import com.beko.DemoBank_v1.repository.TransactRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import javax.servlet.http.HttpSession; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import org.junit.jupiter.api.*; +import com.beko.DemoBank_v1.models.PaymentRequest; +import com.beko.DemoBank_v1.models.TransferRequest; +import com.beko.DemoBank_v1.repository.PaymentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import javax.servlet.http.HttpServletRequest; + +@ExtendWith(MockitoExtension.class) +public class TransactControllerTransfer703Test { + @InjectMocks + private TransactController transactController; + @Mock + private AccountRepository accountRepository; + @Mock + private TransactRepository transactRepository; + @Tag("valid") + @Test + public void testSuccessfulWithdrawalReducesBalanceAndReturnsOk() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "100.00"); + requestMap.put("account_id", "10"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("42"); + double currentBalance = 200.0; + when(accountRepository.getAccountBalance(42, 10)).thenReturn(currentBalance); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(42)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertNotNull(bodyMap); + assertEquals((String) "Withdrawal Successfull!", (String) bodyMap.get("message")); + assertSame((Object) accounts, (Object) bodyMap.get("accounts")); + double expectedNewBalance = (double) (currentBalance - 100.00); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) expectedNewBalance, (int) 10); + verify(transactRepository, times(1)).logTransaction((int) 10, (String) "Withdrawal", (double) 100.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("boundary") + @Test + public void testWithdrawalEqualToBalanceSucceedsAndResultsInZeroBalance() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "100.00"); + requestMap.put("account_id", "5"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("1001"); + when(accountRepository.getAccountBalance(1001, 5)).thenReturn((double) 100.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(1001)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertNotNull(bodyMap); + assertEquals((String) "Withdrawal Successfull!", (String) bodyMap.get("message")); + assertSame((Object) accounts, (Object) bodyMap.get("accounts")); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) 0.0, (int) 5); + verify(transactRepository, times(1)).logTransaction((int) 5, (String) "Withdrawal", (double) 100.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testInsufficientFundsReturnsBadRequestAndLogsFailure() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "75.00"); + requestMap.put("account_id", "10"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("99"); + when(accountRepository.getAccountBalance(99, 10)).thenReturn((double) 50.00); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "You have insufficient Funds to perform this transfer.", (String) response.getBody()); + verify(accountRepository, times(0)).changeAccountsBalanceById(anyDouble(), anyInt()); + verify(transactRepository, times(1)).logTransaction((int) 10, (String) "withdrawal", (double) 75.00, (String) "online", (String) "failed", (String) "Insufficient funds.", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testZeroAmountReturnsBadRequestWithProperMessage() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "0"); + requestMap.put("account_id", "1"); + ResponseEntity response = transactController.transfer(requestMap, mock(HttpSession.class)); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "Withdrawal amount cannot be 0 value.", (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testEmptyFieldsReturnBadRequestWithExactMessage() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", ""); + requestMap.put("account_id", ""); + ResponseEntity response = transactController.transfer(requestMap, mock(HttpSession.class)); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "Account withdrawing from and withdrawal amount cannot be empty!", (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testMissingWithdrawalAmountKeyCausesNullPointerException() { + Map requestMap = new HashMap<>(); + requestMap.put("account_id", "7"); + assertThrows(NullPointerException.class, () -> transactController.transfer(requestMap, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testMissingAccountIdKeyCausesNullPointerException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + assertThrows(NullPointerException.class, () -> transactController.transfer(requestMap, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testNullRequestMapCausesNullPointerException() { + assertThrows(NullPointerException.class, () -> transactController.transfer((Map) null, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testNonNumericAccountIdThrowsNumberFormatException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "50.00"); + requestMap.put("account_id", "ABC"); + assertThrows(NumberFormatException.class, () -> transactController.transfer(requestMap, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testNonNumericWithdrawalAmountThrowsNumberFormatException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "FIFTY"); + requestMap.put("account_id", "2"); + assertThrows(NumberFormatException.class, () -> transactController.transfer(requestMap, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testWhitespaceOnlyValuesCauseNumberFormatException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", " "); + requestMap.put("account_id", " "); + assertThrows(NumberFormatException.class, () -> transactController.transfer(requestMap, mock(HttpSession.class))); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testNullSessionCausesNullPointerException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "3"); + assertThrows(NullPointerException.class, () -> transactController.transfer(requestMap, null)); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testMissingUserInSessionCausesNullPointerException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "3"); + HttpSession session = mock(HttpSession.class); + when(session.getAttribute("user")).thenReturn(null); + assertThrows(NullPointerException.class, () -> transactController.transfer(requestMap, session)); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("invalid") + @Test + public void testNonNumericUserIdOnUserCausesNumberFormatException() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "20.00"); + requestMap.put("account_id", "9"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("user-xyz"); // TODO set a valid numeric user id if implementation changes + assertThrows(NumberFormatException.class, () -> transactController.transfer(requestMap, session)); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("boundary") + @Test + public void testNegativeWithdrawalAmountIncreasesBalanceAndReturnsOk() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "-100.00"); + requestMap.put("account_id", "11"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("500"); + when(accountRepository.getAccountBalance(500, 11)).thenReturn((double) 500.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(500)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertNotNull(bodyMap); + assertEquals((String) "Withdrawal Successfull!", (String) bodyMap.get("message")); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) 600.00, (int) 11); + verify(transactRepository, times(1)).logTransaction((int) 11, (String) "Withdrawal", (double) -100.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("boundary") + @Test + public void testFractionalWithdrawalWithPrecisionSucceeds() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "0.01"); + requestMap.put("account_id", "12"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("700"); + when(accountRepository.getAccountBalance(700, 12)).thenReturn((double) 100.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(700)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertNotNull(bodyMap); + assertEquals((String) "Withdrawal Successfull!", (String) bodyMap.get("message")); + assertSame((Object) accounts, (Object) bodyMap.get("accounts")); + ArgumentCaptor newBalanceCaptor = ArgumentCaptor.forClass(Double.class); + verify(accountRepository, times(1)).changeAccountsBalanceById(newBalanceCaptor.capture(), eq((int) 12)); + double actualNewBalance = (double) newBalanceCaptor.getValue(); + assertTrue((boolean) (Math.abs(((double) 99.99) - actualNewBalance) < 1e-9)); + verify(transactRepository, times(1)).logTransaction((int) 12, (String) "Withdrawal", (double) 0.01, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("boundary") + @Test + public void testMaxIntAccountIdIsParsedAndProcessed() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", String.valueOf(Integer.MAX_VALUE)); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("888"); + when(accountRepository.getAccountBalance(888, Integer.MAX_VALUE)).thenReturn((double) 100.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(888)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) 90.00, (int) Integer.MAX_VALUE); + verify(transactRepository, times(1)).logTransaction((int) Integer.MAX_VALUE, (String) "Withdrawal", (double) 10.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("boundary") + @Test + public void testLeadingZerosInInputsAreParsedCorrectly() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "00050.00"); + requestMap.put("account_id", "000123"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("1010"); + when(accountRepository.getAccountBalance(1010, 123)).thenReturn((double) 1000.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(1010)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) 950.00, (int) 123); + verify(transactRepository, times(1)).logTransaction((int) 123, (String) "Withdrawal", (double) 50.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testRepositoryGetBalanceExceptionPropagates() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "2"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("123"); + when(accountRepository.getAccountBalance(123, 2)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> transactController.transfer(requestMap, session)); + verify(accountRepository, times(0)).changeAccountsBalanceById(anyDouble(), anyInt()); + verify(transactRepository, times(0)).logTransaction(anyInt(), anyString(), anyDouble(), anyString(), anyString(), anyString(), any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testRepositoryChangeBalanceExceptionStopsBeforeSuccessLog() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "2"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("123"); + when(accountRepository.getAccountBalance(123, 2)).thenReturn((double) 100.00); + doThrow(new RuntimeException()).when(accountRepository).changeAccountsBalanceById((double) 90.00, (int) 2); + assertThrows(RuntimeException.class, () -> transactController.transfer(requestMap, session)); + verify(transactRepository, times(0)).logTransaction((int) 2, (String) "Withdrawal", (double) 10.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testTransactionLogFailureAfterBalanceUpdatePropagates() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "4"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("77"); + when(accountRepository.getAccountBalance(77, 4)).thenReturn((double) 100.00); + doNothing().when(accountRepository).changeAccountsBalanceById((double) 90.00, (int) 4); + doThrow(new RuntimeException()).when(transactRepository).logTransaction((int) 4, (String) "Withdrawal", (double) 10.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + assertThrows(RuntimeException.class, () -> transactController.transfer(requestMap, session)); + InOrder inOrder = inOrder(accountRepository, transactRepository); + inOrder.verify(accountRepository, times(1)).getAccountBalance((int) 77, (int) 4); + inOrder.verify(accountRepository, times(1)).changeAccountsBalanceById((double) 90.00, (int) 4); + inOrder.verify(transactRepository, times(1)).logTransaction((int) 4, (String) "Withdrawal", (double) 10.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testGetUserAccountsByIdExceptionPropagatesAfterSuccessPath() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "20.00"); + requestMap.put("account_id", "6"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("555"); + when(accountRepository.getAccountBalance(555, 6)).thenReturn((double) 100.00); + doNothing().when(accountRepository).changeAccountsBalanceById((double) 80.00, (int) 6); + doNothing().when(transactRepository).logTransaction((int) 6, (String) "Withdrawal", (double) 20.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + when(accountRepository.getUserAccountsById(555)).thenThrow(new RuntimeException()); + assertThrows(RuntimeException.class, () -> transactController.transfer(requestMap, session)); + verify(accountRepository, times(1)).changeAccountsBalanceById((double) 80.00, (int) 6); + verify(transactRepository, times(1)).logTransaction((int) 6, (String) "Withdrawal", (double) 20.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("boundary") + @Test + public void testTransactionTypeCaseSensitivityDiffersBetweenFailureAndSuccess() { + Map requestMapFail = new HashMap<>(); + requestMapFail.put("withdrawal_amount", "200.00"); + requestMapFail.put("account_id", "9"); + Map requestMapSuccess = new HashMap<>(); + requestMapSuccess.put("withdrawal_amount", "50.00"); + requestMapSuccess.put("account_id", "9"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("100"); + when(accountRepository.getAccountBalance(100, 9)).thenReturn((double) 100.00).thenReturn((double) 100.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(100)).thenReturn(accounts); + ResponseEntity responseFail = transactController.transfer(requestMapFail, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, responseFail.getStatusCode()); + ResponseEntity responseSuccess = transactController.transfer(requestMapSuccess, session); + assertEquals((HttpStatus) HttpStatus.OK, responseSuccess.getStatusCode()); + verify(transactRepository, times(1)).logTransaction((int) 9, (String) "withdrawal", (double) 200.00, (String) "online", (String) "failed", (String) "Insufficient funds.", any(LocalDateTime.class)); + verify(transactRepository, times(1)).logTransaction((int) 9, (String) "Withdrawal", (double) 50.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("invalid") + @Test + public void testNoRepositoryCallsWhenFieldsAreEmpty() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", ""); + requestMap.put("account_id", "1"); + ResponseEntity response = transactController.transfer(requestMap, mock(HttpSession.class)); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "Account withdrawing from and withdrawal amount cannot be empty!", (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(transactRepository); + } + @Tag("valid") + @Test + public void testSuccessResponseContainsAccountsFromRepository() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "15"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("321"); + when(accountRepository.getAccountBalance(321, 15)).thenReturn((double) 50.00); + HashMap accountsObject = new HashMap<>(); + when(accountRepository.getUserAccountsById(321)).thenReturn(accountsObject); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertSame((Object) accountsObject, (Object) bodyMap.get("accounts")); + } + @Tag("integration") + @Test + public void testCallOrderOnSuccessBalanceThenLogThenFetchAccounts() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "30.00"); + requestMap.put("account_id", "20"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("654"); + when(accountRepository.getAccountBalance(654, 20)).thenReturn((double) 100.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(654)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + InOrder inOrder = inOrder(accountRepository, transactRepository); + inOrder.verify(accountRepository, times(1)).getAccountBalance((int) 654, (int) 20); + inOrder.verify(accountRepository, times(1)).changeAccountsBalanceById((double) 70.00, (int) 20); + inOrder.verify(transactRepository, times(1)).logTransaction((int) 20, (String) "Withdrawal", (double) 30.00, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + inOrder.verify(accountRepository, times(1)).getUserAccountsById((int) 654); + } + @Tag("invalid") + @Test + public void testInsufficientFundsDoesNotUpdateBalanceOrLogSuccess() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "999.99"); + requestMap.put("account_id", "30"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("42"); + when(accountRepository.getAccountBalance(42, 30)).thenReturn((double) 100.00); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "You have insufficient Funds to perform this transfer.", (String) response.getBody()); + verify(accountRepository, times(0)).changeAccountsBalanceById(anyDouble(), anyInt()); + verify(transactRepository, times(1)).logTransaction((int) 30, (String) "withdrawal", (double) 999.99, (String) "online", (String) "failed", (String) "Insufficient funds.", any(LocalDateTime.class)); + verify(transactRepository, times(0)).logTransaction((int) 30, (String) "Withdrawal", (double) 999.99, (String) "online", (String) "success", (String) "Withdrawal Transaction Successfull", any(LocalDateTime.class)); + } + @Tag("valid") + @Test + public void testNewBalanceComputationIsCorrect() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "234.56"); + requestMap.put("account_id", "40"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("777"); + when(accountRepository.getAccountBalance(777, 40)).thenReturn((double) 1234.56); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(777)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + ArgumentCaptor balanceCaptor = ArgumentCaptor.forClass(Double.class); + verify(accountRepository).changeAccountsBalanceById(balanceCaptor.capture(), eq((int) 40)); + double actualNewBalance = (double) balanceCaptor.getValue(); + assertTrue((boolean) (Math.abs(((double) 1000.00) - actualNewBalance) < 1e-9)); + } + @Tag("valid") + @Test + public void testSuccessMessageTextMatchesExactly() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "10.00"); + requestMap.put("account_id", "50"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("1000"); + when(accountRepository.getAccountBalance(1000, 50)).thenReturn((double) 500.00); + HashMap accounts = new HashMap<>(); + when(accountRepository.getUserAccountsById(1000)).thenReturn(accounts); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.OK, response.getStatusCode()); + Map bodyMap = (Map) response.getBody(); + assertEquals((String) "Withdrawal Successfull!", (String) bodyMap.get("message")); + } + @Tag("invalid") + @Test + public void testInsufficientFundsMessageTextMatchesExactly() { + Map requestMap = new HashMap<>(); + requestMap.put("withdrawal_amount", "1000.00"); + requestMap.put("account_id", "60"); + HttpSession session = mock(HttpSession.class); + User sessionUser = mock(User.class); + when(session.getAttribute("user")).thenReturn(sessionUser); + when(sessionUser.getUser_id()).thenReturn("42"); + when(accountRepository.getAccountBalance(42, 60)).thenReturn((double) 100.00); + ResponseEntity response = transactController.transfer(requestMap, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals((String) "You have insufficient Funds to perform this transfer.", (String) response.getBody()); + } +} \ No newline at end of file diff --git a/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer929Test.java b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer929Test.java new file mode 100644 index 0000000..21dfbed --- /dev/null +++ b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransfer929Test.java @@ -0,0 +1,324 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test unit-java-springboot using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=transfer_1f3029f5d2 +ROOST_METHOD_SIG_HASH=transfer_89d8d3e845 + + +Scenario 1: Successful payment when balance is greater than amount + +Details: + TestName: successfulPaymentReturnsOkAndUpdatesBalance + Description: Verifies that when all input fields are valid, the logged-in user exists, and the current account balance is higher than the requested payment amount, the method processes the payment successfully, updates the account balance, logs the transaction as success, and returns HTTP 200 with a response map containing a success message and accounts. + +Execution: + Arrange: + - Mock PaymentRequest to return non-empty beneficiary, account_number, account_id (numeric), reference (any string), and payment_amount (numeric > 0). + - Mock HttpSession to return a User whose getUser_id() returns a numeric string. + - Stub AccountRepository.getAccountBalance(user_id, accountID) to return a balance greater than the parsed payment amount. + - Stub AccountRepository.getUserAccountsById(user_id) to return a non-empty accounts object/list. + Act: + - Call TransactController.transfer(PaymentRequest, HttpSession). + Assert: + - Assert that the ResponseEntity status is 200 OK. + - Assert that the body is a Map containing: + - "message" equal to "Payment Processed Successfully!". + - "accounts" equal to the stubbed accounts object. + - Assert that AccountRepository.changeAccountsBalanceById is invoked with (currentBalance - paymentAmount, accountID). + - Assert that PaymentRepository.makePayment is invoked once with parameters (accountID, beneficiary, account_number, paymentAmount, reference, "success", "Payment Processed Successfully!", currentDateTime). + - Assert that TransactRepository.logTransaction is invoked once with (accountID, "Payment", paymentAmount, "online", "success", "Payment Transaction Successfull", currentDateTime). + +Validation: + Ensures the happy path functions correctly: a valid, sufficient-funds payment produces a successful response, updates the balance accurately, and records both payment and transaction logs as success. Confirms that the response payload includes the accounts list for UI refresh. + +*/ + +// ********RoostGPT******** +package com.beko.DemoBank_v1.controllers; + +import com.beko.DemoBank_v1.models.PaymentRequest; +import com.beko.DemoBank_v1.models.User; +import com.beko.DemoBank_v1.repository.AccountRepository; +import com.beko.DemoBank_v1.repository.PaymentRepository; +import com.beko.DemoBank_v1.repository.TransactRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import javax.servlet.http.HttpSession; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import org.junit.jupiter.api.*; +import com.beko.DemoBank_v1.models.TransferRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import javax.servlet.http.HttpServletRequest; + +@ExtendWith(MockitoExtension.class) +public class TransactControllerTransfer929Test { + + @Mock + private AccountRepository accountRepository; + + @Mock + private PaymentRepository paymentRepository; + + @Mock + private TransactRepository transactRepository; + + @Mock + private HttpSession session; + + @InjectMocks + private TransactController transactController; + + @Test + @Tag("valid") + public void successfulPaymentReturnsOkAndUpdatesBalance() { + PaymentRequest request = mock(PaymentRequest.class); + String beneficiary = "Beneficiary"; // TODO update to realistic beneficiary if + // needed + String accountNumber = "1234567890"; // TODO update to valid account number if + // needed + String accountIdStr = "1001"; // TODO update to valid account id if needed + String reference = "REF-100"; // TODO update to relevant reference if needed + String paymentAmountStr = "100.00"; // TODO update to a payment amount within + // allowed limits if needed + when(request.getBeneficiary()).thenReturn(beneficiary); + when(request.getAccount_number()).thenReturn(accountNumber); + when(request.getAccount_id()).thenReturn(accountIdStr); + when(request.getReference()).thenReturn(reference); + when(request.getPayment_amount()).thenReturn(paymentAmountStr); + User userMock = mock(User.class); + when(userMock.getUser_id()).thenReturn("77"); // TODO adjust user id to match + // existing test data if needed + when(session.getAttribute("user")).thenReturn(userMock); + int userId = 77; + int accountId = 1001; + double currentBalance = 500.00; + double paymentAmount = 100.00; + double expectedNewBalance = currentBalance - paymentAmount; + when(accountRepository.getAccountBalance(userId, accountId)).thenReturn(currentBalance); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.OK, (HttpStatus) response.getStatusCode()); + Map body = (Map) response.getBody(); + assertNotNull((Object) body); + assertEquals((String) "Payment Processed Successfully!", (String) body.get("message")); + assertTrue((boolean) body.containsKey("accounts")); + verify(accountRepository, times(1)).changeAccountsBalanceById(eq(expectedNewBalance), eq(accountId)); + ArgumentCaptor paymentTimeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor transactionTimeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(paymentRepository, times(1)).makePayment(eq(accountId), eq(beneficiary), eq(accountNumber), + eq(paymentAmount), eq(reference), eq("success"), eq("Payment Processed Successfully!"), + paymentTimeCaptor.capture()); + verify(transactRepository, times(1)).logTransaction(eq(accountId), eq("Payment"), eq(paymentAmount), + eq("online"), eq("success"), eq("Payment Transaction Successfull"), transactionTimeCaptor.capture()); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lowerBound = now.minusMinutes(5); + LocalDateTime upperBound = now.plusMinutes(5); + LocalDateTime paymentTime = paymentTimeCaptor.getValue(); + assertNotNull((Object) paymentTime); + assertTrue((boolean) (paymentTime.isAfter(lowerBound) && paymentTime.isBefore(upperBound))); + LocalDateTime transactionTime = transactionTimeCaptor.getValue(); + assertNotNull((Object) transactionTime); + assertTrue((boolean) (transactionTime.isAfter(lowerBound) && transactionTime.isBefore(upperBound))); + } + + @Test + @Tag("invalid") + public void emptyBeneficiaryReturnsBadRequest() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn(""); + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn("1001"); + when(request.getReference()).thenReturn("REF-100"); + when(request.getPayment_amount()).thenReturn("100.00"); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals( + (String) "Beneficiary, account number, account paying from and account payment amount cannot be empty.", + (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(paymentRepository); + verifyNoInteractions(transactRepository); + } + + @Test + @Tag("invalid") + public void emptyAccountNumberReturnsBadRequest() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn(""); + when(request.getAccount_id()).thenReturn("1001"); + when(request.getReference()).thenReturn("REF-100"); + when(request.getPayment_amount()).thenReturn("100.00"); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals( + (String) "Beneficiary, account number, account paying from and account payment amount cannot be empty.", + (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(paymentRepository); + verifyNoInteractions(transactRepository); + } + + @Test + @Tag("invalid") + public void emptyAccountIdReturnsBadRequest() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn(""); + when(request.getReference()).thenReturn("REF-100"); + when(request.getPayment_amount()).thenReturn("100.00"); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals( + (String) "Beneficiary, account number, account paying from and account payment amount cannot be empty.", + (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(paymentRepository); + verifyNoInteractions(transactRepository); + } + + @Test + @Tag("invalid") + public void emptyPaymentAmountReturnsBadRequest() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn("1001"); + when(request.getReference()).thenReturn("REF-100"); + when(request.getPayment_amount()).thenReturn(""); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals( + (String) "Beneficiary, account number, account paying from and account payment amount cannot be empty.", + (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(paymentRepository); + verifyNoInteractions(transactRepository); + } + + @Test + @Tag("boundary") + public void zeroPaymentAmountReturnsBadRequest() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn("1001"); + when(request.getReference()).thenReturn("REF-100"); + when(request.getPayment_amount()).thenReturn("0"); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "Payment amount cannot be 0.", (String) response.getBody()); + verifyNoInteractions(accountRepository); + verifyNoInteractions(paymentRepository); + verifyNoInteractions(transactRepository); + } + + @Test + @Tag("invalid") + public void insufficientFundsReturnsBadRequestAndLogsFailure() { + PaymentRequest request = mock(PaymentRequest.class); + String beneficiary = "Beneficiary"; // TODO update to realistic beneficiary if + // needed + String accountNumber = "1234567890"; // TODO update to valid account number if + // needed + String accountIdStr = "2002"; // TODO update to valid account id if needed + String reference = "REF-200"; // TODO update to relevant reference if needed + String paymentAmountStr = "100.00"; // TODO update to a payment amount within + // allowed limits if needed + when(request.getBeneficiary()).thenReturn(beneficiary); + when(request.getAccount_number()).thenReturn(accountNumber); + when(request.getAccount_id()).thenReturn(accountIdStr); + when(request.getReference()).thenReturn(reference); + when(request.getPayment_amount()).thenReturn(paymentAmountStr); + User userMock = mock(User.class); + when(userMock.getUser_id()).thenReturn("77"); // TODO adjust user id if needed + when(session.getAttribute("user")).thenReturn(userMock); + int userId = 77; + int accountId = 2002; + double currentBalance = 50.00; + double paymentAmount = 100.00; + when(accountRepository.getAccountBalance(userId, accountId)).thenReturn(currentBalance); + ResponseEntity response = transactController.transfer(request, session); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "You have insufficient Funds to perform this payment.", (String) response.getBody()); + verify(paymentRepository, times(1)).makePayment(eq(accountId), eq(beneficiary), eq(accountNumber), + eq(paymentAmount), eq(reference), eq("failed"), + eq("Coult not Processed Payment due to insufficient funds."), + org.mockito.Mockito.any(LocalDateTime.class)); + verify(transactRepository, times(1)).logTransaction(eq(accountId), eq("Payment"), eq(paymentAmount), + eq("online"), eq("failed"), eq("Insufficient funds."), org.mockito.Mockito.any(LocalDateTime.class)); + verify(accountRepository, times(0)).changeAccountsBalanceById(org.mockito.Mockito.anyDouble(), + org.mockito.Mockito.anyInt()); + ArgumentCaptor paymentTimeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + ArgumentCaptor transactionTimeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(paymentRepository).makePayment(eq(accountId), eq(beneficiary), eq(accountNumber), eq(paymentAmount), + eq(reference), eq("failed"), eq("Coult not Processed Payment due to insufficient funds."), + paymentTimeCaptor.capture()); + verify(transactRepository).logTransaction(eq(accountId), eq("Payment"), eq(paymentAmount), eq("online"), + eq("failed"), eq("Insufficient funds."), transactionTimeCaptor.capture()); + LocalDateTime now = LocalDateTime.now(); + LocalDateTime lowerBound = now.minusMinutes(5); + LocalDateTime upperBound = now.plusMinutes(5); + LocalDateTime paymentTime = paymentTimeCaptor.getValue(); + assertNotNull((Object) paymentTime); + assertTrue((boolean) (paymentTime.isAfter(lowerBound) && paymentTime.isBefore(upperBound))); + LocalDateTime transactionTime = transactionTimeCaptor.getValue(); + assertNotNull((Object) transactionTime); + assertTrue((boolean) (transactionTime.isAfter(lowerBound) && transactionTime.isBefore(upperBound))); + } + + @Test + @Tag("invalid") + public void nonNumericAccountIdThrowsNumberFormatException() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn("abc"); // invalid numeric + when(request.getReference()).thenReturn("REF-300"); + when(request.getPayment_amount()).thenReturn("10.00"); + assertThrows(NumberFormatException.class, () -> { + transactController.transfer(request, session); + }); + } + + @Test + @Tag("invalid") + public void nonNumericPaymentAmountThrowsNumberFormatException() { + PaymentRequest request = mock(PaymentRequest.class); + when(request.getBeneficiary()).thenReturn("Beneficiary"); // TODO adjust if needed + when(request.getAccount_number()).thenReturn("1234567890"); + when(request.getAccount_id()).thenReturn("3003"); + when(request.getReference()).thenReturn("REF-301"); + when(request.getPayment_amount()).thenReturn("ten"); // invalid numeric + assertThrows(NumberFormatException.class, () -> { + transactController.transfer(request, session); + }); + } + +} \ No newline at end of file diff --git a/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransferTest.java b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransferTest.java new file mode 100644 index 0000000..bf39bf7 --- /dev/null +++ b/Online-Banking-App-Spring-Boot/src/test/java/com/beko/DemoBank_v1/controllers/TransactControllerTransferTest.java @@ -0,0 +1,239 @@ + +// ********RoostGPT******** +/* +Test generated by RoostGPT for test unit-java-springboot using AI Type Azure Open AI and AI Model gpt-5 + +ROOST_METHOD_HASH=transfer_a34cb5e196 +ROOST_METHOD_SIG_HASH=transfer_23bf246fa5 + +Scenario 1: Returns 400 Bad Request when the source account is empty + +Details: + TestName: returnsBadRequestWhenSourceAccountIsEmpty + Description: Verifies that transfer returns a 400 Bad Request if the TransferRequest has an empty source account string. Ensures no repository interaction occurs and the error message matches exactly. + +Execution: + Arrange: Create a mocked TransferRequest whose getSourceAccount returns an empty string, getTargetAccount returns a valid numeric string (e.g., "2001"), and getAmount returns a valid numeric string (e.g., "50"). Mock HttpSession but do not rely on it (the method should return before session usage). Ensure AccountRepository and TransactRepository are mocked and injected into TransactController. + Act: Invoke TransactController.transfer with the mocked TransferRequest and HttpSession. + Assert: Assert that the ResponseEntity has HttpStatus.BAD_REQUEST and the body equals "The account transferring from and to along with the amount cannot be empty!". Verify no interactions occurred with accountRepository or transactRepository. + +Validation: + Confirms that the controller guards against empty inputs before any parsing or repository interaction, preserving application integrity and providing clear feedback to the client. + +*/ + +// ********RoostGPT******** +package com.beko.DemoBank_v1.controllers; + +import com.beko.DemoBank_v1.models.TransferRequest; +import com.beko.DemoBank_v1.models.User; +import com.beko.DemoBank_v1.repository.AccountRepository; +import com.beko.DemoBank_v1.repository.TransactRepository; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import javax.servlet.http.HttpSession; +import java.time.LocalDateTime; +import java.util.Map; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.anyDouble; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; +import org.junit.jupiter.api.*; +import com.beko.DemoBank_v1.models.PaymentRequest; +import com.beko.DemoBank_v1.repository.PaymentRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; + +@ExtendWith(MockitoExtension.class) +public class TransactControllerTransferTest { + + @Mock + private AccountRepository accountRepository; + + @Mock + private TransactRepository transactRepository; + + @Mock + private HttpSession httpSession; + + @InjectMocks + private TransactController transactController; + + @Test + @Tag("invalid") + public void testReturnsBadRequestWhenSourceAccountIsEmpty() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn(""); + when(request.getTargetAccount()).thenReturn("2001"); + when(request.getAmount()).thenReturn("50"); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "The account transferring from and to along with the amount cannot be empty!", + (String) response.getBody()); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("invalid") + public void testReturnsBadRequestWhenTargetAccountIsEmpty() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("1001"); + when(request.getTargetAccount()).thenReturn(""); + when(request.getAmount()).thenReturn("50"); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "The account transferring from and to along with the amount cannot be empty!", + (String) response.getBody()); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("invalid") + public void testReturnsBadRequestWhenAmountIsEmpty() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("1001"); + when(request.getTargetAccount()).thenReturn("2001"); + when(request.getAmount()).thenReturn(""); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "The account transferring from and to along with the amount cannot be empty!", + (String) response.getBody()); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("invalid") + public void testReturnsBadRequestWhenTransferringIntoSameAccount() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("1001"); + when(request.getTargetAccount()).thenReturn("1001"); + when(request.getAmount()).thenReturn("10"); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals( + (String) "Cannot Transfer Into The Same Account, Please select the appropriate account to perform transfer.", + (String) response.getBody()); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("boundary") + public void testReturnsBadRequestWhenAmountIsZero() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("1001"); + when(request.getTargetAccount()).thenReturn("2001"); + when(request.getAmount()).thenReturn("0"); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "Cannot Transfer an amount of 0 (Zero) value, please enter a value greater than.", + (String) response.getBody()); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("invalid") + public void testReturnsBadRequestWhenInsufficientFundsAndLogsTransaction() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("111"); + when(request.getTargetAccount()).thenReturn("222"); + when(request.getAmount()).thenReturn("75"); + User user = org.mockito.Mockito.mock(User.class); + when(httpSession.getAttribute(eq("user"))).thenReturn(user); + when(user.getUser_id()).thenReturn("123"); // TODO: Change this if your system + // uses a different user id format + when(accountRepository.getAccountBalance(eq(123), eq(111))).thenReturn(50.0); + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.BAD_REQUEST, (HttpStatus) response.getStatusCode()); + assertEquals((String) "You have insufficient Funds to perform this transfer.", (String) response.getBody()); + ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(transactRepository).logTransaction(eq(111), eq("transfer"), eq(75.0), eq("online"), eq("failed"), + eq("Insufficient funds."), timeCaptor.capture()); + LocalDateTime loggedTime = timeCaptor.getValue(); + assertNotNull((Object) loggedTime); + verify(accountRepository, never()).changeAccountsBalanceById(anyDouble(), eq(111)); + verify(accountRepository, never()).changeAccountsBalanceById(anyDouble(), eq(222)); + verify(accountRepository, never()).getAccountBalance(eq(123), eq(222)); + verify(accountRepository, never()).getUserAccountsById(eq(123)); + } + + @Test + @Tag("valid") + public void testReturnsOkWhenTransferSucceedsAndBalancesUpdatedAndTransactionLogged() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("101"); + when(request.getTargetAccount()).thenReturn("202"); + when(request.getAmount()).thenReturn("100"); + User user = org.mockito.Mockito.mock(User.class); + when(httpSession.getAttribute(eq("user"))).thenReturn(user); + when(user.getUser_id()).thenReturn("7"); // TODO: Change this to align with your + // test user id configuration + // Boundary where balance equals transfer amount (allowed) + when(accountRepository.getAccountBalance(eq(7), eq(101))).thenReturn(100.0); + when(accountRepository.getAccountBalance(eq(7), eq(202))).thenReturn(50.0); + // NOTE: Not stubbing getUserAccountsById to avoid generic return type mismatch; + // controller will include whatever mock returns (likely null) in the response. + ResponseEntity response = transactController.transfer(request, httpSession); + assertEquals((HttpStatus) HttpStatus.OK, (HttpStatus) response.getStatusCode()); + @SuppressWarnings("unchecked") + Map body = (Map) response.getBody(); + assertNotNull((Object) body); + assertEquals((String) "Transfer completed successfully.", (String) body.get("message")); + verify(accountRepository).changeAccountsBalanceById(eq(0.0), eq(101)); + verify(accountRepository).changeAccountsBalanceById(eq(150.0), eq(202)); + ArgumentCaptor timeCaptor = ArgumentCaptor.forClass(LocalDateTime.class); + verify(transactRepository).logTransaction(eq(101), eq("Transfer"), eq(100.0), eq("online"), eq("success"), + eq("Transfer Transaction Successfull"), timeCaptor.capture()); + LocalDateTime loggedTime = timeCaptor.getValue(); + assertNotNull((Object) loggedTime); + } + + @Test + @Tag("invalid") + public void testThrowsNumberFormatExceptionWhenAccountIdsAreNonNumeric() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("abc"); + when(request.getTargetAccount()).thenReturn("202"); + when(request.getAmount()).thenReturn("10"); + NumberFormatException ex = (NumberFormatException) assertThrows( + (Class) NumberFormatException.class, () -> { + transactController.transfer(request, httpSession); + }); + assertNotNull((Object) ex); + verifyNoInteractions(accountRepository, transactRepository); + } + + @Test + @Tag("invalid") + public void testThrowsNumberFormatExceptionWhenAmountIsNonNumeric() { + TransferRequest request = org.mockito.Mockito.mock(TransferRequest.class); + when(request.getSourceAccount()).thenReturn("101"); + when(request.getTargetAccount()).thenReturn("202"); + when(request.getAmount()).thenReturn("NaN"); + NumberFormatException ex = (NumberFormatException) assertThrows( + (Class) NumberFormatException.class, () -> { + transactController.transfer(request, httpSession); + }); + assertNotNull((Object) ex); + verifyNoInteractions(accountRepository, transactRepository); + } + +} \ No newline at end of file