diff --git a/README.md b/README.md index a1d2939..213be0b 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ SpringCore ========== +example with xml metadata configuration + +homework - review code, ask questions + +homework - change constructor injection with setter injection + +homework - do validation for AccountMapper class + +homework - do validation for Transaction Validator + +homework - tbd diff --git a/pom.xml b/pom.xml index 902ab97..4eaf1a3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,28 +1,30 @@ - - 4.0.0 app SpringCore 1.0-SNAPSHOT + jar - - - 4.0.2.RELEASE + SpringCore + + 4.0.3.RELEASE + 1.4 + 2.3.2 1.7.6 1.1.1 - 4.11-20120805-1225 + 4.11 + 1.9.5 + 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 +61,32 @@ ${org.springframework.version} - org.springframework - spring-context-support + spring-jdbc ${org.springframework.version} + + org.springframework + spring-test + ${org.springframework.version} + test + + + + + commons-dbcp + commons-dbcp + ${commons-dbcp.version} + + + + org.hsqldb + hsqldb + ${org.hsqldb.version} + + @@ -100,6 +118,12 @@ test + + org.mockito + mockito-core + ${mockito.version} + test + @@ -111,7 +135,12 @@ 1.7 1.7 - + + + + org.apache.maven.plugins + maven-jar-plugin + 2.4 org.codehaus.mojo @@ -119,10 +148,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..b6ada8b --- /dev/null +++ b/src/main/java/app/Main.java @@ -0,0 +1,30 @@ +package app; + +import app.domain.Account; +import app.domain.Transaction; +import app.service.AccountManagerService; +import app.service.MoneyTransferService; +import app.service.TransferService; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +import java.math.BigDecimal; + +public class Main { + + public static void main(String[] args) { + + ApplicationContext context = new ClassPathXmlApplicationContext("/spring/application-config.xml","/spring/database-config.xml"); + + Transaction transaction = new Transaction("12345678901234", "12345678909876", BigDecimal.TEN); + TransferService transferService = context.getBean("transferService", MoneyTransferService.class); + transferService.transferAmount(transaction); + + AccountManagerService accountService = context.getBean("accountService", AccountManagerService.class); + Account payerAccount = accountService.findByNumber("12345678901234"); + Account beneficiaryAccount = accountService.findByNumber("12345678909876"); + + System.out.println(payerAccount); + System.out.println(beneficiaryAccount); + } +} 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/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..538a168 --- /dev/null +++ b/src/main/java/app/repository/JdbcAccountRepository.java @@ -0,0 +1,43 @@ +package app.repository; + +import app.domain.Account; +import app.util.AccountMapper; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; +import java.util.List; + +public class JdbcAccountRepository implements AccountRepository { + + private JdbcTemplate jdbcTemplate; + + 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..71d6876 --- /dev/null +++ b/src/main/java/app/repository/JdbcTransactionRepository.java @@ -0,0 +1,25 @@ +package app.repository; + +import app.domain.Transaction; +import org.springframework.jdbc.core.JdbcTemplate; + +import javax.sql.DataSource; + +public class JdbcTransactionRepository implements TransactionRepository { + + private JdbcTemplate jdbcTemplate; + + 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/AccountManagerService.java b/src/main/java/app/service/AccountManagerService.java new file mode 100644 index 0000000..1af754c --- /dev/null +++ b/src/main/java/app/service/AccountManagerService.java @@ -0,0 +1,51 @@ +package app.service; + +import app.domain.Account; +import app.repository.AccountRepository; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +import java.util.List; + +public class AccountManagerService implements AccountService { + + private AccountRepository accountRepository; + + public AccountManagerService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + @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); + } + + @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); + } + } + + @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/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/MoneyTransferService.java b/src/main/java/app/service/MoneyTransferService.java new file mode 100644 index 0000000..17ec6f6 --- /dev/null +++ b/src/main/java/app/service/MoneyTransferService.java @@ -0,0 +1,37 @@ +package app.service; + +import app.domain.Account; +import app.domain.Transaction; +import app.util.TransactionValidator; + +import java.math.BigDecimal; + +public class MoneyTransferService implements TransferService { + + private AccountService accountService; + private TransactionService transactionService; + + public MoneyTransferService(AccountService accountService, TransactionService transactionService) { + this.accountService = accountService; + this.transactionService = transactionService; + } + + @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/service/TransactionManagerService.java b/src/main/java/app/service/TransactionManagerService.java new file mode 100644 index 0000000..76b36f1 --- /dev/null +++ b/src/main/java/app/service/TransactionManagerService.java @@ -0,0 +1,25 @@ +package app.service; + +import app.domain.Transaction; +import app.repository.TransactionRepository; +import org.springframework.dao.IncorrectResultSizeDataAccessException; + +public class TransactionManagerService implements TransactionService { + + private TransactionRepository transactionRepository; + + public TransactionManagerService(TransactionRepository transactionRepository) { + this.transactionRepository = transactionRepository; + } + + @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/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/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/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/application-config.xml b/src/main/resources/spring/application-config.xml new file mode 100644 index 0000000..916da7e --- /dev/null +++ b/src/main/resources/spring/application-config.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ 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..fd24228 --- /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..f0f01b4 --- /dev/null +++ b/src/main/resources/testdbdata/data.log @@ -0,0 +1,34 @@ +/*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-30') +COMMIT +DISCONNECT +/*C14*/SET SCHEMA PUBLIC +DISCONNECT +/*C15*/SET SCHEMA PUBLIC +DISCONNECT +/*C16*/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..e869e9d --- /dev/null +++ b/src/main/resources/testdbdata/data.properties @@ -0,0 +1,4 @@ +#HSQL Database Engine 2.3.2 +#Sun Mar 30 14:10:53 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..9ee52eb --- /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-30') 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..5fe49a1 --- /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 AccountManagerService 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..7d432ab --- /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 MoneyTransferService 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..56f6674 --- /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 TransactionManagerService 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