feat(12.2-03): add hasMany relation contracts, relation forms and child create
- RelationContract gains Kind (empty is belongsToMany) and ForeignKey, with kind-aware boot checks
- manage.form, view.form and pivot.form compile against the related or pivot model; $/ paths resolve inside the plugin
- view toolbarButtons accept create|update|delete|link|unlink, each the capability of its routes
- POST .../relations/{name}/records creates a child through the manage form; the server sets the hasMany key
- relation schema carries kind, deferrable and the localized forms; 17 new relation message keys in en and pl
This commit is contained in:
80
modules/cabana/relation_child_smoke_test.go
Normal file
80
modules/cabana/relation_child_smoke_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package cabana_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// linkedIDs lists the ids of a gadget relation's linked rows.
|
||||
func (e *conformEnv) linkedIDs(t *testing.T, gadget uint, relation string, headers map[string]string) []uint {
|
||||
t.Helper()
|
||||
rec := e.sendWith(t, http.MethodGet, fmt.Sprintf("/acme/conform/gadgets/%d/relations/%s", gadget, relation), nil, "", headers)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("linked %s status=%d body=%s", relation, rec.Code, rec.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Data []struct {
|
||||
ID uint `json:"id"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := make([]uint, 0, len(body.Data))
|
||||
for _, row := range body.Data {
|
||||
out = append(out, row.ID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// partOwner reads a part's gadget_id straight from the table.
|
||||
func (e *conformEnv) partOwner(t *testing.T, id uint) *uint {
|
||||
t.Helper()
|
||||
var part conformPart
|
||||
if err := e.db.First(&part, id).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return part.GadgetID
|
||||
}
|
||||
|
||||
// TestRelationChildSmokeCreate creates a hasMany child of a saved gadget
|
||||
// through the relation manager: the server sets the foreign key from the
|
||||
// scoped parent, the child lists under that parent only, and a body naming
|
||||
// the foreign key cannot move it.
|
||||
func TestRelationChildSmokeCreate(t *testing.T) {
|
||||
env := newConformEnv(t)
|
||||
env.loginAs(t, env.login)
|
||||
a := env.createGadget(t, "a-"+env.stamp, "")
|
||||
b := env.createGadget(t, "b-"+env.stamp, "")
|
||||
|
||||
rec := env.send(t, http.MethodPost, fmt.Sprintf("/acme/conform/gadgets/%d/relations/parts/records", a), map[string]any{"label": "wheel", "gadget_id": b}, true)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("create part status=%d body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
part := dataID(t, rec.Body.Bytes())
|
||||
if owner := env.partOwner(t, part); owner == nil || *owner != a {
|
||||
t.Fatalf("part gadget_id = %v, want %d", owner, a)
|
||||
}
|
||||
if got := env.linkedIDs(t, a, "parts", nil); !slices.Contains(got, part) {
|
||||
t.Fatalf("gadget A parts = %v, want %d", got, part)
|
||||
}
|
||||
if got := env.linkedIDs(t, b, "parts", nil); slices.Contains(got, part) {
|
||||
t.Fatalf("gadget B lists A's part: %v", got)
|
||||
}
|
||||
|
||||
invalid := env.send(t, http.MethodPost, fmt.Sprintf("/acme/conform/gadgets/%d/relations/parts/records", a), map[string]any{"label": ""}, true)
|
||||
if invalid.Code != http.StatusUnprocessableEntity {
|
||||
t.Fatalf("empty label status=%d body=%s", invalid.Code, invalid.Body.String())
|
||||
}
|
||||
undeclared := env.send(t, http.MethodPost, fmt.Sprintf("/acme/conform/gadgets/%d/relations/members/records", a), map[string]any{"email": "x@example.test"}, true)
|
||||
if undeclared.Code != http.StatusForbidden {
|
||||
t.Fatalf("create on a relation without the create button status=%d body=%s", undeclared.Code, undeclared.Body.String())
|
||||
}
|
||||
missing := env.send(t, http.MethodPost, fmt.Sprintf("/acme/conform/gadgets/%d/relations/parts/records", b+1000), map[string]any{"label": "x"}, true)
|
||||
if missing.Code != http.StatusNotFound {
|
||||
t.Fatalf("create under a missing parent status=%d body=%s", missing.Code, missing.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user