Files
sm-user-plugin/classes/privileged_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

208 lines
7.6 KiB
Go

package classes
import (
"os"
"path/filepath"
"reflect"
"testing"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/compass"
)
// privilegedConfig opens a config whose golem15.user.privileged_groups is
// value; a nil value leaves the key out.
func privilegedConfig(t *testing.T, value any, environ ...string) *compass.Config {
t.Helper()
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: privileged\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: append([]string{"SUMMER_ENV=development"}, environ...)})
if err != nil {
t.Fatal(err)
}
if value != nil {
if err := cfg.Set(privilegedGroupsKey, value); err != nil {
t.Fatal(err)
}
}
return cfg
}
// TestPrivilegedGroupCodes pins how the privileged list is read (D-05): the
// default, an override, blank entries and the comma-separated form of an
// environment variable.
func TestPrivilegedGroupCodes(t *testing.T) {
if got := PrivilegedGroupCodes(nil); !reflect.DeepEqual(got, []string{"admin"}) {
t.Fatalf("nil config = %v", got)
}
for _, tc := range []struct {
name string
value any
want []string
}{
{"a missing key", nil, []string{"admin"}},
{"a list", []string{"admin", "moderator"}, []string{"admin", "moderator"}},
{"a list of any", []any{"owner", 7, "staff"}, []string{"owner", "staff"}},
{"blank entries", []string{" admin ", "", " ", "staff"}, []string{"admin", "staff"}},
{"an empty list", []string{}, []string{}},
{"a comma-separated string", "admin, staff ,,owner", []string{"admin", "staff", "owner"}},
{"an empty string", "", []string{}},
{"a value of another type", 5, []string{}},
} {
t.Run(tc.name, func(t *testing.T) {
got := PrivilegedGroupCodes(privilegedConfig(t, tc.value))
if got == nil || !reflect.DeepEqual(got, tc.want) {
t.Fatalf("codes = %#v, want %#v", got, tc.want)
}
})
}
// The key is read on every call: a change applies without a new config.
cfg := privilegedConfig(t, []string{"first"})
if got := PrivilegedGroupCodes(cfg); !reflect.DeepEqual(got, []string{"first"}) {
t.Fatalf("codes = %v", got)
}
if err := cfg.Set(privilegedGroupsKey, []string{"second"}); err != nil {
t.Fatal(err)
}
if got := PrivilegedGroupCodes(cfg); !reflect.DeepEqual(got, []string{"second"}) {
t.Fatalf("codes after the change = %v", got)
}
if PermissionManagePrivilegedGroups != "golem15.users.manage_privileged_groups" {
t.Fatalf("permission code = %q", PermissionManagePrivilegedGroups)
}
}
// TestIsPrivilegedCode: codes compare case-sensitively and a group without a
// code is never privileged.
func TestIsPrivilegedCode(t *testing.T) {
str := func(s string) *string { return &s }
codes := []string{"admin", "Staff"}
for _, tc := range []struct {
code *string
want bool
}{
{str("admin"), true}, {str("Staff"), true},
{str("Admin"), false}, {str("ADMIN"), false}, {str("staff"), false},
{str("admin "), false}, {str(" admin"), false}, {str("administrator"), false},
{str(""), false}, {nil, false},
} {
if got := IsPrivilegedCode(codes, tc.code); got != tc.want {
label := "<nil>"
if tc.code != nil {
label = *tc.code
}
t.Errorf("IsPrivilegedCode(%q) = %v, want %v", label, got, tc.want)
}
}
if IsPrivilegedCode(nil, str("admin")) || IsPrivilegedCode([]string{}, str("admin")) {
t.Error("an empty list has a privileged code")
}
// An empty entry in the list never matches a group without a code.
if IsPrivilegedCode([]string{""}, str("")) || IsPrivilegedCode([]string{""}, nil) {
t.Error("an empty code is privileged")
}
}
// TestIsPrivilegedMember covers the membership question behind the takeover
// guard (D-30) and PrivilegedGroupIDs behind the groups lock (D-07).
func TestIsPrivilegedMember(t *testing.T) {
ctx := t.Context()
db := throttleDB(t)
str := func(s string) *string { return &s }
admins := models.UserGroup{Name: "Admins", Code: str("admin")}
second := models.UserGroup{Name: "Second admins", Code: str("admin2")}
ordinary := models.UserGroup{Name: "Ordinary", Code: str("ordinary")}
upper := models.UserGroup{Name: "Upper", Code: str("ADMIN")}
uncoded := models.UserGroup{Name: "Uncoded"}
for _, group := range []*models.UserGroup{&admins, &second, &ordinary, &upper, &uncoded} {
if err := db.Create(group).Error; err != nil {
t.Fatal(err)
}
}
user := func(label string, groups ...*models.UserGroup) uint {
t.Helper()
u := models.User{Email: "privileged-" + label + "@example.test", IsActivated: true}
if err := db.Omit("Groups").Create(&u).Error; err != nil {
t.Fatal(err)
}
for _, group := range groups {
if err := db.Create(&models.UsersGroup{UserID: u.ID, UserGroupID: group.ID}).Error; err != nil {
t.Fatal(err)
}
}
return u.ID
}
nobody := user("nobody")
plain := user("plain", &ordinary)
codeless := user("codeless", &uncoded, &upper)
member := user("member", &ordinary, &admins)
gone := user("gone", &admins)
if err := db.Exec(`UPDATE users SET deleted_at = NOW() WHERE id = ?`, gone).Error; err != nil {
t.Fatal(err)
}
defaults := PrivilegedGroupCodes(nil)
for _, tc := range []struct {
name string
codes []string
user uint
want bool
}{
{"a user in no group", defaults, nobody, false},
{"an ordinary group only", defaults, plain, false},
{"a group without a code and a case variant", defaults, codeless, false},
{"a member of a listed group", defaults, member, true},
{"a deactivated member", defaults, gone, true},
{"an overridden list without admin", []string{"ordinary"}, gone, false},
{"an overridden list with the ordinary code", []string{"ordinary"}, plain, true},
{"an overridden list, a member of both", []string{"ordinary"}, member, true},
{"a case variant in the list", []string{"ADMIN"}, codeless, true},
{"an empty list", []string{}, member, false},
{"a nil list", nil, member, false},
{"an unknown user", defaults, 987654, false},
{"the zero user", defaults, 0, false},
} {
t.Run(tc.name, func(t *testing.T) {
got, err := IsPrivilegedMember(ctx, db, tc.codes, tc.user)
if err != nil || got != tc.want {
t.Fatalf("IsPrivilegedMember = %v %v, want %v", got, err, tc.want)
}
})
}
if _, err := IsPrivilegedMember(ctx, nil, defaults, member); err == nil {
t.Fatal("IsPrivilegedMember without a handle")
}
// The handle's own conditions do not leak into the query.
scoped := db.Where("1 = 0")
if got, err := IsPrivilegedMember(ctx, scoped, defaults, member); err != nil || !got {
t.Fatalf("with a scoped handle = %v %v", got, err)
}
// A failing query is an error, never "not a member".
cancelled, cancel := contextCancelled(t)
defer cancel()
if got, err := IsPrivilegedMember(cancelled, db, defaults, member); err == nil || got {
t.Fatalf("a failed query answered %v %v", got, err)
}
ids, err := PrivilegedGroupIDs(ctx, db, []string{"admin", "admin2", "missing"})
if err != nil || !reflect.DeepEqual(ids, []uint{admins.ID, second.ID}) {
t.Fatalf("PrivilegedGroupIDs = %v %v", ids, err)
}
if ids, err = PrivilegedGroupIDs(ctx, scoped, defaults); err != nil || !reflect.DeepEqual(ids, []uint{admins.ID}) {
t.Fatalf("PrivilegedGroupIDs with a scoped handle = %v %v", ids, err)
}
for _, codes := range [][]string{nil, {}, {"nothing"}} {
if ids, err = PrivilegedGroupIDs(ctx, db, codes); err != nil || ids == nil || len(ids) != 0 {
t.Fatalf("PrivilegedGroupIDs(%v) = %#v %v", codes, ids, err)
}
}
if _, err = PrivilegedGroupIDs(ctx, nil, defaults); err == nil {
t.Fatal("PrivilegedGroupIDs without a handle")
}
if _, err = PrivilegedGroupIDs(cancelled, db, defaults); err == nil {
t.Fatal("PrivilegedGroupIDs swallowed a failed query")
}
}