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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Account.AccountId, Account> 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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Account> accountCaptor = ArgumentCaptor.forClass(Account.class);
then(updateAccountStatePort).should(times(accountIds.length))
.updateActivities(accountCaptor.capture());

List<AccountId> 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));
}

}