mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-10-01 19:20:31 +00:00
POST with a non-numeric URL id (e.g. a string primary key) was treated as having no id, so the body primary key was used to look up the existing row. That broke primary key changes. Treat any non-empty, non-zero URL id as the update target, and add an integration test through Handle for POST and PUT.
154 lines
5.5 KiB
Go
154 lines
5.5 KiB
Go
//go:build integration
|
|
|
|
package restheadspec
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/jackc/pgx/v5/stdlib"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
"github.com/testcontainers/testcontainers-go/wait"
|
|
"github.com/uptrace/bun"
|
|
"github.com/uptrace/bun/dialect/pgdialect"
|
|
|
|
"github.com/bitechdev/ResolveSpec/pkg/common"
|
|
"github.com/bitechdev/ResolveSpec/pkg/common/adapters/database"
|
|
"github.com/bitechdev/ResolveSpec/pkg/modelregistry"
|
|
)
|
|
|
|
type pkAsset struct {
|
|
bun.BaseModel `bun:"table:public.t_pkasset,alias:t_pkasset"`
|
|
Category string `json:"category" bun:"category,type:citext"`
|
|
Description string `json:"description" bun:"description,type:citext,pk"`
|
|
}
|
|
|
|
func setupPKTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
pg, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
|
ContainerRequest: testcontainers.ContainerRequest{
|
|
Image: "postgres:15-alpine",
|
|
ExposedPorts: []string{"5432/tcp"},
|
|
Env: map[string]string{
|
|
"POSTGRES_USER": "testuser", "POSTGRES_PASSWORD": "testpass", "POSTGRES_DB": "testdb",
|
|
},
|
|
WaitingFor: wait.ForLog("database system is ready to accept connections").
|
|
WithOccurrence(2).WithStartupTimeout(60 * time.Second),
|
|
},
|
|
Started: true,
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = pg.Terminate(ctx) })
|
|
|
|
host, err := pg.Host(ctx)
|
|
require.NoError(t, err)
|
|
port, err := pg.MappedPort(ctx, "5432")
|
|
require.NoError(t, err)
|
|
|
|
db, err := sql.Open("pgx", fmt.Sprintf("postgres://testuser:testpass@%s:%s/testdb?sslmode=disable", host, port.Port()))
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
require.NoError(t, db.Ping())
|
|
|
|
_, err = db.Exec(`
|
|
CREATE EXTENSION IF NOT EXISTS citext;
|
|
CREATE TABLE public.t_pkasset (
|
|
category citext,
|
|
description citext PRIMARY KEY
|
|
);
|
|
INSERT INTO public.t_pkasset VALUES ('cat', 'old-pk');`)
|
|
require.NoError(t, err)
|
|
return db
|
|
}
|
|
|
|
func pkUpdateCtx(base context.Context) context.Context {
|
|
ctx := WithSchema(base, "public")
|
|
ctx = WithEntity(ctx, "t_pkasset")
|
|
ctx = WithTableName(ctx, "t_pkasset")
|
|
return WithModel(ctx, pkAsset{})
|
|
}
|
|
|
|
func countPK(t *testing.T, db *sql.DB, pk string) int {
|
|
t.Helper()
|
|
var n int
|
|
require.NoError(t, db.QueryRow(`SELECT count(*) FROM public.t_pkasset WHERE description = $1`, pk).Scan(&n))
|
|
return n
|
|
}
|
|
|
|
func TestUpdateChangesPrimaryKeyWhenURLIDGiven(t *testing.T) {
|
|
db := setupPKTestDB(t)
|
|
h := NewHandler(database.NewBunAdapter(bun.NewDB(db, pgdialect.New())), modelregistry.NewModelRegistry())
|
|
|
|
update := func(id string, body map[string]interface{}) *httptest.ResponseRecorder {
|
|
rec := httptest.NewRecorder()
|
|
w, _ := common.WrapHTTPRequest(rec, httptest.NewRequest(http.MethodPut, "/", nil))
|
|
base, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
h.handleUpdate(pkUpdateCtx(base), w, id, nil, body, ExtendedRequestOptions{})
|
|
return rec
|
|
}
|
|
|
|
// URL id = old PK, body carries a different PK: the PK is changed.
|
|
rec := update("old-pk", map[string]interface{}{"description": "new-pk", "category": "cat2"})
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
require.Equal(t, 0, countPK(t, db, "old-pk"))
|
|
require.Equal(t, 1, countPK(t, db, "new-pk"))
|
|
var cat string
|
|
require.NoError(t, db.QueryRow(`SELECT category FROM public.t_pkasset WHERE description = 'new-pk'`).Scan(&cat))
|
|
require.Equal(t, "cat2", cat)
|
|
|
|
// Body PK equal to the URL id: ordinary update, PK untouched.
|
|
rec = update("new-pk", map[string]interface{}{"description": "new-pk", "category": "cat3"})
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
require.Equal(t, 1, countPK(t, db, "new-pk"))
|
|
|
|
// Body without a PK: ordinary update.
|
|
rec = update("new-pk", map[string]interface{}{"category": "cat4"})
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
|
|
// Null PK in the body is ignored, not applied.
|
|
rec = update("new-pk", map[string]interface{}{"description": nil, "category": "cat5"})
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
require.Equal(t, 1, countPK(t, db, "new-pk"))
|
|
var total int
|
|
require.NoError(t, db.QueryRow(`SELECT count(*) FROM public.t_pkasset`).Scan(&total))
|
|
require.Equal(t, 1, total)
|
|
require.NoError(t, db.QueryRow(`SELECT category FROM public.t_pkasset WHERE description = 'new-pk'`).Scan(&cat))
|
|
require.Equal(t, "cat5", cat)
|
|
}
|
|
|
|
// Exercises the real dispatch: a POST with a string id in the URL and a different
|
|
// PK in the body must update the row identified by the URL id.
|
|
func TestHandlePostWithStringURLIDChangesPrimaryKey(t *testing.T) {
|
|
db := setupPKTestDB(t)
|
|
reg := modelregistry.NewModelRegistry()
|
|
require.NoError(t, reg.RegisterModel("public.t_pkasset", pkAsset{}))
|
|
h := NewHandler(database.NewBunAdapter(bun.NewDB(db, pgdialect.New())), reg)
|
|
|
|
for _, method := range []string{http.MethodPost, http.MethodPut} {
|
|
t.Run(method, func(t *testing.T) {
|
|
from, to := "old-pk", "new-pk-"+method
|
|
if method == http.MethodPut {
|
|
from = "new-pk-" + http.MethodPost
|
|
}
|
|
req := httptest.NewRequest(method, "/public/t_pkasset/"+from,
|
|
strings.NewReader(`{"description":"`+to+`","category":"c"}`))
|
|
rec := httptest.NewRecorder()
|
|
w, r := common.WrapHTTPRequest(rec, req)
|
|
h.Handle(w, r, map[string]string{"schema": "public", "entity": "t_pkasset", "id": from})
|
|
|
|
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
|
|
require.Equal(t, 0, countPK(t, db, from))
|
|
require.Equal(t, 1, countPK(t, db, to))
|
|
})
|
|
}
|
|
}
|