Files
sm-user-plugin/models/admin_models_test.go
Jakub Zych 493a3ff47c test: unit tests for the admin class functions, the models and the admin registration
- 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
2026-10-05 15:15:28 +02:00

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