package cabana_test import ( "bytes" "context" "net/http" "net/http/httptest" "strings" "testing" "time" "os" "path/filepath" "git.golem15.com/golem15/summercms/backpack" "git.golem15.com/golem15/summercms/bonfire" "git.golem15.com/golem15/summercms/bouncer" "git.golem15.com/golem15/summercms/cabana" "git.golem15.com/golem15/summercms/compass" "git.golem15.com/golem15/summercms/lagoon" "gorm.io/gorm" ) func TestAdminCreateCommand(t *testing.T) { gdb := adminGorm(t) app := commandApp(t, gdb) create := commandByName(t, cabana.RuntimeCommands(app), "admin:create") var buf bytes.Buffer out := bonfire.NewOutput(nil, &buf, &buf) const password = "correct-horse-battery" if err := create.Run(context.Background(), flagInput{flags: map[string]string{ "email": "Ada@Example.Test", "password": password, "role": "developer", "superuser": "true", }}, out); err != nil { t.Fatal(err) } if strings.Contains(buf.String(), password) || strings.Contains(buf.String(), "$2") || strings.Contains(buf.String(), adminTestSecret) { t.Fatalf("create output leaked a secret: %s", buf.String()) } if !strings.Contains(buf.String(), "ada@example.test") { t.Fatalf("create output = %s", buf.String()) } var user cabana.BackendUser if err := gdb.Preload("Role").Where("login = ?", "ada@example.test").First(&user).Error; err != nil { t.Fatal(err) } if user.Email != "ada@example.test" || !user.IsActivated || !user.IsSuperuser || user.Role.Code != "developer" || user.ActivatedAt == nil { t.Fatalf("created admin = %+v role=%+v", user, user.Role) } if !bouncer.CheckPassword(user.Password, password) { t.Fatal("stored password hash does not match") } buf.Reset() if err := create.Run(context.Background(), flagInput{flags: map[string]string{ "email": "editor@example.test", "password": password, "login": "editor", "role": "publisher", }}, out); err != nil { t.Fatal(err) } var editor cabana.BackendUser if err := gdb.Where("login = ?", "editor").First(&editor).Error; err != nil { t.Fatal(err) } if editor.IsSuperuser || editor.Email != "editor@example.test" { t.Fatalf("editor = %+v", editor) } if err := create.Run(context.Background(), flagInput{flags: map[string]string{ "email": "third@example.test", "password": password, "role": "missing-role", }}, out); err == nil || !strings.Contains(err.Error(), "unknown role") { t.Fatalf("unknown role err = %v", err) } if err := gdb.Exec(`INSERT INTO backend_user_roles (name, code, is_system, created_at, updated_at) VALUES ('Editor A', 'shared', FALSE, NOW(), NOW()), ('Editor B', 'shared', FALSE, NOW(), NOW())`).Error; err != nil { t.Fatal(err) } err := create.Run(context.Background(), flagInput{flags: map[string]string{ "email": "fourth@example.test", "password": password, "role": "shared", }}, out) if err == nil || !strings.Contains(err.Error(), "ambiguous role") || strings.Contains(err.Error(), password) { t.Fatalf("ambiguous role err = %v", err) } } func TestAdminResetPasswordCommand(t *testing.T) { gdb := adminGorm(t) app := commandApp(t, gdb) reset := commandByName(t, cabana.RuntimeCommands(app), "admin:reset-password") const original = "original-password-value" const updated = "replacement-password-value" hash, err := bouncer.HashPassword(10, original) if err != nil { t.Fatal(err) } var roleID uint if err := gdb.Raw(`SELECT id FROM backend_user_roles WHERE code = 'developer'`).Scan(&roleID).Error; err != nil { t.Fatal(err) } user := cabana.BackendUser{Login: "resetme", Email: "resetme@example.test", Password: hash, IsActivated: true, RoleID: &roleID} if err := gdb.Create(&user).Error; err != nil { t.Fatal(err) } token, _, err := bouncer.MintAudience(adminTestSecret, itoa(user.ID), "https://app.test/_admin/api/v1/auth/login", time.Hour, bouncer.AudienceBackend) if err != nil { t.Fatal(err) } guard := bouncer.NewBackendJWTGuard(adminTestSecret, cabana.BackendUsers{DB: gdb}, nil, nil) req := httptest.NewRequest(http.MethodGet, "/_admin/api/v1/auth/me", nil) req.Header.Set("Authorization", "Bearer "+token) if _, err := guard.Authenticate(req); err != nil { t.Fatalf("token before reset: %v", err) } var buf bytes.Buffer out := bonfire.NewOutput(nil, &buf, &buf) if err := reset.Run(context.Background(), flagInput{ args: []string{"ResetMe@Example.Test"}, flags: map[string]string{"password": updated}, }, out); err != nil { t.Fatal(err) } if strings.Contains(buf.String(), updated) || strings.Contains(buf.String(), original) || strings.Contains(buf.String(), "$2") || strings.Contains(buf.String(), token) { t.Fatalf("reset output leaked a secret: %s", buf.String()) } if !strings.Contains(buf.String(), "resetme") { t.Fatalf("reset output = %s", buf.String()) } var fresh cabana.BackendUser if err := gdb.First(&fresh, user.ID).Error; err != nil { t.Fatal(err) } if bouncer.CheckPassword(fresh.Password, original) || !bouncer.CheckPassword(fresh.Password, updated) { t.Fatal("password hash was not replaced") } if fresh.TokensValidAfter == nil { t.Fatal("reset did not advance tokens_valid_after") } if _, err := guard.Authenticate(req); err == nil { t.Fatal("token issued before reset still authenticated") } } func commandByName(t *testing.T, cmds []bonfire.Command, name string) bonfire.Command { t.Helper() for _, cmd := range cmds { if cmd.Name == name && cmd.Run != nil { return cmd } } t.Fatalf("%s was not registered", name) return bonfire.Command{} } func commandApp(t *testing.T, gdb *gorm.DB) *backpack.App { t.Helper() dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: cabana-admin\n"), 0o644); err != nil { t.Fatal(err) } cfg, err := compass.Open(compass.Options{ Dir: dir, Environ: []string{ "SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=" + adminTestSecret, }, }) if err != nil { t.Fatal(err) } app := backpack.New(cfg) if err := lagoon.Publish(app, adminSQL, gdb); err != nil { t.Fatal(err) } return app } type flagInput struct { args []string flags map[string]string } func (f flagInput) Args() []string { return f.args } func (f flagInput) Argument(name string) (string, bool) { if name == "identifier" && len(f.args) > 0 { return f.args[0], true } return "", false } func (f flagInput) Flag(name string) (string, bool) { value, ok := f.flags[name] return value, ok && value != "" } func (f flagInput) Flags(string) []string { return nil }