- 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
186 lines
6.7 KiB
Go
186 lines
6.7 KiB
Go
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")
|
|
}
|
|
}
|