Files
summercms/cabana/commands_test.go
Jakub Zych d27f442c49 test(09-02): add failing tests for admin create and reset commands
- admin:create and admin:reset-password are not registered yet
- Generated app main does not append cabana runtime commands
2026-09-24 17:54:42 +02:00

198 lines
6.4 KiB
Go

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 }