Skip to content
This repository has been archived by the owner on Jan 17, 2022. It is now read-only.

Kamilla: Test Task #61

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,18 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
Expand Down
59 changes: 59 additions & 0 deletions src/main/java/com/devexperts/AccountServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.devexperts;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.AccountService;
import com.devexperts.service.AccountServiceImpl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AccountServiceTest {
private AccountService accountService;
private Account account;

@Before
public void setUp() {
accountService = new AccountServiceImpl();
account = new Account(AccountKey.valueOf(42), "FirstName", "LastName", 0.0);
}

@Test
public void testSingleAccountCreation() {
accountService.createAccount(account);
Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
Assert.assertEquals(account, actual);
}

@Test
public void testAccountsCreation() {
accountService.createAccount(account);
Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
Assert.assertEquals(account, actual);

Account otherAccount = new Account(AccountKey.valueOf(account.getAccountKey().getAccountId() + 1), "FirstName", "LastName", 0.0);
accountService.createAccount(otherAccount);
actual = accountService.getAccount(otherAccount.getAccountKey().getAccountId());
Assert.assertEquals(otherAccount, actual);
Assert.assertNotEquals(account, actual);
}

@Test(expected = IllegalArgumentException.class)
public void testExistingAccountCreation() {
Account account = new Account(AccountKey.valueOf(1), "FirstName", "LastName", 0.0);
accountService.createAccount(account);
accountService.createAccount(account);
}

@Test
public void testAccountsClearing() {
accountService.createAccount(account);
Account actual = accountService.getAccount(account.getAccountKey().getAccountId());
Assert.assertNotNull(actual);

accountService.clear();

actual = accountService.getAccount(account.getAccountKey().getAccountId());
Assert.assertNull(actual);
}
}
43 changes: 43 additions & 0 deletions src/main/java/com/devexperts/MultithreadTransferTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.devexperts;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.AccountServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class MultithreadTransferTest {
AccountServiceImpl service;
Account source, target;
AccountKey skey, tkey;

@BeforeEach
private void setUp() {
service = new AccountServiceImpl();
skey = AccountKey.valueOf(1);
source = new Account(skey, "One", "First", 100.0);

tkey = AccountKey.valueOf(2);
target = new Account(tkey, "Two", "Second", 200.0);
}

@Test
void multithreadTransfer() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
new Thread(()->{
service.transfer(source, target, 10.0);
latch.countDown();
}).start();
}
latch.await(1, TimeUnit.SECONDS);

assertEquals(0.0, source.getBalance());
assertEquals(300.0, target.getBalance());
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/devexperts/TransferTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.devexperts;

import com.devexperts.account.Account;
import com.devexperts.account.AccountKey;
import com.devexperts.service.AccountService;
import com.devexperts.service.AccountServiceImpl;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class TransferTest {
private AccountService accountService;
private Account source;
private Account target;

@BeforeEach
public void setUp() {
accountService = new AccountServiceImpl();

source = new Account(AccountKey.valueOf(1), "One", "First", 100.0);
target = new Account(AccountKey.valueOf(2), "Two", "Second", 150.0);
accountService.createAccount(source);
accountService.createAccount(target);
}

@Test
public void transfer() {
accountService.transfer(source, target, 50.0);
assertEquals(source.getBalance(), 50.0);
assertEquals(target.getBalance(), 200.0);
}
}
19 changes: 19 additions & 0 deletions src/main/java/com/devexperts/account/AccountKey.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.devexperts.account;

import java.util.Objects;

/**
* Unique Account identifier
*
Expand All @@ -17,4 +19,21 @@ private AccountKey(long accountId) {
public static AccountKey valueOf(long accountId) {
return new AccountKey(accountId);
}

public long getAccountId() {
return accountId;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AccountKey that = (AccountKey) o;
return accountId == that.accountId;
}

@Override
public int hashCode() {
return Objects.hash(accountId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
import org.springframework.http.ResponseEntity;

public abstract class AbstractAccountController {
abstract ResponseEntity<Void> transfer(long sourceId, long targetId, double amount);
abstract ResponseEntity<Void> transfer(Long sourceId, Long targetId, Double amount);
}
36 changes: 33 additions & 3 deletions src/main/java/com/devexperts/rest/AccountController.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,44 @@
package com.devexperts.rest;

import com.devexperts.account.Account;
import com.devexperts.service.AccountService;
import com.devexperts.service.exceptions.AccountNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class AccountController extends AbstractAccountController {

public ResponseEntity<Void> transfer(long sourceId, long targetId, double amount) {
return null;
private final AccountService service;

@Autowired
public AccountController (AccountService service) {
this.service = service;
}

@GetMapping("operations/transfer")
public ResponseEntity<Void> transfer(
@RequestParam("sourceId") Long sourceId,
@RequestParam("targetId") Long targetId,
@RequestParam("amount") Double amount) {
if (sourceId == null || targetId == null || amount < 0)
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);

try {
Account source = service.getAccount(sourceId);
Account target = service.getAccount(targetId);
service.transfer(source, target, amount);
return new ResponseEntity<>(HttpStatus.OK);
} catch (AccountNotFoundException e) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} catch (IllegalStateException e) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
}
39 changes: 30 additions & 9 deletions src/main/java/com/devexperts/service/AccountServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import com.devexperts.account.AccountKey;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;

@Service
public class AccountServiceImpl implements AccountService {

private final List<Account> accounts = new ArrayList<>();
private final Map<AccountKey, Account> accounts = new ConcurrentHashMap<>();
private final ReentrantLock lock = new ReentrantLock();

@Override
public void clear() {
Expand All @@ -19,19 +21,38 @@ public void clear() {

@Override
public void createAccount(Account account) {
accounts.add(account);
if (account.getAccountKey() == null) {
throw new NullPointerException();
}
if (accounts.containsKey(account.getAccountKey())) {
throw new IllegalArgumentException("This key is already used.");
}
accounts.put(account.getAccountKey(), account);
}

@Override
public Account getAccount(long id) {
return accounts.stream()
.filter(account -> account.getAccountKey() == AccountKey.valueOf(id))
.findAny()
.orElse(null);
return accounts.getOrDefault(AccountKey.valueOf(id), null);
}

@Override
public void transfer(Account source, Account target, double amount) {
//do nothing for now
if (amount < 0 || source == target)
throw new IllegalArgumentException("Amount can't be negative OR Source can't be same as target.");

lock.lock();
try {
double sourceBalance = source.getBalance();
double targetBalance = target.getBalance();

if (sourceBalance < amount)
throw new IllegalStateException("Source balance is insufficient.");

source.setBalance(sourceBalance - amount);
target.setBalance(targetBalance + amount);

} finally {
lock.unlock();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.devexperts.service.exceptions;

public class AccountNotFoundException extends RuntimeException {
public AccountNotFoundException(String message) {
super(message);
}
}
7 changes: 7 additions & 0 deletions src/main/java/resources/data/accounts.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS Accounts
(
ID BIGINT AUTO_INCREMENT PRIMARY,
FIRST_NAME VARCHAR(255) NOT NULL,
LAST_NAME VARCHAR(255) NOT NULL,
BALANCE BIGINT NOT NULL
);
4 changes: 4 additions & 0 deletions src/main/java/resources/data/select.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
SELECT t.source_id FROM Transfers AS t
WHERE t.transfer_time >= TIMESTAMP '2019-01-01' AND source_id != target_id AND amount > 0
GROUP BY source_id
HAVING SUM(amount) > 1000;
10 changes: 10 additions & 0 deletions src/main/java/resources/transfers.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS Transfers
(
ID BIGINT AUTO_INCREMENT PRIMARY,
SOURCE_ID BIGINT NOT NULL,
TARGET_ID BIGINT NOT NULL,
AMOUNT BIGINT NOT NULL,
TRANSFER_TIME TIMESTAMP NOT NULL,
FOREIGN KEY (SOURCE_ID) REFERENCES accounts (ID),
FOREIGN KEY (TARGET_ID) REFERENCES accounts (ID)
);