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
This commit is contained in:
207
classes/privileged_test.go
Normal file
207
classes/privileged_test.go
Normal file
@@ -0,0 +1,207 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user