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()) } }