test: cover user group codes and the small class helpers
- UserGroupCodes and HasGroupCode against Postgres: id order, code-less groups skipped, membership by code, a missing handle and a failing query reported as errors - mail locale resolution, hook registration (a failing hook stops the chain), the code lifetimes and throttle settings carried on the context, constant-time code comparison, JWT secret loading and the principal lookup without an app or a gorm handle
This commit is contained in:
185
classes/user_groups_test.go
Normal file
185
classes/user_groups_test.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package classes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/sm-user-plugin/models"
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
"git.golem15.com/golem15/summercms/modules/compass"
|
||||
"git.golem15.com/golem15/summercms/modules/towel"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// TestUserGroupCodesAndHasGroupCode reads group codes in user_groups.id
|
||||
// order (groups without a code skipped), checks membership by code, and
|
||||
// reports a missing handle or a failing query as an error.
|
||||
func TestUserGroupCodesAndHasGroupCode(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
db := throttleDB(t)
|
||||
site := models.User{Email: "groups-site@example.test", IsActivated: true}
|
||||
plain := models.User{Email: "groups-plain@example.test", IsActivated: true}
|
||||
for _, u := range []*models.User{&site, &plain} {
|
||||
if err := db.Create(u).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if err := db.Exec(`INSERT INTO user_groups (name, code, created_at, updated_at) VALUES ('Admins', 'admin', NOW(), NOW()), ('Nameless', NULL, NOW(), NOW())`).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Exec(`INSERT INTO users_groups (user_id, user_group_id) SELECT ?, id FROM user_groups WHERE code IN ('admin', 'registered') OR code IS NULL`, site.ID).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
codes, err := UserGroupCodes(ctx, db, site.ID)
|
||||
if err != nil || !reflect.DeepEqual(codes, []string{"registered", "admin"}) {
|
||||
t.Fatalf("site codes %v %v", codes, err)
|
||||
}
|
||||
codes, err = UserGroupCodes(ctx, db, plain.ID)
|
||||
if err != nil || codes == nil || len(codes) != 0 {
|
||||
t.Fatalf("plain codes %#v %v", codes, err)
|
||||
}
|
||||
for _, c := range []struct {
|
||||
user uint
|
||||
code string
|
||||
want bool
|
||||
}{{site.ID, "admin", true}, {site.ID, "guest", false}, {plain.ID, "admin", false}, {site.ID, "", false}} {
|
||||
got, err := HasGroupCode(ctx, db, c.user, c.code)
|
||||
if err != nil || got != c.want {
|
||||
t.Errorf("HasGroupCode(%d, %q) = %v %v", c.user, c.code, got, err)
|
||||
}
|
||||
}
|
||||
if _, err := UserGroupCodes(ctx, nil, site.ID); err == nil {
|
||||
t.Error("UserGroupCodes without a handle")
|
||||
}
|
||||
if _, err := HasGroupCode(ctx, nil, site.ID, "admin"); err == nil {
|
||||
t.Error("HasGroupCode without a handle")
|
||||
}
|
||||
errRollback := errors.New("rollback")
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(`ALTER TABLE users_groups RENAME TO users_groups_gone`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := UserGroupCodes(ctx, tx, site.ID); err == nil {
|
||||
t.Error("UserGroupCodes swallowed a query error")
|
||||
}
|
||||
return errRollback
|
||||
})
|
||||
if !errors.Is(err, errRollback) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Exec(`ALTER TABLE users_groups RENAME TO users_groups_gone`).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := HasGroupCode(ctx, tx, site.ID, "admin"); err == nil {
|
||||
t.Error("HasGroupCode swallowed a query error")
|
||||
}
|
||||
return errRollback
|
||||
})
|
||||
if !errors.Is(err, errRollback) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LookupByEmail(ctx, nil, "x@example.test"); err == nil {
|
||||
t.Error("LookupByEmail without a handle")
|
||||
}
|
||||
}
|
||||
|
||||
// TestUserClassHelpers covers the small helpers around them: mail locale
|
||||
// resolution, hook registration, the code and throttle context settings,
|
||||
// constant-time code comparison, JWT secret loading and the principal
|
||||
// lookup's missing app or handle.
|
||||
func TestUserClassHelpers(t *testing.T) {
|
||||
pl, empty := "pl", ""
|
||||
ctx := towel.WithLocale(context.Background(), "en")
|
||||
if got := ResolveMailLocale(ctx, &models.User{PreferredLocale: &pl}); got != "pl" {
|
||||
t.Errorf("saved locale: %q", got)
|
||||
}
|
||||
if got := ResolveMailLocale(ctx, &models.User{PreferredLocale: &empty}); got != "en" {
|
||||
t.Errorf("request locale: %q", got)
|
||||
}
|
||||
if got := ResolveMailLocale(context.Background(), nil); got != "" {
|
||||
t.Errorf("no locale: %q", got)
|
||||
}
|
||||
if MailTemplate("welcome", "en") != "welcome-en" || MailTemplate("welcome", "pl") != "welcome" {
|
||||
t.Error("MailTemplate")
|
||||
}
|
||||
|
||||
saved := hookRegistrars
|
||||
t.Cleanup(func() { hookRegistrars = saved })
|
||||
hookRegistrars = nil
|
||||
calls := 0
|
||||
RegisterHook(func(*gorm.DB) error { calls++; return nil })
|
||||
if err := RegisterHooks(nil); err != nil || calls != 1 {
|
||||
t.Fatalf("hooks: %v %d", err, calls)
|
||||
}
|
||||
boom := errors.New("boom")
|
||||
RegisterHook(func(*gorm.DB) error { return boom })
|
||||
RegisterHook(func(*gorm.DB) error { calls += 10; return nil })
|
||||
if err := RegisterHooks(nil); !errors.Is(err, boom) || calls != 2 {
|
||||
t.Fatalf("a failing hook stops the chain: %v %d", err, calls)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // a nil context is what the helpers guard against
|
||||
c := ContextWithCodeTTL(nil, 5*time.Minute, 0)
|
||||
if resetTTL(c) != 5*time.Minute || activationTTL(c) != 72*time.Hour || resetTTL(nil) != 60*time.Minute || activationTTL(nil) != 72*time.Hour {
|
||||
t.Error("code lifetimes")
|
||||
}
|
||||
if codesEqual("abc", "abcd") || !codesEqual("abc", "abc") || codesEqual("abd", "abc") {
|
||||
t.Error("codesEqual")
|
||||
}
|
||||
if code, err := newCode(); err != nil || len(code) != 64 {
|
||||
t.Errorf("newCode %q %v", code, err)
|
||||
}
|
||||
|
||||
//nolint:staticcheck // see above
|
||||
if s := throttleFrom(withThrottle(nil, throttleSettings{use: false})); s.use {
|
||||
t.Error("withThrottle on a nil context")
|
||||
}
|
||||
if s := throttleFrom(nil); !s.use || s.limit != 5 || s.minutes != 15 {
|
||||
t.Errorf("default throttle %+v", s)
|
||||
}
|
||||
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: "development", Environ: []string{"SUMMER_ENV=development"}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range map[string]any{"golem15.user.throttle.use_throttle": false, "golem15.user.throttle.attempt_limit": 3, "golem15.user.throttle.suspension_minutes": 9} {
|
||||
if err := cfg.Set(k, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
app := backpack.New(cfg)
|
||||
if s := throttleFrom(ContextWithThrottle(context.Background(), app)); s.use || s.limit != 3 || s.minutes != 9 {
|
||||
t.Errorf("configured throttle %+v", s)
|
||||
}
|
||||
if s := throttleFrom(ContextWithThrottle(context.Background(), nil)); !s.use || s.limit != 5 {
|
||||
t.Errorf("throttle without an app %+v", s)
|
||||
}
|
||||
if err := RejectIfThrottled(ContextWithThrottle(context.Background(), app), &gorm.DB{}, &models.User{ID: 1}, "127.0.0.1"); err != nil {
|
||||
t.Errorf("throttling switched off: %v", err)
|
||||
}
|
||||
if err := RejectIfThrottled(context.Background(), nil, &models.User{ID: 1}, "127.0.0.1"); err != nil {
|
||||
t.Errorf("no handle: %v", err)
|
||||
}
|
||||
|
||||
if _, err := JWTSecret(nil); err == nil {
|
||||
t.Error("JWTSecret without an app")
|
||||
}
|
||||
if _, err := JWTSecret(app); err == nil {
|
||||
t.Error("JWTSecret with an empty secret")
|
||||
}
|
||||
if err := cfg.Set("golem15.user.jwt.secret", " s3cret "); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if s, err := JWTSecret(app); err != nil || s != "s3cret" {
|
||||
t.Errorf("JWTSecret = %q %v", s, err)
|
||||
}
|
||||
if _, err := (GormUsers{}).FindByID(context.Background(), 1); err == nil {
|
||||
t.Error("FindByID without an app")
|
||||
}
|
||||
if _, err := (GormUsers{App: app}).FindByID(context.Background(), 1); err == nil {
|
||||
t.Error("FindByID without a gorm handle")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user