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 extends GrantedAuthority> 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