Skip to content

Commit ee96cb8

Browse files
committed
feat: tests
1 parent 791a8fb commit ee96cb8

6 files changed

Lines changed: 708 additions & 6 deletions

File tree

generic/gorm/gorm.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type (
2424
Error() error
2525
}
2626

27-
// Executer implements the [generic.Executer] interface for a sqlx db
27+
// Executer implements the [generic.Executer] interface for a [GormlikeDB]
2828
Executer[T GormlikeDB[Remote], Remote any] struct {
2929
db T
3030
txOpts *sql.TxOptions
@@ -82,7 +82,7 @@ func (executer Executer[T, Remote]) Execute(_ context.Context, run func(Remote)
8282

8383
db = db.Commit()
8484
if db.Error() != nil {
85-
return errors.Wrap(err, "committing gorm tx")
85+
return errors.Wrap(db.Error(), "committing gorm tx")
8686
}
8787

8888
return nil

generic/gorm/gorm_test.go

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package gorm_test
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"testing"
8+
9+
atomicgorm "github.com/beeemT/go-atomic/generic/gorm"
10+
)
11+
12+
// testRemote is a minimal remote type used in tests.
13+
type testRemote struct{}
14+
15+
// mockGormState is shared mutable state across mockGormDB instances produced by
16+
// Begin/Commit/Rollback, allowing assertions on which operations were called.
17+
type mockGormState struct {
18+
beginErr error
19+
rollbackErr error
20+
commitErr error
21+
22+
beginCalled bool
23+
rollbackCalled bool
24+
commitCalled bool
25+
}
26+
27+
// mockGormDB implements atomicgorm.GormlikeDB[testRemote].
28+
type mockGormDB struct {
29+
state *mockGormState
30+
// err is the error reported by Error() on this specific instance, set by the
31+
// operation that produced it (Begin, Commit, Rollback).
32+
err error
33+
}
34+
35+
func (m *mockGormDB) Begin(opts ...*sql.TxOptions) atomicgorm.GormlikeDB[testRemote] {
36+
m.state.beginCalled = true
37+
return &mockGormDB{state: m.state, err: m.state.beginErr}
38+
}
39+
40+
func (m *mockGormDB) Rollback() atomicgorm.GormlikeDB[testRemote] {
41+
m.state.rollbackCalled = true
42+
return &mockGormDB{state: m.state, err: m.state.rollbackErr}
43+
}
44+
45+
func (m *mockGormDB) Commit() atomicgorm.GormlikeDB[testRemote] {
46+
m.state.commitCalled = true
47+
return &mockGormDB{state: m.state, err: m.state.commitErr}
48+
}
49+
50+
func (m *mockGormDB) Remote() testRemote { return testRemote{} }
51+
func (m *mockGormDB) Error() error { return m.err }
52+
53+
// newMockDB constructs a root mockGormDB with the given per-operation errors.
54+
func newMockDB(beginErr, rollbackErr, commitErr error) *mockGormDB {
55+
return &mockGormDB{
56+
state: &mockGormState{
57+
beginErr: beginErr,
58+
rollbackErr: rollbackErr,
59+
commitErr: commitErr,
60+
},
61+
}
62+
}
63+
64+
// TestGormExecuterSuccess verifies the happy path: Begin → run → Commit, no errors.
65+
func TestGormExecuterSuccess(t *testing.T) {
66+
db := newMockDB(nil, nil, nil)
67+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](db)
68+
69+
runCalled := false
70+
err := executer.Execute(context.Background(), func(_ testRemote) error {
71+
runCalled = true
72+
return nil
73+
})
74+
if err != nil {
75+
t.Fatalf("unexpected error: %v", err)
76+
}
77+
if !runCalled {
78+
t.Error("run must be called on success path")
79+
}
80+
if !db.state.beginCalled {
81+
t.Error("Begin must be called")
82+
}
83+
if !db.state.commitCalled {
84+
t.Error("Commit must be called on success")
85+
}
86+
if db.state.rollbackCalled {
87+
t.Error("Rollback must not be called on success")
88+
}
89+
}
90+
91+
// TestGormExecuterBeginError verifies that a Begin failure is returned before run is called.
92+
func TestGormExecuterBeginError(t *testing.T) {
93+
sentinel := errors.New("begin error")
94+
db := newMockDB(sentinel, nil, nil)
95+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](db)
96+
97+
runCalled := false
98+
err := executer.Execute(context.Background(), func(_ testRemote) error {
99+
runCalled = true
100+
return nil
101+
})
102+
103+
if err == nil {
104+
t.Fatal("expected error, got nil")
105+
}
106+
if !errors.Is(err, sentinel) {
107+
t.Errorf("expected begin sentinel in error chain: %v", err)
108+
}
109+
if runCalled {
110+
t.Error("run must not be called when Begin fails")
111+
}
112+
if db.state.commitCalled || db.state.rollbackCalled {
113+
t.Error("Commit and Rollback must not be called when Begin fails")
114+
}
115+
}
116+
117+
// TestGormExecuterRunError verifies that a run failure triggers Rollback.
118+
func TestGormExecuterRunError(t *testing.T) {
119+
sentinel := errors.New("run error")
120+
db := newMockDB(nil, nil, nil)
121+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](db)
122+
123+
err := executer.Execute(context.Background(), func(_ testRemote) error {
124+
return sentinel
125+
})
126+
127+
if err == nil {
128+
t.Fatal("expected error, got nil")
129+
}
130+
if !errors.Is(err, sentinel) {
131+
t.Errorf("expected run sentinel in error chain: %v", err)
132+
}
133+
if !db.state.rollbackCalled {
134+
t.Error("Rollback must be called when run fails")
135+
}
136+
if db.state.commitCalled {
137+
t.Error("Commit must not be called when run fails")
138+
}
139+
}
140+
141+
// TestGormExecuterRunErrorRollbackError verifies that both run and rollback errors appear
142+
// in the combined error.
143+
func TestGormExecuterRunErrorRollbackError(t *testing.T) {
144+
runSentinel := errors.New("run error")
145+
rollbackSentinel := errors.New("rollback error")
146+
db := newMockDB(nil, rollbackSentinel, nil)
147+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](db)
148+
149+
err := executer.Execute(context.Background(), func(_ testRemote) error {
150+
return runSentinel
151+
})
152+
153+
if err == nil {
154+
t.Fatal("expected error, got nil")
155+
}
156+
if !errors.Is(err, runSentinel) {
157+
t.Errorf("expected run sentinel in error chain: %v", err)
158+
}
159+
if !errors.Is(err, rollbackSentinel) {
160+
t.Errorf("expected rollback sentinel in error chain: %v", err)
161+
}
162+
}
163+
164+
// TestGormExecuterCommitError verifies that a Commit failure is surfaced as an error.
165+
// This covers the bug where errors.Wrap(err, ...) was used instead of errors.Wrap(db.Error(), ...)
166+
// causing the commit error to be silently discarded.
167+
func TestGormExecuterCommitError(t *testing.T) {
168+
sentinel := errors.New("commit error")
169+
db := newMockDB(nil, nil, sentinel)
170+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](db)
171+
172+
err := executer.Execute(context.Background(), func(_ testRemote) error {
173+
return nil
174+
})
175+
176+
if err == nil {
177+
t.Fatal("expected error on commit failure, got nil")
178+
}
179+
if !errors.Is(err, sentinel) {
180+
t.Errorf("expected commit sentinel in error chain: %v", err)
181+
}
182+
}
183+
184+
// TestGormExecuterWithTxOptions verifies that WithTxOptions is accepted without error.
185+
func TestGormExecuterWithTxOptions(t *testing.T) {
186+
db := newMockDB(nil, nil, nil)
187+
txOpts := &sql.TxOptions{Isolation: sql.LevelSerializable, ReadOnly: true}
188+
executer := atomicgorm.NewExecuter[*mockGormDB, testRemote](
189+
db,
190+
atomicgorm.WithTxOptions[*mockGormDB, testRemote](txOpts),
191+
)
192+
193+
err := executer.Execute(context.Background(), func(_ testRemote) error {
194+
return nil
195+
})
196+
if err != nil {
197+
t.Fatalf("unexpected error: %v", err)
198+
}
199+
}

generic/transacter.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
)
1414

1515
type (
16-
// Transacter implements the Transacter interface for sqlx compatible databases.
17-
// It flattens statements on nested uses of the Transact method into one sqlx transaction.
16+
// Transacter implements the Transacter interface for remote executors implementing [Executer].
17+
// It flattens statements on nested uses of the Transact method into one transaction.
1818
Transacter[Remote any, Resources any] struct {
1919
executer Executer[Remote]
2020

@@ -89,7 +89,7 @@ func NewTransacter[Remote any, Resources any](
8989
return transacter
9090
}
9191

92-
// Transact will run run in a sqlx Session.
92+
// Transact will run run in a Session.
9393
// If a session is present in ctx at [atomic.SessionContextKey] it will use the existing session,
9494
// else it will create a new session and insert it into the context.
9595
// It does not support nested transactions, rather all statements are flattened into a single

0 commit comments

Comments
 (0)