mirror of
https://github.com/wahyd4/cash-code-test.git
synced 2026-08-09 05:16:05 +10:00
code before onsite
This commit is contained in:
@@ -0,0 +1 @@
|
||||
.idea/
|
||||
@@ -0,0 +1,8 @@
|
||||
package commands
|
||||
|
||||
import "github.com/wahyd4/cash/domains"
|
||||
|
||||
// ICommand defines the banking app command
|
||||
type ICommand interface {
|
||||
Run(bank *domains.Bank) error
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import "github.com/wahyd4/cash/domains"
|
||||
|
||||
// DepositCommand the command for depositing some money
|
||||
type DepositCommand struct {
|
||||
Account string
|
||||
Money float64
|
||||
}
|
||||
|
||||
// Run deposits some money
|
||||
func (command *DepositCommand) Run(bank *domains.Bank) error {
|
||||
account, err := bank.AccountFromName(command.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bank.DepositFor(account, command.Money)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDepositCommand_Run(t *testing.T) {
|
||||
bank := domains.InitBank()
|
||||
bank.OpenAccount("Tom")
|
||||
|
||||
type fields struct {
|
||||
Account string
|
||||
Money float64
|
||||
}
|
||||
type args struct {
|
||||
bank *domains.Bank
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can deposit some money",
|
||||
fields{
|
||||
Account: "Tom",
|
||||
Money: 20.10,
|
||||
},
|
||||
args{bank: bank},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"fail to deposit some money due to money is negative",
|
||||
fields{
|
||||
Account: "Tom",
|
||||
Money: -20.10,
|
||||
},
|
||||
args{bank: bank},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"fail to deposit some money due to the user is not exist",
|
||||
fields{
|
||||
Account: "NotExist",
|
||||
Money: -20.10,
|
||||
},
|
||||
args{bank: bank},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
command := &DepositCommand{
|
||||
Account: tt.fields.Account,
|
||||
Money: tt.fields.Money,
|
||||
}
|
||||
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/wahyd4/cash/domains"
|
||||
)
|
||||
|
||||
// OpenAccountCommand the command for creating a bank account
|
||||
type OpenAccountCommand struct {
|
||||
Username string
|
||||
}
|
||||
|
||||
// Run creates a bank account
|
||||
func (command *OpenAccountCommand) Run(bank *domains.Bank) error {
|
||||
bank.OpenAccount(command.Username)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
)
|
||||
|
||||
func TestOpenAccountCommand_Run_ShouldCreateABankAccount(t *testing.T) {
|
||||
command := OpenAccountCommand{
|
||||
Username: "Mindy",
|
||||
}
|
||||
|
||||
bank := domains.InitBank()
|
||||
assert.Equal(t, len(bank.TotalAccounts()), 0)
|
||||
command.Run(bank)
|
||||
assert.Equal(t, len(bank.TotalAccounts()), 1)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
)
|
||||
|
||||
// TotalBalanceCommand for getting the balance
|
||||
type TotalBalanceCommand struct {
|
||||
}
|
||||
|
||||
// Run gets the balance for a specific account when account is not nil, otherwise for the bank
|
||||
func (command *TotalBalanceCommand) Run(bank *domains.Bank) error {
|
||||
logrus.Infof("the total balance is %.2f", bank.TotalBalance())
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTotalBalanceCommand_Run(t *testing.T) {
|
||||
bank := domains.InitBank()
|
||||
bank.OpenAccount("Tom")
|
||||
bank.OpenAccount("Lily")
|
||||
|
||||
account, _ := bank.AccountFromName("Tom")
|
||||
account2, _ := bank.AccountFromName("Lily")
|
||||
|
||||
bank.DepositFor(account, 500)
|
||||
bank.DepositFor(account2, 29.10)
|
||||
|
||||
type args struct {
|
||||
bank *domains.Bank
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can print balance for the whole bank",
|
||||
args{bank: bank},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
command := &TotalBalanceCommand{}
|
||||
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
)
|
||||
|
||||
// UserBalanceCommand for getting the balance
|
||||
type UserBalanceCommand struct {
|
||||
Account string
|
||||
}
|
||||
|
||||
// Run gets balance for a specific account when account is not nil, otherwise for the bank
|
||||
func (command *UserBalanceCommand) Run(bank *domains.Bank) error {
|
||||
account, err := bank.AccountFromName(command.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Infof("the balance of %s is %.2f", account.Name, bank.BalanceFor(account))
|
||||
return nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBalanceCommand_Run(t *testing.T) {
|
||||
bank := domains.InitBank()
|
||||
bank.OpenAccount("Tom")
|
||||
bank.OpenAccount("Lily")
|
||||
|
||||
account, _ := bank.AccountFromName("Tom")
|
||||
account2, _ := bank.AccountFromName("Lily")
|
||||
|
||||
bank.DepositFor(account, 500)
|
||||
bank.DepositFor(account2, 29.10)
|
||||
|
||||
type fields struct {
|
||||
Account string
|
||||
}
|
||||
type args struct {
|
||||
bank *domains.Bank
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can print balance for user",
|
||||
fields{Account: "Tom"},
|
||||
args{bank: bank},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can print balance for user",
|
||||
fields{Account: "Lily"},
|
||||
args{bank: bank},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cannot print the user du to the user is not exist",
|
||||
fields{Account: "NotExist"},
|
||||
args{bank: bank},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
command := &UserBalanceCommand{
|
||||
Account: tt.fields.Account,
|
||||
}
|
||||
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package commands
|
||||
|
||||
import "github.com/wahyd4/cash/domains"
|
||||
|
||||
// WithdrawCommand the command for withdrawing some money
|
||||
type WithdrawCommand struct {
|
||||
Account string
|
||||
Money float64
|
||||
}
|
||||
|
||||
// Run withdraws money from the bank, returns error if fails
|
||||
func (command *WithdrawCommand) Run(bank *domains.Bank) error {
|
||||
account, err := bank.AccountFromName(command.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return bank.WithdrawFor(account, command.Money)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWithdrawCommand_Run(t *testing.T) {
|
||||
bank := domains.InitBank()
|
||||
bank.OpenAccount("Tom")
|
||||
account, _ := bank.AccountFromName("Tom")
|
||||
bank.DepositFor(account, 30)
|
||||
|
||||
type fields struct {
|
||||
Account string
|
||||
Money float64
|
||||
}
|
||||
type args struct {
|
||||
bank *domains.Bank
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can withdraw some money",
|
||||
fields{
|
||||
Account: "Tom",
|
||||
Money: 20.10,
|
||||
},
|
||||
args{bank: bank},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cannot withdraw money due to does not have enough money",
|
||||
fields{
|
||||
Account: "Tom",
|
||||
Money: 50.0, //we only have 9.9 left
|
||||
},
|
||||
args{bank: bank},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"cannot withdraw due to the user can't found",
|
||||
fields{
|
||||
Account: "NotExist",
|
||||
Money: 0.00,
|
||||
},
|
||||
args{bank: bank},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
command := &WithdrawCommand{
|
||||
Account: tt.fields.Account,
|
||||
Money: tt.fields.Money,
|
||||
}
|
||||
if err := command.Run(tt.args.bank); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
cash:
|
||||
image: golang:1.13
|
||||
working_dir: /cash
|
||||
volumes:
|
||||
- .:/cash
|
||||
command: ["go", "run", "main.go", "input.txt"]
|
||||
@@ -0,0 +1,32 @@
|
||||
package domains
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
dollarToCents = 100
|
||||
)
|
||||
|
||||
// BankAccount represents for a bank customer
|
||||
type BankAccount struct {
|
||||
Name string
|
||||
balance int // in cent
|
||||
}
|
||||
|
||||
// Balance get balance and return with ($)dollar instead of the unit which is cent
|
||||
func (account *BankAccount) Balance() float64 {
|
||||
return float64(account.balance) / float64(dollarToCents)
|
||||
}
|
||||
|
||||
// Deposit deposits money for the account
|
||||
func (account *BankAccount) Deposit(moneyInCents int) {
|
||||
account.balance += moneyInCents
|
||||
}
|
||||
|
||||
// Withdraw withdraw money for the account
|
||||
func (account *BankAccount) Withdraw(moneyInCents int) error {
|
||||
if account.balance < moneyInCents {
|
||||
return fmt.Errorf("no enough money can be withdrawn")
|
||||
}
|
||||
account.balance -= moneyInCents
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBankAccount_Balance(t *testing.T) {
|
||||
type fields struct {
|
||||
Name string
|
||||
balance int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
"balance is 0",
|
||||
fields{
|
||||
Name: "Tom",
|
||||
balance: 0,
|
||||
},
|
||||
0.00,
|
||||
},
|
||||
{
|
||||
"balance is not 0 and no extra cents",
|
||||
fields{
|
||||
Name: "Lily",
|
||||
balance: 178800,
|
||||
},
|
||||
1788.00,
|
||||
},
|
||||
{
|
||||
"balance is not 0 and with some extra cents",
|
||||
fields{
|
||||
Name: "Dave",
|
||||
balance: 8912,
|
||||
},
|
||||
89.12,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
account := &BankAccount{
|
||||
Name: tt.fields.Name,
|
||||
balance: tt.fields.balance,
|
||||
}
|
||||
if got := account.Balance(); got != tt.want {
|
||||
t.Errorf("Balance() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBankAccount_Deposit(t *testing.T) {
|
||||
type fields struct {
|
||||
Name string
|
||||
balance int
|
||||
}
|
||||
type args struct {
|
||||
moneyInCents int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "deposits some money when balance is 0",
|
||||
fields: fields{
|
||||
Name: "Tom",
|
||||
balance: 0,
|
||||
},
|
||||
args: args{moneyInCents: 3500},
|
||||
},
|
||||
{
|
||||
name: "deposits some money when balance is not 0",
|
||||
fields: fields{
|
||||
Name: "Tom",
|
||||
balance: 2800,
|
||||
},
|
||||
args: args{moneyInCents: 3500},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
account := &BankAccount{
|
||||
Name: tt.fields.Name,
|
||||
balance: tt.fields.balance,
|
||||
}
|
||||
account.Deposit(tt.args.moneyInCents)
|
||||
assert.Equal(t, account.balance, tt.fields.balance+tt.args.moneyInCents)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBankAccount_Withdraw1(t *testing.T) {
|
||||
type fields struct {
|
||||
Name string
|
||||
balance int
|
||||
}
|
||||
type args struct {
|
||||
moneyInCents int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
expectedBalance int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can withdraw some money when user has enough balance",
|
||||
fields{
|
||||
Name: "Tom",
|
||||
balance: 10000,
|
||||
},
|
||||
args{moneyInCents: 3500},
|
||||
6500,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can withdraw some money when user withdraw all the money he has",
|
||||
fields{
|
||||
Name: "Tom",
|
||||
balance: 8765,
|
||||
},
|
||||
args{moneyInCents: 8765},
|
||||
0,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can't withdraw the money more than the user has",
|
||||
fields{
|
||||
Name: "Tom",
|
||||
balance: 8765,
|
||||
},
|
||||
args{moneyInCents: 9888},
|
||||
8765,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"can't withdraw money more than the user has",
|
||||
fields{
|
||||
Name: "Tom",
|
||||
balance: 8765,
|
||||
},
|
||||
args{moneyInCents: 10000},
|
||||
8765,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
account := &BankAccount{
|
||||
Name: tt.fields.Name,
|
||||
balance: tt.fields.balance,
|
||||
}
|
||||
if err := account.Withdraw(tt.args.moneyInCents); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Withdraw() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrActionMoneyAmountInvalid = fmt.Errorf("money can't be negative or 0")
|
||||
)
|
||||
|
||||
const roundFloat = 0.5
|
||||
|
||||
// Bank represents for the bank
|
||||
// can be improved by using sync.Mutex
|
||||
type Bank struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
|
||||
// InitBank inits the bank
|
||||
func InitBank() *Bank {
|
||||
return &Bank{}
|
||||
}
|
||||
|
||||
// OpenAccount opens a account for customer
|
||||
func (bank *Bank) OpenAccount(customerName string) {
|
||||
bank.accounts = append(bank.accounts, &BankAccount{Name: customerName})
|
||||
|
||||
logrus.Infof("opened bank account for: %s", customerName)
|
||||
}
|
||||
|
||||
// DepositFor deposit for a user
|
||||
func (bank *Bank) DepositFor(account *BankAccount, money float64) error {
|
||||
if money <= 0 {
|
||||
return ErrActionMoneyAmountInvalid
|
||||
}
|
||||
moneyInCents := toCents(money)
|
||||
|
||||
account.Deposit(moneyInCents)
|
||||
bank.totalBalance += moneyInCents
|
||||
|
||||
logrus.Infof("deposited $%.2f for %s", money, account.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithdrawFor withdraw money for a user
|
||||
func (bank *Bank) WithdrawFor(account *BankAccount, money float64) error {
|
||||
if money <= 0 {
|
||||
return ErrActionMoneyAmountInvalid
|
||||
}
|
||||
|
||||
moneyInCents := toCents(money)
|
||||
if err := account.Withdraw(moneyInCents); err != nil {
|
||||
return fmt.Errorf("failed to withdraw money %.2f due to %v", money, err)
|
||||
}
|
||||
bank.totalBalance -= moneyInCents
|
||||
|
||||
logrus.Infof("withdrawn $%.2f for %s", money, account.Name)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// BalanceFor gets balance for a user
|
||||
func (bank *Bank) BalanceFor(account *BankAccount) float64 {
|
||||
return float64(account.balance) / float64(dollarToCents)
|
||||
}
|
||||
|
||||
// TotalBalance gets total balance for the bank
|
||||
func (bank *Bank) TotalBalance() float64 {
|
||||
return float64(bank.totalBalance) / float64(dollarToCents)
|
||||
}
|
||||
|
||||
// TotalAccounts returns all the bank accounts for the bank
|
||||
func (bank *Bank) TotalAccounts() []*BankAccount {
|
||||
return bank.accounts
|
||||
}
|
||||
|
||||
// AccountFromName finds account by name, returns error if not found.
|
||||
func (bank *Bank) AccountFromName(accountName string) (*BankAccount, error) {
|
||||
for _, account := range bank.accounts {
|
||||
if account.Name == accountName {
|
||||
return account, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no account with name %s", accountName)
|
||||
}
|
||||
|
||||
// toCents convert dollar to cents and make sure we get accurate result
|
||||
// e.g. 1.23 to $1.23, 1.345 to $1.35
|
||||
func toCents(moneyInDollar float64) int {
|
||||
return int(moneyInDollar*dollarToCents + roundFloat)
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
package domains
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInitBank(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Bank
|
||||
}{
|
||||
{
|
||||
"init bank with empty account list and 0 balance",
|
||||
&Bank{
|
||||
accounts: []*BankAccount{},
|
||||
totalBalance: 0,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := InitBank(); (len(got.accounts) != len(tt.want.accounts)) || (got.totalBalance != tt.want.totalBalance) {
|
||||
t.Errorf("InitBank() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_OpenAccount(t *testing.T) {
|
||||
bank := InitBank()
|
||||
t.Run("should open a account for customer", func(t *testing.T) {
|
||||
bank.OpenAccount("Nick")
|
||||
assert.Equal(t, len(bank.accounts), 1)
|
||||
assert.Equal(t, bank.totalBalance, 0)
|
||||
})
|
||||
}
|
||||
|
||||
func TestBank_DepositFor(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
type args struct {
|
||||
account *BankAccount
|
||||
money float64
|
||||
}
|
||||
tom := &BankAccount{balance: 0, Name: "Tom"}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
expectedBalance int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can deposit some money",
|
||||
fields{
|
||||
accounts: []*BankAccount{tom},
|
||||
totalBalance: 0,
|
||||
},
|
||||
args{
|
||||
account: tom,
|
||||
money: 12.34,
|
||||
},
|
||||
1234,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can deposit some money",
|
||||
fields{
|
||||
accounts: []*BankAccount{tom},
|
||||
totalBalance: 6700,
|
||||
},
|
||||
args{
|
||||
account: tom,
|
||||
money: 210,
|
||||
},
|
||||
27700,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cannot deposit when money is negative",
|
||||
fields{
|
||||
accounts: []*BankAccount{tom},
|
||||
totalBalance: 6700,
|
||||
},
|
||||
args{
|
||||
account: tom,
|
||||
money: -100,
|
||||
},
|
||||
6700,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"cannot deposit when money is 0",
|
||||
fields{
|
||||
accounts: []*BankAccount{tom},
|
||||
totalBalance: 6700,
|
||||
},
|
||||
args{
|
||||
account: tom,
|
||||
money: 0,
|
||||
},
|
||||
6700,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
err := bank.DepositFor(tt.args.account, tt.args.money)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("DepositFor() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.expectedBalance, bank.totalBalance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_WithdrawFor(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
type args struct {
|
||||
account *BankAccount
|
||||
money float64
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
expectedBalance int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can withdraw some money",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
|
||||
totalBalance: 10000,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 8000},
|
||||
money: 12.34,
|
||||
},
|
||||
8766,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can withdraw all money the user has",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
|
||||
totalBalance: 5678,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 8000},
|
||||
money: 56.78,
|
||||
},
|
||||
0,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"cannot withdraw 0",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
|
||||
totalBalance: 10000,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 8000},
|
||||
money: 0,
|
||||
},
|
||||
10000,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"cannot withdraw negative",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 8000}},
|
||||
totalBalance: 10000,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 8000},
|
||||
money: -23,
|
||||
},
|
||||
10000,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"cannot withdraw the money more than the user has",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 800}},
|
||||
totalBalance: 800,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 800},
|
||||
money: 20.88,
|
||||
},
|
||||
800,
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"cannot withdraw the money more than the user has but bank has enough money",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 800}},
|
||||
totalBalance: 1800,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 800},
|
||||
money: 20.88,
|
||||
},
|
||||
800,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
err := bank.WithdrawFor(tt.args.account, tt.args.money)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("WithdrawFor() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.expectedBalance, bank.totalBalance)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_BalanceFor(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
type args struct {
|
||||
account *BankAccount
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
"can get balance a user when user has some money",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 812}},
|
||||
totalBalance: 1800,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 812},
|
||||
},
|
||||
8.12,
|
||||
},
|
||||
{
|
||||
"can get balance a user when user does not have any money",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 0}},
|
||||
totalBalance: 1800,
|
||||
},
|
||||
args{
|
||||
account: &BankAccount{Name: "Tom", balance: 0},
|
||||
},
|
||||
0.00,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
if got := bank.BalanceFor(tt.args.account); got != tt.want {
|
||||
t.Errorf("BalanceFor() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_TotalBalance(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want float64
|
||||
}{
|
||||
{
|
||||
"can get balance for the bank",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 812}},
|
||||
totalBalance: 1876,
|
||||
},
|
||||
18.76,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
if got := bank.TotalBalance(); got != tt.want {
|
||||
t.Errorf("TotalBalance() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_TotalAccounts(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []*BankAccount
|
||||
}{
|
||||
{
|
||||
"can get all the bank accounts",
|
||||
fields{
|
||||
[]*BankAccount{{Name: "Tom", balance: 812}},
|
||||
1000,
|
||||
},
|
||||
[]*BankAccount{{Name: "Tom", balance: 812}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
if got := bank.TotalAccounts(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("TotalAccounts() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBank_AccountFromName(t *testing.T) {
|
||||
type fields struct {
|
||||
accounts []*BankAccount
|
||||
totalBalance int
|
||||
}
|
||||
type args struct {
|
||||
accountName string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want *BankAccount
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"can find account by name",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 812}, {Name: "Lily", balance: 0}},
|
||||
totalBalance: 812,
|
||||
},
|
||||
args{accountName: "Lily"},
|
||||
&BankAccount{Name: "Lily", balance: 0},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"can not find account due to the account not exist",
|
||||
fields{
|
||||
accounts: []*BankAccount{{Name: "Tom", balance: 812}, {Name: "Lily", balance: 0}},
|
||||
totalBalance: 812,
|
||||
},
|
||||
args{accountName: "SomeFakeName"},
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
bank := &Bank{
|
||||
accounts: tt.fields.accounts,
|
||||
totalBalance: tt.fields.totalBalance,
|
||||
}
|
||||
got, err := bank.AccountFromName(tt.args.accountName)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("AccountFromName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("AccountFromName() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
module github.com/wahyd4/cash
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
github.com/stretchr/testify v1.4.0
|
||||
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
@@ -0,0 +1,12 @@
|
||||
OPEN_ACCOUNT Tom
|
||||
DEPOSIT Tom 520.30
|
||||
BALANCE Tom
|
||||
TOTAL_BALANCE
|
||||
WITHDRAW Tom 340.00
|
||||
BALANCE Tom
|
||||
OPEN_ACCOUNT Lily
|
||||
DEPOSIT Lily 1980.55
|
||||
BALANCE Lily
|
||||
WITHDRAW Lily 550.33
|
||||
BALANCE Lily
|
||||
TOTAL_BALANCE
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"github.com/wahyd4/cash/workflow"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 2 {
|
||||
logrus.Fatal("Please run the cash application by: go run main.go <input.txt>")
|
||||
}
|
||||
|
||||
filePathParam := os.Args[1]
|
||||
|
||||
bank := domains.InitBank()
|
||||
app := workflow.APP{Bank: bank}
|
||||
|
||||
commands, err := app.Load(filePathParam)
|
||||
|
||||
if err != nil {
|
||||
logrus.Panic("fail to load input" + err.Error())
|
||||
}
|
||||
|
||||
if err := app.Run(commands); err != nil {
|
||||
logrus.Error(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
OPEN_ACCOUNT Tom
|
||||
DEPOSIT Tom 123
|
||||
BALANCE Tom
|
||||
WITHDRAW Tom 250
|
||||
BALANCE Tom
|
||||
TOTAL_BALANCE
|
||||
@@ -0,0 +1,107 @@
|
||||
# Cash
|
||||
|
||||
- [Cash](#cash)
|
||||
- [About the solution](#about-the-solution)
|
||||
- [Stack](#stack)
|
||||
- [Design](#design)
|
||||
- [Assumptions & Trade offs](#assumptions-amp-trade-offs)
|
||||
- [What would I do more if I have more time](#what-would-i-do-more-if-i-have-more-time)
|
||||
- [How to run](#how-to-run)
|
||||
- [docker-compose](#docker-compose)
|
||||
- [The classic Way](#the-classic-way)
|
||||
- [Run the application against custom inputs](#run-the-application-against-custom-inputs)
|
||||
|
||||
## About the solution
|
||||
|
||||
### Stack
|
||||
|
||||
- Go
|
||||
- [Logrus](https://github.com/sirupsen/logrus) for better logging
|
||||
- [testify/assert](https://github.com/stretchr/testify) some test assertion helpers
|
||||
|
||||
### Design
|
||||
|
||||
In my design, there are `4` core domains,
|
||||
|
||||
- Application, represents the cash application which can load inputs and execute actions.
|
||||
- BankAccount, contains the information of a single bank account and basic deposit/withdraw actions to the user.
|
||||
- Bank, the bank entity which holds all the bank accounts and the total balance. All the `deposit/withdraw` actions go to bank and then acknowledged by bank account.
|
||||
- Command, represent a single action. e.g. `deposit`, `withdraw`, `balance`. way. All commands have been implemented the `ICommand` interface with `Run(bank *Bank)` method, so they can be processed by the application in a standard
|
||||
|
||||
The steps below shows how the application works.
|
||||
|
||||
1. When we start this application it will initialise a `bank` and the `cash application` instance.
|
||||
2. All the actions recorded in a input file will be parsed and transferred to `command`
|
||||
3. The Cash application will take the commands as input, process them and print out some basic information.
|
||||
|
||||
### Assumptions & Trade offs
|
||||
|
||||
In order to start implementing this project, I have made a few assumptions below:
|
||||
|
||||
- Only one seesion for any user at any single moment, which means there will no dirty reads/writes for a user
|
||||
- All the actions will be loaded through a input text file and will be processed following the sequence order, so it means the application currently can only run one command at a time.
|
||||
- I use Australian Dollar($) as the currency type, use `int` to represent `cent` as the currency unit.
|
||||
|
||||
### What would I do more if I have more time
|
||||
|
||||
There are two things I want to do most in terms of building a proper cash app if I have more time.
|
||||
|
||||
- Make it to be a CLI and support multiple users login and run actions at the same time.
|
||||
- Add some basic locks for depositing and withdrawing operations at both user and bank level.
|
||||
|
||||
## How to run
|
||||
|
||||
### docker-compose
|
||||
|
||||
Please make sure you have `docker` and `docker-compose` installed
|
||||
|
||||
Then just simply run the commands below
|
||||
|
||||
```bash
|
||||
# One command to run application
|
||||
docker-compose up
|
||||
|
||||
# If you want to run another failure case with withdrawing more money than the user has
|
||||
docker-compose run cash go run main.go no_enough_money.txt
|
||||
|
||||
# Run tests
|
||||
docker-compose run cash go test ./... -cover
|
||||
```
|
||||
|
||||
Or you can try the classic way.
|
||||
|
||||
### The classic Way
|
||||
|
||||
Similar, you should have `go` installed before we start
|
||||
|
||||
```bash
|
||||
# Install all dependencies
|
||||
go mod download
|
||||
|
||||
# Run application
|
||||
go run main.go input.txt
|
||||
|
||||
# Run Tests
|
||||
go test ./... -cover
|
||||
```
|
||||
|
||||
|
||||
### Run the application against custom inputs
|
||||
|
||||
You can also build your custom input and define as many commands as you want.
|
||||
|
||||
There is a example below with single bank account.
|
||||
|
||||
```
|
||||
OPEN_ACCOUNT Tom
|
||||
DEPOSIT Tom 520.30
|
||||
WITHDRAW Tom 340.00
|
||||
BALANCE Tom
|
||||
TOTAL_BALANCE
|
||||
```
|
||||
|
||||
Then test it
|
||||
|
||||
```bash
|
||||
docker-compose run cash go run main.go yourfile
|
||||
```
|
||||
@@ -0,0 +1,80 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"github.com/wahyd4/cash/commands"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const commandSplitter = " "
|
||||
|
||||
// APP holds the bank app instance
|
||||
type APP struct {
|
||||
Bank *domains.Bank
|
||||
}
|
||||
|
||||
// Run all the commands passed in
|
||||
func (app *APP) Run(commands []commands.ICommand) error {
|
||||
for _, command := range commands {
|
||||
if err := command.Run(app.Bank); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load loads inputs and generate commands
|
||||
func (app *APP) Load(filePath string) ([]commands.ICommand, error) {
|
||||
appCommands := make([]commands.ICommand, 0)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return appCommands, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
commandString := scanner.Text()
|
||||
appCommands = append(appCommands, buildCommand(commandString))
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("fail to read file %s with error: %v", filePath, err)
|
||||
}
|
||||
|
||||
return appCommands, nil
|
||||
}
|
||||
|
||||
func buildCommand(commandString string) commands.ICommand {
|
||||
|
||||
commandArgs := strings.Split(commandString, commandSplitter)
|
||||
switch commandArgs[0] {
|
||||
case "OPEN_ACCOUNT":
|
||||
return &commands.OpenAccountCommand{Username: commandArgs[1]}
|
||||
case "DEPOSIT":
|
||||
money, _ := strconv.ParseFloat(commandArgs[2], 10)
|
||||
return &commands.DepositCommand{
|
||||
Account: commandArgs[1],
|
||||
Money: money,
|
||||
}
|
||||
case "WITHDRAW":
|
||||
money, _ := strconv.ParseFloat(commandArgs[2], 10)
|
||||
return &commands.WithdrawCommand{
|
||||
Account: commandArgs[1],
|
||||
Money: money,
|
||||
}
|
||||
case "BALANCE":
|
||||
return &commands.UserBalanceCommand{
|
||||
Account: commandArgs[1],
|
||||
}
|
||||
case "TOTAL_BALANCE":
|
||||
return &commands.TotalBalanceCommand{}
|
||||
default:
|
||||
return &commands.TotalBalanceCommand{}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package workflow
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/wahyd4/cash/commands"
|
||||
"github.com/wahyd4/cash/domains"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// simpleCommand a dump implementation for test
|
||||
type simpleCommand struct {
|
||||
shouldErr bool
|
||||
}
|
||||
|
||||
func (command *simpleCommand) Run(bank *domains.Bank) error {
|
||||
if command.shouldErr {
|
||||
return errors.New("some error")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAPP_Run(t *testing.T) {
|
||||
type fields struct {
|
||||
Bank *domains.Bank
|
||||
}
|
||||
type args struct {
|
||||
commands []commands.ICommand
|
||||
}
|
||||
|
||||
bank := domains.InitBank()
|
||||
bank.OpenAccount("Dan")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
totalBalanceInFloat float64
|
||||
}{
|
||||
{
|
||||
"can finish all commands successfully",
|
||||
fields{Bank: bank},
|
||||
args{[]commands.ICommand{&simpleCommand{false}}},
|
||||
false,
|
||||
0.00,
|
||||
},
|
||||
{
|
||||
"fail to run some commands",
|
||||
fields{Bank: bank},
|
||||
args{[]commands.ICommand{&simpleCommand{true}}},
|
||||
true,
|
||||
0.00,
|
||||
},
|
||||
{
|
||||
"run real commands",
|
||||
fields{Bank: bank},
|
||||
args{[]commands.ICommand{
|
||||
&commands.OpenAccountCommand{Username: "Tom"},
|
||||
&commands.DepositCommand{
|
||||
Account: "Tom",
|
||||
Money: 520.30,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.WithdrawCommand{
|
||||
Account: "Tom",
|
||||
Money: 340.00,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.TotalBalanceCommand{},
|
||||
},},
|
||||
false,
|
||||
180.30,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := &APP{
|
||||
Bank: tt.fields.Bank,
|
||||
}
|
||||
err := app.Run(tt.args.commands)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Run() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
assert.Equal(t, tt.totalBalanceInFloat, bank.TotalBalance())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPP_Load(t *testing.T) {
|
||||
bank := domains.InitBank()
|
||||
|
||||
type fields struct {
|
||||
Bank *domains.Bank
|
||||
}
|
||||
type args struct {
|
||||
filePath string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
want []commands.ICommand
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"build commands from file input",
|
||||
fields{Bank: bank},
|
||||
args{filePath: "test_input.txt"},
|
||||
[]commands.ICommand{
|
||||
&commands.OpenAccountCommand{Username: "Tom"},
|
||||
&commands.DepositCommand{
|
||||
Account: "Tom",
|
||||
Money: 520.30,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.WithdrawCommand{
|
||||
Account: "Tom",
|
||||
Money: 340.00,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.TotalBalanceCommand{},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"build commands from file input for multiple users",
|
||||
fields{Bank: bank},
|
||||
args{filePath: "test_input_multiple_users.txt"},
|
||||
[]commands.ICommand{
|
||||
&commands.OpenAccountCommand{Username: "Tom"},
|
||||
&commands.DepositCommand{
|
||||
Account: "Tom",
|
||||
Money: 520.30,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.WithdrawCommand{
|
||||
Account: "Tom",
|
||||
Money: 340.00,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Tom"},
|
||||
&commands.OpenAccountCommand{Username: "Lily"},
|
||||
&commands.DepositCommand{
|
||||
Account: "Lily",
|
||||
Money: 1980.55,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Lily"},
|
||||
&commands.WithdrawCommand{
|
||||
Account: "Lily",
|
||||
Money: 550.33,
|
||||
},
|
||||
&commands.UserBalanceCommand{Account: "Lily"},
|
||||
&commands.TotalBalanceCommand{},
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"fail to load inputs",
|
||||
fields{Bank: bank},
|
||||
args{filePath: "doesnt_exist.txt"},
|
||||
[]commands.ICommand{},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
app := &APP{
|
||||
Bank: tt.fields.Bank,
|
||||
}
|
||||
got, err := app.Load(tt.args.filePath)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("Load() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
OPEN_ACCOUNT Tom
|
||||
DEPOSIT Tom 520.30
|
||||
BALANCE Tom
|
||||
WITHDRAW Tom 340.00
|
||||
BALANCE Tom
|
||||
TOTAL_BALANCE
|
||||
@@ -0,0 +1,11 @@
|
||||
OPEN_ACCOUNT Tom
|
||||
DEPOSIT Tom 520.30
|
||||
BALANCE Tom
|
||||
WITHDRAW Tom 340.00
|
||||
BALANCE Tom
|
||||
OPEN_ACCOUNT Lily
|
||||
DEPOSIT Lily 1980.55
|
||||
BALANCE Lily
|
||||
WITHDRAW Lily 550.33
|
||||
BALANCE Lily
|
||||
TOTAL_BALANCE
|
||||
Reference in New Issue
Block a user