test(12.1-05): unit tests for bulk and record actions, row state, forbidden, preview and the form seams
- bulk action: empty, duplicate, unordered, absent, partial, out-of-scope, rollback, concurrent runs, permissions, CSRF, body cap - record action: scope, Applies, strict body, offered order, rollback, Applies error - ForbiddenError from every Form hook, the bulk delete and the relation link and child hooks - permission editor modes, locked codes and provider errors; relation locks on create, update and belongsTo - TestPhase121BootErrors: every boot error of plans 01 and 02 with plugin, controller and file - pact: the action, row state and filter contracts on a sample controller
This commit is contained in:
268
modules/cabana/phase121_permission_test.go
Normal file
268
modules/cabana/phase121_permission_test.go
Normal file
@@ -0,0 +1,268 @@
|
||||
package cabana_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const (
|
||||
permUnknown = "The permissions field contains an unknown permission."
|
||||
permInvalid = "The permissions field contains an invalid value."
|
||||
permShape = "The permissions field must be an object of permission codes."
|
||||
permLocked = "You cannot change this permission."
|
||||
)
|
||||
|
||||
// rosterPermissions reads a person's stored permission object.
|
||||
func rosterPermissions(t *testing.T, gdb *gorm.DB, id uint) map[string]int {
|
||||
t.Helper()
|
||||
out := map[string]int{}
|
||||
if raw := rosterLoad(t, gdb, id).Permissions; raw != nil {
|
||||
if err := json.Unmarshal([]byte(*raw), &out); err != nil {
|
||||
t.Fatalf("stored permissions %q: %v", *raw, err)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// newRosterCheckboxEnv is the roster fixture with the permission editor in
|
||||
// checkbox mode.
|
||||
func newRosterCheckboxEnv(t *testing.T) (*rosterEnv, *gorm.DB) {
|
||||
t.Helper()
|
||||
return newRosterEnvWith(t, func(p *rosterPlugin) {
|
||||
p.fsys = rosterTree(t, map[string]string{rosterFieldsFile: rosterFields(t, " mode: radio\n", " mode: checkbox\n")})
|
||||
})
|
||||
}
|
||||
|
||||
// TestPermissionEditorModes: radio accepts 1 and -1, checkbox accepts 1
|
||||
// only, and 0 means "no value" in both (D-16; T-12.1-13).
|
||||
func TestPermissionEditorModes(t *testing.T) {
|
||||
t.Run("radio", func(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Radio", Active: true})
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":1,"posts.publish":-1,"misc.beta":0}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, id); !reflect.DeepEqual(got, map[string]int{"posts.edit": 1, "posts.publish": -1}) {
|
||||
t.Fatalf("stored = %v, want 1 and -1 kept and 0 left out", got)
|
||||
}
|
||||
// 0 for a stored code removes it.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":0,"posts.publish":-1}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, id); !reflect.DeepEqual(got, map[string]int{"posts.publish": -1}) {
|
||||
t.Fatalf("stored = %v", got)
|
||||
}
|
||||
for _, value := range []string{"2", "-2", "100"} {
|
||||
rec := env.expect(t, http.StatusUnprocessableEntity, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":`+value+`}}`, "bearer")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "validation_failed", "permissions", permInvalid)
|
||||
}
|
||||
_, raw := rosterFormSchema(t, env, "bearer")
|
||||
if !strings.Contains(raw, `"mode":"radio"`) {
|
||||
t.Fatalf("schema mode: %s", raw)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("checkbox", func(t *testing.T) {
|
||||
env, gdb := newRosterCheckboxEnv(t)
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Check", Active: true})
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":1,"posts.publish":0}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, id); !reflect.DeepEqual(got, map[string]int{"posts.edit": 1}) {
|
||||
t.Fatalf("stored = %v", got)
|
||||
}
|
||||
// A denial is not a checkbox value.
|
||||
for _, value := range []string{"-1", "2"} {
|
||||
rec := env.expect(t, http.StatusUnprocessableEntity, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.publish":`+value+`}}`, "bearer")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "validation_failed", "permissions", permInvalid)
|
||||
}
|
||||
if got := rosterPermissions(t, gdb, id); !reflect.DeepEqual(got, map[string]int{"posts.edit": 1}) {
|
||||
t.Fatalf("a refused save changed the stored set: %v", got)
|
||||
}
|
||||
// An empty object unchecks everything that is offered.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"permissions":{}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, id); len(got) != 0 {
|
||||
t.Fatalf("stored after unchecking everything = %v", got)
|
||||
}
|
||||
_, raw := rosterFormSchema(t, env, "bearer")
|
||||
if !strings.Contains(raw, `"mode":"checkbox"`) {
|
||||
t.Fatalf("schema mode: %s", raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPermissionEditorUnknownCode: a code the controller does not offer is
|
||||
// 422, also when it is stored on the record, and the value must be an object
|
||||
// of integers.
|
||||
func TestPermissionEditorUnknownCode(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
stored := `{"legacy.code":1,"posts.edit":1}`
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Perm", Active: true, Permissions: &stored})
|
||||
for body, message := range map[string]string{
|
||||
`{"permissions":{"admin.root":1}}`: permUnknown,
|
||||
`{"permissions":{"posts.edit":1,"admin.root":0}}`: permUnknown,
|
||||
`{"permissions":{"legacy.code":1}}`: permUnknown,
|
||||
`{"permissions":{"POSTS.EDIT":1}}`: permUnknown,
|
||||
`{"permissions":{"":1}}`: permUnknown,
|
||||
`{"permissions":{"posts.edit":"1"}}`: permShape,
|
||||
`{"permissions":{"posts.edit":1.5}}`: permShape,
|
||||
`{"permissions":{"posts.edit":null}}`: permShape,
|
||||
`{"permissions":{"posts.edit":99999999999}}`: permShape,
|
||||
`{"permissions":{"posts.edit":[1]}}`: permShape,
|
||||
`{"permissions":[]}`: permShape,
|
||||
`{"permissions":1}`: permShape,
|
||||
`{"name":"Renamed","permissions":{"admin.root":1}}`: permUnknown,
|
||||
} {
|
||||
rec := env.expect(t, http.StatusUnprocessableEntity, http.MethodPut, rosterPath(id, ""), body, "bearer")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "validation_failed", "permissions", message)
|
||||
}
|
||||
if got := rosterLoad(t, gdb, id); got.Name != "Perm" || got.Permissions == nil || *got.Permissions != stored {
|
||||
t.Fatalf("a refused save wrote: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionEditorLocked: a locked code's stored and submitted value must
|
||||
// be equal for the administrator it is locked for; otherwise 403 and nothing
|
||||
// is written.
|
||||
func TestPermissionEditorLocked(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
stored := `{"reports.export":1}`
|
||||
holder := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Holder", Active: true, Permissions: &stored})
|
||||
bare := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Bare", Active: true})
|
||||
refused := func(id uint, body string) {
|
||||
t.Helper()
|
||||
rec := env.expect(t, http.StatusForbidden, http.MethodPut, rosterPath(id, ""), body, "limited")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "forbidden", "permissions", permLocked)
|
||||
}
|
||||
refused(holder, `{"permissions":{}}`)
|
||||
refused(holder, `{"permissions":{"reports.export":-1}}`)
|
||||
refused(holder, `{"name":"Sneaky","permissions":{"reports.export":0,"posts.edit":1}}`)
|
||||
refused(bare, `{"permissions":{"reports.export":1}}`)
|
||||
refused(bare, `{"permissions":{"reports.export":-1}}`)
|
||||
if got := rosterLoad(t, gdb, holder); got.Name != "Holder" || *got.Permissions != stored {
|
||||
t.Fatalf("a refused save wrote: %+v", got)
|
||||
}
|
||||
if got := rosterLoad(t, gdb, bare); got.Permissions != nil {
|
||||
t.Fatalf("a refused save wrote %q", *got.Permissions)
|
||||
}
|
||||
// The stored value sent back unchanged passes, with other codes changed.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(holder, ""), `{"permissions":{"reports.export":1,"posts.edit":-1}}`, "limited")
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(bare, ""), `{"permissions":{"posts.edit":1}}`, "limited")
|
||||
if got := rosterPermissions(t, gdb, holder); !reflect.DeepEqual(got, map[string]int{"reports.export": 1, "posts.edit": -1}) {
|
||||
t.Fatalf("stored = %v", got)
|
||||
}
|
||||
// A save without the field is not checked.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(holder, ""), `{"name":"Holder B"}`, "limited")
|
||||
// The administrator it is not locked for changes it.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(holder, ""), `{"permissions":{}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, holder); len(got) != 0 {
|
||||
t.Fatalf("the full admin's change was not stored: %v", got)
|
||||
}
|
||||
|
||||
// Known property of the contract: a locked code whose stored value is
|
||||
// outside the mode's set cannot be sent back, so every save that carries
|
||||
// the field is refused for that administrator; a save without the field
|
||||
// still passes. The value has to be repaired by an administrator the code
|
||||
// is not locked for.
|
||||
t.Run("a locked code stored outside the mode's set", func(t *testing.T) {
|
||||
env, gdb := newRosterCheckboxEnv(t)
|
||||
denied := `{"reports.export":-1}`
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Legacy", Active: true, Permissions: &denied})
|
||||
rec := env.expect(t, http.StatusForbidden, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":1}}`, "limited")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "forbidden", "permissions", permLocked)
|
||||
rec = env.expect(t, http.StatusUnprocessableEntity, http.MethodPut, rosterPath(id, ""), `{"permissions":{"reports.export":-1}}`, "limited")
|
||||
rosterErrorDetail(t, rec.Body.Bytes(), "validation_failed", "permissions", permInvalid)
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"name":"Legacy B"}`, "limited")
|
||||
if got := rosterLoad(t, gdb, id); got.Name != "Legacy B" || *got.Permissions != denied {
|
||||
t.Fatalf("stored = %+v", got)
|
||||
}
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"permissions":{"posts.edit":1}}`, "bearer")
|
||||
if got := rosterPermissions(t, gdb, id); !reflect.DeepEqual(got, map[string]int{"posts.edit": 1}) {
|
||||
t.Fatalf("the full admin's repair stored %v", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestPermissionEditorKeepsUnoffered: stored codes the controller does not
|
||||
// offer survive every save, are shown, and cannot be invented.
|
||||
func TestPermissionEditorKeepsUnoffered(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
stored := `{"legacy.code":1,"legacy.denied":-1,"posts.edit":1}`
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Legacy", Active: true, Permissions: &stored})
|
||||
rec := env.expect(t, http.StatusOK, http.MethodGet, rosterPath(id, ""), "", "bearer")
|
||||
if !strings.Contains(rec.Body.String(), `"permissions":{"legacy.code":1,"legacy.denied":-1,"posts.edit":1}`) {
|
||||
t.Fatalf("show = %s", rec.Body.String())
|
||||
}
|
||||
for _, body := range []string{`{"permissions":{}}`, `{"permissions":{"posts.publish":1}}`, `{"permissions":{"posts.publish":0}}`} {
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), body, "bearer")
|
||||
got := rosterPermissions(t, gdb, id)
|
||||
if got["legacy.code"] != 1 || got["legacy.denied"] != -1 {
|
||||
t.Fatalf("after %s the stored codes that are not offered are %v", body, got)
|
||||
}
|
||||
if _, kept := got["posts.edit"]; kept {
|
||||
t.Fatalf("after %s an offered code that was left out is still stored: %v", body, got)
|
||||
}
|
||||
}
|
||||
// A record without stored permissions shows an empty object, never null.
|
||||
blank := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Blank", Active: true})
|
||||
rec = env.expect(t, http.StatusOK, http.MethodGet, rosterPath(blank, ""), "", "bearer")
|
||||
if !strings.Contains(rec.Body.String(), `"permissions":{}`) {
|
||||
t.Fatalf("show without stored permissions = %s", rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionEditorOptionsPerRequest: options, with their locked flag and
|
||||
// localized texts, are asked from the controller for each request and are
|
||||
// never written into the cached schema.
|
||||
func TestPermissionEditorOptionsPerRequest(t *testing.T) {
|
||||
env, _ := newRosterEnv(t)
|
||||
for range 2 {
|
||||
_, limited := rosterFormSchema(t, env, "limited")
|
||||
if strings.Count(limited, `"locked":true`) != 1 || !strings.Contains(limited, `{"code":"reports.export","label":"Export reports","tab":"Reports","locked":true}`) {
|
||||
t.Fatalf("limited admin's options = %s", limited)
|
||||
}
|
||||
_, full := rosterFormSchema(t, env, "bearer")
|
||||
if strings.Contains(full, `"locked"`) {
|
||||
t.Fatalf("an option is locked for the full admin after a limited request: %s", full)
|
||||
}
|
||||
}
|
||||
pl := rosterLocale(env, http.MethodGet, rosterPeople+"/schema/form", "", "pl")
|
||||
if !strings.Contains(pl.Body.String(), `{"code":"posts.edit","label":"Edycja wpisów","tab":"Treści","comment":"Zmiana treści dowolnego wpisu."}`) {
|
||||
t.Fatalf("pl options = %s", pl.Body.String())
|
||||
}
|
||||
_, en := rosterFormSchema(t, env, "bearer")
|
||||
if !strings.Contains(en, `"label":"Edit posts","tab":"Content"`) {
|
||||
t.Fatalf("the cached schema kept another locale's labels: %s", en)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPermissionEditorProviderError: an error from the provider is the
|
||||
// generic 500 on every route that asks it, and nothing is written.
|
||||
func TestPermissionEditorProviderError(t *testing.T) {
|
||||
env, gdb := newRosterEnv(t)
|
||||
stored := `{"posts.edit":1}`
|
||||
id := rosterInsert(t, gdb, rosterPerson{Tenant: "acme", Name: "Perm", Active: true, Permissions: &stored})
|
||||
opaque := func(what, method, rel, body string) {
|
||||
t.Helper()
|
||||
rec := env.expect(t, http.StatusInternalServerError, method, rel, body, "bearer")
|
||||
if got := rosterError(t, rec); got.Code != "error" || len(got.Details) != 0 || strings.Contains(rec.Body.String(), "hunter2") || strings.Contains(rec.Body.String(), "permission table") || strings.Contains(rec.Body.String(), "permission column") {
|
||||
t.Fatalf("%s = %s", what, rec.Body.String())
|
||||
}
|
||||
}
|
||||
env.knobs.permissionOptions.Store(true)
|
||||
opaque("the form schema", http.MethodGet, rosterPeople+"/schema/form", "")
|
||||
opaque("an update with the field", http.MethodPut, rosterPath(id, ""), `{"name":"Changed","permissions":{"posts.publish":1}}`)
|
||||
// A save that does not carry the field does not ask for the options.
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), `{"name":"Perm B"}`, "bearer")
|
||||
env.knobs.permissionOptions.Store(false)
|
||||
|
||||
env.knobs.permissionValues.Store(true)
|
||||
opaque("show", http.MethodGet, rosterPath(id, ""), "")
|
||||
opaque("an update that reads the stored values", http.MethodPut, rosterPath(id, ""), `{"name":"Changed","permissions":{"posts.publish":1}}`)
|
||||
env.knobs.permissionValues.Store(false)
|
||||
|
||||
if got := rosterLoad(t, gdb, id); got.Name != "Perm B" || *got.Permissions != stored {
|
||||
t.Fatalf("a failed save wrote: %+v", got)
|
||||
}
|
||||
env.expect(t, http.StatusOK, http.MethodPut, rosterPath(id, ""), fmt.Sprintf(`{"permissions":%s}`, stored), "bearer")
|
||||
}
|
||||
Reference in New Issue
Block a user