- classes: every admin action for its changed-row count and idempotency, the ban across several throttle rows, the cleanup of a permanent delete - classes: the privileged list and IsPrivilegedMember (D-30), also through a handle that carries conditions - classes: the five-minute boundary of last_seen - models: PermissionSet round trip, the slug table shared with the SPA, Fillable and Rules, fields that never marshal, the group filter value - TestAdminRegistration: permissions, navigation, embedded files and phrase keys in en and pl; the admin secret is required
192 lines
7.5 KiB
Go
192 lines
7.5 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestAdminModelContracts pins what the admin screens read off the models:
|
|
// fill lists, rules, morph names, attachment relations and filter scopes.
|
|
func TestAdminModelContracts(t *testing.T) {
|
|
if got := (UserGroup{}).Fillable(); !reflect.DeepEqual(got, []string{"name", "code", "description"}) {
|
|
t.Fatalf("UserGroup.Fillable = %v", got)
|
|
}
|
|
if got := (UserGroup{}).Rules(); !reflect.DeepEqual(got, map[string]string{"name": "required|between:3,64", "code": "required|unique:user_groups"}) {
|
|
t.Fatalf("UserGroup.Rules = %v", got)
|
|
}
|
|
if got := (Organisation{}).Fillable(); !reflect.DeepEqual(got, []string{"name", "slug", "description"}) {
|
|
t.Fatalf("Organisation.Fillable = %v", got)
|
|
}
|
|
if got := (Organisation{}).Rules(); !reflect.DeepEqual(got, map[string]string{
|
|
"name": "required|max:255", "slug": "required|unique:golem15_user_organisations", "description": "nullable|max:5000",
|
|
}) {
|
|
t.Fatalf("Organisation.Rules = %v", got)
|
|
}
|
|
// Neither fill list reaches a column that carries privilege or ownership.
|
|
for _, forbidden := range []string{"permissions", "id", "users_count", "members", "created_at"} {
|
|
for _, list := range [][]string{(UserGroup{}).Fillable(), (Organisation{}).Fillable()} {
|
|
for _, name := range list {
|
|
if name == forbidden {
|
|
t.Fatalf("%s is fillable", forbidden)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// The user's fill list is the register and update contract and is not
|
|
// widened by the admin screen.
|
|
fillable := strings.Join((User{}).Fillable(), ",")
|
|
for _, protected := range []string{"is_activated", "permissions", "last_seen", "activation_code", "reset_password_code", "tokens_valid_after", "deleted_at", "id"} {
|
|
for _, name := range (User{}).Fillable() {
|
|
if name == protected {
|
|
t.Fatalf("users.%s is fillable (%s)", protected, fillable)
|
|
}
|
|
}
|
|
}
|
|
if !reflect.DeepEqual((User{}).Hidden(), []string{"password", "reset_password_code", "activation_code"}) {
|
|
t.Fatalf("User.Hidden = %v", (User{}).Hidden())
|
|
}
|
|
|
|
if (User{}).MorphName() != `Golem15\User\Models\User` || (Organisation{}).MorphName() != `Golem15\User\Models\Organisation` {
|
|
t.Fatalf("morph names = %q / %q", (User{}).MorphName(), (Organisation{}).MorphName())
|
|
}
|
|
for name, relations := range map[string]int{"user": len((User{}).AttachRelations()), "organisation": len((Organisation{}).AttachRelations())} {
|
|
if relations != 1 {
|
|
t.Fatalf("%s attach relations = %d", name, relations)
|
|
}
|
|
}
|
|
if rel := (User{}).AttachRelations()[0]; AvatarField != "avatar" || rel.Name != AvatarField || !rel.Public {
|
|
t.Fatalf("user avatar relation = %+v", rel)
|
|
}
|
|
if rel := (Organisation{}).AttachRelations()[0]; rel.Name != OrganisationAvatarField || !rel.Public {
|
|
t.Fatalf("organisation avatar relation = %+v", rel)
|
|
}
|
|
|
|
if got := (User{}).FilterScopes(); !reflect.DeepEqual(got, []string{FilterByGroup}) || FilterByGroup != "filterByGroup" {
|
|
t.Fatalf("User.FilterScopes = %v", got)
|
|
}
|
|
// A nil handle passes through for every scope name.
|
|
if (User{}).FilterScope(FilterByGroup, nil, 1) != nil || (User{}).FilterScope("other", nil, 1) != nil {
|
|
t.Fatal("FilterScope invented a handle")
|
|
}
|
|
|
|
for model, table := range map[string][2]string{
|
|
"User": {(User{}).TableName(), "users"}, "UserGroup": {(UserGroup{}).TableName(), "user_groups"},
|
|
"UsersGroup": {(UsersGroup{}).TableName(), "users_groups"}, "Organisation": {(Organisation{}).TableName(), "golem15_user_organisations"},
|
|
"FrontendPermission": {(FrontendPermission{}).TableName(), "golem15_user_frontend_permissions"},
|
|
} {
|
|
if table[0] != table[1] {
|
|
t.Fatalf("%s table = %q, want %q", model, table[0], table[1])
|
|
}
|
|
}
|
|
if GroupGuest != "guest" || GroupRegistered != "registered" {
|
|
t.Fatalf("seeded group codes = %q, %q", GroupGuest, GroupRegistered)
|
|
}
|
|
}
|
|
|
|
// TestOrganisationBeforeValidate: an empty slug is filled from the name, a
|
|
// given slug is kept.
|
|
func TestOrganisationBeforeValidate(t *testing.T) {
|
|
for _, tc := range []struct{ name, slug, want string }{
|
|
{"Acme Records", "", "acme-records"},
|
|
{"Acme Records", "custom", "custom"},
|
|
{"", "", ""},
|
|
{"---", "", ""},
|
|
} {
|
|
organisation := Organisation{Name: tc.name, Slug: tc.slug}
|
|
if err := organisation.BeforeValidate(nil); err != nil || organisation.Slug != tc.want {
|
|
t.Fatalf("BeforeValidate(%q, %q) = %q %v, want %q", tc.name, tc.slug, organisation.Slug, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAdminFieldsNeverMarshal: the columns and relations the admin screens
|
|
// added never appear when a model is serialized (D-08, D-17; T-12.1-24,
|
|
// T-12.1-34).
|
|
func TestAdminFieldsNeverMarshal(t *testing.T) {
|
|
now := time.Now()
|
|
code := "admin"
|
|
secret := "one-time-code"
|
|
user := User{
|
|
ID: 7, Email: "marshal@example.test", Password: "$2a$10$stored-hash", LastSeen: &now, CreatedAt: now, UpdatedAt: now,
|
|
Permissions: PermissionSet{"blog.post": 1},
|
|
ActivationCode: &secret, ResetPasswordCode: &secret,
|
|
Groups: []UserGroup{{ID: 3, Name: "Site admins", Code: &code}},
|
|
}
|
|
raw, err := json.Marshal(user)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var keys map[string]any
|
|
if err := json.Unmarshal(raw, &keys); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, key := range []string{"LastSeen", "Permissions", "CreatedAt", "UpdatedAt", "Groups", "Password", "ActivationCode", "ResetPasswordCode"} {
|
|
if _, ok := keys[key]; ok {
|
|
t.Fatalf("a marshalled user has the key %s: %s", key, raw)
|
|
}
|
|
}
|
|
lower := strings.ToLower(string(raw))
|
|
for _, leak := range []string{"last_seen", "permissions", "groups", "blog.post", "site admins", "stored-hash", "one-time-code"} {
|
|
if strings.Contains(lower, leak) {
|
|
t.Fatalf("a marshalled user carries %q: %s", leak, raw)
|
|
}
|
|
}
|
|
// The rest of the payload is unchanged by the admin fields.
|
|
if keys["Email"] != "marshal@example.test" || keys["ID"] != float64(7) {
|
|
t.Fatalf("marshalled user = %s", raw)
|
|
}
|
|
|
|
organisation, err := json.Marshal(Organisation{ID: 1, Name: "Acme", Members: []User{user}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(organisation), "Members") || strings.Contains(string(organisation), "marshal@example.test") {
|
|
t.Fatalf("a marshalled organisation carries its members: %s", organisation)
|
|
}
|
|
group, err := json.Marshal(UserGroup{ID: 3, Name: "Site admins", UsersCount: 5})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(group), "UsersCount") {
|
|
t.Fatalf("a marshalled group carries the member count: %s", group)
|
|
}
|
|
}
|
|
|
|
// TestModelRegistry: the plugin registers its seven models, the pivot and the
|
|
// permission catalog included.
|
|
func TestModelRegistry(t *testing.T) {
|
|
var names []string
|
|
for _, model := range All() {
|
|
names = append(names, reflect.TypeOf(model).Name())
|
|
}
|
|
sort.Strings(names)
|
|
for _, want := range []string{"FrontendPermission", "Organisation", "User", "UserGroup", "UsersGroup"} {
|
|
found := false
|
|
for _, name := range names {
|
|
found = found || name == want
|
|
}
|
|
if !found {
|
|
t.Fatalf("model %s is not registered: %v", want, names)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGroupFilterID: the group filter takes a positive whole number, as a
|
|
// number or as its decimal text, and nothing else.
|
|
func TestGroupFilterID(t *testing.T) {
|
|
for _, value := range []any{"7", " 7 ", 7, uint(7), int64(7), float64(7), json.Number("7")} {
|
|
if id, ok := groupFilterID(value); !ok || id != 7 {
|
|
t.Errorf("groupFilterID(%#v) = %d %v, want 7", value, id, ok)
|
|
}
|
|
}
|
|
for _, value := range []any{nil, "", " ", "abc", "7 OR 1=1", "7;", "-1", "0", 0, -3, 7.5, "7.0", "1e3", true, []int{7}, "99999999999999999999"} {
|
|
if id, ok := groupFilterID(value); ok {
|
|
t.Errorf("groupFilterID(%#v) = %d, want a refusal", value, id)
|
|
}
|
|
}
|
|
}
|