- 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
134 lines
4.7 KiB
Go
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())
|
|
}
|
|
}
|