mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 04:21:58 +00:00
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.
174 lines
4.7 KiB
Go
174 lines
4.7 KiB
Go
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)
|
|
}
|
|
}
|