Files
summercms/modules/cabana/singleton_test.go
Jakub Zych 91593da1f7 test(17-06): framework and admin SPA unit tests for the blog companions
- signpost: activation refusals, host overrides, D-02 pattern layers
  incl. PatternFrom, ReferenceOptions depths
- seaglass: removal report, bluemonday bypass corpus, Check
- pact Option.Depth omitempty and the menu item type contracts; party
  activates menu item types after Register and before the first Boot
- cabana: repeater compile and decode, dependsOn compile and recheck,
  richeditor keys and notices, reorder and singleton boot checks
  (Docker-free); repeater save, dependent recheck, richeditor save,
  reorder and singleton routes on Postgres; conformance cases for the
  documented 404, 409 and 422 answers of the new routes
- admin: richeditor value contract, link popover, notices and chunk
  failure; repeater bounds, remove and reorder, collapsed rows;
  dependent dropdown states and last request wins; reorder
  announcements, partition switch and tree rules; SingletonForm spec
- gate: required names in the go, admin and postgres stages
2026-10-12 19:29:37 +02:00

114 lines
5.5 KiB
Go

package cabana_test
import (
"fmt"
"net/http"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/pact"
)
// TestSingletonCompile pins the D-11 boot rules, Docker-free: a singleton
// controller needs config_form.yaml and no config_list.yaml, and may keep
// one (its create button is dropped then).
func TestSingletonCompile(t *testing.T) {
if err := activateCompanions(t, companionsFSPlugin{fsys: companionsOverlay(t, nil)}); err != nil {
t.Fatalf("fixture without a list: %v", err)
}
withList := companionsOverlay(t, map[string]string{
"controllers/profiles/config_list.yaml": "list: ~/plugins/acme/companions/models/profile/columns.yaml\nmodelClass: Profile\ntoolbar:\n buttons: [create]\n",
"models/profile/columns.yaml": "columns:\n title:\n label: Title\n",
})
if err := activateCompanions(t, companionsFSPlugin{fsys: withList}); err != nil {
t.Fatalf("singleton with a list: %v", err)
}
err := activateCompanions(t, companionsFSPlugin{fsys: companionsOverlay(t, map[string]string{"controllers/profiles/config_form.yaml": ""})})
if err == nil || !strings.Contains(err.Error(), "acme.companions.profiles: a singleton controller (pact.AdminSingleton) needs config_form.yaml") {
t.Fatalf("singleton without a form: err = %v", err)
}
// A controller that is not a singleton still needs its list.
err = activateCompanions(t, companionsFSPlugin{
fsys: companionsOverlay(t, nil),
controllers: []pact.AdminController{plainProfiles{}},
})
if err == nil || !strings.Contains(err.Error(), "config_list.yaml") {
t.Fatalf("plain controller without a list: err = %v", err)
}
}
// plainProfiles is the profiles controller without pact.AdminSingleton.
type plainProfiles struct{}
func (plainProfiles) ID() string { return "acme.companions.profiles" }
func (plainProfiles) ModelName() string { return "Profile" }
func (plainProfiles) ConfigDir() string { return "controllers/profiles" }
func (plainProfiles) RequiredPermissions() []string { return []string{"acme.companions.access"} }
func (plainProfiles) NewRecord() any { return &companionsProfile{} }
// TestSingleton pins the D-11 routes on Postgres: the singleton route
// creates the record once and answers its id; the record shows and updates; list, create, delete, bulk delete
// and reorder answer 404; the form schema says singleton; a controller that
// is not a singleton answers 404 on the route; anonymous requests 401.
func TestSingleton(t *testing.T) {
e := newCompanionsEnv(t)
first := e.send(t, http.MethodGet, "/acme/companions/profiles/singleton", nil, true)
id := dataID(t, first.Body.Bytes())
for range 2 {
if again := dataID(t, e.send(t, http.MethodGet, "/acme/companions/profiles/singleton", nil, true).Body.Bytes()); again != id {
t.Fatalf("id = %d, want %d", again, id)
}
}
var count int64
if err := e.db.Model(&companionsProfile{}).Count(&count).Error; err != nil || count != 1 {
t.Fatalf("profiles = %d (%v)", count, err)
}
var profile companionsProfile
if err := e.db.First(&profile, id).Error; err != nil || profile.Key != "default" || profile.Title != "Default" {
t.Fatalf("profile = %+v (%v)", profile, err)
}
path := fmt.Sprintf("/acme/companions/profiles/%d", id)
if rec := e.send(t, http.MethodGet, path, nil, true); rec.Code != http.StatusOK || !strings.Contains(rec.Body.String(), `"title":"Default"`) {
t.Fatalf("show = %d %s", rec.Code, rec.Body.String())
}
if rec := e.send(t, http.MethodPut, path, map[string]any{"title": "Renamed"}, true); rec.Code != http.StatusOK {
t.Fatalf("update = %d %s", rec.Code, rec.Body.String())
}
if err := e.db.First(&profile, id).Error; err != nil || profile.Title != "Renamed" || profile.Key != "default" {
t.Fatalf("updated profile = %+v (%v)", profile, err)
}
if again := dataID(t, e.send(t, http.MethodGet, "/acme/companions/profiles/singleton", nil, true).Body.Bytes()); again != id {
t.Fatalf("id after update = %d", again)
}
for _, tc := range []struct {
method, path string
body any
auth bool
status int
}{
{http.MethodGet, "/acme/companions/profiles", nil, true, http.StatusNotFound},
{http.MethodPost, "/acme/companions/profiles", map[string]any{"title": "x"}, true, http.StatusNotFound},
{http.MethodDelete, path, nil, true, http.StatusNotFound},
{http.MethodPost, "/acme/companions/profiles/bulk-delete", map[string]any{"ids": []uint{id}}, true, http.StatusNotFound},
{http.MethodGet, "/acme/companions/profiles/reorder", nil, true, http.StatusNotFound},
{http.MethodGet, "/acme/companions/links/singleton", nil, true, http.StatusNotFound},
{http.MethodGet, "/acme/companions/profiles/singleton", nil, false, http.StatusUnauthorized},
} {
if rec := e.send(t, tc.method, tc.path, tc.body, tc.auth); rec.Code != tc.status {
t.Fatalf("%s %s status=%d want %d body=%s", tc.method, tc.path, rec.Code, tc.status, rec.Body.String())
}
}
if err := e.db.Model(&companionsProfile{}).Count(&count).Error; err != nil || count != 1 {
t.Fatalf("profiles after refused requests = %d (%v)", count, err)
}
schema := e.send(t, http.MethodGet, "/acme/companions/profiles/schema/form", nil, true)
if !strings.Contains(schema.Body.String(), `"singleton":true`) {
t.Fatalf("form schema = %s", schema.Body.String())
}
if links := e.send(t, http.MethodGet, "/acme/companions/links/schema/form", nil, true); strings.Contains(links.Body.String(), `"singleton":true`) {
t.Fatalf("a plain form schema says singleton: %s", links.Body.String())
}
}