Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/example/bankapp/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.bankapp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(8);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("BankAsync-");
executor.initialize();
return executor;
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/example/bankapp/event/TransactionEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.bankapp.event;

import java.math.BigDecimal;

public class TransactionEvent {
private final BigDecimal amount;
private final String type;
private final Long accountId;

public TransactionEvent(BigDecimal amount, String type, Long accountId) {
this.amount = amount;
this.type = type;
this.accountId = accountId;
}

public BigDecimal getAmount() { return amount; }
public String getType() { return type; }
public Long getAccountId() { return accountId; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.bankapp.event;

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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;

@Component
public class TransactionEventListener {

@Autowired
private TransactionRepository transactionRepository;

@Autowired
private AccountRepository accountRepository;

@Async
@EventListener
@Transactional
public void handleTransactionEvent(TransactionEvent event) {
Account account = accountRepository.findById(event.getAccountId())
.orElseThrow(() -> new RuntimeException("Account not found"));

Transaction transaction = new Transaction(
event.getAmount(),
event.getType(),
LocalDateTime.now(),
account
);
transactionRepository.save(transaction);
}
}
50 changes: 16 additions & 34 deletions src/main/java/com/example/bankapp/service/AccountService.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
package com.example.bankapp.service;

import com.example.bankapp.event.TransactionEvent;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
Expand All @@ -31,10 +32,14 @@ public class AccountService implements UserDetailsService {
@Autowired
private TransactionRepository transactionRepository;

@Autowired
private ApplicationEventPublisher eventPublisher;

public Account findAccountByUsername(String username) {
return accountRepository.findByUsername(username).orElseThrow(() -> new RuntimeException("Account not found"));
}

@Transactional
public Account registerAccount(String username, String password) {
if (accountRepository.findByUsername(username).isPresent()) {
throw new RuntimeException("Username already exists");
Expand All @@ -47,37 +52,26 @@ public Account registerAccount(String username, String password) {
return accountRepository.save(account);
}


@Transactional
public void deposit(Account account, BigDecimal amount) {
account.setBalance(account.getBalance().add(amount));
accountRepository.save(account);

Transaction transaction = new Transaction(
amount,
"Deposit",
LocalDateTime.now(),
account
);
transactionRepository.save(transaction);
eventPublisher.publishEvent(new TransactionEvent(amount, "Deposit", account.getId()));
}

@Transactional
public void withdraw(Account account, BigDecimal amount) {
if (account.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("Insufficient funds");
}
account.setBalance(account.getBalance().subtract(amount));
accountRepository.save(account);

Transaction transaction = new Transaction(
amount,
"Withdrawal",
LocalDateTime.now(),
account
);
transactionRepository.save(transaction);
eventPublisher.publishEvent(new TransactionEvent(amount, "Withdrawal", account.getId()));
}

public List<Transaction> getTransactionHistory(Account account) {
public List<com.example.bankapp.model.Transaction> getTransactionHistory(Account account) {
return transactionRepository.findByAccountId(account.getId());
}

Expand All @@ -100,6 +94,7 @@ public Collection<? extends GrantedAuthority> authorities() {
return Arrays.asList(new SimpleGrantedAuthority("USER"));
}

@Transactional
public void transferAmount(Account fromAccount, String toUsername, BigDecimal amount) {
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("Insufficient funds");
Expand All @@ -116,22 +111,9 @@ public void transferAmount(Account fromAccount, String toUsername, BigDecimal am
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(toAccount);

// Create transaction records for both accounts
Transaction debitTransaction = new Transaction(
amount,
"Transfer Out to " + toAccount.getUsername(),
LocalDateTime.now(),
fromAccount
);
transactionRepository.save(debitTransaction);

Transaction creditTransaction = new Transaction(
amount,
"Transfer In from " + fromAccount.getUsername(),
LocalDateTime.now(),
toAccount
);
transactionRepository.save(creditTransaction);
// Publish transaction events for async audit record creation
eventPublisher.publishEvent(new TransactionEvent(amount, "Transfer Out to " + toAccount.getUsername(), fromAccount.getId()));
eventPublisher.publishEvent(new TransactionEvent(amount, "Transfer In from " + fromAccount.getUsername(), toAccount.getId()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.example.bankapp.controller;

import com.example.bankapp.model.Account;
import com.example.bankapp.repository.AccountRepository;
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.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

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
@ActiveProfiles("test")
@Transactional
class BankControllerIntegrationTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private AccountRepository accountRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@BeforeEach
void setUp() {
accountRepository.deleteAll();

Account account = new Account();
account.setUsername("testuser");
account.setPassword(passwordEncoder.encode("password"));
account.setBalance(new BigDecimal("1000"));
accountRepository.save(account);

Account recipient = new Account();
recipient.setUsername("recipient");
recipient.setPassword(passwordEncoder.encode("password"));
recipient.setBalance(new BigDecimal("500"));
accountRepository.save(recipient);
}

@Test
@WithMockUser(username = "testuser")
void dashboard_authenticated() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().isOk())
.andExpect(view().name("dashboard"))
.andExpect(model().attributeExists("account"));
}

@Test
void dashboard_unauthenticated() throws Exception {
mockMvc.perform(get("/dashboard"))
.andExpect(status().is3xxRedirection());
}

@Test
@WithMockUser(username = "testuser")
void deposit_success() throws Exception {
mockMvc.perform(post("/deposit")
.param("amount", "500")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
}

@Test
@WithMockUser(username = "testuser")
void withdraw_success() throws Exception {
mockMvc.perform(post("/withdraw")
.param("amount", "500")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
}

@Test
@WithMockUser(username = "testuser")
void withdraw_insufficientFunds() throws Exception {
mockMvc.perform(post("/withdraw")
.param("amount", "5000")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(view().name("dashboard"))
.andExpect(model().attributeExists("error"));
}

@Test
@WithMockUser(username = "testuser")
void transfer_success() throws Exception {
mockMvc.perform(post("/transfer")
.param("toUsername", "recipient")
.param("amount", "300")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/dashboard"));
}

@Test
@WithMockUser(username = "testuser")
void transfer_recipientNotFound() throws Exception {
mockMvc.perform(post("/transfer")
.param("toUsername", "nonexistent")
.param("amount", "100")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(view().name("dashboard"))
.andExpect(model().attributeExists("error"));
}

@Test
void register_success() throws Exception {
mockMvc.perform(post("/register")
.param("username", "newuser")
.param("password", "newpassword")
.with(csrf()))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/login"));
}

@Test
void register_duplicateUsername() throws Exception {
mockMvc.perform(post("/register")
.param("username", "testuser")
.param("password", "password")
.with(csrf()))
.andExpect(status().isOk())
.andExpect(view().name("register"))
.andExpect(model().attributeExists("error"));
}

@Test
@WithMockUser(username = "testuser")
void transactionHistory() throws Exception {
mockMvc.perform(get("/transactions"))
.andExpect(status().isOk())
.andExpect(view().name("transactions"))
.andExpect(model().attributeExists("transactions"));
}
}
Loading