Files
summercms/modules/lagoon/commands_flags_test.go
Jakub Zych 6720caebd3 test(surf,conga,lagoon): summaries, reports, stats and checklist flags
- serve summary lines at -v and -q, records and HideOnTTY, DSN labels
- shutdown report: aborted requests fail the stop, a drained stop is nil
- request log: request_id cap, default on, off by config or env, never the
  path for matched, 404 and 405 routes, component http; route:list --json
- WorkerStats through runAttempt: done, failed, in flight, handed back
- queue:clear and lagoon's production question on a pseudo-terminal:
  no cancels with exit 1, --force, -n and other environments skip it
- Docker: PendingMigrations, migrate/rollback/deferred:purge --pretend,
  production runs off a terminal never read stdin, migrate:status --json
- key:generate --json and -q
2026-10-21 19:38:18 +02:00

134 lines
4.7 KiB
Go

package lagoon
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"strings"
"testing"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
)
// envApp is an app in environment env with a fixed test app key and an
// optional DSN.
func envApp(t *testing.T, env, dsn string, extra ...string) *backpack.App {
t.Helper()
environ := append([]string{
"SUMMER_ENV=" + env,
"SUMMER_APP__KEY=" + base64.StdEncoding.EncodeToString(bytes.Repeat([]byte("k"), 32)),
}, extra...)
if dsn != "" {
environ = append(environ, "SUMMER_DATABASE__DSN="+dsn)
}
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: env, Environ: environ})
if err != nil {
t.Fatal(err)
}
return backpack.New(cfg)
}
// probeConfirm is a command that runs confirmProduction, as migrate does.
func probeConfirm(app *backpack.App) bonfire.Command {
return bonfire.Command{
Name: "probe:confirm",
Description: "Ask like migrate does",
Flags: []bonfire.Flag{{Name: "force", Description: forceDescription, Bare: true}},
Run: func(_ context.Context, in bonfire.Input, out bonfire.Output) error {
if err := confirmProduction(app, in, out, "Apply 3 migrations to the production database?", "Migration cancelled."); err != nil {
return err
}
out.Println("proceeded")
return nil
},
}
}
// TestConfirmProductionNonTTYProceeds: in production a console that is not
// an interactive terminal never asks and never reads stdin, so migrate,
// migrate:rollback and deferred:purge proceed in deploys and cron;
// bonfire.Call does the same. Every destructive command offers --force.
func TestConfirmProductionNonTTYProceeds(t *testing.T) {
app := envApp(t, "production", "")
in := strings.NewReader("n\n")
var out, errW bytes.Buffer
root, err := bonfire.NewRootIO("acme", []bonfire.Command{probeConfirm(app)}, in, &out, &errW)
if err != nil {
t.Fatal(err)
}
if code := bonfire.Execute(context.Background(), root, []string{"probe:confirm"}); code != bonfire.ExitOK {
t.Fatalf("exit %d\n%s", code, errW.String())
}
if in.Len() != 2 || out.String() != "proceeded\n" {
t.Fatalf("stdin left %d, output %q: the probe asked or did not proceed", in.Len(), out.String())
}
var buf bytes.Buffer
if err := bonfire.Call(context.Background(), []bonfire.Command{probeConfirm(app)}, "probe:confirm", nil, &buf); err != nil || buf.String() != "proceeded\n" {
t.Fatalf("Call: %v, %q", err, buf.String())
}
for _, c := range RuntimeCommands(app, nil) {
switch c.Name {
case "migrate", "migrate:rollback", "deferred:purge":
force := false
for _, f := range c.Flags {
if f.Name == "force" && f.Bare {
force = true
}
}
if !force || !strings.Contains(c.Long, "--force skips the") || !strings.Contains(c.Long, "production") {
t.Errorf("%s does not document the production question and --force", c.Name)
}
}
}
}
// TestKeyGenerateJSON: key:generate --json prints {"key": "..."} with a
// 32-byte base64 key, under -q too.
func TestKeyGenerateJSON(t *testing.T) {
seen := map[string]bool{}
for _, args := range [][]string{{"--json"}, {"--json", "-q"}} {
var buf bytes.Buffer
if err := bonfire.Call(context.Background(), []bonfire.Command{KeyGenerateCommand()}, "key:generate", args, &buf); err != nil {
t.Fatal(err)
}
var got map[string]string
if err := json.Unmarshal(buf.Bytes(), &got); err != nil || len(got) != 1 {
t.Fatalf("%v: not {\"key\": ...}: %v\n%s", args, err, buf.String())
}
raw, err := base64.StdEncoding.DecodeString(got["key"])
if err != nil || len(raw) != 32 {
t.Fatalf("%v: key %q is not 32 base64 bytes", args, got["key"])
}
if seen[got["key"]] {
t.Fatal("key:generate printed the same key twice")
}
seen[got["key"]] = true
}
}
// TestKeyGenerateQuiet: under -q key:generate prints the bare key and a
// newline (KEY=$(bin/acme key:generate -q)); without it the key follows
// the success mark.
func TestKeyGenerateQuiet(t *testing.T) {
var buf bytes.Buffer
if err := bonfire.Call(context.Background(), []bonfire.Command{KeyGenerateCommand()}, "key:generate", []string{"-q"}, &buf); err != nil {
t.Fatal(err)
}
out := buf.String()
key := strings.TrimSuffix(out, "\n")
if strings.Count(out, "\n") != 1 || strings.ContainsAny(key, " ✓") || len(key) != 44 {
t.Fatalf("-q output = %q, want the bare 44-character key", out)
}
buf.Reset()
if err := bonfire.Call(context.Background(), []bonfire.Command{KeyGenerateCommand()}, "key:generate", nil, &buf); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(buf.String(), "✓ ") || len(strings.TrimSpace(strings.TrimPrefix(buf.String(), "✓ "))) != 44 {
t.Fatalf("default output = %q", buf.String())
}
}