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()) } }