diff --git a/pom.xml b/pom.xml index fc5bfeac..da3f7cc2 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,11 @@ spring-security-test test + + com.h2database + h2 + test + diff --git a/src/test/java/com/example/bankapp/config/SecurityConfigTest.java b/src/test/java/com/example/bankapp/config/SecurityConfigTest.java new file mode 100644 index 00000000..9ba4b3b5 --- /dev/null +++ b/src/test/java/com/example/bankapp/config/SecurityConfigTest.java @@ -0,0 +1,175 @@ +package com.example.bankapp.config; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.AccountService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.util.ArrayList; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@SpringBootTest +@AutoConfigureMockMvc +class SecurityConfigTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private PasswordEncoder passwordEncoder; + + @MockBean + private AccountService accountService; + + private Account testAccount; + + @BeforeEach + void setUp() { + testAccount = new Account(); + testAccount.setId(1L); + testAccount.setUsername("testuser"); + testAccount.setPassword("encodedPassword"); + testAccount.setBalance(new BigDecimal("1000.00")); + testAccount.setTransactions(new ArrayList<>()); + } + + @Test + void passwordEncoder_ReturnsBCryptPasswordEncoder() { + assertNotNull(passwordEncoder); + assertTrue(passwordEncoder instanceof BCryptPasswordEncoder); + } + + @Test + void passwordEncoder_EncodesPassword() { + String rawPassword = "testPassword123"; + String encodedPassword = passwordEncoder.encode(rawPassword); + + assertNotNull(encodedPassword); + assertNotEquals(rawPassword, encodedPassword); + assertTrue(passwordEncoder.matches(rawPassword, encodedPassword)); + } + + @Test + void passwordEncoder_DifferentEncodingsForSamePassword() { + String rawPassword = "testPassword123"; + String encodedPassword1 = passwordEncoder.encode(rawPassword); + String encodedPassword2 = passwordEncoder.encode(rawPassword); + + assertNotEquals(encodedPassword1, encodedPassword2); + assertTrue(passwordEncoder.matches(rawPassword, encodedPassword1)); + assertTrue(passwordEncoder.matches(rawPassword, encodedPassword2)); + } + + @Test + void registerEndpoint_IsPublic() throws Exception { + mockMvc.perform(get("/register")) + .andExpect(status().isOk()); + } + + @Test + void loginEndpoint_IsPublic() throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isOk()); + } + + @Test + void dashboardEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(get("/dashboard")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void transactionsEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(get("/transactions")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void depositEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/deposit") + .with(csrf()) + .param("amount", "100.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void withdrawEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/withdraw") + .with(csrf()) + .param("amount", "100.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void transferEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/transfer") + .with(csrf()) + .param("toUsername", "recipient") + .param("amount", "100.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + @WithMockUser(username = "testuser") + void authenticatedUser_CanAccessDashboard() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "testuser") + void authenticatedUser_CanAccessTransactions() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + when(accountService.getTransactionHistory(testAccount)).thenReturn(new ArrayList<>()); + + mockMvc.perform(get("/transactions")) + .andExpect(status().isOk()); + } + + @Test + void logout_RedirectsToLoginWithLogoutParam() throws Exception { + mockMvc.perform(post("/logout") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login?logout")); + } + + @Test + void loginPage_HasCorrectUrl() throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(view().name("login")); + } + + @Test + void registerPost_WithValidData_RedirectsToLogin() throws Exception { + mockMvc.perform(post("/register") + .with(csrf()) + .param("username", "newuser") + .param("password", "password123")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login")); + } +} diff --git a/src/test/java/com/example/bankapp/controller/BankControllerTest.java b/src/test/java/com/example/bankapp/controller/BankControllerTest.java new file mode 100644 index 00000000..f8aa1f59 --- /dev/null +++ b/src/test/java/com/example/bankapp/controller/BankControllerTest.java @@ -0,0 +1,300 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.config.SecurityConfig; +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.service.AccountService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(BankController.class) +@Import(SecurityConfig.class) +class BankControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AccountService accountService; + + private Account testAccount; + + @BeforeEach + void setUp() { + testAccount = new Account(); + testAccount.setId(1L); + testAccount.setUsername("testuser"); + testAccount.setPassword("encodedPassword"); + testAccount.setBalance(new BigDecimal("1000.00")); + testAccount.setTransactions(new ArrayList<>()); + } + + @Test + @WithMockUser(username = "testuser") + void dashboard_Success() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("account")) + .andExpect(model().attribute("account", testAccount)); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + } + + @Test + void dashboard_Unauthenticated() throws Exception { + mockMvc.perform(get("/dashboard")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void showRegistrationForm_Success() throws Exception { + mockMvc.perform(get("/register")) + .andExpect(status().isOk()) + .andExpect(view().name("register")); + } + + @Test + void registerAccount_Success() throws Exception { + when(accountService.registerAccount("newuser", "password123")).thenReturn(testAccount); + + mockMvc.perform(post("/register") + .with(csrf()) + .param("username", "newuser") + .param("password", "password123")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login")); + + verify(accountService, times(1)).registerAccount("newuser", "password123"); + } + + @Test + void registerAccount_UsernameExists() throws Exception { + when(accountService.registerAccount("existinguser", "password123")) + .thenThrow(new RuntimeException("Username already exists")); + + mockMvc.perform(post("/register") + .with(csrf()) + .param("username", "existinguser") + .param("password", "password123")) + .andExpect(status().isOk()) + .andExpect(view().name("register")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attribute("error", "Username already exists")); + + verify(accountService, times(1)).registerAccount("existinguser", "password123"); + } + + @Test + void login_Success() throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(view().name("login")); + } + + @Test + @WithMockUser(username = "testuser") + void deposit_Success() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).deposit(any(Account.class), any(BigDecimal.class)); + + mockMvc.perform(post("/deposit") + .with(csrf()) + .param("amount", "500.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).deposit(eq(testAccount), eq(new BigDecimal("500.00"))); + } + + @Test + void deposit_Unauthenticated() throws Exception { + mockMvc.perform(post("/deposit") + .with(csrf()) + .param("amount", "500.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + @WithMockUser(username = "testuser") + void withdraw_Success() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).withdraw(any(Account.class), any(BigDecimal.class)); + + mockMvc.perform(post("/withdraw") + .with(csrf()) + .param("amount", "300.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).withdraw(eq(testAccount), eq(new BigDecimal("300.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void withdraw_InsufficientFunds() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doThrow(new RuntimeException("Insufficient funds")) + .when(accountService).withdraw(any(Account.class), any(BigDecimal.class)); + + mockMvc.perform(post("/withdraw") + .with(csrf()) + .param("amount", "5000.00")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attribute("error", "Insufficient funds")) + .andExpect(model().attributeExists("account")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).withdraw(eq(testAccount), eq(new BigDecimal("5000.00"))); + } + + @Test + void withdraw_Unauthenticated() throws Exception { + mockMvc.perform(post("/withdraw") + .with(csrf()) + .param("amount", "300.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + @WithMockUser(username = "testuser") + void transactionHistory_Success() throws Exception { + List transactions = new ArrayList<>(); + Transaction t1 = new Transaction(new BigDecimal("100.00"), "Deposit", LocalDateTime.now(), testAccount); + Transaction t2 = new Transaction(new BigDecimal("50.00"), "Withdrawal", LocalDateTime.now(), testAccount); + transactions.add(t1); + transactions.add(t2); + + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + when(accountService.getTransactionHistory(testAccount)).thenReturn(transactions); + + mockMvc.perform(get("/transactions")) + .andExpect(status().isOk()) + .andExpect(view().name("transactions")) + .andExpect(model().attributeExists("transactions")) + .andExpect(model().attribute("transactions", transactions)); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).getTransactionHistory(testAccount); + } + + @Test + @WithMockUser(username = "testuser") + void transactionHistory_Empty() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + when(accountService.getTransactionHistory(testAccount)).thenReturn(new ArrayList<>()); + + mockMvc.perform(get("/transactions")) + .andExpect(status().isOk()) + .andExpect(view().name("transactions")) + .andExpect(model().attributeExists("transactions")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).getTransactionHistory(testAccount); + } + + @Test + void transactionHistory_Unauthenticated() throws Exception { + mockMvc.perform(get("/transactions")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + @WithMockUser(username = "testuser") + void transfer_Success() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).transferAmount(any(Account.class), anyString(), any(BigDecimal.class)); + + mockMvc.perform(post("/transfer") + .with(csrf()) + .param("toUsername", "recipient") + .param("amount", "200.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).transferAmount(eq(testAccount), eq("recipient"), eq(new BigDecimal("200.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void transfer_InsufficientFunds() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doThrow(new RuntimeException("Insufficient funds")) + .when(accountService).transferAmount(any(Account.class), anyString(), any(BigDecimal.class)); + + mockMvc.perform(post("/transfer") + .with(csrf()) + .param("toUsername", "recipient") + .param("amount", "5000.00")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attribute("error", "Insufficient funds")) + .andExpect(model().attributeExists("account")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).transferAmount(eq(testAccount), eq("recipient"), eq(new BigDecimal("5000.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void transfer_RecipientNotFound() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doThrow(new RuntimeException("Recipient account not found")) + .when(accountService).transferAmount(any(Account.class), anyString(), any(BigDecimal.class)); + + mockMvc.perform(post("/transfer") + .with(csrf()) + .param("toUsername", "nonexistent") + .param("amount", "200.00")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attribute("error", "Recipient account not found")) + .andExpect(model().attributeExists("account")); + + verify(accountService, times(1)).findAccountByUsername("testuser"); + verify(accountService, times(1)).transferAmount(eq(testAccount), eq("nonexistent"), eq(new BigDecimal("200.00"))); + } + + @Test + void transfer_Unauthenticated() throws Exception { + mockMvc.perform(post("/transfer") + .with(csrf()) + .param("toUsername", "recipient") + .param("amount", "200.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } +} diff --git a/src/test/java/com/example/bankapp/model/AccountTest.java b/src/test/java/com/example/bankapp/model/AccountTest.java new file mode 100644 index 00000000..f54b572e --- /dev/null +++ b/src/test/java/com/example/bankapp/model/AccountTest.java @@ -0,0 +1,103 @@ +package com.example.bankapp.model; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class AccountTest { + + @Test + void testDefaultConstructor() { + Account account = new Account(); + assertNull(account.getId()); + assertNull(account.getUsername()); + assertNull(account.getPassword()); + assertNull(account.getBalance()); + assertNull(account.getTransactions()); + assertNull(account.getAuthorities()); + } + + @Test + void testParameterizedConstructor() { + List transactions = new ArrayList<>(); + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("USER")); + + Account account = new Account("testuser", "password123", new BigDecimal("1000.00"), transactions, authorities); + + assertEquals("testuser", account.getUsername()); + assertEquals("password123", account.getPassword()); + assertEquals(new BigDecimal("1000.00"), account.getBalance()); + assertEquals(transactions, account.getTransactions()); + assertEquals(authorities, account.getAuthorities()); + } + + @Test + void testSetAndGetId() { + Account account = new Account(); + account.setId(1L); + assertEquals(1L, account.getId()); + } + + @Test + void testSetAndGetUsername() { + Account account = new Account(); + account.setUsername("testuser"); + assertEquals("testuser", account.getUsername()); + } + + @Test + void testSetAndGetPassword() { + Account account = new Account(); + account.setPassword("password123"); + assertEquals("password123", account.getPassword()); + } + + @Test + void testSetAndGetBalance() { + Account account = new Account(); + BigDecimal balance = new BigDecimal("500.50"); + account.setBalance(balance); + assertEquals(balance, account.getBalance()); + } + + @Test + void testSetAndGetTransactions() { + Account account = new Account(); + List transactions = new ArrayList<>(); + Transaction transaction = new Transaction(); + transactions.add(transaction); + account.setTransactions(transactions); + assertEquals(transactions, account.getTransactions()); + assertEquals(1, account.getTransactions().size()); + } + + @Test + void testSetAndGetAuthorities() { + Account account = new Account(); + Collection authorities = Arrays.asList( + new SimpleGrantedAuthority("USER"), + new SimpleGrantedAuthority("ADMIN") + ); + account.setAuthorities(authorities); + assertEquals(authorities, account.getAuthorities()); + assertEquals(2, account.getAuthorities().size()); + } + + @Test + void testUserDetailsImplementation() { + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("USER")); + Account account = new Account("testuser", "password123", new BigDecimal("1000.00"), null, authorities); + + assertEquals("testuser", account.getUsername()); + assertEquals("password123", account.getPassword()); + assertEquals(authorities, account.getAuthorities()); + } +} diff --git a/src/test/java/com/example/bankapp/model/TransactionTest.java b/src/test/java/com/example/bankapp/model/TransactionTest.java new file mode 100644 index 00000000..a7d0ca13 --- /dev/null +++ b/src/test/java/com/example/bankapp/model/TransactionTest.java @@ -0,0 +1,128 @@ +package com.example.bankapp.model; + +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class TransactionTest { + + @Test + void testDefaultConstructor() { + Transaction transaction = new Transaction(); + assertNull(transaction.getId()); + assertNull(transaction.getAmount()); + assertNull(transaction.getType()); + assertNull(transaction.getTimestamp()); + assertNull(transaction.getAccount()); + } + + @Test + void testParameterizedConstructor() { + Account account = new Account(); + account.setId(1L); + account.setUsername("testuser"); + + BigDecimal amount = new BigDecimal("100.00"); + String type = "Deposit"; + LocalDateTime timestamp = LocalDateTime.now(); + + Transaction transaction = new Transaction(amount, type, timestamp, account); + + assertEquals(amount, transaction.getAmount()); + assertEquals(type, transaction.getType()); + assertEquals(timestamp, transaction.getTimestamp()); + assertEquals(account, transaction.getAccount()); + } + + @Test + void testSetAndGetId() { + Transaction transaction = new Transaction(); + transaction.setId(1L); + assertEquals(1L, transaction.getId()); + } + + @Test + void testSetAndGetAmount() { + Transaction transaction = new Transaction(); + BigDecimal amount = new BigDecimal("250.75"); + transaction.setAmount(amount); + assertEquals(amount, transaction.getAmount()); + } + + @Test + void testSetAndGetType() { + Transaction transaction = new Transaction(); + transaction.setType("Withdrawal"); + assertEquals("Withdrawal", transaction.getType()); + } + + @Test + void testSetAndGetTimestamp() { + Transaction transaction = new Transaction(); + LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + transaction.setTimestamp(timestamp); + assertEquals(timestamp, transaction.getTimestamp()); + } + + @Test + void testSetAndGetAccount() { + Transaction transaction = new Transaction(); + Account account = new Account(); + account.setId(1L); + account.setUsername("testuser"); + transaction.setAccount(account); + assertEquals(account, transaction.getAccount()); + assertEquals("testuser", transaction.getAccount().getUsername()); + } + + @Test + void testDepositTransaction() { + Account account = new Account(); + account.setUsername("depositor"); + + Transaction transaction = new Transaction( + new BigDecimal("500.00"), + "Deposit", + LocalDateTime.now(), + account + ); + + assertEquals("Deposit", transaction.getType()); + assertEquals(new BigDecimal("500.00"), transaction.getAmount()); + } + + @Test + void testWithdrawalTransaction() { + Account account = new Account(); + account.setUsername("withdrawer"); + + Transaction transaction = new Transaction( + new BigDecimal("200.00"), + "Withdrawal", + LocalDateTime.now(), + account + ); + + assertEquals("Withdrawal", transaction.getType()); + assertEquals(new BigDecimal("200.00"), transaction.getAmount()); + } + + @Test + void testTransferTransaction() { + Account account = new Account(); + account.setUsername("sender"); + + Transaction transaction = new Transaction( + new BigDecimal("300.00"), + "Transfer Out to recipient", + LocalDateTime.now(), + account + ); + + assertEquals("Transfer Out to recipient", transaction.getType()); + assertEquals(new BigDecimal("300.00"), transaction.getAmount()); + } +} diff --git a/src/test/java/com/example/bankapp/repository/AccountRepositoryTest.java b/src/test/java/com/example/bankapp/repository/AccountRepositoryTest.java new file mode 100644 index 00000000..bf50687b --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/AccountRepositoryTest.java @@ -0,0 +1,178 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Account; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class AccountRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private AccountRepository accountRepository; + + private Account testAccount; + + @BeforeEach + void setUp() { + testAccount = new Account(); + testAccount.setUsername("testuser"); + testAccount.setPassword("encodedPassword"); + testAccount.setBalance(new BigDecimal("1000.00")); + } + + @Test + void findByUsername_Success() { + entityManager.persist(testAccount); + entityManager.flush(); + + Optional found = accountRepository.findByUsername("testuser"); + + assertTrue(found.isPresent()); + assertEquals("testuser", found.get().getUsername()); + assertEquals("encodedPassword", found.get().getPassword()); + assertEquals(new BigDecimal("1000.00"), found.get().getBalance()); + } + + @Test + void findByUsername_NotFound() { + Optional found = accountRepository.findByUsername("nonexistent"); + + assertFalse(found.isPresent()); + } + + @Test + void findByUsername_MultipleAccounts() { + Account account1 = new Account(); + account1.setUsername("user1"); + account1.setPassword("password1"); + account1.setBalance(new BigDecimal("500.00")); + + Account account2 = new Account(); + account2.setUsername("user2"); + account2.setPassword("password2"); + account2.setBalance(new BigDecimal("750.00")); + + entityManager.persist(account1); + entityManager.persist(account2); + entityManager.flush(); + + Optional foundUser1 = accountRepository.findByUsername("user1"); + Optional foundUser2 = accountRepository.findByUsername("user2"); + + assertTrue(foundUser1.isPresent()); + assertTrue(foundUser2.isPresent()); + assertEquals("user1", foundUser1.get().getUsername()); + assertEquals("user2", foundUser2.get().getUsername()); + assertEquals(new BigDecimal("500.00"), foundUser1.get().getBalance()); + assertEquals(new BigDecimal("750.00"), foundUser2.get().getBalance()); + } + + @Test + void save_NewAccount() { + Account savedAccount = accountRepository.save(testAccount); + + assertNotNull(savedAccount.getId()); + assertEquals("testuser", savedAccount.getUsername()); + assertEquals("encodedPassword", savedAccount.getPassword()); + assertEquals(new BigDecimal("1000.00"), savedAccount.getBalance()); + } + + @Test + void save_UpdateAccount() { + entityManager.persist(testAccount); + entityManager.flush(); + + testAccount.setBalance(new BigDecimal("1500.00")); + Account updatedAccount = accountRepository.save(testAccount); + + assertEquals(new BigDecimal("1500.00"), updatedAccount.getBalance()); + } + + @Test + void findById_Success() { + entityManager.persist(testAccount); + entityManager.flush(); + + Optional found = accountRepository.findById(testAccount.getId()); + + assertTrue(found.isPresent()); + assertEquals(testAccount.getId(), found.get().getId()); + assertEquals("testuser", found.get().getUsername()); + } + + @Test + void findById_NotFound() { + Optional found = accountRepository.findById(999L); + + assertFalse(found.isPresent()); + } + + @Test + void delete_Account() { + entityManager.persist(testAccount); + entityManager.flush(); + + Long accountId = testAccount.getId(); + accountRepository.delete(testAccount); + entityManager.flush(); + + Optional found = accountRepository.findById(accountId); + assertFalse(found.isPresent()); + } + + @Test + void findAll_Empty() { + assertTrue(accountRepository.findAll().isEmpty()); + } + + @Test + void findAll_MultipleAccounts() { + Account account1 = new Account(); + account1.setUsername("user1"); + account1.setPassword("password1"); + account1.setBalance(new BigDecimal("500.00")); + + Account account2 = new Account(); + account2.setUsername("user2"); + account2.setPassword("password2"); + account2.setBalance(new BigDecimal("750.00")); + + entityManager.persist(account1); + entityManager.persist(account2); + entityManager.flush(); + + assertEquals(2, accountRepository.findAll().size()); + } + + @Test + void count_Accounts() { + entityManager.persist(testAccount); + entityManager.flush(); + + assertEquals(1, accountRepository.count()); + } + + @Test + void existsById_True() { + entityManager.persist(testAccount); + entityManager.flush(); + + assertTrue(accountRepository.existsById(testAccount.getId())); + } + + @Test + void existsById_False() { + assertFalse(accountRepository.existsById(999L)); + } +} diff --git a/src/test/java/com/example/bankapp/repository/TransactionRepositoryTest.java b/src/test/java/com/example/bankapp/repository/TransactionRepositoryTest.java new file mode 100644 index 00000000..834d467e --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/TransactionRepositoryTest.java @@ -0,0 +1,250 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +class TransactionRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private TransactionRepository transactionRepository; + + private Account testAccount; + private Transaction testTransaction; + + @BeforeEach + void setUp() { + testAccount = new Account(); + testAccount.setUsername("testuser"); + testAccount.setPassword("encodedPassword"); + testAccount.setBalance(new BigDecimal("1000.00")); + entityManager.persist(testAccount); + + testTransaction = new Transaction(); + testTransaction.setAmount(new BigDecimal("100.00")); + testTransaction.setType("Deposit"); + testTransaction.setTimestamp(LocalDateTime.now()); + testTransaction.setAccount(testAccount); + } + + @Test + void findByAccountId_Success() { + entityManager.persist(testTransaction); + entityManager.flush(); + + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertFalse(transactions.isEmpty()); + assertEquals(1, transactions.size()); + assertEquals(new BigDecimal("100.00"), transactions.get(0).getAmount()); + assertEquals("Deposit", transactions.get(0).getType()); + } + + @Test + void findByAccountId_MultipleTransactions() { + Transaction t1 = new Transaction(new BigDecimal("100.00"), "Deposit", LocalDateTime.now(), testAccount); + Transaction t2 = new Transaction(new BigDecimal("50.00"), "Withdrawal", LocalDateTime.now(), testAccount); + Transaction t3 = new Transaction(new BigDecimal("200.00"), "Transfer In from sender", LocalDateTime.now(), testAccount); + + entityManager.persist(t1); + entityManager.persist(t2); + entityManager.persist(t3); + entityManager.flush(); + + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertEquals(3, transactions.size()); + } + + @Test + void findByAccountId_NoTransactions() { + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertTrue(transactions.isEmpty()); + } + + @Test + void findByAccountId_DifferentAccounts() { + Account account2 = new Account(); + account2.setUsername("user2"); + account2.setPassword("password2"); + account2.setBalance(new BigDecimal("500.00")); + entityManager.persist(account2); + + Transaction t1 = new Transaction(new BigDecimal("100.00"), "Deposit", LocalDateTime.now(), testAccount); + Transaction t2 = new Transaction(new BigDecimal("200.00"), "Deposit", LocalDateTime.now(), account2); + + entityManager.persist(t1); + entityManager.persist(t2); + entityManager.flush(); + + List transactionsAccount1 = transactionRepository.findByAccountId(testAccount.getId()); + List transactionsAccount2 = transactionRepository.findByAccountId(account2.getId()); + + assertEquals(1, transactionsAccount1.size()); + assertEquals(1, transactionsAccount2.size()); + assertEquals(new BigDecimal("100.00"), transactionsAccount1.get(0).getAmount()); + assertEquals(new BigDecimal("200.00"), transactionsAccount2.get(0).getAmount()); + } + + @Test + void findByAccountId_NonExistentAccount() { + List transactions = transactionRepository.findByAccountId(999L); + + assertTrue(transactions.isEmpty()); + } + + @Test + void save_NewTransaction() { + Transaction savedTransaction = transactionRepository.save(testTransaction); + + assertNotNull(savedTransaction.getId()); + assertEquals(new BigDecimal("100.00"), savedTransaction.getAmount()); + assertEquals("Deposit", savedTransaction.getType()); + assertEquals(testAccount, savedTransaction.getAccount()); + } + + @Test + void save_UpdateTransaction() { + entityManager.persist(testTransaction); + entityManager.flush(); + + testTransaction.setAmount(new BigDecimal("150.00")); + Transaction updatedTransaction = transactionRepository.save(testTransaction); + + assertEquals(new BigDecimal("150.00"), updatedTransaction.getAmount()); + } + + @Test + void findById_Success() { + entityManager.persist(testTransaction); + entityManager.flush(); + + Optional found = transactionRepository.findById(testTransaction.getId()); + + assertTrue(found.isPresent()); + assertEquals(testTransaction.getId(), found.get().getId()); + assertEquals("Deposit", found.get().getType()); + } + + @Test + void findById_NotFound() { + Optional found = transactionRepository.findById(999L); + + assertFalse(found.isPresent()); + } + + @Test + void delete_Transaction() { + entityManager.persist(testTransaction); + entityManager.flush(); + + Long transactionId = testTransaction.getId(); + transactionRepository.delete(testTransaction); + entityManager.flush(); + + Optional found = transactionRepository.findById(transactionId); + assertFalse(found.isPresent()); + } + + @Test + void findAll_Empty() { + assertTrue(transactionRepository.findAll().isEmpty()); + } + + @Test + void findAll_MultipleTransactions() { + Transaction t1 = new Transaction(new BigDecimal("100.00"), "Deposit", LocalDateTime.now(), testAccount); + Transaction t2 = new Transaction(new BigDecimal("50.00"), "Withdrawal", LocalDateTime.now(), testAccount); + + entityManager.persist(t1); + entityManager.persist(t2); + entityManager.flush(); + + assertEquals(2, transactionRepository.findAll().size()); + } + + @Test + void count_Transactions() { + entityManager.persist(testTransaction); + entityManager.flush(); + + assertEquals(1, transactionRepository.count()); + } + + @Test + void existsById_True() { + entityManager.persist(testTransaction); + entityManager.flush(); + + assertTrue(transactionRepository.existsById(testTransaction.getId())); + } + + @Test + void existsById_False() { + assertFalse(transactionRepository.existsById(999L)); + } + + @Test + void saveDepositTransaction() { + Transaction deposit = new Transaction( + new BigDecimal("500.00"), + "Deposit", + LocalDateTime.now(), + testAccount + ); + + Transaction saved = transactionRepository.save(deposit); + + assertNotNull(saved.getId()); + assertEquals("Deposit", saved.getType()); + assertEquals(new BigDecimal("500.00"), saved.getAmount()); + } + + @Test + void saveWithdrawalTransaction() { + Transaction withdrawal = new Transaction( + new BigDecimal("200.00"), + "Withdrawal", + LocalDateTime.now(), + testAccount + ); + + Transaction saved = transactionRepository.save(withdrawal); + + assertNotNull(saved.getId()); + assertEquals("Withdrawal", saved.getType()); + assertEquals(new BigDecimal("200.00"), saved.getAmount()); + } + + @Test + void saveTransferTransaction() { + Transaction transfer = new Transaction( + new BigDecimal("300.00"), + "Transfer Out to recipient", + LocalDateTime.now(), + testAccount + ); + + Transaction saved = transactionRepository.save(transfer); + + assertNotNull(saved.getId()); + assertTrue(saved.getType().contains("Transfer")); + assertEquals(new BigDecimal("300.00"), saved.getAmount()); + } +} diff --git a/src/test/java/com/example/bankapp/service/AccountServiceTest.java b/src/test/java/com/example/bankapp/service/AccountServiceTest.java new file mode 100644 index 00000000..cecafb53 --- /dev/null +++ b/src/test/java/com/example/bankapp/service/AccountServiceTest.java @@ -0,0 +1,366 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.repository.AccountRepository; +import com.example.bankapp.repository.TransactionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class AccountServiceTest { + + @Mock + private AccountRepository accountRepository; + + @Mock + private TransactionRepository transactionRepository; + + @Mock + private PasswordEncoder passwordEncoder; + + @InjectMocks + private AccountService accountService; + + private Account testAccount; + + @BeforeEach + void setUp() { + testAccount = new Account(); + testAccount.setId(1L); + testAccount.setUsername("testuser"); + testAccount.setPassword("encodedPassword"); + testAccount.setBalance(new BigDecimal("1000.00")); + testAccount.setTransactions(new ArrayList<>()); + } + + @Test + void findAccountByUsername_Success() { + when(accountRepository.findByUsername("testuser")).thenReturn(Optional.of(testAccount)); + + Account result = accountService.findAccountByUsername("testuser"); + + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + assertEquals(new BigDecimal("1000.00"), result.getBalance()); + verify(accountRepository, times(1)).findByUsername("testuser"); + } + + @Test + void findAccountByUsername_NotFound() { + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.findAccountByUsername("nonexistent"); + }); + + assertEquals("Account not found", exception.getMessage()); + verify(accountRepository, times(1)).findByUsername("nonexistent"); + } + + @Test + void registerAccount_Success() { + when(accountRepository.findByUsername("newuser")).thenReturn(Optional.empty()); + when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); + when(accountRepository.save(any(Account.class))).thenAnswer(invocation -> { + Account savedAccount = invocation.getArgument(0); + savedAccount.setId(2L); + return savedAccount; + }); + + Account result = accountService.registerAccount("newuser", "password123"); + + assertNotNull(result); + assertEquals("newuser", result.getUsername()); + assertEquals("encodedPassword", result.getPassword()); + assertEquals(BigDecimal.ZERO, result.getBalance()); + verify(accountRepository, times(1)).findByUsername("newuser"); + verify(passwordEncoder, times(1)).encode("password123"); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + void registerAccount_UsernameAlreadyExists() { + when(accountRepository.findByUsername("existinguser")).thenReturn(Optional.of(testAccount)); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.registerAccount("existinguser", "password123"); + }); + + assertEquals("Username already exists", exception.getMessage()); + verify(accountRepository, times(1)).findByUsername("existinguser"); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + void deposit_Success() { + BigDecimal depositAmount = new BigDecimal("500.00"); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + accountService.deposit(testAccount, depositAmount); + + assertEquals(new BigDecimal("1500.00"), testAccount.getBalance()); + verify(accountRepository, times(1)).save(testAccount); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + void deposit_VerifyTransactionCreated() { + BigDecimal depositAmount = new BigDecimal("250.00"); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> { + Transaction savedTransaction = invocation.getArgument(0); + assertEquals(depositAmount, savedTransaction.getAmount()); + assertEquals("Deposit", savedTransaction.getType()); + assertEquals(testAccount, savedTransaction.getAccount()); + assertNotNull(savedTransaction.getTimestamp()); + return savedTransaction; + }); + + accountService.deposit(testAccount, depositAmount); + + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + void withdraw_Success() { + BigDecimal withdrawAmount = new BigDecimal("300.00"); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + accountService.withdraw(testAccount, withdrawAmount); + + assertEquals(new BigDecimal("700.00"), testAccount.getBalance()); + verify(accountRepository, times(1)).save(testAccount); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + void withdraw_InsufficientFunds() { + BigDecimal withdrawAmount = new BigDecimal("1500.00"); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.withdraw(testAccount, withdrawAmount); + }); + + assertEquals("Insufficient funds", exception.getMessage()); + assertEquals(new BigDecimal("1000.00"), testAccount.getBalance()); + verify(accountRepository, never()).save(any(Account.class)); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + void withdraw_ExactBalance() { + BigDecimal withdrawAmount = new BigDecimal("1000.00"); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + accountService.withdraw(testAccount, withdrawAmount); + + assertEquals(BigDecimal.ZERO.setScale(2), testAccount.getBalance().setScale(2)); + verify(accountRepository, times(1)).save(testAccount); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + void withdraw_VerifyTransactionCreated() { + BigDecimal withdrawAmount = new BigDecimal("200.00"); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> { + Transaction savedTransaction = invocation.getArgument(0); + assertEquals(withdrawAmount, savedTransaction.getAmount()); + assertEquals("Withdrawal", savedTransaction.getType()); + assertEquals(testAccount, savedTransaction.getAccount()); + assertNotNull(savedTransaction.getTimestamp()); + return savedTransaction; + }); + + accountService.withdraw(testAccount, withdrawAmount); + + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + void getTransactionHistory_Success() { + List transactions = new ArrayList<>(); + Transaction t1 = new Transaction(new BigDecimal("100.00"), "Deposit", null, testAccount); + Transaction t2 = new Transaction(new BigDecimal("50.00"), "Withdrawal", null, testAccount); + transactions.add(t1); + transactions.add(t2); + + when(transactionRepository.findByAccountId(1L)).thenReturn(transactions); + + List result = accountService.getTransactionHistory(testAccount); + + assertNotNull(result); + assertEquals(2, result.size()); + verify(transactionRepository, times(1)).findByAccountId(1L); + } + + @Test + void getTransactionHistory_Empty() { + when(transactionRepository.findByAccountId(1L)).thenReturn(new ArrayList<>()); + + List result = accountService.getTransactionHistory(testAccount); + + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(transactionRepository, times(1)).findByAccountId(1L); + } + + @Test + void loadUserByUsername_Success() { + when(accountRepository.findByUsername("testuser")).thenReturn(Optional.of(testAccount)); + + UserDetails result = accountService.loadUserByUsername("testuser"); + + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + assertEquals("encodedPassword", result.getPassword()); + assertNotNull(result.getAuthorities()); + verify(accountRepository, times(1)).findByUsername("testuser"); + } + + @Test + void loadUserByUsername_NotFound() { + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> { + accountService.loadUserByUsername("nonexistent"); + }); + + verify(accountRepository, times(1)).findByUsername("nonexistent"); + } + + @Test + void authorities_ReturnsUserAuthority() { + Collection authorities = accountService.authorities(); + + assertNotNull(authorities); + assertEquals(1, authorities.size()); + assertTrue(authorities.stream().anyMatch(a -> a.getAuthority().equals("USER"))); + } + + @Test + void transferAmount_Success() { + Account toAccount = new Account(); + toAccount.setId(2L); + toAccount.setUsername("recipient"); + toAccount.setBalance(new BigDecimal("500.00")); + + BigDecimal transferAmount = new BigDecimal("200.00"); + + when(accountRepository.findByUsername("recipient")).thenReturn(Optional.of(toAccount)); + when(accountRepository.save(any(Account.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + accountService.transferAmount(testAccount, "recipient", transferAmount); + + assertEquals(new BigDecimal("800.00"), testAccount.getBalance()); + assertEquals(new BigDecimal("700.00"), toAccount.getBalance()); + verify(accountRepository, times(2)).save(any(Account.class)); + verify(transactionRepository, times(2)).save(any(Transaction.class)); + } + + @Test + void transferAmount_InsufficientFunds() { + BigDecimal transferAmount = new BigDecimal("1500.00"); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.transferAmount(testAccount, "recipient", transferAmount); + }); + + assertEquals("Insufficient funds", exception.getMessage()); + assertEquals(new BigDecimal("1000.00"), testAccount.getBalance()); + verify(accountRepository, never()).save(any(Account.class)); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + void transferAmount_RecipientNotFound() { + BigDecimal transferAmount = new BigDecimal("200.00"); + + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.transferAmount(testAccount, "nonexistent", transferAmount); + }); + + assertEquals("Recipient account not found", exception.getMessage()); + verify(accountRepository, times(1)).findByUsername("nonexistent"); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + void transferAmount_VerifyTransactionsCreated() { + Account toAccount = new Account(); + toAccount.setId(2L); + toAccount.setUsername("recipient"); + toAccount.setBalance(new BigDecimal("500.00")); + + BigDecimal transferAmount = new BigDecimal("100.00"); + + when(accountRepository.findByUsername("recipient")).thenReturn(Optional.of(toAccount)); + when(accountRepository.save(any(Account.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + List savedTransactions = new ArrayList<>(); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> { + Transaction t = invocation.getArgument(0); + savedTransactions.add(t); + return t; + }); + + accountService.transferAmount(testAccount, "recipient", transferAmount); + + assertEquals(2, savedTransactions.size()); + + Transaction debitTransaction = savedTransactions.get(0); + assertEquals(transferAmount, debitTransaction.getAmount()); + assertTrue(debitTransaction.getType().contains("Transfer Out")); + assertEquals(testAccount, debitTransaction.getAccount()); + + Transaction creditTransaction = savedTransactions.get(1); + assertEquals(transferAmount, creditTransaction.getAmount()); + assertTrue(creditTransaction.getType().contains("Transfer In")); + assertEquals(toAccount, creditTransaction.getAccount()); + } + + @Test + void transferAmount_ExactBalance() { + Account toAccount = new Account(); + toAccount.setId(2L); + toAccount.setUsername("recipient"); + toAccount.setBalance(new BigDecimal("500.00")); + + BigDecimal transferAmount = new BigDecimal("1000.00"); + + when(accountRepository.findByUsername("recipient")).thenReturn(Optional.of(toAccount)); + when(accountRepository.save(any(Account.class))).thenAnswer(invocation -> invocation.getArgument(0)); + when(transactionRepository.save(any(Transaction.class))).thenAnswer(invocation -> invocation.getArgument(0)); + + accountService.transferAmount(testAccount, "recipient", transferAmount); + + assertEquals(BigDecimal.ZERO.setScale(2), testAccount.getBalance().setScale(2)); + assertEquals(new BigDecimal("1500.00"), toAccount.getBalance()); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..1d443813 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,11 @@ +# H2 Database Configuration for Testing +spring.datasource.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=false + +# Disable Thymeleaf caching for tests +spring.thymeleaf.cache=false