diff --git a/build.gradle b/build.gradle index d0022b6..a631b53 100644 --- a/build.gradle +++ b/build.gradle @@ -21,10 +21,13 @@ compileJava { } dependencies { - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' + compileOnly 'org.projectlombok:lombok:1.18.24' + annotationProcessor 'org.projectlombok:lombok:1.18.24' - implementation ('org.springframework.boot:spring-boot-starter-web') + testCompileOnly 'org.projectlombok:lombok:1.18.24' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.24' + + implementation('org.springframework.boot:spring-boot-starter-web') implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' diff --git a/src/test/java/io/reflectoring/buckpal/account/application/service/FakeAccountRepository.java b/src/test/java/io/reflectoring/buckpal/account/application/service/FakeAccountRepository.java new file mode 100644 index 0000000..6ad66b0 --- /dev/null +++ b/src/test/java/io/reflectoring/buckpal/account/application/service/FakeAccountRepository.java @@ -0,0 +1,28 @@ +package io.reflectoring.buckpal.account.application.service; + +import io.reflectoring.buckpal.account.application.port.out.LoadAccountPort; +import io.reflectoring.buckpal.account.application.port.out.UpdateAccountStatePort; +import io.reflectoring.buckpal.account.domain.Account; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +public class FakeAccountRepository implements LoadAccountPort, UpdateAccountStatePort { + + Map accounts = new HashMap(); + + public void addAccount(Account account) { + accounts.put(account.getId().get(), account); + } + + @Override + public Account loadAccount(Account.AccountId accountId, LocalDateTime baselineDate) { + return accounts.get(accountId); + } + + @Override + public void updateActivities(Account account) { + // no op + } +} diff --git a/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java b/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java index 1cb61c8..6a93698 100644 --- a/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java +++ b/src/test/java/io/reflectoring/buckpal/account/application/service/SendMoneyServiceTest.java @@ -6,143 +6,102 @@ import io.reflectoring.buckpal.account.application.port.out.UpdateAccountStatePort; import io.reflectoring.buckpal.account.domain.Account; import io.reflectoring.buckpal.account.domain.Account.AccountId; +import io.reflectoring.buckpal.account.domain.ActivityWindow; import io.reflectoring.buckpal.account.domain.Money; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; -import java.time.LocalDateTime; -import java.util.List; -import java.util.Optional; -import java.util.stream.Collectors; +import java.math.BigInteger; +import java.util.ArrayList; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.BDDMockito.*; +import static org.assertj.core.api.Assertions.assertThat; class SendMoneyServiceTest { - private final LoadAccountPort loadAccountPort = - Mockito.mock(LoadAccountPort.class); - - private final AccountLock accountLock = - Mockito.mock(AccountLock.class); - - private final UpdateAccountStatePort updateAccountStatePort = - Mockito.mock(UpdateAccountStatePort.class); - - private final SendMoneyService sendMoneyService = - new SendMoneyService(loadAccountPort, accountLock, updateAccountStatePort, moneyTransferProperties()); - - @Test - void givenWithdrawalFails_thenOnlySourceAccountIsLockedAndReleased() { - - AccountId sourceAccountId = new AccountId(41L); - Account sourceAccount = givenAnAccountWithId(sourceAccountId); - - AccountId targetAccountId = new AccountId(42L); - Account targetAccount = givenAnAccountWithId(targetAccountId); - - givenWithdrawalWillFail(sourceAccount); - givenDepositWillSucceed(targetAccount); - - SendMoneyCommand command = new SendMoneyCommand( - sourceAccountId, - targetAccountId, - Money.of(300L)); - - boolean success = sendMoneyService.sendMoney(command); - - assertThat(success).isFalse(); - - then(accountLock).should().lockAccount(eq(sourceAccountId)); - then(accountLock).should().releaseAccount(eq(sourceAccountId)); - then(accountLock).should(times(0)).lockAccount(eq(targetAccountId)); - } - - @Test - void transactionSucceeds() { - - Account sourceAccount = givenSourceAccount(); - Account targetAccount = givenTargetAccount(); - - givenWithdrawalWillSucceed(sourceAccount); - givenDepositWillSucceed(targetAccount); - - Money money = Money.of(500L); - - SendMoneyCommand command = new SendMoneyCommand( - sourceAccount.getId().get(), - targetAccount.getId().get(), - money); - - boolean success = sendMoneyService.sendMoney(command); - - assertThat(success).isTrue(); - - AccountId sourceAccountId = sourceAccount.getId().get(); - AccountId targetAccountId = targetAccount.getId().get(); - - then(accountLock).should().lockAccount(eq(sourceAccountId)); - then(sourceAccount).should().withdraw(eq(money), eq(targetAccountId)); - then(accountLock).should().releaseAccount(eq(sourceAccountId)); - - then(accountLock).should().lockAccount(eq(targetAccountId)); - then(targetAccount).should().deposit(eq(money), eq(sourceAccountId)); - then(accountLock).should().releaseAccount(eq(targetAccountId)); - - thenAccountsHaveBeenUpdated(sourceAccountId, targetAccountId); - } - - private void thenAccountsHaveBeenUpdated(AccountId... accountIds){ - ArgumentCaptor accountCaptor = ArgumentCaptor.forClass(Account.class); - then(updateAccountStatePort).should(times(accountIds.length)) - .updateActivities(accountCaptor.capture()); - - List updatedAccountIds = accountCaptor.getAllValues() - .stream() - .map(Account::getId) - .map(Optional::get) - .collect(Collectors.toList()); - - for(AccountId accountId : accountIds){ - assertThat(updatedAccountIds).contains(accountId); - } - } - - private void givenDepositWillSucceed(Account account) { - given(account.deposit(any(Money.class), any(AccountId.class))) - .willReturn(true); - } - - private void givenWithdrawalWillFail(Account account) { - given(account.withdraw(any(Money.class), any(AccountId.class))) - .willReturn(false); - } - - private void givenWithdrawalWillSucceed(Account account) { - given(account.withdraw(any(Money.class), any(AccountId.class))) - .willReturn(true); - } - - private Account givenTargetAccount(){ - return givenAnAccountWithId(new AccountId(42L)); - } - - private Account givenSourceAccount(){ - return givenAnAccountWithId(new AccountId(41L)); - } - - private Account givenAnAccountWithId(AccountId id) { - Account account = Mockito.mock(Account.class); - given(account.getId()) - .willReturn(Optional.of(id)); - given(loadAccountPort.loadAccount(eq(account.getId().get()), any(LocalDateTime.class))) - .willReturn(account); - return account; - } - - private MoneyTransferProperties moneyTransferProperties(){ - return new MoneyTransferProperties(Money.of(Long.MAX_VALUE)); - } + FakeAccountRepository repo = new FakeAccountRepository(); + + private final LoadAccountPort loadAccountPort = repo; + + private final AccountLock accountLock = new NoOpAccountLock(); + + private final UpdateAccountStatePort updateAccountStatePort = repo; + + private final SendMoneyService sendMoneyService = + new SendMoneyService(loadAccountPort, accountLock, updateAccountStatePort, moneyTransferProperties()); + + @Test + void sendMoney_accountHasNotEnoughMoney_transactionFails() { + // arrange + Account sourceAccount = Account.withId(new AccountId(1234L), + new Money(BigInteger.valueOf(200)), + new ActivityWindow(new ArrayList<>())); + Account targetAccount = Account.withId(new AccountId(4321L), + new Money(BigInteger.valueOf(0)), + new ActivityWindow(new ArrayList<>())); + repo.addAccount(sourceAccount); + repo.addAccount(targetAccount); + + // act + SendMoneyCommand command = new SendMoneyCommand( + sourceAccount.getId().get(), + targetAccount.getId().get(), + Money.of(500L)); // withdraw more money than sourceAccount has + boolean success = sendMoneyService.sendMoney(command); + + // assert + assertThat(success).isFalse(); + } + + @Test + void sendMoney_amountOK_transactionSucceeds() { + // arrange + Account sourceAccount = Account.withId(new AccountId(1234L), + new Money(BigInteger.valueOf(1000)), + new ActivityWindow(new ArrayList<>())); + Account targetAccount = Account.withId(new AccountId(4321L), + new Money(BigInteger.valueOf(0)), + new ActivityWindow(new ArrayList<>())); + repo.addAccount(sourceAccount); + repo.addAccount(targetAccount); + Money money = Money.of(500L); + + // act + SendMoneyCommand command = new SendMoneyCommand( + sourceAccount.getId().get(), + targetAccount.getId().get(), + money); + boolean success = sendMoneyService.sendMoney(command); + + // assert + assertThat(success).isTrue(); + } + + @Test + void sendMoney_amountOK_moneyIsTransferred() { + // arrange + Account sourceAccount = Account.withId(new AccountId(1234L), + new Money(BigInteger.valueOf(1000)), + new ActivityWindow(new ArrayList<>())); + Account targetAccount = Account.withId(new AccountId(4321L), + new Money(BigInteger.valueOf(0)), + new ActivityWindow(new ArrayList<>())); + repo.addAccount(sourceAccount); + repo.addAccount(targetAccount); + + // act + SendMoneyCommand command = new SendMoneyCommand( + sourceAccount.getId().get(), + targetAccount.getId().get(), + Money.of(300L)); + sendMoneyService.sendMoney(command); + + // assert + Assertions.assertEquals(Money.of(700), sourceAccount.calculateBalance()); + Assertions.assertEquals(Money.of(300), targetAccount.calculateBalance()); + } + + private MoneyTransferProperties moneyTransferProperties() { + return new MoneyTransferProperties(Money.of(Long.MAX_VALUE)); + } }