test(tx): regression tests for per-request transactions across all specs

This commit is contained in:
2026-09-30 23:19:58 +02:00
parent 2042205817
commit 7f84debdc5
9 changed files with 712 additions and 1 deletions
+99
View File
@@ -11,12 +11,14 @@ import (
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect/pgdialect"
"github.com/bitechdev/ResolveSpec/pkg/cache"
"github.com/bitechdev/ResolveSpec/pkg/common"
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
)
func TestAfterReadRunsInSecondTransaction(t *testing.T) {
resetTotalCache(t)
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
@@ -60,3 +62,100 @@ func TestAfterReadRunsInSecondTransaction(t *testing.T) {
t.Fatal("BeforeRead must run on the first tx and AfterRead on the second")
}
}
// resetTotalCache empties the process-wide query-total cache. Its key ignores the
// record id, so a cached total would skip the count query and desync the mock.
func resetTotalCache(t *testing.T) {
t.Helper()
_ = cache.GetDefaultCache().Clear(context.Background())
t.Cleanup(func() { _ = cache.GetDefaultCache().Clear(context.Background()) })
}
func newBunHarness(t *testing.T) (*Handler, sqlmock.Sqlmock) {
t.Helper()
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatal(err)
}
sqlDB.SetMaxOpenConns(1)
t.Cleanup(func() { _ = sqlDB.Close() })
return NewHandler(database.NewBunAdapter(bun.NewDB(sqlDB, pgdialect.New())), modelregistry.NewModelRegistry()), mock
}
func itemCtx(t *testing.T) context.Context {
t.Helper()
base, cancel := context.WithTimeout(context.Background(), 2*time.Second)
t.Cleanup(cancel)
ctx := WithSchema(base, "public")
ctx = WithEntity(ctx, "items")
ctx = WithTableName(ctx, "items")
return WithModel(ctx, delItem{})
}
func TestAfterReadErrorFailsRequestOnSecondTransaction(t *testing.T) {
resetTotalCache(t)
h, mock := newBunHarness(t)
h.Hooks().Register(AfterRead, func(*HookContext) error { return http.ErrAbortHandler })
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "a"))
mock.ExpectCommit()
mock.ExpectBegin()
mock.ExpectRollback()
rec := httptest.NewRecorder()
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodGet, "/", nil))
h.handleRead(itemCtx(t), w, "7", ExtendedRequestOptions{})
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status %d body %s", rec.Code, rec.Body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestBeforeReadErrorRollsBackWithoutQueries(t *testing.T) {
h, mock := newBunHarness(t)
h.Hooks().Register(BeforeRead, func(*HookContext) error { return http.ErrAbortHandler })
mock.ExpectBegin()
mock.ExpectRollback()
rec := httptest.NewRecorder()
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodGet, "/", nil))
h.handleRead(itemCtx(t), w, "7", ExtendedRequestOptions{})
if rec.Code == http.StatusOK {
t.Fatalf("a failing BeforeRead must not return data: %s", rec.Body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}
func TestAfterUpdateErrorFailsRequest(t *testing.T) {
h, mock := newBunHarness(t)
h.Hooks().Register(AfterUpdate, func(*HookContext) error { return http.ErrAbortHandler })
cols := []string{"id", "name"}
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows(cols).AddRow(7, "a"))
mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectCommit()
mock.ExpectBegin()
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows(cols).AddRow(7, "b"))
mock.ExpectRollback()
rec := httptest.NewRecorder()
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodPut, "/", nil))
h.handleUpdate(itemCtx(t), w, "7", nil, map[string]interface{}{"name": "b"}, ExtendedRequestOptions{})
if rec.Code != http.StatusInternalServerError {
t.Fatalf("status %d body %s", rec.Code, rec.Body)
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatal(err)
}
}