package main_test import ( "os" "os/exec" "path/filepath" "strings" "sync" "testing" "time" "github.com/carlosframework/platform/internal/console" ) // buildConsole compiles the console binary ONCE per test run and hands every // test the same path: these are end-to-end verb tests (the flag parsing and // the exit codes are half of what is under test, and neither is reachable from // a unit test), and a `go build` per test is the whole cost of the package. var ( buildOnce sync.Once consoleBin string buildDir string buildErr error ) func buildConsole(t *testing.T) string { t.Helper() buildOnce.Do(func() { goBin, err := exec.LookPath("go") if err != nil { buildErr = err return } buildDir, buildErr = os.MkdirTemp("", "console-bin") if buildErr != nil { return } consoleBin = filepath.Join(buildDir, "console") build := exec.Command(goBin, "build", "-o", consoleBin, ".") build.Env = os.Environ() if out, err := build.CombinedOutput(); err != nil { buildErr = err t.Logf("build console: %s", out) } }) if buildErr != nil { t.Skipf("cannot build the console binary: %v", buildErr) } return consoleBin } // runner returns a "run these args against the built binary" helper with env // pinned — combined output plus the error, because a verb's refusal is as // much under test as its success. func runner(t *testing.T, bin string, env ...string) func(args ...string) (string, error) { full := append(os.Environ(), env...) return func(args ...string) (string, error) { t.Helper() cmd := exec.Command(bin, args...) cmd.Env = full out, err := cmd.CombinedOutput() return string(out), err } } func TestMain(m *testing.M) { // The GeoIP verifier verb: openGeoIP re-runs its own binary to verify // the override in a child process, and under `go test` that binary is // this one. if len(os.Args) > 1 && os.Args[1] == console.GeoipVerifyVerb { os.Exit(console.RunGeoipVerify(os.Args[2:], os.Stdout)) } code := m.Run() if buildDir != "" { os.RemoveAll(buildDir) } os.Exit(code) } // migrate-app is the CONSOLE half of `carlos accounts migrate`. It lives here // rather than in the carlos binary on purpose: the console's app→account row // is console state, and the carlos CLI has no business opening the console's // database — box-side authority changes go through this binary, exactly like // `console grant`. func TestMigrateAppVerb(t *testing.T) { bin := buildConsole(t) tmp := t.TempDir() dbPath := filepath.Join(tmp, "console.db") run := runner(t, bin, "CONSOLE_DB="+dbPath) // A console in the shape a deployment is in before the migration: ops // owns kass. st, err := console.Open(dbPath) if err != nil { t.Fatal(err) } now := time.Now() if err := st.CreateAccount(console.OpsAccountID, "bab", "ops", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if err := st.CreateAccount(793481920157, "kqmvexample", "Kass", "paul@keymail.dev", now); err != nil { t.Fatal(err) } if _, err := st.CreateAppIn(console.OpsAccountID, "kass", "paul@keymail.dev", now); err != nil { t.Fatal(err) } st.Close() // Unknown app and unknown account are refused, clearly, before anything // is written. if out, err := run("migrate-app", "-app", "ghost", "-to", "kqmvexample"); err == nil { t.Errorf("migrating an unknown app succeeded: %s", out) } else if !strings.Contains(out, "ghost") { t.Errorf("unknown-app error does not name the app: %s", out) } if out, err := run("migrate-app", "-app", "kass", "-to", "nosuch"); err == nil { t.Errorf("migrating to an unknown account succeeded: %s", out) } else if !strings.Contains(out, "nosuch") { t.Errorf("unknown-account error does not name the account: %s", out) } // The move. if out, err := run("migrate-app", "-app", "kass", "-to", "kqmvexample"); err != nil { t.Fatalf("migrate-app: %v\n%s", err, out) } st, err = console.Open(dbPath) if err != nil { t.Fatal(err) } apps, err := st.AppsIn(793481920157) if err != nil || len(apps) != 1 || apps[0] != "kass" { t.Fatalf("AppsIn(Kass) = %v %v, want [kass]", apps, err) } if apps, _ := st.AppsIn(console.OpsAccountID); len(apps) != 0 { t.Errorf("app still in ops: %v", apps) } st.Close() // Idempotent: an operator who is not sure whether it landed can just run // it again. out, err := run("migrate-app", "-app", "kass", "-to", "kqmvexample") if err != nil { t.Fatalf("re-running migrate-app must be a no-op: %v\n%s", err, out) } if !strings.Contains(out, "already") { t.Errorf("re-run output does not say it was already done: %s", out) } st, err = console.Open(dbPath) if err != nil { t.Fatal(err) } defer st.Close() if apps, _ := st.AppsIn(793481920157); len(apps) != 1 { t.Errorf("re-run changed things: %v", apps) } }