diff --git a/README.md b/README.md index a1d2939..9a0599a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,16 @@ -SpringCore +Spring AOP ========== + +There is defined an @Authenticated annotation, for which there needs to be created and applied the SecurityAspect. + +For this to happen, you need to: + +1. enable @AspectJ support in aop-config.xml + +2. make SecurityAspect class as aspect + +3. make SecurityAspect class candidate for bean detection + +4. annotate TransferServiceImpl#transferAmount(...) with @Authenticated + +5. define SecurityAspect#secure() as advice and also define it's pointcut that should intercept all methods annotated with @Authenticated diff --git a/pom.xml b/pom.xml index 902ab97..7bf0439 100644 --- a/pom.xml +++ b/pom.xml @@ -1,28 +1,32 @@ - - 4.0.0 app - SpringCore + SpringAOP 1.0-SNAPSHOT + jar - - - 4.0.2.RELEASE + SpringAOP + + 4.0.3.RELEASE + 1.4 + 2.3.2 1.7.6 1.1.1 - 4.11-20120805-1225 + 4.11 + 1.9.5 + 1.6.8 + 1.6.8 + Core utilities used by other modules. + Define this if you use Spring Utility APIs (org.springframework.core.*/org.springframework.util.*) + --> org.springframework spring-core @@ -59,16 +63,50 @@ ${org.springframework.version} - org.springframework - spring-context-support + spring-jdbc + ${org.springframework.version} + + + + org.springframework + spring-aop ${org.springframework.version} + + org.springframework + spring-test + ${org.springframework.version} + test + + + + + commons-dbcp + commons-dbcp + ${commons-dbcp.version} + + + + org.hsqldb + hsqldb + ${org.hsqldb.version} + + + + org.aspectj + aspectjrt + ${aspectjrt.version} + + + + org.aspectj + aspectjweaver + ${aspectjweaver.version} + + @@ -100,6 +138,12 @@ test + + org.mockito + mockito-core + ${mockito.version} + test + @@ -111,7 +155,12 @@ 1.7 1.7 - + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 org.codehaus.mojo @@ -119,10 +168,24 @@ 2.1 false + false + + org.apache.maven.plugins + maven-pmd-plugin + 3.0.1 + + + compile + + check + cpd-check + + + + - - \ No newline at end of file + diff --git a/src/main/java/app/Main.java b/src/main/java/app/Main.java new file mode 100644 index 0000000..e9959ac --- /dev/null +++ b/src/main/java/app/Main.java @@ -0,0 +1,75 @@ +package app; + +import app.aop.security.SecurityContext; +import app.domain.Account; +import app.domain.Transaction; +import app.service.AccountService; +import app.service.SecurityService; +import app.service.TransferService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import java.math.BigDecimal; + +public class Main { + + private static final String PAYER_ACCOUNT = "12345678901234"; + private static final String BENEFICIARY_ACCOUNT = "12345678909876"; + + private ApplicationContext context; + + private TransferService transferService; + private AccountService accountService; + private SecurityService securityService; + + public static void main(String[] args) { + Main app = new Main(); + app.loadContext(); + app.loadServices(); + + app.login("guest", "endava"); + app.printAccountsBalance(); + app.transferMoney(BigDecimal.TEN); + app.printAccountsBalance(); + + app.cleanUp(); + } + + private void loadContext() { + context = new ClassPathXmlApplicationContext("/spring/application-config.xml", + "/spring/aop-config.xml", + "/spring/database-config.xml"); + } + + private void loadServices() { + transferService = context.getBean(TransferService.class); + accountService = context.getBean(AccountService.class); + securityService = context.getBean(SecurityService.class); + } + + private void login(String username, String password) { + securityService.authenticate(username, password); + } + + private void transferMoney(BigDecimal amount) { + // create transaction + Transaction transaction = new Transaction(PAYER_ACCOUNT, BENEFICIARY_ACCOUNT, amount); + transferService.transferAmount(transaction); + } + + private void printAccountsBalance() { + // check accounts + Account payerAccount = accountService.findByNumber(PAYER_ACCOUNT); + Account beneficiaryAccount = accountService.findByNumber(BENEFICIARY_ACCOUNT); + + // print details + System.out.println("--- ACCOUNT BALANCE ---"); + System.out.println(payerAccount); + System.out.println(beneficiaryAccount); + } + + private void cleanUp() { + SecurityContext.clear(); + } + +} diff --git a/src/main/java/app/aop/security/Authenticated.java b/src/main/java/app/aop/security/Authenticated.java new file mode 100644 index 0000000..7b7d719 --- /dev/null +++ b/src/main/java/app/aop/security/Authenticated.java @@ -0,0 +1,14 @@ +package app.aop.security; + +import java.lang.annotation.*; + +/** + * Used to mark behaviors which should be executed only if authentication occur. + * + * @author dvizireanu + */ +@Target({ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Authenticated { +} diff --git a/src/main/java/app/aop/security/SecurityAspect.java b/src/main/java/app/aop/security/SecurityAspect.java new file mode 100644 index 0000000..2c6cd37 --- /dev/null +++ b/src/main/java/app/aop/security/SecurityAspect.java @@ -0,0 +1,22 @@ +package app.aop.security; + +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +/** + * Security aspect which check authentication / authorization in order to allow the access/execution of targeted resources. + * + * @author dvizireanu + */ +// TODO -> 2. make class as aspect +// TODO -> 3. make class as aspect +public class SecurityAspect { + + // TODO -> 5. define it as advice and also define it's pointcut that should intercept all methods annotated with @Authenticated + public void secure() { + if (!SecurityContext.isAuthenticated()) { + throw new app.exception.SecurityException("Not authenticated !"); + } + } +} diff --git a/src/main/java/app/aop/security/SecurityContext.java b/src/main/java/app/aop/security/SecurityContext.java new file mode 100644 index 0000000..fd1cfca --- /dev/null +++ b/src/main/java/app/aop/security/SecurityContext.java @@ -0,0 +1,23 @@ +package app.aop.security; + +/** + * Security context, which holds information about logged user. + * + * @author dvizireanu + */ +public class SecurityContext { + + private static final ThreadLocal currentUser = new ThreadLocal<>(); + + public static boolean isAuthenticated() { + return currentUser.get() != null; + } + + public static void setCurrentUser(String username) { + currentUser.set(username); + } + + public static void clear() { + currentUser.remove(); + } +} diff --git a/src/main/java/app/domain/Account.java b/src/main/java/app/domain/Account.java new file mode 100644 index 0000000..d2809b8 --- /dev/null +++ b/src/main/java/app/domain/Account.java @@ -0,0 +1,41 @@ +package app.domain; + +import java.math.BigDecimal; + +public class Account extends Entity { + + private String number; + private String name; + private BigDecimal moneyAmount; + + public Account(String number, String name, BigDecimal moneyAmount) { + this.number = number; + this.name = name; + this.moneyAmount = moneyAmount; + } + + public String getNumber() { + return number; + } + + public String getName() { + return name; + } + + public BigDecimal getMoneyAmount() { + return moneyAmount; + } + + public void setMoneyAmount(BigDecimal moneyAmount) { + this.moneyAmount = moneyAmount; + } + + @Override + public String toString() { + return "Account{" + + "number='" + number + '\'' + + ", name='" + name + '\'' + + ", moneyAmount=" + moneyAmount + + '}'; + } +} diff --git a/src/main/java/app/domain/Entity.java b/src/main/java/app/domain/Entity.java new file mode 100644 index 0000000..969e057 --- /dev/null +++ b/src/main/java/app/domain/Entity.java @@ -0,0 +1,21 @@ +package app.domain; + +public class Entity { + + private long entityId; + + public long getEntityId() { + return entityId; + } + + public void setEntityId(long entityId) { + this.entityId = entityId; + } + + @Override + public String toString() { + return "Entity{" + + "entityId=" + entityId + + '}'; + } +} diff --git a/src/main/java/app/domain/Transaction.java b/src/main/java/app/domain/Transaction.java new file mode 100644 index 0000000..6271c7e --- /dev/null +++ b/src/main/java/app/domain/Transaction.java @@ -0,0 +1,45 @@ +package app.domain; + +import java.math.BigDecimal; +import java.util.Date; + +public class Transaction extends Entity { + + private String payerAccountNumber; + private String beneficiaryAccountNumber; + private BigDecimal moneyAmount; + private Date date; + + public Transaction(String payerAccountNumber, String beneficiaryAccount, BigDecimal moneyAmount) { + this.payerAccountNumber = payerAccountNumber; + this.beneficiaryAccountNumber = beneficiaryAccount; + this.moneyAmount = moneyAmount; + this.date = new Date(); + } + + public String getPayerAccountNumber() { + return payerAccountNumber; + } + + public String getBeneficiaryAccountNumber() { + return beneficiaryAccountNumber; + } + + public BigDecimal getMoneyAmount() { + return moneyAmount; + } + + public Date getDate() { + return date; + } + + @Override + public String toString() { + return "Transaction{" + + ", payerAccountNumber='" + payerAccountNumber + '\'' + + ", beneficiaryAccountNumber='" + beneficiaryAccountNumber + '\'' + + ", moneyAmount=" + moneyAmount + + ", date=" + date + + '}'; + } +} diff --git a/src/main/java/app/exception/SecurityException.java b/src/main/java/app/exception/SecurityException.java new file mode 100644 index 0000000..faac6bc --- /dev/null +++ b/src/main/java/app/exception/SecurityException.java @@ -0,0 +1,13 @@ +package app.exception; + +/** + * Exception describing an security error case. + * + * @author dvizireanu + */ +public class SecurityException extends RuntimeException { + + public SecurityException(String message) { + super(message); + } +} diff --git a/src/main/java/app/repository/AccountRepository.java b/src/main/java/app/repository/AccountRepository.java new file mode 100644 index 0000000..e1698c9 --- /dev/null +++ b/src/main/java/app/repository/AccountRepository.java @@ -0,0 +1,15 @@ +package app.repository; + + +import app.domain.Account; + +import java.util.List; + +public interface AccountRepository { + + List findByNumber(String number); + + int insert(Account account); + + int updateMoneyAmount(Account account); +} diff --git a/src/main/java/app/repository/JdbcAccountRepository.java b/src/main/java/app/repository/JdbcAccountRepository.java new file mode 100644 index 0000000..04fb970 --- /dev/null +++ b/src/main/java/app/repository/JdbcAccountRepository.java @@ -0,0 +1,47 @@ +package app.repository; + +import app.domain.Account; +import app.util.AccountMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.util.List; + +@Repository +public class JdbcAccountRepository implements AccountRepository { + + private JdbcTemplate jdbcTemplate; + + @Autowired + public JdbcAccountRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public List findByNumber(String number) { + return jdbcTemplate.query( + "SELECT * FROM t_account WHERE number = ?", + new AccountMapper(), + number); + } + + @Override + public int insert(Account account) { + return jdbcTemplate.update( + "INSERT INTO t_account (number, name, money_amount) VALUES(?, ?, ?)", + account.getNumber(), + account.getName(), + account.getMoneyAmount()); + } + + @Override + public int updateMoneyAmount(Account account) { + return jdbcTemplate.update( + "UPDATE t_account SET money_amount = ? WHERE id = ?", + account.getMoneyAmount(), + account.getEntityId() + ); + } +} diff --git a/src/main/java/app/repository/JdbcTransactionRepository.java b/src/main/java/app/repository/JdbcTransactionRepository.java new file mode 100644 index 0000000..b60fe1a --- /dev/null +++ b/src/main/java/app/repository/JdbcTransactionRepository.java @@ -0,0 +1,29 @@ +package app.repository; + +import app.domain.Transaction; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; + +@Repository +public class JdbcTransactionRepository implements TransactionRepository { + + private JdbcTemplate jdbcTemplate; + + @Autowired + public JdbcTransactionRepository(DataSource dataSource) { + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Override + public int insert(Transaction transaction) { + return jdbcTemplate.update( + "INSERT INTO t_transaction (payer_account, beneficiary_account, money_amount, date) VALUES(?, ?, ?, ?)", + transaction.getPayerAccountNumber(), + transaction.getBeneficiaryAccountNumber(), + transaction.getMoneyAmount(), + transaction.getDate()); + } +} \ No newline at end of file diff --git a/src/main/java/app/repository/TransactionRepository.java b/src/main/java/app/repository/TransactionRepository.java new file mode 100644 index 0000000..9301e14 --- /dev/null +++ b/src/main/java/app/repository/TransactionRepository.java @@ -0,0 +1,9 @@ +package app.repository; + +import app.domain.Transaction; + +public interface TransactionRepository { + + int insert(Transaction transaction); + +} diff --git a/src/main/java/app/service/AccountService.java b/src/main/java/app/service/AccountService.java new file mode 100644 index 0000000..49c9dd3 --- /dev/null +++ b/src/main/java/app/service/AccountService.java @@ -0,0 +1,12 @@ +package app.service; + +import app.domain.Account; + +public interface AccountService { + + Account findByNumber(String number); + + void save(Account account); + + void updateMoneyAmount(Account account); +} diff --git a/src/main/java/app/service/AccountServiceImpl.java b/src/main/java/app/service/AccountServiceImpl.java new file mode 100644 index 0000000..13f6db0 --- /dev/null +++ b/src/main/java/app/service/AccountServiceImpl.java @@ -0,0 +1,59 @@ +package app.service; + +import app.aop.security.Authenticated; +import app.domain.Account; +import app.repository.AccountRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class AccountServiceImpl implements AccountService { + + private AccountRepository accountRepository; + + @Autowired + public AccountServiceImpl(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @Authenticated + @Override + public Account findByNumber(String number) { + List accounts = accountRepository.findByNumber(number); + if (accounts == null || accounts.size() ==0) { + throw new EmptyResultDataAccessException("No account found for number " + number + ".", 1); + } + if (accounts.size() > 1) { + throw new IncorrectResultSizeDataAccessException("More than one account found for number" + number + ".", 1); + } + return accounts.get(0); + } + + @Authenticated + @Override + public void save(Account account) { + int count = accountRepository.insert(account); + if (count == 0) { + throw new IncorrectResultSizeDataAccessException("Account not saved.", 1); + } + if (count > 1) { + throw new IncorrectResultSizeDataAccessException("More than once account was saved.", 1); + } + } + + @Authenticated + @Override + public void updateMoneyAmount(Account account) { + int count = accountRepository.updateMoneyAmount(account); + if (count == 0) { + throw new IncorrectResultSizeDataAccessException("No accounts were updated", 1); + } + if (count > 1) { + throw new IncorrectResultSizeDataAccessException("More than once account was updated", 1); + } + } +} diff --git a/src/main/java/app/service/SecurityService.java b/src/main/java/app/service/SecurityService.java new file mode 100644 index 0000000..c316771 --- /dev/null +++ b/src/main/java/app/service/SecurityService.java @@ -0,0 +1,17 @@ +package app.service; + +/** + * Service responsible of authenticating and authorizing an user. + * + * @author dvizireanu + */ +public interface SecurityService { + + /** + * Authenticate user. + * + * @param username + * @param password + */ + void authenticate(String username, String password); +} diff --git a/src/main/java/app/service/SecurityServiceImpl.java b/src/main/java/app/service/SecurityServiceImpl.java new file mode 100644 index 0000000..658c5e9 --- /dev/null +++ b/src/main/java/app/service/SecurityServiceImpl.java @@ -0,0 +1,23 @@ +package app.service; + +import app.aop.security.SecurityContext; +import app.exception.SecurityException; +import org.springframework.stereotype.Service; + +/** + * @author dvizireanu + */ +@Service +public class SecurityServiceImpl implements SecurityService { + + @Override + public void authenticate(String username, String password) { + + if ("guest".equals(username) && "endava".equals(password)) { + SecurityContext.setCurrentUser(username); + } else { + throw new SecurityException("Wrong credentials !"); + } + + } +} diff --git a/src/main/java/app/service/TransactionService.java b/src/main/java/app/service/TransactionService.java new file mode 100644 index 0000000..3773b81 --- /dev/null +++ b/src/main/java/app/service/TransactionService.java @@ -0,0 +1,9 @@ +package app.service; + +import app.domain.Transaction; + +public interface TransactionService { + + void save(Transaction transaction); + +} diff --git a/src/main/java/app/service/TransactionServiceImpl.java b/src/main/java/app/service/TransactionServiceImpl.java new file mode 100644 index 0000000..3d44e9b --- /dev/null +++ b/src/main/java/app/service/TransactionServiceImpl.java @@ -0,0 +1,31 @@ +package app.service; + +import app.aop.security.Authenticated; +import app.domain.Transaction; +import app.repository.TransactionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.stereotype.Service; + +@Service +public class TransactionServiceImpl implements TransactionService { + + private TransactionRepository transactionRepository; + + @Autowired + public TransactionServiceImpl(TransactionRepository transactionRepository) { + this.transactionRepository = transactionRepository; + } + + @Authenticated + @Override + public void save(Transaction transaction) { + int count = transactionRepository.insert(transaction); + if (count == 0) { + throw new IncorrectResultSizeDataAccessException("Transaction not saved.", 1); + } + if (count > 1) { + throw new IncorrectResultSizeDataAccessException("More than once transaction was saved.", 1); + } + } +} diff --git a/src/main/java/app/service/TransferService.java b/src/main/java/app/service/TransferService.java new file mode 100644 index 0000000..9256b57 --- /dev/null +++ b/src/main/java/app/service/TransferService.java @@ -0,0 +1,9 @@ +package app.service; + +import app.domain.Transaction; + +public interface TransferService { + + void transferAmount(Transaction transaction); + +} diff --git a/src/main/java/app/service/TransferServiceImpl.java b/src/main/java/app/service/TransferServiceImpl.java new file mode 100644 index 0000000..8301458 --- /dev/null +++ b/src/main/java/app/service/TransferServiceImpl.java @@ -0,0 +1,43 @@ +package app.service; + +import app.aop.security.Authenticated; +import app.domain.Account; +import app.domain.Transaction; +import app.util.TransactionValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class TransferServiceImpl implements TransferService { + + private AccountService accountService; + private TransactionService transactionService; + + @Autowired + public TransferServiceImpl(AccountService accountService, TransactionService transactionService) { + this.accountService = accountService; + this.transactionService = transactionService; + } + + // TODO -> 4. annotate to secure method + @Override + public void transferAmount(Transaction transaction) { + + TransactionValidator.validate(transaction); + + Account payerAccount = accountService.findByNumber(transaction.getPayerAccountNumber()); + Account beneficiaryAccount = accountService.findByNumber(transaction.getBeneficiaryAccountNumber()); + + BigDecimal amount = transaction.getMoneyAmount(); + payerAccount.setMoneyAmount(payerAccount.getMoneyAmount().subtract(amount)); + beneficiaryAccount.setMoneyAmount(beneficiaryAccount.getMoneyAmount().add(amount)); + + accountService.updateMoneyAmount(payerAccount); + accountService.updateMoneyAmount(beneficiaryAccount); + + transactionService.save(transaction); + } + +} diff --git a/src/main/java/app/util/AccountMapper.java b/src/main/java/app/util/AccountMapper.java new file mode 100644 index 0000000..c7adef8 --- /dev/null +++ b/src/main/java/app/util/AccountMapper.java @@ -0,0 +1,20 @@ +package app.util; + +import app.domain.Account; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; + +public class AccountMapper implements RowMapper { + + @Override + public Account mapRow(ResultSet resultSet, int i) throws SQLException { + Account account = new Account( + resultSet.getString("number"), + resultSet.getString("name"), + resultSet.getBigDecimal("money_amount", 2)); + account.setEntityId(resultSet.getLong("id")); + return account; + } +} diff --git a/src/main/java/app/util/TransactionValidator.java b/src/main/java/app/util/TransactionValidator.java new file mode 100644 index 0000000..8ff3c2f --- /dev/null +++ b/src/main/java/app/util/TransactionValidator.java @@ -0,0 +1,30 @@ +package app.util; + +import app.domain.Transaction; + +import java.math.BigDecimal; + +public class TransactionValidator { + + private static final String ACCOUNT_NUMBER_FORMAT_REGEXP = "\\d{14}"; + + public static void validate(Transaction transaction) { + + BigDecimal amount = transaction.getMoneyAmount(); + if (amount == null || amount.compareTo(BigDecimal.ZERO) < 0) { + throw new IllegalArgumentException("Invalid transaction, amount value is null or less than zero."); + } + String payerAccount = transaction.getPayerAccountNumber(); + if (payerAccount == null || !isValidAccountNumberFormat(payerAccount)) { + throw new IllegalArgumentException("Invalid transaction, payer account has an invalid format."); + } + String beneficiaryAccount = transaction.getBeneficiaryAccountNumber(); + if (beneficiaryAccount == null || !isValidAccountNumberFormat(beneficiaryAccount)) { + throw new IllegalArgumentException("Invalid transaction, beneficiary account has an invalid format."); + } + } + + private static boolean isValidAccountNumberFormat(String accountNumber) { + return accountNumber.matches(ACCOUNT_NUMBER_FORMAT_REGEXP); + } +} diff --git a/src/main/resources/database.properties b/src/main/resources/database.properties new file mode 100644 index 0000000..3867621 --- /dev/null +++ b/src/main/resources/database.properties @@ -0,0 +1,4 @@ +db.driver=org.hsqldb.jdbcDriver +db.url=jdbc:hsqldb:file:src/main/resources/testdbdata/data +db.username=sa +db.password= \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..50b03e0 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,16 @@ + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{5} - %msg%n + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/aop-config.xml b/src/main/resources/spring/aop-config.xml new file mode 100644 index 0000000..8591b21 --- /dev/null +++ b/src/main/resources/spring/aop-config.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/application-config.xml b/src/main/resources/spring/application-config.xml new file mode 100644 index 0000000..fecdfc7 --- /dev/null +++ b/src/main/resources/spring/application-config.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/spring/database-config.xml b/src/main/resources/spring/database-config.xml new file mode 100644 index 0000000..0c899c6 --- /dev/null +++ b/src/main/resources/spring/database-config.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/testdb/data.sql b/src/main/resources/testdb/data.sql new file mode 100644 index 0000000..f83b7d3 --- /dev/null +++ b/src/main/resources/testdb/data.sql @@ -0,0 +1,2 @@ +insert into T_ACCOUNT (NUMBER, NAME, MONEY_AMOUNT) values ('12345678901234', 'Bogdan', '100.00'); +insert into T_ACCOUNT (NUMBER, NAME, MONEY_AMOUNT) values ('12345678909876', 'Dragos', '100.00'); diff --git a/src/main/resources/testdb/schema.sql b/src/main/resources/testdb/schema.sql new file mode 100644 index 0000000..3606469 --- /dev/null +++ b/src/main/resources/testdb/schema.sql @@ -0,0 +1,19 @@ +drop table T_ACCOUNT if exists; +drop table T_TRANSACTION if exists; + +create table T_ACCOUNT( + ID integer identity primary key, + NUMBER varchar(14) not null, + NAME varchar(50) not null, + MONEY_AMOUNT double not null, + unique(NUMBER) +); + +create table T_TRANSACTION( + ID integer identity primary key, + PAYER_ACCOUNT varchar(14) not null, + BENEFICIARY_ACCOUNT varchar(14) not null, + MONEY_AMOUNT double not null, + DATE date not null, + unique(PAYER_ACCOUNT, BENEFICIARY_ACCOUNT, MONEY_AMOUNT, DATE) +); \ No newline at end of file diff --git a/src/main/resources/testdbdata/data.log b/src/main/resources/testdbdata/data.log new file mode 100644 index 0000000..04da4a1 --- /dev/null +++ b/src/main/resources/testdbdata/data.log @@ -0,0 +1,32 @@ +/*C8*/SET SCHEMA PUBLIC +drop table T_ACCOUNT if exists +drop table T_TRANSACTION if exists +create table T_ACCOUNT( ID integer identity primary key, NUMBER varchar(14) not null, NAME varchar(50) not null, MONEY_AMOUNT double not null, unique(NUMBER) ) +create table T_TRANSACTION( ID integer identity primary key, PAYER_ACCOUNT varchar(14) not null, BENEFICIARY_ACCOUNT varchar(14) not null, MONEY_AMOUNT double not null, DATE date not null, unique(PAYER_ACCOUNT, BENEFICIARY_ACCOUNT, MONEY_AMOUNT, DATE) ) +INSERT INTO T_ACCOUNT VALUES(0,'12345678901234','Bogdan',100.0E0) +COMMIT +INSERT INTO T_ACCOUNT VALUES(1,'12345678909876','Dragos',100.0E0) +COMMIT +DISCONNECT +/*C9*/SET SCHEMA PUBLIC +DISCONNECT +/*C10*/SET SCHEMA PUBLIC +DISCONNECT +/*C11*/SET SCHEMA PUBLIC +DELETE FROM T_ACCOUNT WHERE ID=0 +INSERT INTO T_ACCOUNT VALUES(0,'12345678901234','Bogdan',90.0E0) +COMMIT +DISCONNECT +/*C12*/SET SCHEMA PUBLIC +DELETE FROM T_ACCOUNT WHERE ID=1 +INSERT INTO T_ACCOUNT VALUES(1,'12345678909876','Dragos',110.0E0) +COMMIT +DISCONNECT +/*C13*/SET SCHEMA PUBLIC +INSERT INTO T_TRANSACTION VALUES(0,'12345678901234','12345678909876',10.0E0,'2014-03-31') +COMMIT +DISCONNECT +/*C14*/SET SCHEMA PUBLIC +DISCONNECT +/*C15*/SET SCHEMA PUBLIC +DISCONNECT diff --git a/src/main/resources/testdbdata/data.properties b/src/main/resources/testdbdata/data.properties new file mode 100644 index 0000000..3e3289e --- /dev/null +++ b/src/main/resources/testdbdata/data.properties @@ -0,0 +1,4 @@ +#HSQL Database Engine 2.3.2 +#Mon Mar 31 22:03:20 EEST 2014 +version=2.3.2 +modified=yes diff --git a/src/main/resources/testdbdata/data.script b/src/main/resources/testdbdata/data.script new file mode 100644 index 0000000..49cdec5 --- /dev/null +++ b/src/main/resources/testdbdata/data.script @@ -0,0 +1,53 @@ +SET DATABASE UNIQUE NAME HSQLDB451294B2C7 +SET DATABASE GC 0 +SET DATABASE DEFAULT RESULT MEMORY ROWS 0 +SET DATABASE EVENT LOG LEVEL 0 +SET DATABASE TRANSACTION CONTROL LOCKS +SET DATABASE DEFAULT ISOLATION LEVEL READ COMMITTED +SET DATABASE TRANSACTION ROLLBACK ON CONFLICT TRUE +SET DATABASE TEXT TABLE DEFAULTS '' +SET DATABASE SQL NAMES FALSE +SET DATABASE SQL REFERENCES FALSE +SET DATABASE SQL SIZE TRUE +SET DATABASE SQL TYPES FALSE +SET DATABASE SQL TDC DELETE TRUE +SET DATABASE SQL TDC UPDATE TRUE +SET DATABASE SQL TRANSLATE TTI TYPES TRUE +SET DATABASE SQL CONCAT NULLS TRUE +SET DATABASE SQL UNIQUE NULLS TRUE +SET DATABASE SQL CONVERT TRUNCATE TRUE +SET DATABASE SQL AVG SCALE 0 +SET DATABASE SQL DOUBLE NAN TRUE +SET FILES WRITE DELAY 500 MILLIS +SET FILES BACKUP INCREMENT TRUE +SET FILES CACHE SIZE 10000 +SET FILES CACHE ROWS 50000 +SET FILES SCALE 32 +SET FILES LOB SCALE 32 +SET FILES DEFRAG 0 +SET FILES NIO TRUE +SET FILES NIO SIZE 256 +SET FILES LOG TRUE +SET FILES LOG SIZE 50 +CREATE USER SA PASSWORD DIGEST 'd41d8cd98f00b204e9800998ecf8427e' +ALTER USER SA SET LOCAL TRUE +CREATE SCHEMA PUBLIC AUTHORIZATION DBA +SET SCHEMA PUBLIC +CREATE MEMORY TABLE PUBLIC.T_ACCOUNT(ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL PRIMARY KEY,NUMBER VARCHAR(14) NOT NULL,NAME VARCHAR(50) NOT NULL,MONEY_AMOUNT DOUBLE NOT NULL,UNIQUE(NUMBER)) +ALTER TABLE PUBLIC.T_ACCOUNT ALTER COLUMN ID RESTART WITH 2 +CREATE MEMORY TABLE PUBLIC.T_TRANSACTION(ID INTEGER GENERATED BY DEFAULT AS IDENTITY(START WITH 0) NOT NULL PRIMARY KEY,PAYER_ACCOUNT VARCHAR(14) NOT NULL,BENEFICIARY_ACCOUNT VARCHAR(14) NOT NULL,MONEY_AMOUNT DOUBLE NOT NULL,DATE DATE NOT NULL,UNIQUE(PAYER_ACCOUNT,BENEFICIARY_ACCOUNT,MONEY_AMOUNT,DATE)) +ALTER TABLE PUBLIC.T_TRANSACTION ALTER COLUMN ID RESTART WITH 1 +ALTER SEQUENCE SYSTEM_LOBS.LOB_ID RESTART WITH 1 +SET DATABASE DEFAULT INITIAL SCHEMA PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.SQL_IDENTIFIER TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.YES_OR_NO TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.TIME_STAMP TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.CARDINAL_NUMBER TO PUBLIC +GRANT USAGE ON DOMAIN INFORMATION_SCHEMA.CHARACTER_DATA TO PUBLIC +GRANT DBA TO SA +SET SCHEMA SYSTEM_LOBS +INSERT INTO BLOCKS VALUES(0,2147483647,0) +SET SCHEMA PUBLIC +INSERT INTO T_ACCOUNT VALUES(0,'12345678901234','Bogdan',90.0E0) +INSERT INTO T_ACCOUNT VALUES(1,'12345678909876','Dragos',110.0E0) +INSERT INTO T_TRANSACTION VALUES(0,'12345678901234','12345678909876',10.0E0,'2014-03-31') diff --git a/src/test/java/app/repository/JdbcAccountRepositoryIT.java b/src/test/java/app/repository/JdbcAccountRepositoryIT.java new file mode 100644 index 0000000..3dd2425 --- /dev/null +++ b/src/test/java/app/repository/JdbcAccountRepositoryIT.java @@ -0,0 +1,140 @@ +package app.repository; + +import app.domain.Account; +import app.util.AccountMapper; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNull.notNullValue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class JdbcAccountRepositoryIT { + + private static String ACCOUNT_NUMBER = "12345678901234"; + private static String ACCOUNT_NAME = "NAME"; + private static BigDecimal ACCOUNT_MONEY_AMOUNT = new BigDecimal("100.00"); + + @Autowired + private AccountRepository accountRepository; + + @Autowired + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Test + @DirtiesContext + public void testFindByNumberReturnOnceResult() throws Exception { + + jdbcTemplate.update( + "INSERT INTO t_account (number, name, money_amount) VALUES(?, ?, ?)", + ACCOUNT_NUMBER, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT); + + List accounts = accountRepository.findByNumber(ACCOUNT_NUMBER); + + assertThat(accounts, is(notNullValue())); + assertThat(accounts.size(), is(1)); + + Account account = accounts.get(0); + assertThat(account.getEntityId(), is(0L)); + assertThat(account.getName(), is(ACCOUNT_NAME)); + assertThat(account.getNumber(), is(ACCOUNT_NUMBER)); + assertThat(account.getMoneyAmount(), is(ACCOUNT_MONEY_AMOUNT)); + } + + @Test + public void testFindByNumberReturnEmptyList() throws Exception { + + List accounts = accountRepository.findByNumber(ACCOUNT_NUMBER); + + assertThat(accounts, is(notNullValue())); + assertThat(accounts.size(), is(0)); + } + + @Test + @DirtiesContext + public void testInsertSuccess() throws Exception { + + accountRepository.insert(new Account(ACCOUNT_NUMBER, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT)); + + List accounts = jdbcTemplate.query( + "SELECT * FROM t_account WHERE number = ?", + new AccountMapper(), + ACCOUNT_NUMBER); + + assertThat(accounts, is(notNullValue())); + assertThat(accounts.size(), is(1)); + + Account account = accounts.get(0); + assertThat(account.getEntityId(), is(0L)); + assertThat(account.getName(), is(ACCOUNT_NAME)); + assertThat(account.getNumber(), is(ACCOUNT_NUMBER)); + assertThat(account.getMoneyAmount(), is(ACCOUNT_MONEY_AMOUNT)); + } + + @Test + public void testInsertFailed() throws Exception { + exception.expect(DataIntegrityViolationException.class); + + accountRepository.insert(new Account(null, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT)); + + } + + @Test + @DirtiesContext + public void testUpdateMoneyAmountSuccess() throws Exception { + + jdbcTemplate.update( + "INSERT INTO t_account (number, name, money_amount) VALUES(?, ?, ?)", + ACCOUNT_NUMBER, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT); + + List accounts = jdbcTemplate.query("SELECT * FROM t_account WHERE number = ?", + new AccountMapper(), + ACCOUNT_NUMBER); + + BigDecimal NEW_MONEY_AMOUNT = ACCOUNT_MONEY_AMOUNT.add(BigDecimal.TEN); + Account account = accounts.get(0); + account.setMoneyAmount(NEW_MONEY_AMOUNT); + + accountRepository.updateMoneyAmount(account); + + BigDecimal moneyAmount = jdbcTemplate.queryForObject( + "SELECT money_amount FROM t_account WHERE id = ?", BigDecimal.class, account.getEntityId()); + moneyAmount = moneyAmount.setScale(2); + + assertThat(moneyAmount, is(NEW_MONEY_AMOUNT)); + } + + @Test + public void testUpdateMoneyAmountFail() throws Exception { + + Account account = new Account(ACCOUNT_NUMBER, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT); + account.setEntityId(0L); + + accountRepository.updateMoneyAmount(account); + } +} \ No newline at end of file diff --git a/src/test/java/app/repository/JdbcTransactionRepositoryIT.java b/src/test/java/app/repository/JdbcTransactionRepositoryIT.java new file mode 100644 index 0000000..c366680 --- /dev/null +++ b/src/test/java/app/repository/JdbcTransactionRepositoryIT.java @@ -0,0 +1,91 @@ +package app.repository; + +import app.domain.Transaction; +import org.hamcrest.CoreMatchers; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import javax.sql.DataSource; +import java.math.BigDecimal; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsNull.notNullValue; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class JdbcTransactionRepositoryIT { + + private static final String PAYER_ACCOUNT_NUMBER = "12345678901234"; + private static final String BENEFICIARY_ACCOUNT_NUMBER = "12345678909876"; + private static final BigDecimal MONEY_AMOUNT = new BigDecimal("10.00"); + + @Autowired + private TransactionRepository transactionRepository; + + @Autowired + private DataSource dataSource; + + private JdbcTemplate jdbcTemplate; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + jdbcTemplate = new JdbcTemplate(dataSource); + } + + @Test + @DirtiesContext + public void testInsertSuccess() throws Exception { + + transactionRepository.insert(new Transaction(PAYER_ACCOUNT_NUMBER, BENEFICIARY_ACCOUNT_NUMBER, MONEY_AMOUNT)); + + List transactions = jdbcTemplate.query( + "SELECT * FROM t_transaction WHERE payer_account = ? and beneficiary_account = ? and money_amount = ?", + new RowMapper() { + @Override + public Transaction mapRow(ResultSet resultSet, int i) throws SQLException { + Transaction transaction = new Transaction( + resultSet.getString("payer_account"), + resultSet.getString("beneficiary_account"), + resultSet.getBigDecimal("money_amount", 2)); + transaction.setEntityId(resultSet.getLong("id")); + return transaction; + } + }, + PAYER_ACCOUNT_NUMBER, BENEFICIARY_ACCOUNT_NUMBER, MONEY_AMOUNT); + + assertThat(transactions, is(notNullValue())); + assertThat(transactions.size(), is(1)); + + Transaction transaction = transactions.get(0); + assertThat(transaction.getEntityId(), is(0L)); + assertThat(transaction.getPayerAccountNumber(), is(PAYER_ACCOUNT_NUMBER)); + assertThat(transaction.getBeneficiaryAccountNumber(), is(BENEFICIARY_ACCOUNT_NUMBER)); + assertThat(transaction.getMoneyAmount(), is(MONEY_AMOUNT)); + assertThat(transaction.getDate(), is(CoreMatchers.notNullValue())); + } + + @Test + public void testInsertFailed() throws Exception { + exception.expect(DataIntegrityViolationException.class); + + transactionRepository.insert(new Transaction(null, BENEFICIARY_ACCOUNT_NUMBER, MONEY_AMOUNT)); + + } +} diff --git a/src/test/java/app/service/AccountManagerServiceTest.java b/src/test/java/app/service/AccountManagerServiceTest.java new file mode 100644 index 0000000..aa9fbe8 --- /dev/null +++ b/src/test/java/app/service/AccountManagerServiceTest.java @@ -0,0 +1,121 @@ +package app.service; + +import app.domain.Account; +import app.repository.AccountRepository; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class AccountManagerServiceTest { + + private static String ACCOUNT_NUMBER = "12345678901234"; + private static String ACCOUNT_NAME = "NAME"; + private static BigDecimal ACCOUNT_MONEY_AMOUNT = new BigDecimal("100.00"); + private static Account ACCOUNT = new Account(ACCOUNT_NUMBER, ACCOUNT_NAME, ACCOUNT_MONEY_AMOUNT); + + @InjectMocks + private AccountServiceImpl accountService; + + @Mock + private AccountRepository accountRepository; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void testFindByNumberSuccess() throws Exception { + + when(accountRepository.findByNumber(ACCOUNT_NUMBER)).thenReturn(Arrays.asList(ACCOUNT)); + + Account actualAccount = accountService.findByNumber(ACCOUNT_NUMBER); + + assertThat(actualAccount, is(ACCOUNT)); + verify(accountRepository, times(1)).findByNumber(ACCOUNT_NUMBER); + } + + @Test + public void testFindByNumberFail() throws Exception { + exception.expect(EmptyResultDataAccessException.class); + exception.expectMessage("No account found for number " + ACCOUNT_NUMBER); + + when(accountRepository.findByNumber(ACCOUNT_NUMBER)).thenReturn(Collections.emptyList()); + + accountService.findByNumber(ACCOUNT_NUMBER); + } + + @Test + public void testSaveSuccess() throws Exception { + + when(accountRepository.insert(ACCOUNT)).thenReturn(1); + + accountService.save(ACCOUNT); + + verify(accountRepository, times(1)).insert(ACCOUNT); + } + + @Test + public void testSaveFailNoUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("Account not saved."); + + when(accountRepository.insert(ACCOUNT)).thenReturn(0); + + accountService.save(ACCOUNT); + + } + + @Test + public void testSaveFailMoreThanOneFieldUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("More than once account was saved."); + + when(accountRepository.insert(ACCOUNT)).thenReturn(2); + + accountService.save(ACCOUNT); + } + + @Test + public void testUpdateMoneyAmountSuccess() throws Exception { + + when(accountRepository.updateMoneyAmount(ACCOUNT)).thenReturn(1); + + accountService.updateMoneyAmount(ACCOUNT); + + verify(accountRepository, times(1)).updateMoneyAmount(ACCOUNT); + } + + @Test + public void testUpdateMoneyAmountFailNoUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("No accounts were updated"); + + when(accountRepository.updateMoneyAmount(ACCOUNT)).thenReturn(0); + + accountService.updateMoneyAmount(ACCOUNT); + } + + @Test + public void testUpdateMoneyAmountMoreThanOneFieldUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("More than once account was updated"); + + when(accountRepository.updateMoneyAmount(ACCOUNT)).thenReturn(2); + + accountService.updateMoneyAmount(ACCOUNT); + } +} \ No newline at end of file diff --git a/src/test/java/app/service/MoneyTransferServiceTest.java b/src/test/java/app/service/MoneyTransferServiceTest.java new file mode 100644 index 0000000..3457d79 --- /dev/null +++ b/src/test/java/app/service/MoneyTransferServiceTest.java @@ -0,0 +1,53 @@ +package app.service; + +import app.domain.Account; +import app.domain.Transaction; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.math.BigDecimal; + +import static org.mockito.Mockito.*; + +@RunWith(MockitoJUnitRunner.class) +public class MoneyTransferServiceTest { + + private static String PAYER_ACCOUNT_NUMBER = "12345678901234"; + private static String BENEFICIARY_ACCOUNT_NUMBER = "12345678909876"; + private static BigDecimal MONEY_AMOUNT = BigDecimal.TEN; + private static Account PAYER_ACCOUNT = new Account(PAYER_ACCOUNT_NUMBER, "NAME1", new BigDecimal("100.00")); + private static Account BENEFICIARY_ACCOUNT = new Account(BENEFICIARY_ACCOUNT_NUMBER, "NAME2", new BigDecimal("100.00")); + private static Transaction TRANSACTION = new Transaction(PAYER_ACCOUNT_NUMBER, BENEFICIARY_ACCOUNT_NUMBER, MONEY_AMOUNT); + + @InjectMocks + private TransferServiceImpl transferService; + + @Mock + private TransactionService transactionService; + + @Mock + private AccountService accountService; + + @Before + public void setUp() throws Exception { + + when(accountService.findByNumber(PAYER_ACCOUNT_NUMBER)).thenReturn(PAYER_ACCOUNT); + when(accountService.findByNumber(BENEFICIARY_ACCOUNT_NUMBER)).thenReturn(BENEFICIARY_ACCOUNT); + } + + @Test + public void testTransferAmount() throws Exception { + + transferService.transferAmount(TRANSACTION); + + verify(accountService,times(1)).updateMoneyAmount(PAYER_ACCOUNT); + verify(accountService,times(1)).updateMoneyAmount(BENEFICIARY_ACCOUNT); + + verify(transactionService, times(1)).save(TRANSACTION); + } + +} diff --git a/src/test/java/app/service/TransactionManagerServiceTest.java b/src/test/java/app/service/TransactionManagerServiceTest.java new file mode 100644 index 0000000..b1ed028 --- /dev/null +++ b/src/test/java/app/service/TransactionManagerServiceTest.java @@ -0,0 +1,67 @@ +package app.service; + +import app.domain.Transaction; +import app.repository.TransactionRepository; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import java.math.BigDecimal; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TransactionManagerServiceTest { + + private static final String PAYER_ACCOUNT_NUMBER = "12345678901234"; + private static final String BENEFICIARY_ACCOUNT_NUMBER = "12345678909876"; + private static final BigDecimal MONEY_AMOUNT = new BigDecimal("10.00"); + private static final Transaction TRANSACTION = new Transaction(PAYER_ACCOUNT_NUMBER, BENEFICIARY_ACCOUNT_NUMBER, MONEY_AMOUNT); + + @InjectMocks + private TransactionServiceImpl transactionService; + + @Mock + private TransactionRepository transactionRepository; + + @Rule + public ExpectedException exception = ExpectedException.none(); + + + @Test + public void testSaveSuccess() throws Exception { + + when(transactionRepository.insert(TRANSACTION)).thenReturn(1); + + transactionService.save(TRANSACTION); + + verify(transactionRepository, times(1)).insert(TRANSACTION); + } + + @Test + public void testSaveFailNoUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("Transaction not saved."); + + when(transactionRepository.insert(TRANSACTION)).thenReturn(0); + + transactionService.save(TRANSACTION); + } + + @Test + public void testSaveFailMoreThanOneFieldUpdate() throws Exception { + exception.expect(IncorrectResultSizeDataAccessException.class); + exception.expectMessage("More than once transaction was saved."); + + when(transactionRepository.insert(TRANSACTION)).thenReturn(2); + + transactionService.save(TRANSACTION); + } +} diff --git a/src/test/resources/app/repository/JdbcAccountRepositoryIT-context.xml b/src/test/resources/app/repository/JdbcAccountRepositoryIT-context.xml new file mode 100644 index 0000000..425a54d --- /dev/null +++ b/src/test/resources/app/repository/JdbcAccountRepositoryIT-context.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/src/test/resources/app/repository/JdbcTransactionRepositoryIT-context.xml b/src/test/resources/app/repository/JdbcTransactionRepositoryIT-context.xml new file mode 100644 index 0000000..622b028 --- /dev/null +++ b/src/test/resources/app/repository/JdbcTransactionRepositoryIT-context.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/src/test/resources/spring/database-test-config.xml b/src/test/resources/spring/database-test-config.xml new file mode 100644 index 0000000..dd79093 --- /dev/null +++ b/src/test/resources/spring/database-test-config.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file