| 1 | package console |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "net/http" |
| 7 | "strings" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/objstore" |
| 11 | ) |
| 12 | |
| 13 | // The store doors take the environment as ?environment=, the same spelling |
| 14 | // the config plane uses — a store delivers INTO a bundle, so the two naming |
| 15 | // it differently would be a seam with no meaning. These pin the doors that |
| 16 | // need no backend: declare and status. |
| 17 | |
| 18 | func storeEnvURL(fx *consoleFixture, app, env string) string { |
| 19 | return storeURL(fx, app) + "?environment=" + env |
| 20 | } |
| 21 | |
| 22 | func decodeReply(t *testing.T, body string) map[string]any { |
| 23 | t.Helper() |
| 24 | var out map[string]any |
| 25 | if err := json.Unmarshal([]byte(body), &out); err != nil { |
| 26 | t.Fatalf("decode reply %q: %v", body, err) |
| 27 | } |
| 28 | return out |
| 29 | } |
| 30 | |
| 31 | // Declaring into an environment writes THAT environment's record, stamped |
| 32 | // with the environment it lives under, and leaves the app's own store absent. |
| 33 | func TestAPIStoreCreateInEnvironment(t *testing.T) { |
| 34 | fx, token := storeFixture(t, "code-store-env-create") |
| 35 | |
| 36 | status, body := postRaw(t, storeEnvURL(fx, "titogo", "beta"), map[string]any{"env_prefix": "BLOBS"}, token) |
| 37 | if status != http.StatusOK { |
| 38 | t.Fatalf("create in an environment = %d %s, want 200", status, body) |
| 39 | } |
| 40 | if got := decodeReply(t, body)["environment"]; got != "beta" { |
| 41 | t.Errorf("reply environment = %v, want %q", got, "beta") |
| 42 | } |
| 43 | |
| 44 | ctx := context.Background() |
| 45 | rec, _, err := objstore.GetIn(ctx, fx.rel, "bab", "titogo", "beta") |
| 46 | if err != nil || rec == nil { |
| 47 | t.Fatalf("environment record = %+v, %v", rec, err) |
| 48 | } |
| 49 | if rec.Environment != "beta" { |
| 50 | t.Errorf("stored environment = %q, want %q", rec.Environment, "beta") |
| 51 | } |
| 52 | if rec.Declared == nil || rec.Declared.EnvPrefix != "BLOBS" { |
| 53 | t.Errorf("declared = %+v, want the BLOBS prefix", rec.Declared) |
| 54 | } |
| 55 | // The app's own store is a different store, and nobody declared it. |
| 56 | appRec, _, err := objstore.Get(ctx, fx.rel, "bab", "titogo") |
| 57 | if err != nil { |
| 58 | t.Fatal(err) |
| 59 | } |
| 60 | if appRec != nil { |
| 61 | t.Errorf("declaring in an environment also wrote the app's own store: %+v", appRec) |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | // An environment name the config plane would refuse is refused here, before |
| 66 | // anything is written — including "default", which names the app's own |
| 67 | // bundle rather than a named overlay. |
| 68 | func TestAPIStoreEnvironmentNameRefused(t *testing.T) { |
| 69 | fx, token := storeFixture(t, "code-store-env-bad") |
| 70 | |
| 71 | for _, env := range []string{"Beta", "default", "a_b", "this-name-is-far-too-long-to-be-an-environment"} { |
| 72 | status, body := postRaw(t, storeEnvURL(fx, "titogo", env), map[string]any{}, token) |
| 73 | if status != http.StatusBadRequest { |
| 74 | t.Errorf("create with environment %q = %d %s, want 400", env, status, body) |
| 75 | } |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | // Status answers about the store it was asked about, and an app-store status |
| 80 | // additionally names every environment this app has a store for — an |
| 81 | // environment store nobody remembers is a bucket nobody remembers. |
| 82 | func TestAPIStoreStatusListsEnvironments(t *testing.T) { |
| 83 | fx, token := storeFixture(t, "code-store-env-status") |
| 84 | |
| 85 | for _, env := range []string{"beta", "alpha"} { |
| 86 | if status, body := postRaw(t, storeEnvURL(fx, "titogo", env), map[string]any{}, token); status != http.StatusOK { |
| 87 | t.Fatalf("declare %s = %d %s", env, status, body) |
| 88 | } |
| 89 | } |
| 90 | // The app's own store, so the app-store status has something to answer |
| 91 | // about at all. |
| 92 | if status, body := postRaw(t, storeURL(fx, "titogo"), map[string]any{}, token); status != http.StatusOK { |
| 93 | t.Fatalf("declare the app store = %d %s", status, body) |
| 94 | } |
| 95 | |
| 96 | status, body := getRaw(t, storeURL(fx, "titogo"), token) |
| 97 | if status != http.StatusOK { |
| 98 | t.Fatalf("app store status = %d %s", status, body) |
| 99 | } |
| 100 | reply := decodeReply(t, body) |
| 101 | if got := reply["environment"]; got != "" { |
| 102 | t.Errorf("app store status environment = %v, want \"\"", got) |
| 103 | } |
| 104 | envs, _ := reply["environments"].([]any) |
| 105 | if len(envs) != 2 || envs[0] != "alpha" || envs[1] != "beta" { |
| 106 | t.Errorf("environments = %v, want [alpha beta] sorted", envs) |
| 107 | } |
| 108 | |
| 109 | // Asked about one environment, it answers about that one and lists none. |
| 110 | status, body = getRaw(t, storeEnvURL(fx, "titogo", "beta"), token) |
| 111 | if status != http.StatusOK { |
| 112 | t.Fatalf("environment status = %d %s", status, body) |
| 113 | } |
| 114 | reply = decodeReply(t, body) |
| 115 | if got := reply["environment"]; got != "beta" { |
| 116 | t.Errorf("environment status environment = %v, want %q", got, "beta") |
| 117 | } |
| 118 | if _, ok := reply["environments"]; ok { |
| 119 | t.Error("a status aimed at one environment listed the app's environments") |
| 120 | } |
| 121 | } |
| 122 | |
| 123 | // An environment with no store 404s with a sentence naming the verb that |
| 124 | // would create it — the app-store door's own convention, carried over. |
| 125 | func TestAPIStoreStatusUndeclaredEnvironment(t *testing.T) { |
| 126 | fx, token := storeFixture(t, "code-store-env-404") |
| 127 | |
| 128 | status, body := getRaw(t, storeEnvURL(fx, "titogo", "ghost"), token) |
| 129 | if status != http.StatusNotFound { |
| 130 | t.Fatalf("status for an undeclared environment = %d %s, want 404", status, body) |
| 131 | } |
| 132 | if !jsonErrorContains(t, body, "--environment ghost") { |
| 133 | t.Errorf("404 body = %q, want it to name the environment and the verb", body) |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | func jsonErrorContains(t *testing.T, body, want string) bool { |
| 138 | t.Helper() |
| 139 | var out struct { |
| 140 | Error string `json:"error"` |
| 141 | } |
| 142 | if err := json.Unmarshal([]byte(body), &out); err != nil { |
| 143 | return false |
| 144 | } |
| 145 | return out.Error != "" && strings.Contains(out.Error, want) |
| 146 | } |
| 147 | |