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 := "" 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") } }