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