Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.math.BigDecimal;
Expand All @@ -35,10 +36,10 @@ public class AccountController {


@GetMapping
public ResponseEntity<List<AccountResponse>> getUserAccounts(Authentication authentication) {
public ResponseEntity<List<AccountResponse>> getUserAccounts() {
Span span = tracer.spanBuilder("get-user-accounts").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Fetching accounts for user: {}", userId);

List<AccountResponse> accounts = accountService.getUserAccounts(userId);
Expand All @@ -57,11 +58,10 @@ public ResponseEntity<List<AccountResponse>> getUserAccounts(Authentication auth

@PostMapping
public ResponseEntity<AccountResponse> createAccount(
@Valid @RequestBody AccountCreateRequest request,
Authentication authentication) {
@Valid @RequestBody AccountCreateRequest request) {
Span span = tracer.spanBuilder("create-account").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Creating account for user: {}", userId);

AccountResponse account = accountService.createAccount(request, userId);
Expand All @@ -81,11 +81,10 @@ public ResponseEntity<AccountResponse> createAccount(

@GetMapping("/{accountId}")
public ResponseEntity<AccountResponse> getAccountById(
@PathVariable Long accountId,
Authentication authentication) {
@PathVariable Long accountId) {
Span span = tracer.spanBuilder("get-account-by-id").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Fetching account {} for user: {}", accountId, userId);

AccountResponse account = accountService.getAccountById(accountId, userId);
Expand All @@ -109,11 +108,10 @@ public ResponseEntity<AccountResponse> getAccountById(

@GetMapping("/{accountId}/balance")
public ResponseEntity<BalanceResponse> getAccountBalance(
@PathVariable Long accountId,
Authentication authentication) {
@PathVariable Long accountId) {
Span span = tracer.spanBuilder("get-account-balance").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Fetching balance for account {} for user: {}", accountId, userId);

BalanceResponse balance = accountService.getAccountBalance(accountId, userId);
Expand All @@ -138,11 +136,10 @@ public ResponseEntity<BalanceResponse> getAccountBalance(
@PutMapping("/{accountId}/balance")
public ResponseEntity<Void> updateAccountBalance(
@PathVariable Long accountId,
@RequestBody Map<String, Object> requestBody,
Authentication authentication) {
@RequestBody Map<String, Object> requestBody) {
Span span = tracer.spanBuilder("update-account-balance").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Updating balance for account {} for user: {}", accountId, userId);

Object balanceObj = requestBody.get("balance");
Expand Down Expand Up @@ -170,7 +167,11 @@ public ResponseEntity<Void> updateAccountBalance(
}
}

private Long getUserIdFromAuthentication(Authentication authentication) {
private Long getUserIdFromAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getDetails() == null) {
throw new RuntimeException("User not authenticated");
}
UserAuthenticationDetails details = (UserAuthenticationDetails) authentication.getDetails();
return details.getUserId();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ void getUserAccounts_Success() throws Exception {
List<AccountResponse> accounts = Arrays.asList(testAccountResponse);
when(accountService.getUserAccounts(testUserId)).thenReturn(accounts);

mockMvc.perform(get("/account"))
mockMvc.perform(get("/accounts"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].accountNumber").value("123456789012"));

Expand All @@ -111,7 +111,7 @@ void getUserAccounts_Success() throws Exception {
void getAccountById_Success() throws Exception {
when(accountService.getAccountById(testAccountId, testUserId)).thenReturn(testAccountResponse);

mockMvc.perform(get("/account/{accountId}", testAccountId))
mockMvc.perform(get("/accounts/{accountId}", testAccountId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.accountNumber").value("123456789012"));

Expand All @@ -122,15 +122,15 @@ void getAccountById_Success() throws Exception {
void getAccountById_NotFound() throws Exception {
when(accountService.getAccountById(anyLong(), anyLong())).thenThrow(new RuntimeException("Account not found"));

mockMvc.perform(get("/account/{accountId}", testAccountId))
mockMvc.perform(get("/accounts/{accountId}", testAccountId))
.andExpect(status().isNotFound());
}

@Test
void getAccountBalance_Success() throws Exception {
when(accountService.getAccountBalance(testAccountId, testUserId)).thenReturn(testBalanceResponse);

mockMvc.perform(get("/account/{accountId}/balance", testAccountId))
mockMvc.perform(get("/accounts/{accountId}/balance", testAccountId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.balance").value(1000.00));
}
Expand All @@ -139,7 +139,7 @@ void getAccountBalance_Success() throws Exception {
void createAccount_Success() throws Exception {
when(accountService.createAccount(any(AccountCreateRequest.class), eq(testUserId))).thenReturn(testAccountResponse);

mockMvc.perform(post("/account")
mockMvc.perform(post("/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createRequest)))
.andExpect(status().isCreated())
Expand All @@ -150,7 +150,7 @@ void createAccount_Success() throws Exception {
void createAccount_InvalidInput() throws Exception {
AccountCreateRequest invalidRequest = new AccountCreateRequest("");

mockMvc.perform(post("/account")
mockMvc.perform(post("/accounts")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(invalidRequest)))
.andExpect(status().isBadRequest());
Expand All @@ -162,7 +162,7 @@ void createAccount_InvalidInput() throws Exception {
void updateBalance_Success() throws Exception {
when(accountService.updateBalance(eq(testAccountId), any(BigDecimal.class), eq(testUserId))).thenReturn(true);

mockMvc.perform(put("/account/{accountId}/balance", testAccountId)
mockMvc.perform(put("/accounts/{accountId}/balance", testAccountId)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"balance\": 1500.00}"))
.andExpect(status().isOk());
Expand All @@ -173,7 +173,7 @@ void updateBalance_Unauthorized() throws Exception {
when(accountService.updateBalance(eq(testAccountId), any(BigDecimal.class), eq(testUserId)))
.thenThrow(new RuntimeException("Account not found or access denied"));

mockMvc.perform(put("/account/{accountId}/balance", testAccountId)
mockMvc.perform(put("/accounts/{accountId}/balance", testAccountId)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"balance\": 1500.00}"))
.andExpect(status().isNotFound());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.HashMap;
Expand All @@ -33,10 +34,10 @@ public class TransactionController {


@GetMapping
public ResponseEntity<List<TransactionResponse>> getUserTransactions(Authentication authentication) {
public ResponseEntity<List<TransactionResponse>> getUserTransactions() {
Span span = tracer.spanBuilder("get-user-transactions").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
logger.info("Fetching transactions for user: {}", userId);

List<TransactionResponse> transactions = transactionService.getUserTransactions(userId);
Expand All @@ -56,11 +57,10 @@ public ResponseEntity<List<TransactionResponse>> getUserTransactions(Authenticat
@PostMapping("/deposit")
public ResponseEntity<TransactionResponse> deposit(
@Valid @RequestBody DepositRequest request,
Authentication authentication,
HttpServletRequest httpRequest) {
Span span = tracer.spanBuilder("process-deposit").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
String jwtToken = httpRequest.getHeader("Authorization");

logger.info("Processing deposit for user: {}, account: {}, amount: {}",
Expand Down Expand Up @@ -89,11 +89,10 @@ public ResponseEntity<TransactionResponse> deposit(
@PostMapping("/withdraw")
public ResponseEntity<TransactionResponse> withdraw(
@Valid @RequestBody WithdrawRequest request,
Authentication authentication,
HttpServletRequest httpRequest) {
Span span = tracer.spanBuilder("process-withdrawal").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
Long userId = getUserIdFromAuthentication();
String jwtToken = httpRequest.getHeader("Authorization");

logger.info("Processing withdrawal for user: {}, account: {}, amount: {}",
Expand Down Expand Up @@ -122,11 +121,16 @@ public ResponseEntity<TransactionResponse> withdraw(
@PostMapping("/transfer")
public ResponseEntity<TransactionResponse> transfer(
@Valid @RequestBody TransferRequest request,
Authentication authentication,
HttpServletRequest httpRequest) {
Span span = tracer.spanBuilder("process-transfer").startSpan();
try {
Long userId = getUserIdFromAuthentication(authentication);
// Validate same account transfer
if (request.getFromAccountId().equals(request.getToAccountId())) {
logger.warn("Transfer rejected: from and to accounts are the same");
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}

Long userId = getUserIdFromAuthentication();
String jwtToken = httpRequest.getHeader("Authorization");

logger.info("Processing transfer for user: {}, from: {}, to: {}, amount: {}",
Expand All @@ -153,7 +157,11 @@ public ResponseEntity<TransactionResponse> transfer(
}
}

private Long getUserIdFromAuthentication(Authentication authentication) {
private Long getUserIdFromAuthentication() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getDetails() == null) {
throw new RuntimeException("User not authenticated");
}
UserAuthenticationDetails details = (UserAuthenticationDetails) authentication.getDetails();
return details.getUserId();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.banking.transactionsservice.dto.*;
import com.banking.transactionsservice.entity.Transaction;
import com.banking.transactionsservice.security.UserAuthenticationDetails;
import com.banking.transactionsservice.service.TransactionService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.opentelemetry.api.metrics.LongCounter;
Expand All @@ -21,20 +22,23 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.http.MediaType;
import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import jakarta.servlet.http.HttpServletRequest;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.jwt;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

Expand All @@ -44,6 +48,7 @@
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration.class
})
@ContextConfiguration(classes = {TransactionController.class, TransactionControllerTest.TestConfig.class})
@WithMockUser(username="testuser")
class TransactionControllerTest {

@Autowired
Expand Down Expand Up @@ -79,10 +84,14 @@ void setUp() {
depositRequest = new DepositRequest(testAccountId, BigDecimal.valueOf(100.00), "Test deposit");
withdrawRequest = new WithdrawRequest(testAccountId, BigDecimal.valueOf(50.00), "Test withdrawal");
transferRequest = new TransferRequest(testAccountId, 2L, BigDecimal.valueOf(75.00), "Test transfer");
}

private SecurityMockMvcRequestPostProcessors.JwtRequestPostProcessor jwtWithUserId() {
return jwt().jwt(builder -> builder.claim("userId", testUserId));

// Manually set up the SecurityContext
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
UserAuthenticationDetails userDetails = new UserAuthenticationDetails(mockRequest, testUserId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
"testuser", null, Collections.emptyList());
authentication.setDetails(userDetails);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

@Test
Expand All @@ -92,7 +101,7 @@ void getUserTransactions_Success() throws Exception {
when(transactionService.getUserTransactions(testUserId)).thenReturn(transactions);

// Act & Assert
mockMvc.perform(get("/transactions").with(jwtWithUserId()))
mockMvc.perform(get("/transactions"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].id").value(1L))
.andExpect(jsonPath("$[0].amount").value(100.00))
Expand All @@ -109,7 +118,7 @@ void deposit_Success() throws Exception {
.thenReturn(testTransactionResponse);

// Act & Assert
mockMvc.perform(post("/transactions/deposit").with(jwtWithUserId())
mockMvc.perform(post("/transactions/deposit")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(depositRequest)))
Expand All @@ -128,7 +137,7 @@ void deposit_InvalidInput() throws Exception {
depositRequest.setAmount(BigDecimal.valueOf(-10.00)); // Invalid negative amount

// Act & Assert
mockMvc.perform(post("/transactions/deposit").with(jwtWithUserId())
mockMvc.perform(post("/transactions/deposit")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(depositRequest)))
Expand All @@ -144,7 +153,7 @@ void deposit_AccountNotOwned() throws Exception {
.thenThrow(new RuntimeException("Account not owned by user"));

// Act & Assert
mockMvc.perform(post("/transactions/deposit").with(jwtWithUserId())
mockMvc.perform(post("/transactions/deposit")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(depositRequest)))
Expand All @@ -171,7 +180,7 @@ void withdraw_Success() throws Exception {
.thenReturn(withdrawResponse);

// Act & Assert
mockMvc.perform(post("/transactions/withdraw").with(jwtWithUserId())
mockMvc.perform(post("/transactions/withdraw")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(withdrawRequest)))
Expand All @@ -191,7 +200,7 @@ void withdraw_InsufficientBalance() throws Exception {
.thenThrow(new RuntimeException("Insufficient balance"));

// Act & Assert
mockMvc.perform(post("/transactions/withdraw").with(jwtWithUserId())
mockMvc.perform(post("/transactions/withdraw")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(withdrawRequest)))
Expand Down Expand Up @@ -219,7 +228,7 @@ void transfer_Success() throws Exception {
.thenReturn(transferResponse);

// Act & Assert
mockMvc.perform(post("/transactions/transfer").with(jwtWithUserId())
mockMvc.perform(post("/transactions/transfer")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(transferRequest)))
Expand All @@ -238,7 +247,7 @@ void transfer_SameAccount() throws Exception {
transferRequest.setToAccountId(testAccountId); // Same as fromAccountId

// Act & Assert
mockMvc.perform(post("/transactions/transfer").with(jwtWithUserId())
mockMvc.perform(post("/transactions/transfer")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(transferRequest)))
Expand All @@ -254,7 +263,7 @@ void transfer_InsufficientBalance() throws Exception {
.thenThrow(new RuntimeException("Insufficient balance"));

// Act & Assert
mockMvc.perform(post("/transactions/transfer").with(jwtWithUserId())
mockMvc.perform(post("/transactions/transfer")
.header("Authorization", testJwtToken)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(transferRequest)))
Expand Down
Loading