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
This commit is contained in:
11
cabana/commands.go
Normal file
11
cabana/commands.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package cabana
|
||||
|
||||
import (
|
||||
"git.golem15.com/golem15/summercms/backpack"
|
||||
"git.golem15.com/golem15/summercms/bonfire"
|
||||
)
|
||||
|
||||
// RuntimeCommands returns the operator commands for backend administrators.
|
||||
func RuntimeCommands(app *backpack.App) []bonfire.Command {
|
||||
return nil
|
||||
}
|
||||
197
cabana/commands_test.go
Normal file
197
cabana/commands_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
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 }
|
||||
@@ -109,6 +109,22 @@ func TestGenerateStableQuotedImportsInManifestOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateMainRegistersCabanaRuntimeCommands(t *testing.T) {
|
||||
mainSrc, err := generateMain(Manifest{Module: "example.com/app", Binary: "hello"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if bytes.Count(mainSrc, []byte("cabana.RuntimeCommands")) != 1 {
|
||||
t.Fatalf("cabana.RuntimeCommands count = %d\n%s", bytes.Count(mainSrc, []byte("cabana.RuntimeCommands")), mainSrc)
|
||||
}
|
||||
if !bytes.Contains(mainSrc, []byte("commands = append(commands, cabana.RuntimeCommands(app)...)")) {
|
||||
t.Fatalf("generated main does not append cabana runtime commands once:\n%s", mainSrc)
|
||||
}
|
||||
if bytes.Contains(mainSrc, []byte("examples/hello")) {
|
||||
t.Fatal("generated main hard-codes examples/hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteIfChangedSkipsIdenticalBytes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "out.go")
|
||||
|
||||
Reference in New Issue
Block a user