diff --git a/Jenkinsfile b/Jenkinsfile index 96e1e341..b505b547 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -27,6 +27,14 @@ pipeline { } } } + + stage("Maven: Run Tests") { + steps { + script { + sh 'mvn test' + } + } + } stage("Trivy: Filesystem scan"){ steps{ diff --git a/pom.xml b/pom.xml index fc5bfeac..4573026f 100644 --- a/pom.xml +++ b/pom.xml @@ -67,6 +67,11 @@ spring-security-test test + + com.h2database + h2 + test + @@ -84,6 +89,25 @@ 1.8 + + org.jacoco + jacoco-maven-plugin + 0.8.10 + + + + prepare-agent + + + + report + test + + report + + + + 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..26c699bf --- /dev/null +++ b/src/test/java/com/example/bankapp/config/SecurityConfigTest.java @@ -0,0 +1,147 @@ +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("password"); + testAccount.setBalance(new BigDecimal("1000.00")); + testAccount.setTransactions(new ArrayList<>()); + + when(accountService.findAccountByUsername(anyString())).thenReturn(testAccount); + when(accountService.getTransactionHistory(testAccount)).thenReturn(new ArrayList<>()); + } + + @Test + void passwordEncoder_ShouldReturnBCryptPasswordEncoder() { + assertNotNull(passwordEncoder); + String rawPassword = "testPassword123"; + String encodedPassword = passwordEncoder.encode(rawPassword); + + assertNotEquals(rawPassword, encodedPassword); + assertTrue(passwordEncoder.matches(rawPassword, encodedPassword)); + } + + @Test + void registerPage_ShouldBeAccessibleWithoutAuthentication() throws Exception { + mockMvc.perform(get("/register")) + .andExpect(status().isOk()); + } + + @Test + void loginPage_ShouldBeAccessibleWithoutAuthentication() throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isOk()); + } + + @Test + void dashboard_ShouldRequireAuthentication() throws Exception { + mockMvc.perform(get("/dashboard")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + void transactions_ShouldRequireAuthentication() throws Exception { + mockMvc.perform(get("/transactions")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrlPattern("**/login")); + } + + @Test + @WithMockUser(username = "testuser") + void dashboard_ShouldBeAccessibleWhenAuthenticated() throws Exception { + mockMvc.perform(get("/dashboard")) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(username = "testuser") + void transactions_ShouldBeAccessibleWhenAuthenticated() throws Exception { + mockMvc.perform(get("/transactions")) + .andExpect(status().isOk()); + } + + @Test + void logout_ShouldRedirectToLoginPage() throws Exception { + mockMvc.perform(post("/logout").with(csrf())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login?logout")); + } + + @Test + void loginProcessingUrl_ShouldBeConfigured() throws Exception { + mockMvc.perform(post("/login") + .with(csrf()) + .param("username", "testuser") + .param("password", "password")) + .andExpect(status().is3xxRedirection()); + } + + @Test + void passwordEncoder_ShouldHandleEmptyPassword() { + String emptyPassword = ""; + String encodedPassword = passwordEncoder.encode(emptyPassword); + + assertNotNull(encodedPassword); + assertTrue(passwordEncoder.matches(emptyPassword, encodedPassword)); + } + + @Test + void passwordEncoder_ShouldHandleSpecialCharacters() { + String specialPassword = "p@$$w0rd!#$%^&*()"; + String encodedPassword = passwordEncoder.encode(specialPassword); + + assertTrue(passwordEncoder.matches(specialPassword, encodedPassword)); + } + + @Test + void passwordEncoder_ShouldGenerateDifferentHashesForSamePassword() { + String password = "samePassword"; + String encoded1 = passwordEncoder.encode(password); + String encoded2 = passwordEncoder.encode(password); + + assertNotEquals(encoded1, encoded2); + assertTrue(passwordEncoder.matches(password, encoded1)); + assertTrue(passwordEncoder.matches(password, encoded2)); + } +} 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..00291711 --- /dev/null +++ b/src/test/java/com/example/bankapp/controller/BankControllerTest.java @@ -0,0 +1,226 @@ +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.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("password"); + 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")); + + verify(accountService).findAccountByUsername("testuser"); + } + + @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).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")); + } + + @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).findAccountByUsername("testuser"); + verify(accountService).deposit(any(Account.class), any(BigDecimal.class)); + } + + @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", "200.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).withdraw(any(Account.class), any(BigDecimal.class)); + } + + @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().attributeExists("account")); + } + + @Test + @WithMockUser(username = "testuser") + void transactionHistory_Success() throws Exception { + List transactions = new ArrayList<>(); + Transaction t1 = new Transaction(); + t1.setId(1L); + t1.setAmount(new BigDecimal("100.00")); + t1.setType("Deposit"); + t1.setTimestamp(LocalDateTime.now()); + transactions.add(t1); + + 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_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", "100.00")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/dashboard")); + + verify(accountService).findAccountByUsername("testuser"); + verify(accountService).transferAmount(any(Account.class), eq("recipient"), any(BigDecimal.class)); + } + + @Test + @WithMockUser(username = "testuser") + void transferAmount_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().attributeExists("account")); + } + + @Test + @WithMockUser(username = "testuser") + void transferAmount_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", "100.00")) + .andExpect(status().isOk()) + .andExpect(view().name("dashboard")) + .andExpect(model().attributeExists("error")); + } +} 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..e0218998 --- /dev/null +++ b/src/test/java/com/example/bankapp/model/AccountTest.java @@ -0,0 +1,87 @@ +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() { + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("USER")); + List transactions = new ArrayList<>(); + + 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 testSettersAndGetters() { + Account account = new Account(); + + account.setId(1L); + account.setUsername("testuser"); + account.setPassword("password123"); + account.setBalance(new BigDecimal("500.00")); + + List transactions = new ArrayList<>(); + account.setTransactions(transactions); + + Collection authorities = Arrays.asList(new SimpleGrantedAuthority("ADMIN")); + account.setAuthorities(authorities); + + assertEquals(1L, account.getId()); + assertEquals("testuser", account.getUsername()); + assertEquals("password123", account.getPassword()); + assertEquals(new BigDecimal("500.00"), account.getBalance()); + assertEquals(transactions, account.getTransactions()); + assertEquals(authorities, account.getAuthorities()); + } + + @Test + void testBalanceOperations() { + Account account = new Account(); + account.setBalance(new BigDecimal("100.00")); + + BigDecimal newBalance = account.getBalance().add(new BigDecimal("50.00")); + account.setBalance(newBalance); + + assertEquals(new BigDecimal("150.00"), account.getBalance()); + } + + @Test + void testGetAuthoritiesReturnsCorrectValue() { + Collection authorities = Arrays.asList( + new SimpleGrantedAuthority("USER"), + new SimpleGrantedAuthority("ADMIN") + ); + + Account account = new Account("user", "pass", BigDecimal.ZERO, null, authorities); + + assertEquals(2, account.getAuthorities().size()); + } +} 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..ecee3d7a --- /dev/null +++ b/src/test/java/com/example/bankapp/model/TransactionTest.java @@ -0,0 +1,85 @@ +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"); + + LocalDateTime timestamp = LocalDateTime.now(); + BigDecimal amount = new BigDecimal("100.00"); + + Transaction transaction = new Transaction(amount, "Deposit", timestamp, account); + + assertEquals(amount, transaction.getAmount()); + assertEquals("Deposit", transaction.getType()); + assertEquals(timestamp, transaction.getTimestamp()); + assertEquals(account, transaction.getAccount()); + } + + @Test + void testSettersAndGetters() { + Transaction transaction = new Transaction(); + Account account = new Account(); + account.setId(1L); + + LocalDateTime timestamp = LocalDateTime.of(2024, 1, 15, 10, 30); + + transaction.setId(1L); + transaction.setAmount(new BigDecimal("250.50")); + transaction.setType("Withdrawal"); + transaction.setTimestamp(timestamp); + transaction.setAccount(account); + + assertEquals(1L, transaction.getId()); + assertEquals(new BigDecimal("250.50"), transaction.getAmount()); + assertEquals("Withdrawal", transaction.getType()); + assertEquals(timestamp, transaction.getTimestamp()); + assertEquals(account, transaction.getAccount()); + } + + @Test + void testDifferentTransactionTypes() { + Transaction deposit = new Transaction(); + deposit.setType("Deposit"); + assertEquals("Deposit", deposit.getType()); + + Transaction withdrawal = new Transaction(); + withdrawal.setType("Withdrawal"); + assertEquals("Withdrawal", withdrawal.getType()); + + Transaction transferOut = new Transaction(); + transferOut.setType("Transfer Out to user2"); + assertEquals("Transfer Out to user2", transferOut.getType()); + + Transaction transferIn = new Transaction(); + transferIn.setType("Transfer In from user1"); + assertEquals("Transfer In from user1", transferIn.getType()); + } + + @Test + void testAmountPrecision() { + Transaction transaction = new Transaction(); + transaction.setAmount(new BigDecimal("1234567.89")); + assertEquals(new BigDecimal("1234567.89"), 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..bfbe1f32 --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/AccountRepositoryTest.java @@ -0,0 +1,109 @@ +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("password123"); + testAccount.setBalance(new BigDecimal("1000.00")); + entityManager.persistAndFlush(testAccount); + } + + @Test + void findByUsername_ExistingUser_ReturnsAccount() { + Optional found = accountRepository.findByUsername("testuser"); + + assertTrue(found.isPresent()); + assertEquals("testuser", found.get().getUsername()); + assertEquals(new BigDecimal("1000.00"), found.get().getBalance()); + } + + @Test + void findByUsername_NonExistingUser_ReturnsEmpty() { + Optional found = accountRepository.findByUsername("nonexistent"); + + assertFalse(found.isPresent()); + } + + @Test + void save_NewAccount_Success() { + Account newAccount = new Account(); + newAccount.setUsername("newuser"); + newAccount.setPassword("newpassword"); + newAccount.setBalance(new BigDecimal("500.00")); + + Account saved = accountRepository.save(newAccount); + + assertNotNull(saved.getId()); + assertEquals("newuser", saved.getUsername()); + } + + @Test + void findById_ExistingAccount_ReturnsAccount() { + Optional found = accountRepository.findById(testAccount.getId()); + + assertTrue(found.isPresent()); + assertEquals("testuser", found.get().getUsername()); + } + + @Test + void findById_NonExistingAccount_ReturnsEmpty() { + Optional found = accountRepository.findById(999L); + + assertFalse(found.isPresent()); + } + + @Test + void updateBalance_Success() { + testAccount.setBalance(new BigDecimal("1500.00")); + Account updated = accountRepository.save(testAccount); + + assertEquals(new BigDecimal("1500.00"), updated.getBalance()); + } + + @Test + void deleteAccount_Success() { + Long accountId = testAccount.getId(); + accountRepository.delete(testAccount); + entityManager.flush(); + + Optional found = accountRepository.findById(accountId); + assertFalse(found.isPresent()); + } + + @Test + void findAll_ReturnsAllAccounts() { + Account anotherAccount = new Account(); + anotherAccount.setUsername("anotheruser"); + anotherAccount.setPassword("password"); + anotherAccount.setBalance(BigDecimal.ZERO); + entityManager.persistAndFlush(anotherAccount); + + var accounts = accountRepository.findAll(); + + assertEquals(2, accounts.size()); + } +} 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..bd7ab78e --- /dev/null +++ b/src/test/java/com/example/bankapp/repository/TransactionRepositoryTest.java @@ -0,0 +1,148 @@ +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("password123"); + 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); + entityManager.persistAndFlush(testTransaction); + } + + @Test + void findByAccountId_ExistingAccount_ReturnsTransactions() { + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertFalse(transactions.isEmpty()); + assertEquals(1, transactions.size()); + assertEquals("Deposit", transactions.get(0).getType()); + } + + @Test + void findByAccountId_NonExistingAccount_ReturnsEmptyList() { + List transactions = transactionRepository.findByAccountId(999L); + + assertTrue(transactions.isEmpty()); + } + + @Test + void findByAccountId_MultipleTransactions_ReturnsAll() { + Transaction t2 = new Transaction(); + t2.setAmount(new BigDecimal("50.00")); + t2.setType("Withdrawal"); + t2.setTimestamp(LocalDateTime.now()); + t2.setAccount(testAccount); + entityManager.persistAndFlush(t2); + + Transaction t3 = new Transaction(); + t3.setAmount(new BigDecimal("200.00")); + t3.setType("Transfer Out to user2"); + t3.setTimestamp(LocalDateTime.now()); + t3.setAccount(testAccount); + entityManager.persistAndFlush(t3); + + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertEquals(3, transactions.size()); + } + + @Test + void save_NewTransaction_Success() { + Transaction newTransaction = new Transaction(); + newTransaction.setAmount(new BigDecimal("250.00")); + newTransaction.setType("Deposit"); + newTransaction.setTimestamp(LocalDateTime.now()); + newTransaction.setAccount(testAccount); + + Transaction saved = transactionRepository.save(newTransaction); + + assertNotNull(saved.getId()); + assertEquals(new BigDecimal("250.00"), saved.getAmount()); + } + + @Test + void findById_ExistingTransaction_ReturnsTransaction() { + var found = transactionRepository.findById(testTransaction.getId()); + + assertTrue(found.isPresent()); + assertEquals("Deposit", found.get().getType()); + } + + @Test + void findById_NonExistingTransaction_ReturnsEmpty() { + var found = transactionRepository.findById(999L); + + assertFalse(found.isPresent()); + } + + @Test + void deleteTransaction_Success() { + Long transactionId = testTransaction.getId(); + transactionRepository.delete(testTransaction); + entityManager.flush(); + + var found = transactionRepository.findById(transactionId); + assertFalse(found.isPresent()); + } + + @Test + void findAll_ReturnsAllTransactions() { + Account anotherAccount = new Account(); + anotherAccount.setUsername("anotheruser"); + anotherAccount.setPassword("password"); + anotherAccount.setBalance(BigDecimal.ZERO); + entityManager.persistAndFlush(anotherAccount); + + Transaction t2 = new Transaction(); + t2.setAmount(new BigDecimal("75.00")); + t2.setType("Deposit"); + t2.setTimestamp(LocalDateTime.now()); + t2.setAccount(anotherAccount); + entityManager.persistAndFlush(t2); + + var transactions = transactionRepository.findAll(); + + assertEquals(2, transactions.size()); + } + + @Test + void transactionBelongsToCorrectAccount() { + List transactions = transactionRepository.findByAccountId(testAccount.getId()); + + assertFalse(transactions.isEmpty()); + assertEquals(testAccount.getId(), transactions.get(0).getAccount().getId()); + } +} 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..2782e5a5 --- /dev/null +++ b/src/test/java/com/example/bankapp/service/AccountServiceTest.java @@ -0,0 +1,234 @@ +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()); + verify(accountRepository).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()); + } + + @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 saved = invocation.getArgument(0); + saved.setId(1L); + return saved; + }); + + 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_UsernameExists() { + 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_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).save(testAccount); + verify(transactionRepository).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).save(testAccount); + verify(transactionRepository).save(any(Transaction.class)); + } + + @Test + void withdraw_InsufficientFunds() { + 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_Success() { + List transactions = new ArrayList<>(); + Transaction t1 = new Transaction(); + t1.setId(1L); + t1.setAmount(new BigDecimal("100.00")); + t1.setType("Deposit"); + transactions.add(t1); + + 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_Success() { + when(accountRepository.findByUsername("testuser")).thenReturn(Optional.of(testAccount)); + + UserDetails result = accountService.loadUserByUsername("testuser"); + + assertNotNull(result); + assertEquals("testuser", result.getUsername()); + assertEquals("encodedPassword", result.getPassword()); + } + + @Test + void loadUserByUsername_NotFound() { + when(accountRepository.findByUsername("nonexistent")).thenReturn(Optional.empty()); + + assertThrows(RuntimeException.class, + () -> accountService.loadUserByUsername("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("2000.00"); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> accountService.transferAmount(testAccount, "recipient", transferAmount)); + + assertEquals("Insufficient funds", exception.getMessage()); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + void transferAmount_RecipientNotFound() { + BigDecimal transferAmount = new BigDecimal("100.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..c7ee8fe7 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,7 @@ +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