Skip to content

Commit 570c113

Browse files
committed
Add redis tests with miniredis and improve coverage
1 parent 2b1473c commit 570c113

37 files changed

+2286
-1828
lines changed

adapters/grpc/server.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
11
package grpc
22

33
// Placeholder for a gRPC transport exposing GamifyService.
4-
5-

adapters/jsonfile/storage_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package jsonfile
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"gamifykit/core"
10+
)
11+
12+
func TestStorePersistAndLoad(t *testing.T) {
13+
dir := t.TempDir()
14+
path := filepath.Join(dir, "state.json")
15+
16+
store, err := New(path)
17+
if err != nil {
18+
t.Fatalf("new store: %v", err)
19+
}
20+
21+
total, err := store.AddPoints(context.Background(), "alice", core.MetricXP, 50)
22+
if err != nil || total != 50 {
23+
t.Fatalf("add points: total=%d err=%v", total, err)
24+
}
25+
26+
if err := store.AwardBadge(context.Background(), "alice", "onboarded"); err != nil {
27+
t.Fatalf("award badge: %v", err)
28+
}
29+
if err := store.SetLevel(context.Background(), "alice", core.MetricXP, 2); err != nil {
30+
t.Fatalf("set level: %v", err)
31+
}
32+
33+
// ensure file written
34+
if _, err := os.Stat(path); err != nil {
35+
t.Fatalf("expected file at %s", path)
36+
}
37+
38+
// reload
39+
reloaded, err := New(path)
40+
if err != nil {
41+
t.Fatalf("reload: %v", err)
42+
}
43+
44+
state, err := reloaded.GetState(context.Background(), "alice")
45+
if err != nil {
46+
t.Fatalf("get state: %v", err)
47+
}
48+
if state.Points[core.MetricXP] != 50 {
49+
t.Fatalf("expected points 50, got %d", state.Points[core.MetricXP])
50+
}
51+
if _, ok := state.Badges[core.Badge("onboarded")]; !ok {
52+
t.Fatalf("expected badge onboarded")
53+
}
54+
if state.Levels[core.MetricXP] != 2 {
55+
t.Fatalf("expected level 2, got %d", state.Levels[core.MetricXP])
56+
}
57+
}

adapters/memory/storage.go

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,74 +1,82 @@
11
package memory
22

33
import (
4-
"context"
5-
"sync"
6-
"time"
4+
"context"
5+
"sync"
6+
"time"
77

8-
"gamifykit/core"
8+
"gamifykit/core"
99
)
1010

1111
// Store is a concurrent in-memory Storage implementation.
1212
type Store struct {
13-
users sync.Map // map[core.UserID]*userRecord
13+
users sync.Map // map[core.UserID]*userRecord
1414
}
1515

1616
type userRecord struct {
17-
mu sync.Mutex
18-
state core.UserState
17+
mu sync.Mutex
18+
state core.UserState
1919
}
2020

2121
func New() *Store { return &Store{} }
2222

2323
func (s *Store) getOrCreate(user core.UserID) *userRecord {
24-
if v, ok := s.users.Load(user); ok {
25-
return v.(*userRecord)
26-
}
27-
rec := &userRecord{state: core.UserState{
28-
UserID: user,
29-
Points: map[core.Metric]int64{},
30-
Badges: map[core.Badge]struct{}{},
31-
Levels: map[core.Metric]int64{},
32-
Updated: time.Now().UTC(),
33-
}}
34-
actual, _ := s.users.LoadOrStore(user, rec)
35-
return actual.(*userRecord)
24+
if v, ok := s.users.Load(user); ok {
25+
return v.(*userRecord)
26+
}
27+
rec := &userRecord{state: core.UserState{
28+
UserID: user,
29+
Points: map[core.Metric]int64{},
30+
Badges: map[core.Badge]struct{}{},
31+
Levels: map[core.Metric]int64{},
32+
Updated: time.Now().UTC(),
33+
}}
34+
actual, _ := s.users.LoadOrStore(user, rec)
35+
return actual.(*userRecord)
3636
}
3737

3838
func (s *Store) AddPoints(_ context.Context, user core.UserID, metric core.Metric, delta int64) (int64, error) {
39-
rec := s.getOrCreate(user)
40-
rec.mu.Lock()
41-
defer rec.mu.Unlock()
42-
current := rec.state.Points[metric]
43-
next, err := core.AddSafe(current, delta)
44-
if err != nil { return 0, err }
45-
rec.state.Points[metric] = next
46-
rec.state.Updated = time.Now().UTC()
47-
return next, nil
39+
rec := s.getOrCreate(user)
40+
rec.mu.Lock()
41+
defer rec.mu.Unlock()
42+
current := rec.state.Points[metric]
43+
next, err := core.AddSafe(current, delta)
44+
if err != nil {
45+
return 0, err
46+
}
47+
rec.state.Points[metric] = next
48+
rec.state.Updated = time.Now().UTC()
49+
return next, nil
4850
}
4951

5052
func (s *Store) AwardBadge(_ context.Context, user core.UserID, badge core.Badge) error {
51-
rec := s.getOrCreate(user)
52-
rec.mu.Lock(); defer rec.mu.Unlock()
53-
rec.state.Badges[badge] = struct{}{}
54-
rec.state.Updated = time.Now().UTC()
55-
return nil
53+
rec := s.getOrCreate(user)
54+
rec.mu.Lock()
55+
defer rec.mu.Unlock()
56+
rec.state.Badges[badge] = struct{}{}
57+
rec.state.Updated = time.Now().UTC()
58+
return nil
5659
}
5760

5861
func (s *Store) GetState(_ context.Context, user core.UserID) (core.UserState, error) {
59-
rec := s.getOrCreate(user)
60-
rec.mu.Lock(); defer rec.mu.Unlock()
61-
return rec.state.Clone(), nil
62+
rec := s.getOrCreate(user)
63+
rec.mu.Lock()
64+
defer rec.mu.Unlock()
65+
return rec.state.Clone(), nil
6266
}
6367

6468
func (s *Store) SetLevel(_ context.Context, user core.UserID, metric core.Metric, level int64) error {
65-
rec := s.getOrCreate(user)
66-
rec.mu.Lock(); defer rec.mu.Unlock()
67-
rec.state.Levels[metric] = level
68-
rec.state.Updated = time.Now().UTC()
69-
return nil
69+
rec := s.getOrCreate(user)
70+
rec.mu.Lock()
71+
defer rec.mu.Unlock()
72+
rec.state.Levels[metric] = level
73+
rec.state.Updated = time.Now().UTC()
74+
return nil
7075
}
7176

72-
var _ interface{ AddPoints(context.Context, core.UserID, core.Metric, int64) (int64, error); AwardBadge(context.Context, core.UserID, core.Badge) error; GetState(context.Context, core.UserID) (core.UserState, error); SetLevel(context.Context, core.UserID, core.Metric, int64) error } = (*Store)(nil)
73-
74-
77+
var _ interface {
78+
AddPoints(context.Context, core.UserID, core.Metric, int64) (int64, error)
79+
AwardBadge(context.Context, core.UserID, core.Badge) error
80+
GetState(context.Context, core.UserID) (core.UserState, error)
81+
SetLevel(context.Context, core.UserID, core.Metric, int64) error
82+
} = (*Store)(nil)

adapters/memory/storage_test.go

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
package memory
22

33
import (
4-
"context"
5-
"testing"
6-
"gamifykit/core"
4+
"context"
5+
"gamifykit/core"
6+
"testing"
77
)
88

99
func TestMemoryStore(t *testing.T) {
10-
s := New()
11-
total, err := s.AddPoints(context.Background(), core.UserID("u"), core.MetricXP, 5)
12-
if err != nil || total != 5 { t.Fatalf("got %v %v", total, err) }
13-
if err := s.AwardBadge(context.Background(), core.UserID("u"), core.Badge("starter")); err != nil { t.Fatal(err) }
14-
st, _ := s.GetState(context.Background(), core.UserID("u"))
15-
if _, ok := st.Badges[core.Badge("starter")]; !ok { t.Fatal("badge missing") }
10+
s := New()
11+
total, err := s.AddPoints(context.Background(), core.UserID("u"), core.MetricXP, 5)
12+
if err != nil || total != 5 {
13+
t.Fatalf("got %v %v", total, err)
14+
}
15+
if err := s.AwardBadge(context.Background(), core.UserID("u"), core.Badge("starter")); err != nil {
16+
t.Fatal(err)
17+
}
18+
st, _ := s.GetState(context.Background(), core.UserID("u"))
19+
if _, ok := st.Badges[core.Badge("starter")]; !ok {
20+
t.Fatal("badge missing")
21+
}
1622
}
17-
18-

adapters/redis/storage_test.go

Lines changed: 26 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,42 @@ package redis
22

33
import (
44
"context"
5-
"os"
65
"testing"
76
"time"
87

9-
"gamifykit/core"
10-
8+
miniredis "github.com/alicebob/miniredis/v2"
119
"github.com/redis/go-redis/v9"
1210
"github.com/stretchr/testify/assert"
1311
"github.com/stretchr/testify/require"
14-
)
15-
16-
// TestMain sets up the test environment
17-
func TestMain(m *testing.M) {
18-
// Skip Redis tests if Redis is not available
19-
if os.Getenv("SKIP_REDIS_TESTS") == "true" {
20-
os.Exit(0)
21-
}
22-
23-
os.Exit(m.Run())
24-
}
25-
26-
// redisClient creates a Redis client for testing
27-
func redisClient() *redis.Client {
28-
return redis.NewClient(&redis.Options{
29-
Addr: "localhost:6379",
30-
Password: "",
31-
DB: 15, // Use a separate DB for tests
32-
})
33-
}
34-
35-
// skipIfNoRedis skips the test if Redis is not available
36-
func skipIfNoRedis(t *testing.T) *redis.Client {
37-
client := redisClient()
3812

39-
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
40-
defer cancel()
13+
"gamifykit/core"
14+
)
4115

42-
if err := client.Ping(ctx).Err(); err != nil {
43-
t.Skip("Redis not available, skipping test:", err)
44-
return nil
16+
// newTestClient spins up a miniredis server and returns a client plus cleanup.
17+
func newTestClient(t *testing.T) (*redis.Client, func()) {
18+
t.Helper()
19+
mr := miniredis.RunT(t)
20+
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
21+
cleanup := func() {
22+
_ = client.Close()
23+
mr.Close()
4524
}
46-
47-
return client
25+
return client, cleanup
4826
}
4927

50-
// cleanupTestData removes test data from Redis
5128
func cleanupTestData(t *testing.T, client *redis.Client, userID core.UserID) {
29+
t.Helper()
5230
ctx := context.Background()
5331
pattern := "user:" + string(userID) + ":*"
5432
keys, err := client.Keys(ctx, pattern).Result()
5533
if err == nil && len(keys) > 0 {
56-
client.Del(ctx, keys...)
34+
_, _ = client.Del(ctx, keys...).Result()
5735
}
5836
}
5937

6038
func TestStore_AddPoints(t *testing.T) {
61-
client := skipIfNoRedis(t)
62-
if client == nil {
63-
return
64-
}
65-
defer client.Close()
39+
client, cleanup := newTestClient(t)
40+
defer cleanup()
6641

6742
store := NewWithClient(client)
6843
ctx := context.Background()
@@ -103,11 +78,8 @@ func TestStore_AddPoints_ZeroDelta(t *testing.T) {
10378
}
10479

10580
func TestStore_AwardBadge(t *testing.T) {
106-
client := skipIfNoRedis(t)
107-
if client == nil {
108-
return
109-
}
110-
defer client.Close()
81+
client, cleanup := newTestClient(t)
82+
defer cleanup()
11183

11284
store := NewWithClient(client)
11385
ctx := context.Background()
@@ -138,11 +110,8 @@ func TestStore_AwardBadge(t *testing.T) {
138110
}
139111

140112
func TestStore_GetState(t *testing.T) {
141-
client := skipIfNoRedis(t)
142-
if client == nil {
143-
return
144-
}
145-
defer client.Close()
113+
client, cleanup := newTestClient(t)
114+
defer cleanup()
146115

147116
store := NewWithClient(client)
148117
ctx := context.Background()
@@ -177,11 +146,8 @@ func TestStore_GetState(t *testing.T) {
177146
}
178147

179148
func TestStore_GetState_Cache(t *testing.T) {
180-
client := skipIfNoRedis(t)
181-
if client == nil {
182-
return
183-
}
184-
defer client.Close()
149+
client, cleanup := newTestClient(t)
150+
defer cleanup()
185151

186152
store := NewWithClient(client)
187153
ctx := context.Background()
@@ -227,11 +193,8 @@ func TestStore_GetState_Cache(t *testing.T) {
227193
}
228194

229195
func TestStore_SetLevel(t *testing.T) {
230-
client := skipIfNoRedis(t)
231-
if client == nil {
232-
return
233-
}
234-
defer client.Close()
196+
client, cleanup := newTestClient(t)
197+
defer cleanup()
235198

236199
store := NewWithClient(client)
237200
ctx := context.Background()
@@ -261,11 +224,8 @@ func TestStore_SetLevel(t *testing.T) {
261224
}
262225

263226
func TestStore_EmptyUser(t *testing.T) {
264-
client := skipIfNoRedis(t)
265-
if client == nil {
266-
return
267-
}
268-
defer client.Close()
227+
client, cleanup := newTestClient(t)
228+
defer cleanup()
269229

270230
store := NewWithClient(client)
271231
ctx := context.Background()

0 commit comments

Comments
 (0)