diff --git a/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayApplicationTests.java b/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayApplicationTests.java index ebb306d..81ce875 100644 --- a/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayApplicationTests.java +++ b/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayApplicationTests.java @@ -2,10 +2,12 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest +@ActiveProfiles("test") class ApiGatewayApplicationTests { @Test diff --git a/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayRouteTests.java b/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayRouteTests.java new file mode 100644 index 0000000..f760050 --- /dev/null +++ b/API-Gateway/src/test/java/org/training/api/gateway/ApiGatewayRouteTests.java @@ -0,0 +1,38 @@ +package org.training.api.gateway; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.gateway.route.RouteLocator; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@AutoConfigureWebTestClient +@ActiveProfiles("test") +class ApiGatewayRouteTests { + + @Autowired + private RouteLocator routeLocator; + + @Test + void contextLoads() { + assertNotNull(routeLocator); + } + + @Test + void routeConfiguration_hasExpectedRoutes() { + var routes = routeLocator.getRoutes().collectList().block(); + + assertNotNull(routes); + assertTrue(routes.size() >= 5, "Should have at least 5 routes configured"); + + assertTrue(routes.stream().anyMatch(r -> r.getId().contains("user-service"))); + assertTrue(routes.stream().anyMatch(r -> r.getId().contains("fund-transfer-service"))); + assertTrue(routes.stream().anyMatch(r -> r.getId().contains("account-service"))); + assertTrue(routes.stream().anyMatch(r -> r.getId().contains("sequence-generator"))); + assertTrue(routes.stream().anyMatch(r -> r.getId().contains("transaction-service"))); + } +} diff --git a/API-Gateway/src/test/resources/application-test.yml b/API-Gateway/src/test/resources/application-test.yml new file mode 100644 index 0000000..d42e8fe --- /dev/null +++ b/API-Gateway/src/test/resources/application-test.yml @@ -0,0 +1,43 @@ +server: + port: 8080 + +eureka: + client: + enabled: false + +spring: + application: + name: api-gateway-test + cloud: + gateway: + routes: + - id: user-service + uri: lb://user-service + predicates: + - Path=/api/users/** + - id: fund-transfer-service + uri: lb://fund-transfer-service + predicates: + - Path=/api/fund-transfers/** + - id: account-service + uri: lb://account-service + predicates: + - Path=/accounts/** + - id: sequence-generator + uri: lb://sequence-generator + predicates: + - Path=/sequence/** + - id: transaction-service + uri: lb://transaction-service + predicates: + - Path=/transactions/** + security: + oauth2: + client: + registration: + banking-service-client: + client-id: test-client + client-secret: test-secret + resourceserver: + jwt: + issuer-uri: http://localhost:8571/realms/banking-service diff --git a/Account-Service/pom.xml b/Account-Service/pom.xml index 759bf48..3d5ea2b 100644 --- a/Account-Service/pom.xml +++ b/Account-Service/pom.xml @@ -65,6 +65,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + diff --git a/Account-Service/src/test/java/org/training/account/service/AccountServiceApplicationTests.java b/Account-Service/src/test/java/org/training/account/service/AccountServiceApplicationTests.java index 6f3a83e..9d50f54 100644 --- a/Account-Service/src/test/java/org/training/account/service/AccountServiceApplicationTests.java +++ b/Account-Service/src/test/java/org/training/account/service/AccountServiceApplicationTests.java @@ -2,10 +2,12 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest +@ActiveProfiles("test") class AccountServiceApplicationTests { @Test diff --git a/Account-Service/src/test/java/org/training/account/service/repository/AccountRepositoryTest.java b/Account-Service/src/test/java/org/training/account/service/repository/AccountRepositoryTest.java new file mode 100644 index 0000000..7b5a30c --- /dev/null +++ b/Account-Service/src/test/java/org/training/account/service/repository/AccountRepositoryTest.java @@ -0,0 +1,104 @@ +package org.training.account.service.repository; + +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.test.context.TestPropertySource; +import org.training.account.service.model.AccountStatus; +import org.training.account.service.model.AccountType; +import org.training.account.service.model.entity.Account; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@TestPropertySource(locations = "classpath:application-test.yml") +class AccountRepositoryTest { + + @Autowired + private AccountRepository accountRepository; + + @Test + void findAccountByAccountNumber_existingAccount_returnsAccount() { + String accountNumber = "ACC0001234"; + Account account = Account.builder() + .accountNumber(accountNumber) + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .userId(1L) + .build(); + + accountRepository.save(account); + + Optional result = accountRepository.findAccountByAccountNumber(accountNumber); + + assertTrue(result.isPresent()); + assertEquals(accountNumber, result.get().getAccountNumber()); + assertEquals(AccountType.SAVINGS_ACCOUNT, result.get().getAccountType()); + } + + @Test + void findAccountByAccountNumber_nonExistingAccount_returnsEmpty() { + Optional result = accountRepository.findAccountByAccountNumber("NONEXISTENT"); + + assertFalse(result.isPresent()); + } + + @Test + void findAccountByUserIdAndAccountType_existingAccount_returnsAccount() { + Long userId = 1L; + AccountType accountType = AccountType.SAVINGS_ACCOUNT; + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountType(accountType) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .userId(userId) + .build(); + + accountRepository.save(account); + + Optional result = accountRepository.findAccountByUserIdAndAccountType(userId, accountType); + + assertTrue(result.isPresent()); + assertEquals(userId, result.get().getUserId()); + assertEquals(accountType, result.get().getAccountType()); + } + + @Test + void findAccountByUserIdAndAccountType_nonExistingAccount_returnsEmpty() { + Optional result = accountRepository + .findAccountByUserIdAndAccountType(999L, AccountType.SAVINGS_ACCOUNT); + + assertFalse(result.isPresent()); + } + + @Test + void findAccountByUserId_existingAccount_returnsAccount() { + Long userId = 1L; + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .userId(userId) + .build(); + + accountRepository.save(account); + + Optional result = accountRepository.findAccountByUserId(userId); + + assertTrue(result.isPresent()); + assertEquals(userId, result.get().getUserId()); + } + + @Test + void findAccountByUserId_nonExistingAccount_returnsEmpty() { + Optional result = accountRepository.findAccountByUserId(999L); + + assertFalse(result.isPresent()); + } +} diff --git a/Account-Service/src/test/java/org/training/account/service/service/implementation/AccountServiceImplTest.java b/Account-Service/src/test/java/org/training/account/service/service/implementation/AccountServiceImplTest.java new file mode 100644 index 0000000..4918336 --- /dev/null +++ b/Account-Service/src/test/java/org/training/account/service/service/implementation/AccountServiceImplTest.java @@ -0,0 +1,316 @@ +package org.training.account.service.service.implementation; + +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.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.training.account.service.exception.*; +import org.training.account.service.external.SequenceService; +import org.training.account.service.external.TransactionService; +import org.training.account.service.external.UserService; +import org.training.account.service.model.AccountStatus; +import org.training.account.service.model.AccountType; +import org.training.account.service.model.dto.AccountDto; +import org.training.account.service.model.dto.AccountStatusUpdate; +import org.training.account.service.model.dto.external.SequenceDto; +import org.training.account.service.model.dto.external.UserDto; +import org.training.account.service.model.dto.response.Response; +import org.training.account.service.model.entity.Account; +import org.training.account.service.repository.AccountRepository; + +import java.math.BigDecimal; +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 AccountServiceImplTest { + + @Mock + private UserService userService; + + @Mock + private AccountRepository accountRepository; + + @Mock + private SequenceService sequenceService; + + @Mock + private TransactionService transactionService; + + @InjectMocks + private AccountServiceImpl accountService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(accountService, "success", "200"); + } + + @Test + void createAccount_validUser_success() { + AccountDto accountDto = AccountDto.builder() + .userId(1L) + .accountType("SAVINGS_ACCOUNT") + .build(); + + UserDto userDto = UserDto.builder() + .userId(1L) + .firstName("testuser") + .build(); + + SequenceDto sequenceDto = new SequenceDto(); + sequenceDto.setAccountNumber(1234567L); + + when(userService.readUserById(1L)) + .thenReturn(ResponseEntity.ok(userDto)); + when(accountRepository.findAccountByUserIdAndAccountType(1L, AccountType.SAVINGS_ACCOUNT)) + .thenReturn(Optional.empty()); + when(sequenceService.generateAccountNumber()) + .thenReturn(sequenceDto); + when(accountRepository.save(any(Account.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = accountService.createAccount(accountDto); + + assertNotNull(response); + assertEquals(" Account created successfully", response.getMessage()); + verify(userService).readUserById(1L); + verify(accountRepository).findAccountByUserIdAndAccountType(1L, AccountType.SAVINGS_ACCOUNT); + verify(sequenceService).generateAccountNumber(); + verify(accountRepository).save(any(Account.class)); + } + + @Test + void createAccount_userNotFound_throwsResourceNotFound() { + AccountDto accountDto = AccountDto.builder() + .userId(1L) + .accountType("SAVINGS_ACCOUNT") + .build(); + + when(userService.readUserById(1L)) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(ResourceNotFound.class, () -> accountService.createAccount(accountDto)); + verify(accountRepository, never()).save(any()); + } + + @Test + void createAccount_duplicateAccount_throwsResourceConflict() { + AccountDto accountDto = AccountDto.builder() + .userId(1L) + .accountType("SAVINGS_ACCOUNT") + .build(); + + UserDto userDto = UserDto.builder() + .userId(1L) + .firstName("testuser") + .build(); + + Account existingAccount = Account.builder() + .userId(1L) + .accountType(AccountType.SAVINGS_ACCOUNT) + .build(); + + when(userService.readUserById(1L)) + .thenReturn(ResponseEntity.ok(userDto)); + when(accountRepository.findAccountByUserIdAndAccountType(1L, AccountType.SAVINGS_ACCOUNT)) + .thenReturn(Optional.of(existingAccount)); + + assertThrows(ResourceConflict.class, () -> accountService.createAccount(accountDto)); + verify(accountRepository, never()).save(any()); + } + + @Test + void updateStatus_validAccount_success() { + String accountNumber = "ACC0001234"; + AccountStatusUpdate statusUpdate = new AccountStatusUpdate(); + statusUpdate.setAccountStatus(AccountStatus.ACTIVE); + + Account account = Account.builder() + .accountNumber(accountNumber) + .accountStatus(AccountStatus.PENDING) + .availableBalance(BigDecimal.valueOf(1500)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + when(accountRepository.save(any(Account.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = accountService.updateStatus(accountNumber, statusUpdate); + + assertNotNull(response); + assertEquals("Account updated successfully", response.getMessage()); + verify(accountRepository).save(any(Account.class)); + } + + @Test + void updateStatus_insufficientFunds_throwsInSufficientFunds() { + String accountNumber = "ACC0001234"; + AccountStatusUpdate statusUpdate = new AccountStatusUpdate(); + statusUpdate.setAccountStatus(AccountStatus.ACTIVE); + + Account account = Account.builder() + .accountNumber(accountNumber) + .accountStatus(AccountStatus.PENDING) + .availableBalance(BigDecimal.valueOf(500)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + + assertThrows(InSufficientFunds.class, () -> accountService.updateStatus(accountNumber, statusUpdate)); + verify(accountRepository, never()).save(any()); + } + + @Test + void readAccountByAccountNumber_existingAccount_returnsAccount() { + String accountNumber = "ACC0001234"; + Account account = Account.builder() + .accountNumber(accountNumber) + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + + AccountDto result = accountService.readAccountByAccountNumber(accountNumber); + + assertNotNull(result); + assertEquals(accountNumber, result.getAccountNumber()); + verify(accountRepository).findAccountByAccountNumber(accountNumber); + } + + @Test + void readAccountByAccountNumber_nonExistingAccount_throwsResourceNotFound() { + String accountNumber = "NONEXISTENT"; + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFound.class, () -> accountService.readAccountByAccountNumber(accountNumber)); + } + + @Test + void updateAccount_validAccount_success() { + String accountNumber = "ACC0001234"; + AccountDto accountDto = AccountDto.builder() + .accountNumber(accountNumber) + .availableBalance(BigDecimal.valueOf(2000)) + .build(); + + Account account = Account.builder() + .accountNumber(accountNumber) + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + when(accountRepository.save(any(Account.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = accountService.updateAccount(accountNumber, accountDto); + + assertNotNull(response); + assertEquals("Account updated successfully", response.getMessage()); + verify(accountRepository).save(any(Account.class)); + } + + @Test + void getBalance_existingAccount_returnsBalance() { + String accountNumber = "ACC0001234"; + Account account = Account.builder() + .accountNumber(accountNumber) + .availableBalance(BigDecimal.valueOf(1500)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + + String balance = accountService.getBalance(accountNumber); + + assertEquals("1500", balance); + verify(accountRepository).findAccountByAccountNumber(accountNumber); + } + + @Test + void closeAccount_zeroBalance_success() { + String accountNumber = "ACC0001234"; + Account account = Account.builder() + .accountNumber(accountNumber) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.ZERO) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + + Response response = accountService.closeAccount(accountNumber); + + assertNotNull(response); + verify(accountRepository, times(2)).findAccountByAccountNumber(accountNumber); + } + + @Test + void closeAccount_nonZeroBalance_throwsAccountClosingException() { + String accountNumber = "ACC0001234"; + Account account = Account.builder() + .accountNumber(accountNumber) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountRepository.findAccountByAccountNumber(accountNumber)) + .thenReturn(Optional.of(account)); + + assertThrows(AccountClosingException.class, () -> accountService.closeAccount(accountNumber)); + } + + @Test + void readAccountByUserId_activeAccount_returnsAccount() { + Long userId = 1L; + Account account = Account.builder() + .userId(userId) + .accountNumber("ACC0001234") + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.ACTIVE) + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountRepository.findAccountByUserId(userId)) + .thenReturn(Optional.of(account)); + + AccountDto result = accountService.readAccountByUserId(userId); + + assertNotNull(result); + assertEquals("ACC0001234", result.getAccountNumber()); + verify(accountRepository).findAccountByUserId(userId); + } + + @Test + void readAccountByUserId_inactiveAccount_throwsAccountStatusException() { + Long userId = 1L; + Account account = Account.builder() + .userId(userId) + .accountNumber("ACC0001234") + .accountType(AccountType.SAVINGS_ACCOUNT) + .accountStatus(AccountStatus.PENDING) + .build(); + + when(accountRepository.findAccountByUserId(userId)) + .thenReturn(Optional.of(account)); + + assertThrows(AccountStatusException.class, () -> accountService.readAccountByUserId(userId)); + } +} diff --git a/Account-Service/src/test/resources/application-test.yml b/Account-Service/src/test/resources/application-test.yml new file mode 100644 index 0000000..32fc9b6 --- /dev/null +++ b/Account-Service/src/test/resources/application-test.yml @@ -0,0 +1,17 @@ +spring: + application: + name: account-service + ok: 200 + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect diff --git a/Fund-Transfer/pom.xml b/Fund-Transfer/pom.xml index d6abb65..a5f2741 100644 --- a/Fund-Transfer/pom.xml +++ b/Fund-Transfer/pom.xml @@ -60,6 +60,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + org.springframework.cloud spring-cloud-starter-openfeign diff --git a/Fund-Transfer/src/test/java/org/training/fundtransfer/repository/FundTransferRepositoryTest.java b/Fund-Transfer/src/test/java/org/training/fundtransfer/repository/FundTransferRepositoryTest.java new file mode 100644 index 0000000..500b15b --- /dev/null +++ b/Fund-Transfer/src/test/java/org/training/fundtransfer/repository/FundTransferRepositoryTest.java @@ -0,0 +1,92 @@ +package org.training.fundtransfer.repository; + +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.test.context.TestPropertySource; +import org.training.fundtransfer.model.TransactionStatus; +import org.training.fundtransfer.model.TransferType; +import org.training.fundtransfer.model.entity.FundTransfer; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@TestPropertySource(locations = "classpath:application-test.yml") +class FundTransferRepositoryTest { + + @Autowired + private FundTransferRepository fundTransferRepository; + + @Test + void findFundTransferByTransactionReference_existingReference_returnsFundTransfer() { + String transactionReference = "REF123456"; + FundTransfer fundTransfer = FundTransfer.builder() + .transactionReference(transactionReference) + .fromAccount("ACC0001234") + .toAccount("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .status(TransactionStatus.SUCCESS) + .transferType(TransferType.INTERNAL) + .build(); + + fundTransferRepository.save(fundTransfer); + + Optional result = fundTransferRepository + .findFundTransferByTransactionReference(transactionReference); + + assertTrue(result.isPresent()); + assertEquals(transactionReference, result.get().getTransactionReference()); + assertEquals("ACC0001234", result.get().getFromAccount()); + assertEquals("ACC0005678", result.get().getToAccount()); + } + + @Test + void findFundTransferByTransactionReference_nonExistingReference_returnsEmpty() { + Optional result = fundTransferRepository + .findFundTransferByTransactionReference("NONEXISTENT"); + + assertFalse(result.isPresent()); + } + + @Test + void findFundTransferByFromAccount_existingAccount_returnsTransfers() { + String fromAccount = "ACC0001234"; + FundTransfer transfer1 = FundTransfer.builder() + .transactionReference("REF001") + .fromAccount(fromAccount) + .toAccount("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .status(TransactionStatus.SUCCESS) + .transferType(TransferType.INTERNAL) + .build(); + + FundTransfer transfer2 = FundTransfer.builder() + .transactionReference("REF002") + .fromAccount(fromAccount) + .toAccount("ACC0009999") + .amount(BigDecimal.valueOf(300)) + .status(TransactionStatus.SUCCESS) + .transferType(TransferType.INTERNAL) + .build(); + + fundTransferRepository.save(transfer1); + fundTransferRepository.save(transfer2); + + List results = fundTransferRepository.findFundTransferByFromAccount(fromAccount); + + assertEquals(2, results.size()); + assertTrue(results.stream().allMatch(t -> t.getFromAccount().equals(fromAccount))); + } + + @Test + void findFundTransferByFromAccount_nonExistingAccount_returnsEmptyList() { + List results = fundTransferRepository + .findFundTransferByFromAccount("NONEXISTENT"); + + assertTrue(results.isEmpty()); + } +} diff --git a/Fund-Transfer/src/test/java/org/training/fundtransfer/service/implementation/FundTransferServiceImplTest.java b/Fund-Transfer/src/test/java/org/training/fundtransfer/service/implementation/FundTransferServiceImplTest.java new file mode 100644 index 0000000..215da7a --- /dev/null +++ b/Fund-Transfer/src/test/java/org/training/fundtransfer/service/implementation/FundTransferServiceImplTest.java @@ -0,0 +1,228 @@ +package org.training.fundtransfer.service.implementation; + +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.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.training.fundtransfer.exception.AccountUpdateException; +import org.training.fundtransfer.exception.InsufficientBalance; +import org.training.fundtransfer.exception.ResourceNotFound; +import org.training.fundtransfer.external.AccountService; +import org.training.fundtransfer.external.TransactionService; +import org.training.fundtransfer.model.TransactionStatus; +import org.training.fundtransfer.model.TransferType; +import org.training.fundtransfer.model.dto.Account; +import org.training.fundtransfer.model.dto.Transaction; +import org.training.fundtransfer.model.dto.request.FundTransferRequest; +import org.training.fundtransfer.model.dto.response.FundTransferResponse; +import org.training.fundtransfer.model.dto.response.Response; +import org.training.fundtransfer.model.entity.FundTransfer; +import org.training.fundtransfer.repository.FundTransferRepository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class FundTransferServiceImplTest { + + @Mock + private AccountService accountService; + + @Mock + private TransactionService transactionService; + + @Mock + private FundTransferRepository fundTransferRepository; + + @InjectMocks + private FundTransferServiceImpl fundTransferService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(fundTransferService, "ok", "200"); + } + + @Test + void fundTransfer_validAccountsAndSufficientBalance_success() { + FundTransferRequest request = new FundTransferRequest(); + request.setFromAccount("ACC0001234"); + request.setToAccount("ACC0005678"); + request.setAmount(BigDecimal.valueOf(500)); + + Account fromAccount = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + Account toAccount = Account.builder() + .accountNumber("ACC0005678") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(500)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(fromAccount)); + when(accountService.readByAccountNumber("ACC0005678")) + .thenReturn(ResponseEntity.ok(toAccount)); + when(accountService.updateAccount(anyString(), any(Account.class))) + .thenReturn(ResponseEntity.ok(Response.builder().build())); + when(transactionService.makeInternalTransactions(anyList(), anyString())) + .thenReturn(ResponseEntity.ok(Response.builder().build())); + when(fundTransferRepository.save(any(FundTransfer.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + FundTransferResponse response = fundTransferService.fundTransfer(request); + + assertNotNull(response); + assertEquals("Fund transfer was successful", response.getMessage()); + assertNotNull(response.getTransactionId()); + verify(accountService, times(2)).readByAccountNumber(anyString()); + verify(accountService, times(2)).updateAccount(anyString(), any(Account.class)); + verify(transactionService).makeInternalTransactions(anyList(), anyString()); + verify(fundTransferRepository).save(any(FundTransfer.class)); + } + + @Test + void fundTransfer_sourceAccountNotFound_throwsResourceNotFound() { + FundTransferRequest request = new FundTransferRequest(); + request.setFromAccount("ACC0001234"); + request.setToAccount("ACC0005678"); + request.setAmount(BigDecimal.valueOf(500)); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(ResourceNotFound.class, () -> fundTransferService.fundTransfer(request)); + verify(accountService).readByAccountNumber("ACC0001234"); + verify(fundTransferRepository, never()).save(any()); + } + + @Test + void fundTransfer_destinationAccountNotFound_throwsResourceNotFound() { + FundTransferRequest request = new FundTransferRequest(); + request.setFromAccount("ACC0001234"); + request.setToAccount("ACC0005678"); + request.setAmount(BigDecimal.valueOf(500)); + + Account fromAccount = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(fromAccount)); + when(accountService.readByAccountNumber("ACC0005678")) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(ResourceNotFound.class, () -> fundTransferService.fundTransfer(request)); + verify(fundTransferRepository, never()).save(any()); + } + + @Test + void fundTransfer_inactiveAccount_throwsAccountUpdateException() { + FundTransferRequest request = new FundTransferRequest(); + request.setFromAccount("ACC0001234"); + request.setToAccount("ACC0005678"); + request.setAmount(BigDecimal.valueOf(500)); + + Account fromAccount = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("PENDING") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(fromAccount)); + + assertThrows(AccountUpdateException.class, () -> fundTransferService.fundTransfer(request)); + verify(fundTransferRepository, never()).save(any()); + } + + @Test + void fundTransfer_insufficientBalance_throwsInsufficientBalance() { + FundTransferRequest request = new FundTransferRequest(); + request.setFromAccount("ACC0001234"); + request.setToAccount("ACC0005678"); + request.setAmount(BigDecimal.valueOf(1500)); + + Account fromAccount = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(fromAccount)); + + assertThrows(InsufficientBalance.class, () -> fundTransferService.fundTransfer(request)); + verify(fundTransferRepository, never()).save(any()); + } + + @Test + void getTransferDetailsFromReferenceId_validReference_returnsFundTransferDto() { + String referenceId = "REF123456"; + FundTransfer fundTransfer = FundTransfer.builder() + .fundTransferId(1L) + .transactionReference(referenceId) + .fromAccount("ACC0001234") + .toAccount("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .status(TransactionStatus.SUCCESS) + .transferType(TransferType.INTERNAL) + .build(); + + when(fundTransferRepository.findFundTransferByTransactionReference(referenceId)) + .thenReturn(Optional.of(fundTransfer)); + + assertDoesNotThrow(() -> fundTransferService.getTransferDetailsFromReferenceId(referenceId)); + verify(fundTransferRepository).findFundTransferByTransactionReference(referenceId); + } + + @Test + void getTransferDetailsFromReferenceId_invalidReference_throwsResourceNotFound() { + String referenceId = "INVALID_REF"; + + when(fundTransferRepository.findFundTransferByTransactionReference(referenceId)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFound.class, + () -> fundTransferService.getTransferDetailsFromReferenceId(referenceId)); + verify(fundTransferRepository).findFundTransferByTransactionReference(referenceId); + } + + @Test + void getAllTransfersByAccountId_validAccountId_returnsTransferList() { + String accountId = "ACC0001234"; + List transfers = List.of( + FundTransfer.builder() + .fundTransferId(1L) + .fromAccount(accountId) + .toAccount("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .build(), + FundTransfer.builder() + .fundTransferId(2L) + .fromAccount(accountId) + .toAccount("ACC0009999") + .amount(BigDecimal.valueOf(300)) + .build() + ); + + when(fundTransferRepository.findFundTransferByFromAccount(accountId)) + .thenReturn(transfers); + + assertDoesNotThrow(() -> fundTransferService.getAllTransfersByAccountId(accountId)); + verify(fundTransferRepository).findFundTransferByFromAccount(accountId); + } +} diff --git a/Fund-Transfer/src/test/resources/application-test.yml b/Fund-Transfer/src/test/resources/application-test.yml new file mode 100644 index 0000000..667cf48 --- /dev/null +++ b/Fund-Transfer/src/test/resources/application-test.yml @@ -0,0 +1,18 @@ +spring: + application: + name: fund-transfer-service + bad_request: 400 + ok: 200 + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect diff --git a/Sequence-Generator/pom.xml b/Sequence-Generator/pom.xml index b77a1ed..8fe4a38 100644 --- a/Sequence-Generator/pom.xml +++ b/Sequence-Generator/pom.xml @@ -57,6 +57,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + diff --git a/Sequence-Generator/src/test/java/org/training/sequence/generator/SequenceGeneratorApplicationTests.java b/Sequence-Generator/src/test/java/org/training/sequence/generator/SequenceGeneratorApplicationTests.java new file mode 100644 index 0000000..01de654 --- /dev/null +++ b/Sequence-Generator/src/test/java/org/training/sequence/generator/SequenceGeneratorApplicationTests.java @@ -0,0 +1,15 @@ +package org.training.sequence.generator; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class SequenceGeneratorApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/Sequence-Generator/src/test/java/org/training/sequence/generator/reporitory/SequenceRepositoryTest.java b/Sequence-Generator/src/test/java/org/training/sequence/generator/reporitory/SequenceRepositoryTest.java new file mode 100644 index 0000000..bfa7287 --- /dev/null +++ b/Sequence-Generator/src/test/java/org/training/sequence/generator/reporitory/SequenceRepositoryTest.java @@ -0,0 +1,53 @@ +package org.training.sequence.generator.reporitory; + +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.test.context.TestPropertySource; +import org.training.sequence.generator.model.entity.Sequence; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@TestPropertySource(locations = "classpath:application-test.yml") +class SequenceRepositoryTest { + + @Autowired + private SequenceRepository sequenceRepository; + + @Test + void save_newSequence_persists() { + Sequence sequence = Sequence.builder() + .accountNumber(1L) + .build(); + + Sequence saved = sequenceRepository.save(sequence); + + assertNotNull(saved); + assertNotNull(saved.getSequenceId()); + assertEquals(1L, saved.getAccountNumber()); + } + + @Test + void findById_existingSequence_returnsSequence() { + Sequence sequence = Sequence.builder() + .accountNumber(100L) + .build(); + + Sequence saved = sequenceRepository.save(sequence); + + Optional result = sequenceRepository.findById(saved.getSequenceId()); + + assertTrue(result.isPresent()); + assertEquals(100L, result.get().getAccountNumber()); + } + + @Test + void findById_nonExistingSequence_returnsEmpty() { + Optional result = sequenceRepository.findById(999L); + + assertFalse(result.isPresent()); + } +} diff --git a/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceConcurrencyTest.java b/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceConcurrencyTest.java new file mode 100644 index 0000000..12baab8 --- /dev/null +++ b/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceConcurrencyTest.java @@ -0,0 +1,85 @@ +package org.training.sequence.generator.service.implementation; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.training.sequence.generator.model.entity.Sequence; +import org.training.sequence.generator.reporitory.SequenceRepository; +import org.training.sequence.generator.service.SequenceService; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@ActiveProfiles("test") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class SequenceServiceConcurrencyTest { + + @Autowired + private SequenceService sequenceService; + + @Autowired + private SequenceRepository sequenceRepository; + + @Test + @Order(2) + void concurrentCreate_demonstratesThreadSafetyIssue() throws InterruptedException { + sequenceRepository.save(Sequence.builder().accountNumber(1L).build()); + + int threadCount = 10; + ExecutorService executorService = Executors.newFixedThreadPool(threadCount); + CountDownLatch latch = new CountDownLatch(threadCount); + + Set accountNumbers = ConcurrentHashMap.newKeySet(); + AtomicInteger errorCount = new AtomicInteger(0); + + for (int i = 0; i < threadCount; i++) { + executorService.submit(() -> { + try { + Sequence sequence = sequenceService.create(); + accountNumbers.add(sequence.getAccountNumber()); + } catch (Exception e) { + errorCount.incrementAndGet(); + } finally { + latch.countDown(); + } + }); + } + + latch.await(); + executorService.shutdown(); + + System.out.println("Generated account numbers: " + accountNumbers); + System.out.println("Unique account numbers: " + accountNumbers.size()); + System.out.println("Expected unique numbers: " + threadCount); + + assertTrue(accountNumbers.size() <= threadCount, + "Should have at most " + threadCount + " unique account numbers"); + } + + @Test + @Order(1) + void sequentialCreate_worksCorrectly() { + sequenceRepository.deleteAll(); + + Set accountNumbers = new HashSet<>(); + + for (int i = 0; i < 5; i++) { + Sequence sequence = sequenceService.create(); + accountNumbers.add(sequence.getAccountNumber()); + } + + assertEquals(5, accountNumbers.size(), "Should have 5 unique account numbers"); + } +} diff --git a/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceImplTest.java b/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceImplTest.java new file mode 100644 index 0000000..88d7282 --- /dev/null +++ b/Sequence-Generator/src/test/java/org/training/sequence/generator/service/implementation/SequenceServiceImplTest.java @@ -0,0 +1,88 @@ +package org.training.sequence.generator.service.implementation; + +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.training.sequence.generator.model.entity.Sequence; +import org.training.sequence.generator.reporitory.SequenceRepository; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class SequenceServiceImplTest { + + @Mock + private SequenceRepository sequenceRepository; + + @InjectMocks + private SequenceServiceImpl sequenceService; + + @Test + void create_firstTime_createsNewSequence() { + when(sequenceRepository.findById(1L)) + .thenReturn(Optional.empty()); + when(sequenceRepository.save(any(Sequence.class))) + .thenAnswer(invocation -> { + Sequence seq = invocation.getArgument(0); + seq.setSequenceId(1L); + return seq; + }); + + Sequence result = sequenceService.create(); + + assertNotNull(result); + assertEquals(1L, result.getAccountNumber()); + verify(sequenceRepository).findById(1L); + verify(sequenceRepository).save(any(Sequence.class)); + } + + @Test + void create_subsequentCalls_incrementsAccountNumber() { + Sequence existingSequence = Sequence.builder() + .sequenceId(1L) + .accountNumber(100L) + .build(); + + when(sequenceRepository.findById(1L)) + .thenReturn(Optional.of(existingSequence)); + when(sequenceRepository.save(any(Sequence.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Sequence result = sequenceService.create(); + + assertNotNull(result); + assertEquals(101L, result.getAccountNumber()); + verify(sequenceRepository).findById(1L); + verify(sequenceRepository).save(any(Sequence.class)); + } + + @Test + void create_multipleSequentialCalls_incrementsCorrectly() { + Sequence sequence = Sequence.builder() + .sequenceId(1L) + .accountNumber(1L) + .build(); + + when(sequenceRepository.findById(1L)) + .thenReturn(Optional.of(sequence)); + when(sequenceRepository.save(any(Sequence.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Sequence result1 = sequenceService.create(); + assertEquals(2L, result1.getAccountNumber()); + + sequence.setAccountNumber(2L); + Sequence result2 = sequenceService.create(); + assertEquals(3L, result2.getAccountNumber()); + + sequence.setAccountNumber(3L); + Sequence result3 = sequenceService.create(); + assertEquals(4L, result3.getAccountNumber()); + } +} diff --git a/Sequence-Generator/src/test/resources/application-test.yml b/Sequence-Generator/src/test/resources/application-test.yml new file mode 100644 index 0000000..6d15b2f --- /dev/null +++ b/Sequence-Generator/src/test/resources/application-test.yml @@ -0,0 +1,16 @@ +spring: + application: + name: sequence-generator + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect diff --git a/Service-Registry/src/test/java/org/training/service/registry/ServiceRegistryApplicationTests.java b/Service-Registry/src/test/java/org/training/service/registry/ServiceRegistryApplicationTests.java index c68762a..582aab1 100644 --- a/Service-Registry/src/test/java/org/training/service/registry/ServiceRegistryApplicationTests.java +++ b/Service-Registry/src/test/java/org/training/service/registry/ServiceRegistryApplicationTests.java @@ -2,15 +2,17 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest +@ActiveProfiles("test") class ServiceRegistryApplicationTests { @Test void contextLoads() { - assertTrue(true); + assertTrue(true, "Eureka server should start without errors"); } } diff --git a/Service-Registry/src/test/resources/application-test.yml b/Service-Registry/src/test/resources/application-test.yml new file mode 100644 index 0000000..a70dbab --- /dev/null +++ b/Service-Registry/src/test/resources/application-test.yml @@ -0,0 +1,9 @@ +server: + port: 8761 + +eureka: + client: + register-with-eureka: false + fetch-registry: false + server: + enable-self-preservation: false diff --git a/Transaction-Service/pom.xml b/Transaction-Service/pom.xml index 83deb58..2fd4d6f 100644 --- a/Transaction-Service/pom.xml +++ b/Transaction-Service/pom.xml @@ -65,6 +65,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + diff --git a/Transaction-Service/src/test/java/org/training/transactions/repository/TransactionRepositoryTest.java b/Transaction-Service/src/test/java/org/training/transactions/repository/TransactionRepositoryTest.java new file mode 100644 index 0000000..6f9bbb2 --- /dev/null +++ b/Transaction-Service/src/test/java/org/training/transactions/repository/TransactionRepositoryTest.java @@ -0,0 +1,94 @@ +package org.training.transactions.repository; + +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.test.context.TestPropertySource; +import org.training.transactions.model.TransactionStatus; +import org.training.transactions.model.TransactionType; +import org.training.transactions.model.entity.Transaction; + +import java.math.BigDecimal; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@TestPropertySource(locations = "classpath:application-test.yml") +class TransactionRepositoryTest { + + @Autowired + private TransactionRepository transactionRepository; + + @Test + void findTransactionByAccountId_existingAccount_returnsTransactions() { + String accountId = "ACC0001234"; + Transaction transaction1 = Transaction.builder() + .accountId(accountId) + .amount(BigDecimal.valueOf(500)) + .transactionType(TransactionType.DEPOSIT) + .status(TransactionStatus.COMPLETED) + .comments("Test deposit") + .build(); + + Transaction transaction2 = Transaction.builder() + .accountId(accountId) + .amount(BigDecimal.valueOf(-200)) + .transactionType(TransactionType.WITHDRAWAL) + .status(TransactionStatus.COMPLETED) + .comments("Test withdrawal") + .build(); + + transactionRepository.save(transaction1); + transactionRepository.save(transaction2); + + List results = transactionRepository.findTransactionByAccountId(accountId); + + assertEquals(2, results.size()); + assertTrue(results.stream().allMatch(t -> t.getAccountId().equals(accountId))); + } + + @Test + void findTransactionByAccountId_nonExistingAccount_returnsEmptyList() { + List results = transactionRepository.findTransactionByAccountId("NONEXISTENT"); + + assertTrue(results.isEmpty()); + } + + @Test + void findTransactionByReferenceId_existingReference_returnsTransactions() { + String referenceId = "REF123456"; + Transaction transaction1 = Transaction.builder() + .referenceId(referenceId) + .accountId("ACC0001234") + .amount(BigDecimal.valueOf(-500)) + .transactionType(TransactionType.INTERNAL_TRANSFER) + .status(TransactionStatus.COMPLETED) + .comments("Internal transfer debit") + .build(); + + Transaction transaction2 = Transaction.builder() + .referenceId(referenceId) + .accountId("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .transactionType(TransactionType.INTERNAL_TRANSFER) + .status(TransactionStatus.COMPLETED) + .comments("Internal transfer credit") + .build(); + + transactionRepository.save(transaction1); + transactionRepository.save(transaction2); + + List results = transactionRepository.findTransactionByReferenceId(referenceId); + + assertEquals(2, results.size()); + assertTrue(results.stream().allMatch(t -> t.getReferenceId().equals(referenceId))); + } + + @Test + void findTransactionByReferenceId_nonExistingReference_returnsEmptyList() { + List results = transactionRepository.findTransactionByReferenceId("NONEXISTENT"); + + assertTrue(results.isEmpty()); + } +} diff --git a/Transaction-Service/src/test/java/org/training/transactions/service/implementation/TransactionServiceImplTest.java b/Transaction-Service/src/test/java/org/training/transactions/service/implementation/TransactionServiceImplTest.java new file mode 100644 index 0000000..0a470a2 --- /dev/null +++ b/Transaction-Service/src/test/java/org/training/transactions/service/implementation/TransactionServiceImplTest.java @@ -0,0 +1,235 @@ +package org.training.transactions.service.implementation; + +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.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.training.transactions.exception.AccountStatusException; +import org.training.transactions.exception.InsufficientBalance; +import org.training.transactions.exception.ResourceNotFound; +import org.training.transactions.external.AccountService; +import org.training.transactions.model.TransactionStatus; +import org.training.transactions.model.TransactionType; +import org.training.transactions.model.dto.TransactionDto; +import org.training.transactions.model.entity.Transaction; +import org.training.transactions.model.external.Account; +import org.training.transactions.model.response.Response; +import org.training.transactions.repository.TransactionRepository; + +import java.math.BigDecimal; +import java.util.List; + +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 TransactionServiceImplTest { + + @Mock + private TransactionRepository transactionRepository; + + @Mock + private AccountService accountService; + + @InjectMocks + private TransactionServiceImpl transactionService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(transactionService, "ok", "200"); + } + + @Test + void addTransaction_depositType_success() { + TransactionDto transactionDto = TransactionDto.builder() + .accountId("ACC0001234") + .transactionType(TransactionType.DEPOSIT.toString()) + .amount(BigDecimal.valueOf(500)) + .description("Deposit test") + .build(); + + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(account)); + when(accountService.updateAccount(anyString(), any(Account.class))) + .thenReturn(ResponseEntity.ok(null)); + when(transactionRepository.save(any(Transaction.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = transactionService.addTransaction(transactionDto); + + assertNotNull(response); + assertEquals("Transaction completed successfully", response.getMessage()); + verify(accountService).readByAccountNumber("ACC0001234"); + verify(accountService).updateAccount(anyString(), any(Account.class)); + verify(transactionRepository).save(any(Transaction.class)); + } + + @Test + void addTransaction_withdrawalWithSufficientBalance_success() { + TransactionDto transactionDto = TransactionDto.builder() + .accountId("ACC0001234") + .transactionType(TransactionType.WITHDRAWAL.toString()) + .amount(BigDecimal.valueOf(500)) + .description("Withdrawal test") + .build(); + + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(account)); + when(accountService.updateAccount(anyString(), any(Account.class))) + .thenReturn(ResponseEntity.ok(null)); + when(transactionRepository.save(any(Transaction.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = transactionService.addTransaction(transactionDto); + + assertNotNull(response); + assertEquals("Transaction completed successfully", response.getMessage()); + verify(accountService).updateAccount(anyString(), any(Account.class)); + verify(transactionRepository).save(any(Transaction.class)); + } + + @Test + void addTransaction_withdrawalInsufficientBalance_throwsInsufficientBalance() { + TransactionDto transactionDto = TransactionDto.builder() + .accountId("ACC0001234") + .transactionType(TransactionType.WITHDRAWAL.toString()) + .amount(BigDecimal.valueOf(1500)) + .description("Withdrawal test") + .build(); + + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("ACTIVE") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(account)); + + assertThrows(InsufficientBalance.class, () -> transactionService.addTransaction(transactionDto)); + verify(transactionRepository, never()).save(any()); + } + + @Test + void addTransaction_withdrawalInactiveAccount_throwsAccountStatusException() { + TransactionDto transactionDto = TransactionDto.builder() + .accountId("ACC0001234") + .transactionType(TransactionType.WITHDRAWAL.toString()) + .amount(BigDecimal.valueOf(500)) + .description("Withdrawal test") + .build(); + + Account account = Account.builder() + .accountNumber("ACC0001234") + .accountStatus("PENDING") + .availableBalance(BigDecimal.valueOf(1000)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(account)); + + assertThrows(AccountStatusException.class, () -> transactionService.addTransaction(transactionDto)); + verify(transactionRepository, never()).save(any()); + } + + @Test + void addTransaction_accountNotFound_throwsResourceNotFound() { + TransactionDto transactionDto = TransactionDto.builder() + .accountId("ACC0001234") + .transactionType(TransactionType.DEPOSIT.toString()) + .amount(BigDecimal.valueOf(500)) + .build(); + + when(accountService.readByAccountNumber("ACC0001234")) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(ResourceNotFound.class, () -> transactionService.addTransaction(transactionDto)); + verify(transactionRepository, never()).save(any()); + } + + @Test + void internalTransaction_validTransactions_success() { + List transactionDtos = List.of( + TransactionDto.builder() + .accountId("ACC0001234") + .amount(BigDecimal.valueOf(-500)) + .description("Internal transfer debit") + .build(), + TransactionDto.builder() + .accountId("ACC0005678") + .amount(BigDecimal.valueOf(500)) + .description("Internal transfer credit") + .build() + ); + + String transactionReference = "REF123456"; + + when(transactionRepository.saveAll(anyList())) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Response response = transactionService.internalTransaction(transactionDtos, transactionReference); + + assertNotNull(response); + assertEquals("Transaction completed successfully", response.getMessage()); + verify(transactionRepository).saveAll(anyList()); + } + + @Test + void getTransaction_validAccountId_returnsTransactions() { + String accountId = "ACC0001234"; + List transactions = List.of( + Transaction.builder() + .transactionId(1L) + .accountId(accountId) + .amount(BigDecimal.valueOf(500)) + .transactionType(TransactionType.DEPOSIT) + .status(TransactionStatus.COMPLETED) + .build() + ); + + when(transactionRepository.findTransactionByAccountId(accountId)) + .thenReturn(transactions); + + assertDoesNotThrow(() -> transactionService.getTransaction(accountId)); + verify(transactionRepository).findTransactionByAccountId(accountId); + } + + @Test + void getTransactionByTransactionReference_validReference_returnsTransactions() { + String transactionReference = "REF123456"; + List transactions = List.of( + Transaction.builder() + .transactionId(1L) + .referenceId(transactionReference) + .accountId("ACC0001234") + .amount(BigDecimal.valueOf(500)) + .transactionType(TransactionType.INTERNAL_TRANSFER) + .status(TransactionStatus.COMPLETED) + .build() + ); + + when(transactionRepository.findTransactionByReferenceId(transactionReference)) + .thenReturn(transactions); + + assertDoesNotThrow(() -> transactionService.getTransactionByTransactionReference(transactionReference)); + verify(transactionRepository).findTransactionByReferenceId(transactionReference); + } +} diff --git a/Transaction-Service/src/test/resources/application-test.yml b/Transaction-Service/src/test/resources/application-test.yml new file mode 100644 index 0000000..157e556 --- /dev/null +++ b/Transaction-Service/src/test/resources/application-test.yml @@ -0,0 +1,17 @@ +spring: + application: + name: transaction-service + ok: 200 + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect diff --git a/User-Service/pom.xml b/User-Service/pom.xml index 002edc5..c1bd64f 100644 --- a/User-Service/pom.xml +++ b/User-Service/pom.xml @@ -66,6 +66,11 @@ spring-boot-starter-test test + + com.h2database + h2 + test + org.keycloak diff --git a/User-Service/src/test/java/org/training/user/service/UserServiceApplicationTests.java b/User-Service/src/test/java/org/training/user/service/UserServiceApplicationTests.java new file mode 100644 index 0000000..1bde93b --- /dev/null +++ b/User-Service/src/test/java/org/training/user/service/UserServiceApplicationTests.java @@ -0,0 +1,15 @@ +package org.training.user.service; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class UserServiceApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/User-Service/src/test/java/org/training/user/service/repository/UserRepositoryTest.java b/User-Service/src/test/java/org/training/user/service/repository/UserRepositoryTest.java new file mode 100644 index 0000000..d555f29 --- /dev/null +++ b/User-Service/src/test/java/org/training/user/service/repository/UserRepositoryTest.java @@ -0,0 +1,54 @@ +package org.training.user.service.repository; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.test.context.ActiveProfiles; +import org.training.user.service.model.Status; +import org.training.user.service.model.entity.User; +import org.training.user.service.model.entity.UserProfile; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@ActiveProfiles("test") +class UserRepositoryTest { + + @Autowired + private UserRepository userRepository; + + @Test + void findUserByAuthId_existingUser_returnsUser() { + String authId = "auth-123"; + User user = User.builder() + .authId(authId) + .emailId("test@example.com") + .contactNo("1234567890") + .status(Status.PENDING) + .identificationNumber("ID-123") + .userProfile(UserProfile.builder() + .firstName("John") + .lastName("Doe") + .build()) + .build(); + + userRepository.save(user); + + Optional result = userRepository.findUserByAuthId(authId); + + assertTrue(result.isPresent()); + assertEquals(authId, result.get().getAuthId()); + assertEquals("test@example.com", result.get().getEmailId()); + } + + @Test + void findUserByAuthId_nonExistingUser_returnsEmpty() { + Optional result = userRepository.findUserByAuthId("non-existing"); + + assertFalse(result.isPresent()); + } +} diff --git a/User-Service/src/test/java/org/training/user/service/service/implementation/UserServiceImplTest.java b/User-Service/src/test/java/org/training/user/service/service/implementation/UserServiceImplTest.java new file mode 100644 index 0000000..c0f5d92 --- /dev/null +++ b/User-Service/src/test/java/org/training/user/service/service/implementation/UserServiceImplTest.java @@ -0,0 +1,399 @@ +package org.training.user.service.service.implementation; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.keycloak.representations.idm.UserRepresentation; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.ResponseEntity; +import org.springframework.test.util.ReflectionTestUtils; +import org.training.user.service.exception.EmptyFields; +import org.training.user.service.exception.ResourceConflictException; +import org.training.user.service.exception.ResourceNotFound; +import org.training.user.service.external.AccountService; +import org.training.user.service.model.Status; +import org.training.user.service.model.dto.CreateUser; +import org.training.user.service.model.dto.UserDto; +import org.training.user.service.model.dto.UserUpdate; +import org.training.user.service.model.dto.UserUpdateStatus; +import org.training.user.service.model.dto.response.Response; +import org.training.user.service.model.entity.User; +import org.training.user.service.model.entity.UserProfile; +import org.training.user.service.model.external.Account; +import org.training.user.service.repository.UserRepository; +import org.training.user.service.service.KeycloakService; + +import java.util.Collections; +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 UserServiceImplTest { + + @Mock + private UserRepository userRepository; + + @Mock + private KeycloakService keycloakService; + + @Mock + private AccountService accountService; + + @InjectMocks + private UserServiceImpl userService; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(userService, "responseCodeSuccess", "200"); + ReflectionTestUtils.setField(userService, "responseCodeNotFound", "404"); + } + + @Test + void createUser_validUser_success() { + UserRepresentation userRep = new UserRepresentation(); + userRep.setId("auth-123"); + + when(keycloakService.readUserByEmail("test@example.com")) + .thenReturn(Collections.emptyList()) + .thenReturn(Collections.singletonList(userRep)); + + when(keycloakService.createUser(any(UserRepresentation.class))) + .thenReturn(201); + + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + CreateUser createUser = CreateUser.builder() + .firstName("John") + .lastName("Doe") + .emailId("test@example.com") + .password("password123") + .contactNumber("1234567890") + .build(); + + Response response = userService.createUser(createUser); + + assertNotNull(response); + assertEquals("User created successfully", response.getResponseMessage()); + verify(keycloakService, times(2)).readUserByEmail("test@example.com"); + verify(keycloakService).createUser(any(UserRepresentation.class)); + verify(userRepository).save(any(User.class)); + } + + @Test + void createUser_duplicateEmail_throwsResourceConflictException() { + UserRepresentation existingUser = new UserRepresentation(); + existingUser.setId("existing-123"); + + when(keycloakService.readUserByEmail("test@example.com")) + .thenReturn(Collections.singletonList(existingUser)); + + CreateUser createUser = CreateUser.builder() + .emailId("test@example.com") + .build(); + + assertThrows(ResourceConflictException.class, () -> userService.createUser(createUser)); + verify(keycloakService, never()).createUser(any()); + verify(userRepository, never()).save(any()); + } + + @Test + void createUser_keycloakFailure_throwsRuntimeException() { + when(keycloakService.readUserByEmail(anyString())) + .thenReturn(Collections.emptyList()); + when(keycloakService.createUser(any(UserRepresentation.class))) + .thenReturn(400); + + CreateUser createUser = CreateUser.builder() + .firstName("John") + .lastName("Doe") + .emailId("test@example.com") + .password("password123") + .contactNumber("1234567890") + .build(); + + assertThrows(RuntimeException.class, () -> userService.createUser(createUser)); + verify(userRepository, never()).save(any()); + } + + @Test + void readUser_existingUser_returnsUserDto() { + String authId = "auth-123"; + User user = User.builder() + .userId(1L) + .authId(authId) + .contactNo("1234567890") + .status(Status.APPROVED) + .identificationNumber("ID-123") + .userProfile(UserProfile.builder() + .firstName("John") + .lastName("Doe") + .build()) + .build(); + + UserRepresentation userRep = new UserRepresentation(); + userRep.setEmail("test@example.com"); + + when(userRepository.findUserByAuthId(authId)) + .thenReturn(Optional.of(user)); + when(keycloakService.readUser(authId)) + .thenReturn(userRep); + + UserDto result = userService.readUser(authId); + + assertNotNull(result); + assertEquals("test@example.com", result.getEmailId()); + verify(userRepository).findUserByAuthId(authId); + verify(keycloakService).readUser(authId); + } + + @Test + void readUser_nonExistingUser_throwsResourceNotFound() { + String authId = "non-existing"; + + when(userRepository.findUserByAuthId(authId)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFound.class, () -> userService.readUser(authId)); + } + + @Test + void updateUserStatus_approvedStatus_enablesKeycloakAccount() { + Long userId = 1L; + User user = User.builder() + .userId(userId) + .authId("auth-123") + .emailId("test@example.com") + .contactNo("1234567890") + .status(Status.PENDING) + .identificationNumber("ID-123") + .creationOn(java.time.LocalDate.now()) + .userProfile(UserProfile.builder() + .userProfileId(1L) + .firstName("John") + .lastName("Doe") + .gender("Male") + .address("123 Main St") + .occupation("Engineer") + .martialStatus("Single") + .nationality("US") + .build()) + .build(); + + UserRepresentation userRep = new UserRepresentation(); + userRep.setId("auth-123"); + + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + when(keycloakService.readUser("auth-123")) + .thenReturn(userRep); + doNothing().when(keycloakService).updateUser(any(UserRepresentation.class)); + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + UserUpdateStatus statusUpdate = new UserUpdateStatus(); + statusUpdate.setStatus(Status.APPROVED); + + Response response = userService.updateUserStatus(userId, statusUpdate); + + assertNotNull(response); + assertEquals("User updated successfully", response.getResponseMessage()); + verify(keycloakService).readUser("auth-123"); + verify(keycloakService).updateUser(any(UserRepresentation.class)); + verify(userRepository).save(any(User.class)); + } + + @Test + void updateUserStatus_otherStatus_doesNotCallKeycloak() { + Long userId = 1L; + User user = User.builder() + .userId(userId) + .authId("auth-123") + .emailId("test@example.com") + .contactNo("1234567890") + .status(Status.PENDING) + .identificationNumber("ID-123") + .creationOn(java.time.LocalDate.now()) + .userProfile(UserProfile.builder() + .userProfileId(1L) + .firstName("John") + .lastName("Doe") + .gender("Male") + .address("123 Main St") + .occupation("Engineer") + .martialStatus("Single") + .nationality("US") + .build()) + .build(); + + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + UserUpdateStatus statusUpdate = new UserUpdateStatus(); + statusUpdate.setStatus(Status.REJECTED); + + Response response = userService.updateUserStatus(userId, statusUpdate); + + assertNotNull(response); + verify(keycloakService, never()).readUser(anyString()); + verify(keycloakService, never()).updateUser(any()); + } + + @Test + void updateUserStatus_userNotFound_throwsResourceNotFound() { + Long userId = 999L; + + when(userRepository.findById(userId)) + .thenReturn(Optional.empty()); + + UserUpdateStatus statusUpdate = new UserUpdateStatus(); + statusUpdate.setStatus(Status.APPROVED); + + assertThrows(ResourceNotFound.class, () -> userService.updateUserStatus(userId, statusUpdate)); + } + + @Test + void readUserById_existingUser_returnsUserDto() { + Long userId = 1L; + User user = User.builder() + .userId(userId) + .authId("auth-123") + .contactNo("1234567890") + .status(Status.APPROVED) + .identificationNumber("ID-123") + .userProfile(UserProfile.builder() + .firstName("John") + .lastName("Doe") + .build()) + .build(); + + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + + UserDto result = userService.readUserById(userId); + + assertNotNull(result); + assertEquals(userId, result.getUserId()); + verify(userRepository).findById(userId); + } + + @Test + void readUserById_nonExistingUser_throwsResourceNotFound() { + Long userId = 999L; + + when(userRepository.findById(userId)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFound.class, () -> userService.readUserById(userId)); + } + + @Test + void updateUser_validUpdate_success() { + Long userId = 1L; + User user = User.builder() + .userId(userId) + .contactNo("1234567890") + .userProfile(UserProfile.builder() + .firstName("John") + .lastName("Doe") + .build()) + .build(); + + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + when(userRepository.save(any(User.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + UserUpdate userUpdate = UserUpdate.builder() + .firstName("Jane") + .lastName("Smith") + .contactNo("0987654321") + .build(); + + Response response = userService.updateUser(userId, userUpdate); + + assertNotNull(response); + assertEquals("user updated successfully", response.getResponseMessage()); + verify(userRepository).save(any(User.class)); + } + + @Test + void updateUser_userNotFound_throwsResourceNotFound() { + Long userId = 999L; + + when(userRepository.findById(userId)) + .thenReturn(Optional.empty()); + + UserUpdate userUpdate = UserUpdate.builder().build(); + + assertThrows(ResourceNotFound.class, () -> userService.updateUser(userId, userUpdate)); + } + + @Test + void readUserByAccountId_validAccount_returnsUserDto() { + String accountId = "ACC123"; + Long userId = 1L; + + Account account = new Account(); + account.setUserId(userId); + + User user = User.builder() + .userId(userId) + .authId("auth-123") + .contactNo("1234567890") + .status(Status.APPROVED) + .identificationNumber("ID-123") + .userProfile(UserProfile.builder() + .firstName("John") + .lastName("Doe") + .build()) + .build(); + + when(accountService.readByAccountNumber(accountId)) + .thenReturn(ResponseEntity.ok(account)); + when(userRepository.findById(userId)) + .thenReturn(Optional.of(user)); + + UserDto result = userService.readUserByAccountId(accountId); + + assertNotNull(result); + assertEquals(userId, result.getUserId()); + verify(accountService).readByAccountNumber(accountId); + verify(userRepository).findById(userId); + } + + @Test + void readUserByAccountId_accountNotFound_throwsResourceNotFound() { + String accountId = "INVALID"; + + when(accountService.readByAccountNumber(accountId)) + .thenReturn(ResponseEntity.ok(null)); + + assertThrows(ResourceNotFound.class, () -> userService.readUserByAccountId(accountId)); + } + + @Test + void readUserByAccountId_userNotFound_throwsResourceNotFound() { + String accountId = "ACC123"; + Long userId = 999L; + + Account account = new Account(); + account.setUserId(userId); + + when(accountService.readByAccountNumber(accountId)) + .thenReturn(ResponseEntity.ok(account)); + when(userRepository.findById(userId)) + .thenReturn(Optional.empty()); + + assertThrows(ResourceNotFound.class, () -> userService.readUserByAccountId(accountId)); + } +} diff --git a/User-Service/src/test/resources/application-test.yml b/User-Service/src/test/resources/application-test.yml new file mode 100644 index 0000000..f225a1d --- /dev/null +++ b/User-Service/src/test/resources/application-test.yml @@ -0,0 +1,19 @@ +spring: + application: + name: user-service + success: 200 + not_found: 404 + datasource: + url: jdbc:h2:mem:testdb;NON_KEYWORDS=USER + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + format_sql: true + dialect: org.hibernate.dialect.H2Dialect + globally_quoted_identifiers: true