246 lines
7.4 KiB
Go
246 lines
7.4 KiB
Go
package processor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"apskel-pos-be/internal/constants"
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/models"
|
|
"apskel-pos-be/internal/repository"
|
|
)
|
|
|
|
// snapshot saves everything the fake wallet holds and returns what puts it back, so
|
|
// a test transaction can roll back like a real one.
|
|
func (f *walletRepoFake) snapshot() func() {
|
|
wallets := make(map[uuid.UUID]*entities.CustomerWallet, len(f.wallets))
|
|
for id, w := range f.wallets {
|
|
c := *w
|
|
wallets[id] = &c
|
|
}
|
|
transactions := make([]*entities.WalletTransaction, len(f.transactions))
|
|
for i, t := range f.transactions {
|
|
c := *t
|
|
transactions[i] = &c
|
|
}
|
|
lots := make([]*entities.WalletLot, len(f.lots))
|
|
for i, l := range f.lots {
|
|
c := *l
|
|
lots[i] = &c
|
|
}
|
|
allocations := append([]entities.WalletLotAllocation(nil), f.allocations...)
|
|
return func() {
|
|
f.wallets, f.transactions, f.lots, f.allocations = wallets, transactions, lots, allocations
|
|
}
|
|
}
|
|
|
|
// rollbackTx runs fn in place and, when it fails, puts back what each save saved.
|
|
type rollbackTx struct{ saves []func() func() }
|
|
|
|
func (r rollbackTx) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
|
restores := make([]func(), 0, len(r.saves))
|
|
for _, save := range r.saves {
|
|
restores = append(restores, save())
|
|
}
|
|
err := fn(ctx)
|
|
if err != nil {
|
|
for _, restore := range restores {
|
|
restore()
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
type gameFake struct{ games map[uuid.UUID]*entities.Game }
|
|
|
|
func (f gameFake) GetByID(_ context.Context, id uuid.UUID) (*entities.Game, error) {
|
|
g, ok := f.games[id]
|
|
if !ok {
|
|
return nil, errors.New("record not found")
|
|
}
|
|
return g, nil
|
|
}
|
|
|
|
type prizeStockFake struct {
|
|
prizes []entities.GamePrize
|
|
stockErr error
|
|
}
|
|
|
|
func (f *prizeStockFake) GetAvailablePrizes(_ context.Context, gameID uuid.UUID) ([]entities.GamePrize, error) {
|
|
var out []entities.GamePrize
|
|
for _, p := range f.prizes {
|
|
if p.GameID == gameID && p.Stock > 0 {
|
|
out = append(out, p)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *prizeStockFake) DecreaseStock(_ context.Context, id uuid.UUID, amount int) error {
|
|
if f.stockErr != nil {
|
|
return f.stockErr
|
|
}
|
|
for i := range f.prizes {
|
|
if f.prizes[i].ID == id {
|
|
if f.prizes[i].Stock < amount {
|
|
return repository.ErrGamePrizeOutOfStock
|
|
}
|
|
f.prizes[i].Stock -= amount
|
|
return nil
|
|
}
|
|
}
|
|
return repository.ErrGamePrizeOutOfStock
|
|
}
|
|
|
|
func (f *prizeStockFake) snapshot() func() {
|
|
prizes := append([]entities.GamePrize(nil), f.prizes...)
|
|
return func() { f.prizes = prizes }
|
|
}
|
|
|
|
// gamePlayRepoFake records plays; the other methods of the interface are not used.
|
|
type gamePlayRepoFake struct {
|
|
repository.GamePlayRepository
|
|
plays []entities.GamePlay
|
|
}
|
|
|
|
func (f *gamePlayRepoFake) Create(_ context.Context, gp *entities.GamePlay) error {
|
|
f.plays = append(f.plays, *gp)
|
|
return nil
|
|
}
|
|
|
|
func (f *gamePlayRepoFake) snapshot() func() {
|
|
plays := append([]entities.GamePlay(nil), f.plays...)
|
|
return func() { f.plays = plays }
|
|
}
|
|
|
|
type gameTestEnv struct {
|
|
*walletMoveEnv
|
|
game *entities.Game
|
|
prize entities.GamePrize
|
|
stock *prizeStockFake
|
|
plays *gamePlayRepoFake
|
|
player uuid.UUID
|
|
p *GamePlayProcessor
|
|
}
|
|
|
|
func newGameTestEnv(t *testing.T, metadata entities.Metadata) *gameTestEnv {
|
|
e := &gameTestEnv{walletMoveEnv: newWalletMoveEnv(t)}
|
|
e.game = &entities.Game{ID: uuid.New(), Name: "Spin Wheel", Type: entities.GameTypeSpin, IsActive: true, Metadata: metadata}
|
|
e.prize = entities.GamePrize{ID: uuid.New(), GameID: e.game.ID, Name: "Voucher 10rb", Weight: 1, Stock: 5}
|
|
e.stock = &prizeStockFake{prizes: []entities.GamePrize{e.prize}}
|
|
e.plays = &gamePlayRepoFake{}
|
|
e.player = e.member("Budi", "081234561234")
|
|
tx := rollbackTx{saves: []func() func(){e.repo.snapshot, e.stock.snapshot, e.plays.snapshot}}
|
|
e.p = NewGamePlayProcessor(e.plays, gameFake{games: map[uuid.UUID]*entities.Game{e.game.ID: e.game}}, e.stock, e.walletTestEnv.p, e, tx)
|
|
return e
|
|
}
|
|
|
|
func (e *gameTestEnv) play() (*models.PlayGameResponse, error) {
|
|
return e.p.PlayGame(e.ctx, &models.PlayGameRequest{GameID: e.game.ID, CustomerID: e.player})
|
|
}
|
|
|
|
func TestPlayGame_SpendsTheGamesCoinCost(t *testing.T) {
|
|
// coin_cost comes back from JSONB as a float64.
|
|
e := newGameTestEnv(t, entities.Metadata{"coin_cost": float64(3)})
|
|
e.earnCoins(t, e.player, 10, nil)
|
|
|
|
res, err := e.play()
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, int64(7), res.CoinsRemaining)
|
|
require.NotNil(t, res.PrizeWon)
|
|
assert.Equal(t, e.prize.ID, res.PrizeWon.ID)
|
|
require.Len(t, e.plays.plays, 1)
|
|
play := e.plays.plays[0]
|
|
assert.Equal(t, 3, play.CoinsUsed)
|
|
assert.Equal(t, e.prize.ID, *play.PrizeID)
|
|
assert.Equal(t, 4, e.stock.prizes[0].Stock)
|
|
|
|
spend := e.repo.transactions[len(e.repo.transactions)-1]
|
|
assert.Equal(t, constants.WalletTxTypeGameSpend, spend.Type)
|
|
assert.Equal(t, constants.WalletCurrencyCoin, spend.Currency)
|
|
assert.Equal(t, int64(-3), spend.Amount)
|
|
assert.Equal(t, constants.WalletRefTypeGamePlay, spend.ReferenceType)
|
|
assert.Equal(t, play.ID, spend.ReferenceID, "GAME_SPEND points at the game play")
|
|
assert.Equal(t, "Main Spin Wheel: dapat Voucher 10rb", spend.Description)
|
|
}
|
|
|
|
func TestPlayGame_CostsOneCoinByDefault(t *testing.T) {
|
|
e := newGameTestEnv(t, entities.Metadata{})
|
|
e.earnCoins(t, e.player, 2, nil)
|
|
|
|
res, err := e.play()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int64(1), res.CoinsRemaining)
|
|
assert.Equal(t, 1, e.plays.plays[0].CoinsUsed)
|
|
}
|
|
|
|
func TestPlayGame_NotEnoughCoinsRecordsNothing(t *testing.T) {
|
|
e := newGameTestEnv(t, entities.Metadata{"coin_cost": float64(3)})
|
|
e.earnCoins(t, e.player, 2, nil)
|
|
|
|
_, err := e.play()
|
|
assert.ErrorIs(t, err, ErrGamePlayRefused)
|
|
assert.Empty(t, e.plays.plays)
|
|
assert.Equal(t, 5, e.stock.prizes[0].Stock)
|
|
assert.Equal(t, int64(2), e.coinBalance(t, e.player))
|
|
}
|
|
|
|
func TestPlayGame_PrizeStockFailureCancelsThePlay(t *testing.T) {
|
|
e := newGameTestEnv(t, entities.Metadata{})
|
|
e.earnCoins(t, e.player, 5, nil)
|
|
rows := len(e.repo.transactions)
|
|
|
|
// Another play took the last one between reading the prizes and taking it.
|
|
e.stock.stockErr = repository.ErrGamePrizeOutOfStock
|
|
_, err := e.play()
|
|
assert.ErrorIs(t, err, ErrGamePlayRefused)
|
|
|
|
// Anything else going wrong with the stock cancels it too.
|
|
e.stock.stockErr = errors.New("connection reset")
|
|
_, err = e.play()
|
|
assert.Error(t, err)
|
|
|
|
assert.Empty(t, e.plays.plays)
|
|
assert.Len(t, e.repo.transactions, rows, "no GAME_SPEND is left behind")
|
|
assert.Equal(t, int64(5), e.coinBalance(t, e.player))
|
|
}
|
|
|
|
func TestPlayGame_RefusesWhatCannotBePlayed(t *testing.T) {
|
|
for name, metadata := range map[string]entities.Metadata{
|
|
"zero": {"coin_cost": float64(0)},
|
|
"fraction": {"coin_cost": 1.5},
|
|
"not number": {"coin_cost": "2"},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
e := newGameTestEnv(t, metadata)
|
|
e.earnCoins(t, e.player, 5, nil)
|
|
_, err := e.play()
|
|
assert.ErrorIs(t, err, ErrGamePlayRefused)
|
|
assert.Equal(t, int64(5), e.coinBalance(t, e.player))
|
|
})
|
|
}
|
|
|
|
t.Run("inactive", func(t *testing.T) {
|
|
e := newGameTestEnv(t, entities.Metadata{})
|
|
e.earnCoins(t, e.player, 5, nil)
|
|
e.game.IsActive = false
|
|
_, err := e.play()
|
|
assert.ErrorIs(t, err, ErrGamePlayRefused)
|
|
})
|
|
|
|
t.Run("no prizes left", func(t *testing.T) {
|
|
e := newGameTestEnv(t, entities.Metadata{})
|
|
e.earnCoins(t, e.player, 5, nil)
|
|
e.stock.prizes[0].Stock = 0
|
|
_, err := e.play()
|
|
assert.ErrorIs(t, err, ErrGamePlayRefused)
|
|
assert.Equal(t, int64(5), e.coinBalance(t, e.player))
|
|
})
|
|
}
|