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..07848dec --- /dev/null +++ b/src/test/java/com/example/bankapp/config/SecurityConfigTest.java @@ -0,0 +1,150 @@ +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.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.ArgumentMatchers.anyString; +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); + String encoded = passwordEncoder.encode("testPassword"); + assertTrue(passwordEncoder.matches("testPassword", encoded)); + } + + @Test + void passwordEncoder_EncodesPasswordDifferentlyEachTime() { + String encoded1 = passwordEncoder.encode("testPassword"); + String encoded2 = passwordEncoder.encode("testPassword"); + assertNotEquals(encoded1, encoded2); + } + + @Test + void registerEndpoint_IsAccessibleWithoutAuthentication() throws Exception { + mockMvc.perform(get("/register")) + .andExpect(status().isOk()); + } + + @Test + void loginEndpoint_IsAccessibleWithoutAuthentication() 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 + @WithMockUser(username = "testuser") + void dashboardEndpoint_IsAccessibleWhenAuthenticated() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()); + } + + @Test + void depositEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/deposit") + .param("amount", "100.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void withdrawEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/withdraw") + .param("amount", "100.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void transferEndpoint_RequiresAuthentication() throws Exception { + mockMvc.perform(post("/transfer") + .param("toUsername", "recipient") + .param("amount", "100.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void logout_InvalidatesSessionAndRedirectsToLogin() throws Exception { + mockMvc.perform(get("/logout")) + .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_IsAccessibleWithoutAuthentication() throws Exception { + mockMvc.perform(post("/register") + .param("username", "newuser") + .param("password", "password123") + .with(csrf())) + .andExpect(status().is3xxRedirection()); + } +} 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..4f4d0626 --- /dev/null +++ b/src/test/java/com/example/bankapp/controller/BankControllerTest.java @@ -0,0 +1,228 @@ +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_WhenAuthenticated_ReturnsAccountInfo() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("account")); + + verify(accountService).findAccountByUsername("testuser"); + } + + @Test + void showRegistrationForm_ReturnsRegisterView() throws Exception { + mockMvc.perform(get("/register")) + .andExpect(status().isOk()) + .andExpect(view().name("register")); + } + + @Test + void registerAccount_WhenSuccessful_RedirectsToLogin() throws Exception { + when(accountService.registerAccount("newuser", "password123")).thenReturn(testAccount); + + mockMvc.perform(post("/register") + .param("username", "newuser") + .param("password", "password123") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login")); + + verify(accountService).registerAccount("newuser", "password123"); + } + + @Test + void registerAccount_WhenUsernameExists_ReturnsRegisterWithError() throws Exception { + when(accountService.registerAccount("existinguser", "password123")) + .thenThrow(new RuntimeException("Username already exists")); + + mockMvc.perform(post("/register") + .param("username", "existinguser") + .param("password", "password123") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("register")) + .andExpect(model().attributeExists("error")); + } + + @Test + void login_ReturnsLoginView() throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isOk()) + .andExpect(view().name("login")); + } + + @Test + @WithMockUser(username = "testuser") + void deposit_WhenAuthenticated_PerformsDepositAndRedirects() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).deposit(any(Account.class), any(BigDecimal.class)); + + mockMvc.perform(post("/deposit") + .param("amount", "500.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).deposit(any(Account.class), eq(new BigDecimal("500.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void withdraw_WhenSufficientFunds_PerformsWithdrawalAndRedirects() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).withdraw(any(Account.class), any(BigDecimal.class)); + + mockMvc.perform(post("/withdraw") + .param("amount", "300.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).withdraw(any(Account.class), eq(new BigDecimal("300.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void withdraw_WhenInsufficientFunds_ReturnsDashboardWithError() 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") + .param("amount", "2000.00") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attributeExists("account")); + } + + @Test + @WithMockUser(username = "testuser") + void transactionHistory_WhenAuthenticated_ReturnsTransactions() throws Exception { + List transactions = new ArrayList<>(); + Transaction transaction = new Transaction(); + transaction.setId(1L); + transaction.setAmount(new BigDecimal("100.00")); + transaction.setType("Deposit"); + transaction.setTimestamp(LocalDateTime.now()); + transactions.add(transaction); + + 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")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).getTransactionHistory(testAccount); + } + + @Test + @WithMockUser(username = "testuser") + void transferAmount_WhenSuccessful_RedirectsToDashboard() throws Exception { + when(accountService.findAccountByUsername("testuser")).thenReturn(testAccount); + doNothing().when(accountService).transferAmount(any(Account.class), anyString(), any(BigDecimal.class)); + + mockMvc.perform(post("/transfer") + .param("toUsername", "recipient") + .param("amount", "200.00") + .with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).transferAmount(any(Account.class), eq("recipient"), eq(new BigDecimal("200.00"))); + } + + @Test + @WithMockUser(username = "testuser") + void transferAmount_WhenInsufficientFunds_ReturnsDashboardWithError() 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") + .param("toUsername", "recipient") + .param("amount", "2000.00") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attributeExists("account")); + } + + @Test + @WithMockUser(username = "testuser") + void transferAmount_WhenRecipientNotFound_ReturnsDashboardWithError() 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") + .param("toUsername", "nonexistent") + .param("amount", "200.00") + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")) + .andExpect(model().attributeExists("account")); + } +} 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..262685fa --- /dev/null +++ b/src/test/java/com/example/bankapp/model/AccountTest.java @@ -0,0 +1,118 @@ +package com.example.bankapp.model; + +import org.junit.jupiter.api.BeforeEach; +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 { + + private Account account; + + @BeforeEach + void setUp() { + account = new Account(); + } + + @Test + void defaultConstructor_CreatesEmptyAccount() { + Account newAccount = new Account(); + assertNotNull(newAccount); + assertNull(newAccount.getId()); + assertNull(newAccount.getUsername()); + assertNull(newAccount.getPassword()); + assertNull(newAccount.getBalance()); + } + + @Test + void parameterizedConstructor_SetsAllFields() { + List transactions = new ArrayList<>(); + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("USER")); + + Account newAccount = new Account("testuser", "password123", new BigDecimal("1000.00"), transactions, authorities); + + assertEquals("testuser", newAccount.getUsername()); + assertEquals("password123", newAccount.getPassword()); + assertEquals(new BigDecimal("1000.00"), newAccount.getBalance()); + assertEquals(transactions, newAccount.getTransactions()); + assertEquals(authorities, newAccount.getAuthorities()); + } + + @Test + void setAndGetId_WorksCorrectly() { + account.setId(1L); + assertEquals(1L, account.getId()); + } + + @Test + void setAndGetUsername_WorksCorrectly() { + account.setUsername("testuser"); + assertEquals("testuser", account.getUsername()); + } + + @Test + void setAndGetPassword_WorksCorrectly() { + account.setPassword("password123"); + assertEquals("password123", account.getPassword()); + } + + @Test + void setAndGetBalance_WorksCorrectly() { + BigDecimal balance = new BigDecimal("500.00"); + account.setBalance(balance); + assertEquals(balance, account.getBalance()); + } + + @Test + void setAndGetTransactions_WorksCorrectly() { + List transactions = new ArrayList<>(); + Transaction transaction = new Transaction(); + transaction.setId(1L); + transactions.add(transaction); + + account.setTransactions(transactions); + assertEquals(transactions, account.getTransactions()); + assertEquals(1, account.getTransactions().size()); + } + + @Test + void setAndGetAuthorities_WorksCorrectly() { + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("USER")); + account.setAuthorities(authorities); + assertEquals(authorities, account.getAuthorities()); + } + + @Test + void getAuthorities_WhenNotSet_ReturnsNull() { + assertNull(account.getAuthorities()); + } + + @Test + void balance_CanBeZero() { + account.setBalance(BigDecimal.ZERO); + assertEquals(BigDecimal.ZERO, account.getBalance()); + } + + @Test + void balance_CanBeNegative() { + BigDecimal negativeBalance = new BigDecimal("-100.00"); + account.setBalance(negativeBalance); + assertEquals(negativeBalance, account.getBalance()); + } + + @Test + void transactions_CanBeEmptyList() { + List emptyTransactions = new ArrayList<>(); + account.setTransactions(emptyTransactions); + assertNotNull(account.getTransactions()); + assertTrue(account.getTransactions().isEmpty()); + } +} 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..70d71f74 --- /dev/null +++ b/src/test/java/com/example/bankapp/model/TransactionTest.java @@ -0,0 +1,130 @@ +package com.example.bankapp.model; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +import static org.junit.jupiter.api.Assertions.*; + +class TransactionTest { + + private Transaction transaction; + + @BeforeEach + void setUp() { + transaction = new Transaction(); + } + + @Test + void defaultConstructor_CreatesEmptyTransaction() { + Transaction newTransaction = new Transaction(); + assertNotNull(newTransaction); + assertNull(newTransaction.getId()); + assertNull(newTransaction.getAmount()); + assertNull(newTransaction.getType()); + assertNull(newTransaction.getTimestamp()); + assertNull(newTransaction.getAccount()); + } + + @Test + void parameterizedConstructor_SetsAllFields() { + Account account = new Account(); + account.setId(1L); + account.setUsername("testuser"); + + BigDecimal amount = new BigDecimal("500.00"); + String type = "Deposit"; + LocalDateTime timestamp = LocalDateTime.now(); + + Transaction newTransaction = new Transaction(amount, type, timestamp, account); + + assertEquals(amount, newTransaction.getAmount()); + assertEquals(type, newTransaction.getType()); + assertEquals(timestamp, newTransaction.getTimestamp()); + assertEquals(account, newTransaction.getAccount()); + } + + @Test + void setAndGetId_WorksCorrectly() { + transaction.setId(1L); + assertEquals(1L, transaction.getId()); + } + + @Test + void setAndGetAmount_WorksCorrectly() { + BigDecimal amount = new BigDecimal("250.50"); + transaction.setAmount(amount); + assertEquals(amount, transaction.getAmount()); + } + + @Test + void setAndGetType_WorksCorrectly() { + transaction.setType("Withdrawal"); + assertEquals("Withdrawal", transaction.getType()); + } + + @Test + void setAndGetTimestamp_WorksCorrectly() { + LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30, 0); + transaction.setTimestamp(timestamp); + assertEquals(timestamp, transaction.getTimestamp()); + } + + @Test + void setAndGetAccount_WorksCorrectly() { + Account account = new Account(); + account.setId(1L); + account.setUsername("testuser"); + + transaction.setAccount(account); + assertEquals(account, transaction.getAccount()); + assertEquals(1L, transaction.getAccount().getId()); + assertEquals("testuser", transaction.getAccount().getUsername()); + } + + @Test + void type_CanBeDeposit() { + transaction.setType("Deposit"); + assertEquals("Deposit", transaction.getType()); + } + + @Test + void type_CanBeWithdrawal() { + transaction.setType("Withdrawal"); + assertEquals("Withdrawal", transaction.getType()); + } + + @Test + void type_CanBeTransferOut() { + transaction.setType("Transfer Out to recipient"); + assertEquals("Transfer Out to recipient", transaction.getType()); + } + + @Test + void type_CanBeTransferIn() { + transaction.setType("Transfer In from sender"); + assertEquals("Transfer In from sender", transaction.getType()); + } + + @Test + void amount_CanBeZero() { + transaction.setAmount(BigDecimal.ZERO); + assertEquals(BigDecimal.ZERO, transaction.getAmount()); + } + + @Test + void amount_CanHaveDecimalPlaces() { + BigDecimal amount = new BigDecimal("123.45"); + transaction.setAmount(amount); + assertEquals(amount, transaction.getAmount()); + } + + @Test + void timestamp_CanBeCurrentTime() { + LocalDateTime now = LocalDateTime.now(); + transaction.setTimestamp(now); + assertNotNull(transaction.getTimestamp()); + } +} 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..4937b8d5 --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/AccountRepositoryTest.java @@ -0,0 +1,131 @@ +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_WhenAccountExists_ReturnsAccount() { + entityManager.persistAndFlush(testAccount); + + Optional found = accountRepository.findByUsername("testuser"); + + assertTrue(found.isPresent()); + assertEquals("testuser", found.get().getUsername()); + assertEquals(new BigDecimal("1000.00"), found.get().getBalance()); + } + + @Test + void findByUsername_WhenAccountNotExists_ReturnsEmpty() { + Optional found = accountRepository.findByUsername("nonexistent"); + + assertFalse(found.isPresent()); + } + + @Test + void save_PersistsNewAccount() { + Account savedAccount = accountRepository.save(testAccount); + + assertNotNull(savedAccount.getId()); + assertEquals("testuser", savedAccount.getUsername()); + } + + @Test + void findById_WhenAccountExists_ReturnsAccount() { + Account persistedAccount = entityManager.persistAndFlush(testAccount); + + Optional found = accountRepository.findById(persistedAccount.getId()); + + assertTrue(found.isPresent()); + assertEquals(persistedAccount.getId(), found.get().getId()); + } + + @Test + void findById_WhenAccountNotExists_ReturnsEmpty() { + Optional found = accountRepository.findById(999L); + + assertFalse(found.isPresent()); + } + + @Test + void delete_RemovesAccount() { + Account persistedAccount = entityManager.persistAndFlush(testAccount); + Long accountId = persistedAccount.getId(); + + accountRepository.delete(persistedAccount); + entityManager.flush(); + + Optional found = accountRepository.findById(accountId); + assertFalse(found.isPresent()); + } + + @Test + void update_ModifiesExistingAccount() { + Account persistedAccount = entityManager.persistAndFlush(testAccount); + + persistedAccount.setBalance(new BigDecimal("2000.00")); + accountRepository.save(persistedAccount); + entityManager.flush(); + + Optional found = accountRepository.findById(persistedAccount.getId()); + assertTrue(found.isPresent()); + assertEquals(new BigDecimal("2000.00"), found.get().getBalance()); + } + + @Test + void findByUsername_IsCaseSensitive() { + entityManager.persistAndFlush(testAccount); + + Optional foundLower = accountRepository.findByUsername("testuser"); + Optional foundUpper = accountRepository.findByUsername("TESTUSER"); + + assertTrue(foundLower.isPresent()); + assertFalse(foundUpper.isPresent()); + } + + @Test + void save_MultipleAccounts_AllPersisted() { + Account account1 = new Account(); + account1.setUsername("user1"); + account1.setPassword("password1"); + account1.setBalance(new BigDecimal("100.00")); + + Account account2 = new Account(); + account2.setUsername("user2"); + account2.setPassword("password2"); + account2.setBalance(new BigDecimal("200.00")); + + accountRepository.save(account1); + accountRepository.save(account2); + entityManager.flush(); + + assertEquals(2, accountRepository.count()); + } +} 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..c2653d52 --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/TransactionRepositoryTest.java @@ -0,0 +1,156 @@ +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 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.persistAndFlush(testAccount); + + testTransaction = new Transaction(); + testTransaction.setAmount(new BigDecimal("100.00")); + testTransaction.setType("Deposit"); + testTransaction.setTimestamp(LocalDateTime.now()); + testTransaction.setAccount(testAccount); + } + + @Test + void findByAccountId_WhenTransactionsExist_ReturnsTransactions() { + entityManager.persistAndFlush(testTransaction); + + List found = transactionRepository.findByAccountId(testAccount.getId()); + + assertFalse(found.isEmpty()); + assertEquals(1, found.size()); + assertEquals("Deposit", found.get(0).getType()); + } + + @Test + void findByAccountId_WhenNoTransactions_ReturnsEmptyList() { + List found = transactionRepository.findByAccountId(testAccount.getId()); + + assertTrue(found.isEmpty()); + } + + @Test + void findByAccountId_WhenAccountNotExists_ReturnsEmptyList() { + List found = transactionRepository.findByAccountId(999L); + + assertTrue(found.isEmpty()); + } + + @Test + void save_PersistsNewTransaction() { + Transaction savedTransaction = transactionRepository.save(testTransaction); + + assertNotNull(savedTransaction.getId()); + assertEquals("Deposit", savedTransaction.getType()); + assertEquals(new BigDecimal("100.00"), savedTransaction.getAmount()); + } + + @Test + void findByAccountId_MultipleTransactions_ReturnsAll() { + Transaction transaction1 = new Transaction(); + transaction1.setAmount(new BigDecimal("100.00")); + transaction1.setType("Deposit"); + transaction1.setTimestamp(LocalDateTime.now()); + transaction1.setAccount(testAccount); + + Transaction transaction2 = new Transaction(); + transaction2.setAmount(new BigDecimal("50.00")); + transaction2.setType("Withdrawal"); + transaction2.setTimestamp(LocalDateTime.now()); + transaction2.setAccount(testAccount); + + entityManager.persistAndFlush(transaction1); + entityManager.persistAndFlush(transaction2); + + List found = transactionRepository.findByAccountId(testAccount.getId()); + + assertEquals(2, found.size()); + } + + @Test + void findByAccountId_OnlyReturnsTransactionsForSpecificAccount() { + Account anotherAccount = new Account(); + anotherAccount.setUsername("anotheruser"); + anotherAccount.setPassword("password"); + anotherAccount.setBalance(new BigDecimal("500.00")); + entityManager.persistAndFlush(anotherAccount); + + Transaction transactionForTestAccount = new Transaction(); + transactionForTestAccount.setAmount(new BigDecimal("100.00")); + transactionForTestAccount.setType("Deposit"); + transactionForTestAccount.setTimestamp(LocalDateTime.now()); + transactionForTestAccount.setAccount(testAccount); + + Transaction transactionForAnotherAccount = new Transaction(); + transactionForAnotherAccount.setAmount(new BigDecimal("200.00")); + transactionForAnotherAccount.setType("Deposit"); + transactionForAnotherAccount.setTimestamp(LocalDateTime.now()); + transactionForAnotherAccount.setAccount(anotherAccount); + + entityManager.persistAndFlush(transactionForTestAccount); + entityManager.persistAndFlush(transactionForAnotherAccount); + + List foundForTestAccount = transactionRepository.findByAccountId(testAccount.getId()); + List foundForAnotherAccount = transactionRepository.findByAccountId(anotherAccount.getId()); + + assertEquals(1, foundForTestAccount.size()); + assertEquals(1, foundForAnotherAccount.size()); + assertEquals(new BigDecimal("100.00"), foundForTestAccount.get(0).getAmount()); + assertEquals(new BigDecimal("200.00"), foundForAnotherAccount.get(0).getAmount()); + } + + @Test + void delete_RemovesTransaction() { + Transaction persistedTransaction = entityManager.persistAndFlush(testTransaction); + Long transactionId = persistedTransaction.getId(); + + transactionRepository.delete(persistedTransaction); + entityManager.flush(); + + assertFalse(transactionRepository.findById(transactionId).isPresent()); + } + + @Test + void count_ReturnsCorrectCount() { + entityManager.persistAndFlush(testTransaction); + + Transaction anotherTransaction = new Transaction(); + anotherTransaction.setAmount(new BigDecimal("200.00")); + anotherTransaction.setType("Withdrawal"); + anotherTransaction.setTimestamp(LocalDateTime.now()); + anotherTransaction.setAccount(testAccount); + entityManager.persistAndFlush(anotherTransaction); + + assertEquals(2, transactionRepository.count()); + } +} 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..7f45bd3a --- /dev/null +++ b/src/test/java/com/example/bankapp/service/AccountServiceTest.java @@ -0,0 +1,246 @@ +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.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.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_WhenAccountExists_ReturnsAccount() { + when(accountRepository.findByUsername("testuser")).thenReturn(Optional.of(testAccount)); + + Account result = accountService.findAccountByUsername("testuser"); + + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + verify(accountRepository).findByUsername("testuser"); + } + + @Test + void findAccountByUsername_WhenAccountNotFound_ThrowsException() { + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.findAccountByUsername("nonexistent"); + }); + + assertEquals("Account not found", exception.getMessage()); + } + + @Test + void registerAccount_WhenUsernameAvailable_CreatesAccount() { + when(accountRepository.findByUsername("newuser")).thenReturn(Optional.empty()); + when(passwordEncoder.encode("password123")).thenReturn("encodedPassword"); + when(accountRepository.save(any(Account.class))).thenAnswer(invocation -> { + Account account = invocation.getArgument(0); + account.setId(1L); + return account; + }); + + Account result = accountService.registerAccount("newuser", "password123"); + + assertNotNull(result); + assertEquals("newuser", result.getUsername()); + assertEquals("encodedPassword", result.getPassword()); + assertEquals(BigDecimal.ZERO, result.getBalance()); + verify(accountRepository).save(any(Account.class)); + } + + @Test + void registerAccount_WhenUsernameExists_ThrowsException() { + 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, never()).save(any(Account.class)); + } + + @Test + void deposit_AddsAmountToBalanceAndCreatesTransaction() { + BigDecimal depositAmount = new BigDecimal("500.00"); + BigDecimal expectedBalance = new BigDecimal("1500.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(expectedBalance, testAccount.getBalance()); + verify(accountRepository).save(testAccount); + verify(transactionRepository).save(any(Transaction.class)); + } + + @Test + void withdraw_WhenSufficientFunds_SubtractsAmountAndCreatesTransaction() { + BigDecimal withdrawAmount = new BigDecimal("300.00"); + BigDecimal expectedBalance = new BigDecimal("700.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(expectedBalance, testAccount.getBalance()); + verify(accountRepository).save(testAccount); + verify(transactionRepository).save(any(Transaction.class)); + } + + @Test + void withdraw_WhenInsufficientFunds_ThrowsException() { + BigDecimal withdrawAmount = new BigDecimal("2000.00"); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.withdraw(testAccount, withdrawAmount); + }); + + assertEquals("Insufficient funds", exception.getMessage()); + verify(accountRepository, never()).save(any(Account.class)); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + void getTransactionHistory_ReturnsTransactionsForAccount() { + List transactions = new ArrayList<>(); + Transaction transaction1 = new Transaction(); + transaction1.setId(1L); + transaction1.setAmount(new BigDecimal("100.00")); + transaction1.setType("Deposit"); + transactions.add(transaction1); + + when(transactionRepository.findByAccountId(1L)).thenReturn(transactions); + + List result = accountService.getTransactionHistory(testAccount); + + assertNotNull(result); + assertEquals(1, result.size()); + assertEquals("Deposit", result.get(0).getType()); + verify(transactionRepository).findByAccountId(1L); + } + + @Test + void loadUserByUsername_WhenUserExists_ReturnsUserDetails() { + 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()); + } + + @Test + void loadUserByUsername_WhenUserNotFound_ThrowsUsernameNotFoundException() { + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, () -> { + accountService.loadUserByUsername("nonexistent"); + }); + } + + @Test + void authorities_ReturnsUserAuthority() { + var authorities = accountService.authorities(); + + assertNotNull(authorities); + assertEquals(1, authorities.size()); + assertTrue(authorities.stream().anyMatch(a -> a.getAuthority().equals("USER"))); + } + + @Test + void transferAmount_WhenSufficientFunds_TransfersSuccessfully() { + Account toAccount = new Account(); + toAccount.setId(2L); + toAccount.setUsername("recipient"); + toAccount.setPassword("encodedPassword"); + 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_WhenInsufficientFunds_ThrowsException() { + BigDecimal transferAmount = new BigDecimal("2000.00"); + + RuntimeException exception = assertThrows(RuntimeException.class, () -> { + accountService.transferAmount(testAccount, "recipient", transferAmount); + }); + + assertEquals("Insufficient funds", exception.getMessage()); + verify(accountRepository, never()).save(any(Account.class)); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + void transferAmount_WhenRecipientNotFound_ThrowsException() { + 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()); + } +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 00000000..35f22750 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,8 @@ +# H2 Database Configuration for Tests +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.h2.console.enabled=false