Files
apskel-pos-backend/internal/repository/game_reward_counter_repository_test.go
T

111 lines
3.9 KiB
Go
Raw Normal View History

package repository
import (
"context"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"apskel-pos-be/internal/constants"
)
// The Economy Guard counters against Postgres (docs/rfc-enakgame.md §9). Needs
// TEST_DATABASE_URL; see wallet_repository_test.go.
func TestGameRewardCounterRepository_ConcurrentConsumeNeverPassesTheLimit(t *testing.T) {
db := walletTestDB(t)
org, game := uuid.New(), uuid.New()
t.Cleanup(func() { db.Exec(`DELETE FROM game_reward_counters WHERE organization_id = ?`, org) })
repo := NewGameRewardCounterRepository(db)
txm := NewTxManager(db)
day := time.Date(2026, 10, 7, 0, 0, 0, 0, time.UTC)
for _, round := range []struct {
each int64
wantTotal int64
}{{10, 100}, {7, 100}} {
var granted atomic.Int64
var wg sync.WaitGroup
begin := make(chan struct{})
for i := 0; i < 20; i++ {
wg.Add(1)
go func() {
defer wg.Done()
<-begin
require.NoError(t, txm.WithTransaction(context.Background(), func(ctx context.Context) error {
got, err := repo.Consume(ctx, org, constants.GameRewardScopeGame, game, day, round.each, 100)
granted.Add(got)
return err
}))
}()
}
close(begin)
wg.Wait()
total, err := repo.Get(context.Background(), org, constants.GameRewardScopeGame, game, day)
require.NoError(t, err)
assert.EqualValues(t, round.wantTotal, total)
if round.each == 10 {
assert.EqualValues(t, 100, granted.Load(), "exactly what fits")
} else {
assert.Zero(t, granted.Load(), "nothing is left")
}
}
}
func TestGameRewardCounterRepository(t *testing.T) {
db := walletTestDB(t)
org, customer := uuid.New(), uuid.New()
t.Cleanup(func() { db.Exec(`DELETE FROM game_reward_counters WHERE organization_id = ?`, org) })
repo := NewGameRewardCounterRepository(db)
txm := NewTxManager(db)
ctx := context.Background()
today, tomorrow := time.Date(2026, 10, 7, 23, 0, 0, 0, time.FixedZone("WIB", 7*3600)), time.Date(2026, 10, 8, 0, 0, 0, 0, time.UTC)
consume := func(day time.Time, amount, limit int64) int64 {
t.Helper()
var got int64
require.NoError(t, txm.WithTransaction(ctx, func(ctx context.Context) error {
var err error
got, err = repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, day, amount, limit)
return err
}))
return got
}
assert.EqualValues(t, 30, consume(today, 30, 40))
assert.EqualValues(t, 10, consume(today, 30, 40), "cut to what is left")
assert.EqualValues(t, 0, consume(today, 5, 40))
assert.EqualValues(t, 5, consume(today, 5, 0), "no limit, still counted")
assert.EqualValues(t, 30, consume(tomorrow, 30, 40), "a new day starts over")
got, err := repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today)
require.NoError(t, err)
assert.EqualValues(t, 45, got)
require.NoError(t, txm.WithTransaction(ctx, func(ctx context.Context) error {
return repo.Release(ctx, org, constants.GameRewardScopeUser, customer, today, 15)
}))
got, _ = repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today)
assert.EqualValues(t, 30, got)
assert.Error(t, txm.WithTransaction(ctx, func(ctx context.Context) error {
return repo.Release(ctx, org, constants.GameRewardScopeUser, customer, today, 31)
}), "cannot release more than counted")
// A rolled back reward leaves the counter as it was.
_ = txm.WithTransaction(ctx, func(ctx context.Context) error {
_, err := repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, today, 10, 0)
require.NoError(t, err)
return assert.AnError
})
got, _ = repo.Get(ctx, org, constants.GameRewardScopeUser, customer, today)
assert.EqualValues(t, 30, got)
_, err = repo.Consume(ctx, org, constants.GameRewardScopeUser, customer, today, 1, 0)
assert.ErrorIs(t, err, ErrGameRewardCounterTxRequired)
got, err = repo.Get(ctx, org, constants.GameRewardScopeGlobal, org, today)
require.NoError(t, err)
assert.Zero(t, got)
}