mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 12:31:59 +00:00
- resolvespec/restheadspec: single and batch delete use one transaction - add sqlmock tests for delete transaction behaviour - testmodels: serial integer ids; update tests accordingly - add compose testserver, smoke script, podman-first Makefile targets
172 lines
5.0 KiB
Go
172 lines
5.0 KiB
Go
package restheadspec
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"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 delItem struct {
|
|
ID int `json:"id" bun:"id,pk"`
|
|
Name string `json:"name" bun:"name"`
|
|
}
|
|
|
|
func newDeleteHarness(t *testing.T) (*Handler, sqlmock.Sqlmock, *sql.DB) {
|
|
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 request context timeout.
|
|
db.SetMaxOpenConns(1)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
return NewHandler(database.NewPgSQLAdapter(db), modelregistry.NewModelRegistry()), mock, db
|
|
}
|
|
|
|
func runDelete(h *Handler, id string, data interface{}) *httptest.ResponseRecorder {
|
|
rec := httptest.NewRecorder()
|
|
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodPost, "/", nil))
|
|
base, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
ctx := WithSchema(base, "public")
|
|
ctx = WithEntity(ctx, "items")
|
|
ctx = WithTableName(ctx, "items")
|
|
ctx = WithModel(ctx, &delItem{})
|
|
h.handleDelete(ctx, w, id, data)
|
|
return rec
|
|
}
|
|
|
|
// recordDeleteHook registers a BeforeDelete hook that captures hookCtx.Tx.
|
|
func recordDeleteHook(h *Handler, hookErr error) *[]common.Database {
|
|
var seen []common.Database
|
|
h.Hooks().Register(BeforeDelete, func(ctx *HookContext) error {
|
|
seen = append(seen, ctx.Tx)
|
|
return hookErr
|
|
})
|
|
return &seen
|
|
}
|
|
|
|
func TestDeleteSingleUsesOneTransaction(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
seen := recordDeleteHook(h, nil)
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "a"))
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectCommit()
|
|
|
|
rec := runDelete(h, "7", nil)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(*seen) != 1 || (*seen)[0] == h.db {
|
|
t.Fatalf("BeforeDelete must run once on the transaction, got %v", *seen)
|
|
}
|
|
}
|
|
|
|
func TestDeleteSingleNotFoundRollsBack(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}))
|
|
mock.ExpectRollback()
|
|
|
|
if rec := runDelete(h, "7", nil); rec.Code != http.StatusNotFound {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteHookErrorRollsBack(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
recordDeleteHook(h, errors.New("denied"))
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "a"))
|
|
mock.ExpectRollback()
|
|
|
|
if rec := runDelete(h, "7", nil); rec.Code != http.StatusBadRequest {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteSingleExecErrorRollsBack(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "name"}).AddRow(7, "a"))
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnError(errors.New("boom"))
|
|
mock.ExpectRollback()
|
|
|
|
if rec := runDelete(h, "7", nil); rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteBatchUsesOneTransaction(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
seen := recordDeleteHook(h, nil)
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectCommit()
|
|
|
|
rec := runDelete(h, "", []interface{}{"1", "2"})
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// restheadspec fires the hook per item
|
|
if len(*seen) != 2 || (*seen)[0] == h.db || (*seen)[1] == h.db {
|
|
t.Fatalf("BeforeDelete must run per item on the transaction, got %v", *seen)
|
|
}
|
|
var resp map[string]float64
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil || resp["deleted"] != 2 {
|
|
t.Fatalf("unexpected body %s (%v)", rec.Body, err)
|
|
}
|
|
}
|
|
|
|
func TestDeleteBatchFailureRollsBackAll(t *testing.T) {
|
|
h, mock, _ := newDeleteHarness(t)
|
|
|
|
mock.ExpectBegin()
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnResult(sqlmock.NewResult(0, 1))
|
|
mock.ExpectExec(`DELETE FROM`).WillReturnError(errors.New("boom"))
|
|
mock.ExpectRollback()
|
|
|
|
if rec := runDelete(h, "", []string{"1", "2"}); rec.Code != http.StatusInternalServerError {
|
|
t.Fatalf("status %d body %s", rec.Code, rec.Body)
|
|
}
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|