carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package main_test
2
3import (
4 "os"
5 "os/exec"
6 "path/filepath"
7 "strings"
8 "sync"
9 "testing"
10 "time"
11
12 "github.com/carlosframework/platform/internal/console"
13)
14
15// buildConsole compiles the console binary ONCE per test run and hands every
16// test the same path: these are end-to-end verb tests (the flag parsing and
17// the exit codes are half of what is under test, and neither is reachable from
18// a unit test), and a `go build` per test is the whole cost of the package.
19var (
20 buildOnce sync.Once
21 consoleBin string
22 buildDir string
23 buildErr error
24)
25
26func buildConsole(t *testing.T) string {
27 t.Helper()
28 buildOnce.Do(func() {
29 goBin, err := exec.LookPath("go")
30 if err != nil {
31 buildErr = err
32 return
33 }
34 buildDir, buildErr = os.MkdirTemp("", "console-bin")
35 if buildErr != nil {
36 return
37 }
38 consoleBin = filepath.Join(buildDir, "console")
39 build := exec.Command(goBin, "build", "-o", consoleBin, ".")
40 build.Env = os.Environ()
41 if out, err := build.CombinedOutput(); err != nil {
42 buildErr = err
43 t.Logf("build console: %s", out)
44 }
45 })
46 if buildErr != nil {
47 t.Skipf("cannot build the console binary: %v", buildErr)
48 }
49 return consoleBin
50}
51
52// runner returns a "run these args against the built binary" helper with env
53// pinned — combined output plus the error, because a verb's refusal is as
54// much under test as its success.
55func runner(t *testing.T, bin string, env ...string) func(args ...string) (string, error) {
56 full := append(os.Environ(), env...)
57 return func(args ...string) (string, error) {
58 t.Helper()
59 cmd := exec.Command(bin, args...)
60 cmd.Env = full
61 out, err := cmd.CombinedOutput()
62 return string(out), err
63 }
64}
65
66func TestMain(m *testing.M) {
67 // The GeoIP verifier verb: openGeoIP re-runs its own binary to verify
68 // the override in a child process, and under `go test` that binary is
69 // this one.
70 if len(os.Args) > 1 && os.Args[1] == console.GeoipVerifyVerb {
71 os.Exit(console.RunGeoipVerify(os.Args[2:], os.Stdout))
72 }
73 code := m.Run()
74 if buildDir != "" {
75 os.RemoveAll(buildDir)
76 }
77 os.Exit(code)
78}
79
80// migrate-app is the CONSOLE half of `carlos accounts migrate`. It lives here
81// rather than in the carlos binary on purpose: the console's app→account row
82// is console state, and the carlos CLI has no business opening the console's
83// database — box-side authority changes go through this binary, exactly like
84// `console grant`.
85func TestMigrateAppVerb(t *testing.T) {
86 bin := buildConsole(t)
87 tmp := t.TempDir()
88 dbPath := filepath.Join(tmp, "console.db")
89 run := runner(t, bin, "CONSOLE_DB="+dbPath)
90
91 // A console in the shape a deployment is in before the migration: ops
92 // owns kass.
93 st, err := console.Open(dbPath)
94 if err != nil {
95 t.Fatal(err)
96 }
97 now := time.Now()
98 if err := st.CreateAccount(console.OpsAccountID, "bab", "ops", "paul@keymail.dev", now); err != nil {
99 t.Fatal(err)
100 }
101 if err := st.CreateAccount(793481920157, "kqmvexample", "Kass", "paul@keymail.dev", now); err != nil {
102 t.Fatal(err)
103 }
104 if _, err := st.CreateAppIn(console.OpsAccountID, "kass", "paul@keymail.dev", now); err != nil {
105 t.Fatal(err)
106 }
107 st.Close()
108
109 // Unknown app and unknown account are refused, clearly, before anything
110 // is written.
111 if out, err := run("migrate-app", "-app", "ghost", "-to", "kqmvexample"); err == nil {
112 t.Errorf("migrating an unknown app succeeded: %s", out)
113 } else if !strings.Contains(out, "ghost") {
114 t.Errorf("unknown-app error does not name the app: %s", out)
115 }
116 if out, err := run("migrate-app", "-app", "kass", "-to", "nosuch"); err == nil {
117 t.Errorf("migrating to an unknown account succeeded: %s", out)
118 } else if !strings.Contains(out, "nosuch") {
119 t.Errorf("unknown-account error does not name the account: %s", out)
120 }
121
122 // The move.
123 if out, err := run("migrate-app", "-app", "kass", "-to", "kqmvexample"); err != nil {
124 t.Fatalf("migrate-app: %v\n%s", err, out)
125 }
126 st, err = console.Open(dbPath)
127 if err != nil {
128 t.Fatal(err)
129 }
130 apps, err := st.AppsIn(793481920157)
131 if err != nil || len(apps) != 1 || apps[0] != "kass" {
132 t.Fatalf("AppsIn(Kass) = %v %v, want [kass]", apps, err)
133 }
134 if apps, _ := st.AppsIn(console.OpsAccountID); len(apps) != 0 {
135 t.Errorf("app still in ops: %v", apps)
136 }
137 st.Close()
138
139 // Idempotent: an operator who is not sure whether it landed can just run
140 // it again.
141 out, err := run("migrate-app", "-app", "kass", "-to", "kqmvexample")
142 if err != nil {
143 t.Fatalf("re-running migrate-app must be a no-op: %v\n%s", err, out)
144 }
145 if !strings.Contains(out, "already") {
146 t.Errorf("re-run output does not say it was already done: %s", out)
147 }
148 st, err = console.Open(dbPath)
149 if err != nil {
150 t.Fatal(err)
151 }
152 defer st.Close()
153 if apps, _ := st.AppsIn(793481920157); len(apps) != 1 {
154 t.Errorf("re-run changed things: %v", apps)
155 }
156}
157