package cabana_test import ( "fmt" "net/http" "strings" "testing" "git.golem15.com/golem15/summercms/modules/pact" ) // TestInvisibleColumnSearchAndRows: a column with invisible: true is flagged // in the schema, searched and sorted on the server, and never part of a row // (D-27 G6). func TestInvisibleColumnSearchAndRows(t *testing.T) { env, gdb := newRosterEnv(t) rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Ada", Email: "ada@hidden.example.test", Slug: "ada", Active: true}) rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bob", Email: "bob@example.test", Slug: "bob", Active: true}) rosterInsert(t, gdb, rosterPerson{Tenant: "other", Name: "Zed", Email: "zed@hidden.example.test", Active: true}) schema := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/schema/list", "", "bearer").Body.String() if !strings.Contains(schema, `{"key":"email","label":"Email","searchable":true,"sortable":true,"invisible":true}`) || strings.Count(schema, `"invisible"`) != 1 { t.Fatalf("list schema = %s", schema) } for _, query := range []string{"", "?search=ada", "?search=hidden.example", "?sort=email&dir=asc"} { raw := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+query, "", "bearer").Body.String() if strings.Contains(raw, `"email"`) || strings.Contains(raw, "example.test") { t.Fatalf("rows of %q carry the invisible column: %s", query, raw) } // The visible columns are there. if !strings.Contains(raw, `"slug":"ada"`) { t.Fatalf("rows of %q lack a visible column: %s", query, raw) } } // Search by the invisible column finds the row, inside the list scope only. found := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?search=hidden.example", "", "bearer").Body.String() if !strings.Contains(found, `"name":"Ada"`) || strings.Contains(found, `"name":"Bob"`) || strings.Contains(found, `"name":"Zed"`) || !strings.Contains(found, `"total":1`) { t.Fatalf("search by the invisible column = %s", found) } // It sorts in both directions. asc := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?sort=email&dir=asc", "", "bearer").Body.String() desc := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"?sort=email&dir=desc", "", "bearer").Body.String() if strings.Index(asc, `"name":"Ada"`) > strings.Index(asc, `"name":"Bob"`) || strings.Index(desc, `"name":"Ada"`) < strings.Index(desc, `"name":"Bob"`) { t.Fatalf("sort by the invisible column:\nasc %s\ndesc %s", asc, desc) } // The record response is the form's, not the list's: email is a form field. id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Cy", Email: "cy@example.test", Active: true}) if shown := env.expect(t, http.StatusOK, http.MethodGet, rosterPath(id, ""), "", "bearer").Body.String(); !strings.Contains(shown, `"email":"cy@example.test"`) { t.Fatalf("show = %s", shown) } } // TestFilterOptionsControllerFirst: a controller that implements // pact.FilterOptions serves a scope filter's choices, from the database, // although the model does not. func TestFilterOptionsControllerFirst(t *testing.T) { env, gdb := newRosterEnv(t) news, beta := rosterTag{Name: "news"}, rosterTag{Name: "beta"} rosterSeed(t, gdb, &news) rosterSeed(t, gdb, &beta) tagged := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Tagged", Active: true}) rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bare", Active: true}) rosterSeed(t, gdb, &rosterPersonTag{PersonID: tagged, TagID: news.ID}) options := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "limited").Body.String() want := fmt.Sprintf(`"data":[{"value":"%d","label":"beta"},{"value":"%d","label":"news"}]`, beta.ID, news.ID) if !strings.Contains(options, want) { t.Fatalf("options = %s, want %s", options, want) } // A row added after boot is offered: the choices are read per request. late := rosterTag{Name: "alpha"} rosterSeed(t, gdb, &late) if options = env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "bearer").Body.String(); !strings.Contains(options, `"label":"alpha"`) { t.Fatalf("options after an insert = %s", options) } // The scope is still the model's. filtered := env.expect(t, http.StatusOK, http.MethodGet, fmt.Sprintf("%s?filter[tagged]=%d", rosterPeople, news.ID), "", "bearer").Body.String() if !strings.Contains(filtered, `"name":"Tagged"`) || strings.Contains(filtered, `"name":"Bare"`) { t.Fatalf("filtered list = %s", filtered) } env.expect(t, http.StatusNotFound, http.MethodGet, rosterPeople+"/filters/missing/options", "", "bearer") if _, ok := any(rosterPerson{}).(pact.FilterOptions); ok { t.Fatal("the fixture model serves filter options itself") } } // TestFilterOptionsModelFallback: without FilterOptions on the controller the // model serves the choices, and its labels are translated. func TestFilterOptionsModelFallback(t *testing.T) { env, _ := newRosterEnvWith(t, func(p *rosterPlugin) { p.wrap = func(inner rosterController) pact.AdminController { return rosterBare{inner: inner, newRecord: func() any { return &rosterOptionsPerson{} }} } }) options := env.expect(t, http.StatusOK, http.MethodGet, rosterPeople+"/filters/tagged/options", "", "bearer").Body.String() if !strings.Contains(options, `"data":[{"value":"1","label":"Tags"},{"value":"2","label":"Plain label"}]`) { t.Fatalf("model options = %s", options) } if _, ok := any(rosterBare{}).(pact.FilterOptions); ok { t.Fatal("the bare controller serves filter options") } }