mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-07 05:46:27 +00:00
feat(tx): run websocketspec and mqttspec operations in per-message transactions
Reads and deletes run in one transaction; create and update write in one and re-fetch plus After hooks in a second. OnTxBegin fires first in each.
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package websocketspec
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
||||
)
|
||||
|
||||
type txItem struct {
|
||||
ID int `json:"id" bun:"id,pk"`
|
||||
Name string `json:"name" bun:"name"`
|
||||
}
|
||||
|
||||
func newTxHarness(t *testing.T) (*Handler, sqlmock.Sqlmock, *Connection, *HookContext) {
|
||||
t.Helper()
|
||||
db, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// One connection: any statement that bypasses the transaction while it is
|
||||
// open cannot get a connection and fails on the context timeout.
|
||||
db.SetMaxOpenConns(1)
|
||||
t.Cleanup(func() { _ = db.Close() })
|
||||
h := NewHandler(database.NewPgSQLAdapter(db), modelregistry.NewModelRegistry())
|
||||
conn := NewConnection("c1", nil, h)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
hookCtx := &HookContext{
|
||||
Context: ctx,
|
||||
Handler: h,
|
||||
Schema: "public",
|
||||
Entity: "items",
|
||||
TableName: "public.items",
|
||||
Model: txItem{},
|
||||
ModelPtr: &txItem{},
|
||||
ID: "7",
|
||||
Tx: h.db,
|
||||
Metadata: map[string]interface{}{},
|
||||
}
|
||||
return h, mock, conn, hookCtx
|
||||
}
|
||||
|
||||
func response(t *testing.T, conn *Connection) ResponseMessage {
|
||||
t.Helper()
|
||||
select {
|
||||
case raw := <-conn.send:
|
||||
var resp ResponseMessage
|
||||
if err := json.Unmarshal(raw, &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return resp
|
||||
default:
|
||||
t.Fatal("no response sent")
|
||||
return ResponseMessage{}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRunsHooksAndDeleteInOneTransaction(t *testing.T) {
|
||||
h, mock, conn, hookCtx := newTxHarness(t)
|
||||
var order []string
|
||||
var txs []common.Database
|
||||
h.Hooks().Register(OnTxBegin, func(c *HookContext) error {
|
||||
order = append(order, "begin")
|
||||
txs = append(txs, c.Tx)
|
||||
return nil
|
||||
})
|
||||
h.Hooks().Register(BeforeDelete, func(c *HookContext) error {
|
||||
order = append(order, "before")
|
||||
return nil
|
||||
})
|
||||
h.Hooks().Register(AfterDelete, func(c *HookContext) error {
|
||||
order = append(order, "after")
|
||||
return nil
|
||||
})
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`DELETE FROM`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
|
||||
h.handleDelete(conn, &Message{ID: "m1"}, hookCtx)
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !response(t, conn).Success {
|
||||
t.Fatal("expected success")
|
||||
}
|
||||
if len(txs) != 1 || txs[0] == h.db || len(order) != 3 || order[0] != "begin" {
|
||||
t.Fatalf("OnTxBegin must fire once, first, on the transaction: %v", order)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRefetchAndAfterHookRunInSecondTransaction(t *testing.T) {
|
||||
h, mock, conn, hookCtx := newTxHarness(t)
|
||||
hookCtx.Data = map[string]interface{}{"name": "b"}
|
||||
var begins []common.Database
|
||||
var afterTx common.Database
|
||||
h.Hooks().Register(OnTxBegin, func(c *HookContext) error {
|
||||
begins = append(begins, c.Tx)
|
||||
return nil
|
||||
})
|
||||
h.Hooks().Register(AfterUpdate, func(c *HookContext) error {
|
||||
afterTx = c.Tx
|
||||
return nil
|
||||
})
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "b"))
|
||||
mock.ExpectCommit()
|
||||
|
||||
h.handleUpdate(conn, &Message{ID: "m1"}, hookCtx)
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !response(t, conn).Success {
|
||||
t.Fatal("expected success")
|
||||
}
|
||||
if len(begins) != 2 || begins[0] == begins[1] || afterTx != begins[1] {
|
||||
t.Fatalf("OnTxBegin must fire per transaction and AfterUpdate run on the second, got %d", len(begins))
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadRunsInOneTransaction(t *testing.T) {
|
||||
h, mock, conn, hookCtx := newTxHarness(t)
|
||||
var beforeTx common.Database
|
||||
h.Hooks().Register(BeforeRead, func(c *HookContext) error {
|
||||
beforeTx = c.Tx
|
||||
return nil
|
||||
})
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "a"))
|
||||
mock.ExpectCommit()
|
||||
|
||||
h.handleRead(conn, &Message{ID: "m1"}, hookCtx)
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !response(t, conn).Success || beforeTx == nil || beforeTx == h.db {
|
||||
t.Fatal("BeforeRead must run on the transaction")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnTxBeginErrorRollsBackWithoutDetail(t *testing.T) {
|
||||
h, mock, conn, hookCtx := newTxHarness(t)
|
||||
h.Hooks().Register(OnTxBegin, func(c *HookContext) error { return errors.New("secret detail") })
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectRollback()
|
||||
|
||||
h.handleDelete(conn, &Message{ID: "m1"}, hookCtx)
|
||||
|
||||
if err := mock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp := response(t, conn)
|
||||
if resp.Success || resp.Error == nil || resp.Error.Code != "transaction_error" || resp.Error.Message != "Transaction failed" {
|
||||
t.Fatalf("unexpected response %+v", resp)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user