package main import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "sync/atomic" "testing" "time" "github.com/carlosframework/platform/internal/console" "github.com/carlosframework/platform/internal/release" ) // regionE2E is an in-process console with titogo claimed at appRegion, the // signed-in CLI credentials saved, and a front that can fail or strip the // /regions answer and counts every write reaching the console. type regionE2E struct { bin string // built before useTempCredentials changes directory creds string url string store release.Store mode atomic.Value // "", "fail", "legacy", "region-unknown", "placement-unknown" writes atomic.Int64 // creates counts instance-create requests (POST …/instances), so a // test can tell a duplicate create from the other writes (Task 22). creates atomic.Int64 cst *console.Store // failGet is a store key whose reads fail while the listing succeeds, // the way one bucket GET can (Task 22, codex plan-R3 1). failGet atomic.Value } // flakyStore fails every read of one key. type flakyStore struct { release.Store key *atomic.Value } func (f flakyStore) Get(ctx context.Context, key string) ([]byte, string, error) { if k, _ := f.key.Load().(string); k != "" && k == key { return nil, "", errors.New("read timeout") } return f.Store.Get(ctx, key) } // DeleteIfMatch forwards the conditional delete the wrapped DirStore has, // as production's S3 and directory stores do: the embedded interface does // not promote it, and the console's instance delete is conditional on the // etag its import check read (final review F7), with no unconditional // fallback. func (f flakyStore) DeleteIfMatch(ctx context.Context, key, etag string) error { return release.DeleteIfMatch(ctx, f.Store, key, etag) } // regionE2EConsole starts the console with titogo claimed in appRegion. An // empty appRegion is a home app, the way CreateAppIn records one: stored "". func regionE2EConsole(t *testing.T, appRegion string) *regionE2E { t.Helper() bin := buildCarlos(t) // first: useTempCredentials chdirs out of the module creds := useTempCredentials(t) clearDeploymentEnv(t) const addr, app = "paul@keymail.dev", "titogo" tmp := t.TempDir() e := ®ionE2E{bin: bin, creds: creds, store: release.NewDirStore(filepath.Join(tmp, "store"))} e.mode.Store("") e.failGet.Store("") seed, pubB64, err := release.GenerateKey() if err != nil { t.Fatal(err) } priv, err := release.ParseSigningKey(seed, "e2e") if err != nil { t.Fatal(err) } pub, err := release.ParsePublicKey(pubB64, "e2e") if err != nil { t.Fatal(err) } e.cst, err = console.Open(filepath.Join(tmp, "console.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { e.cst.Close() }) var srv *console.Server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet && r.Method != http.MethodHead && !strings.Contains(r.URL.Path, "/auth/") { e.writes.Add(1) } if r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/instances") { e.creates.Add(1) } if strings.HasSuffix(r.URL.Path, "/regions") { switch e.mode.Load().(string) { case "fail": http.Error(w, `{"error":"boom"}`, http.StatusInternalServerError) return case "legacy", "region-unknown", "placement-unknown": // A successful 200 whose body an older console, or a // console that could not read one fact, would send. rec := httptest.NewRecorder() srv.ServeHTTP(rec, r) var m map[string]any json.Unmarshal(rec.Body.Bytes(), &m) d, _ := m["app_default"].(map[string]any) switch e.mode.Load().(string) { case "legacy": delete(m, "app_default") case "region-unknown": d["region_known"], d["region"], d["effective"] = false, "", "" case "placement-unknown": d["placement_known"], d["reason"] = false, "policy-unreadable" } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(m) return } } srv.ServeHTTP(w, r) })) t.Cleanup(ts.Close) e.url = ts.URL srv, err = console.NewServer(console.Config{ Origin: ts.URL, Keymail: "http://keymail.invalid", Registry: filepath.Join(tmp, "registry.db"), Releases: flakyStore{e.store, &e.failGet}, ReleasePub: pub, Key: priv, AppScheme: "http", AppsDomain: "oncarlos.test", HomeRegion: "ie", }, e.cst) if err != nil { t.Fatal(err) } if err := e.cst.AddMember(console.OpsAccountID, addr, console.RoleOwner, time.Now()); err != nil { t.Fatal(err) } if appRegion == "" { _, err = e.cst.CreateAppIn(console.OpsAccountID, app, addr, time.Now()) } else { _, err = e.cst.CreateAppInRegion(console.OpsAccountID, app, addr, appRegion, "chosen", time.Now()) } if err != nil { t.Fatal(err) } token, expiresAt := e2eDeviceLogin(t, ts.URL, e.cst, addr, "e2e@laptop") credFile, err := loadCredentials() if err != nil { t.Fatal(err) } credFile.put(credential{Console: ts.URL, Address: addr, Token: token, ExpiresAt: expiresAt}) if err := credFile.save(); err != nil { t.Fatal(err) } e.writes.Store(0) // the login above is setup, not the command under test return e } // run runs the built carlos binary as a signed-in user with no project // config in reach. func (e *regionE2E) run(t *testing.T, args ...string) (string, error) { t.Helper() cmd := exec.Command(e.bin, args...) cmd.Env = carlosTestEnv(t, "CARLOS_CREDENTIALS="+e.creds) cmd.Dir = t.TempDir() out, err := cmd.CombinedOutput() return string(out), err } func (e *regionE2E) releases(t *testing.T) int { t.Helper() keys, err := e.store.List(context.Background(), release.AppPrefix(release.OpsAccount, "titogo")) if err != nil { t.Fatal(err) } n := 0 for _, k := range keys { if strings.Contains(k, "/releases/") { n++ } } return n } func artifact(t *testing.T) string { t.Helper() p := filepath.Join(t.TempDir(), "app") if err := os.WriteFile(p, []byte("#!/bin/sh\n"), 0o755); err != nil { t.Fatal(err) } return p } // deployAsking runs `carlos deploy --region asked` on titogo. func (e *regionE2E) deployAsking(t *testing.T, asked string) (string, error) { t.Helper() return e.run(t, "deploy", "--app", "titogo", "--region", asked, "--console", e.url, "--channel", "canary/e2e", "--version", "abc1234", "--timeout", "1s", artifact(t)) } // Every refusal is the whole approved sentence, with the app and the place // in their own slots, and happens before any write. func TestDeployRegionRefusalsShipNothing(t *testing.T) { for _, c := range []struct{ mode, want string }{ // C15: the stored region is uk; with no edge publishing a name for // it, the console's place is the code itself. {"", "deploy: " + deployRegionMismatch("titogo", "uk")}, // C15b: its %v is the console's own words, so the fixed lead and // tail are asserted whole. {"fail", "deploy: could not read titogo's region ("}, {"region-unknown", "deploy: " + deployRegionUnknown("titogo")}, // C15c: a 200 that does not know } { // A subtest each: useTempCredentials chdirs, and only a subtest // restores the directory before the next case builds the binary. t.Run("mode="+c.mode, func(t *testing.T) { e := regionE2EConsole(t, "uk") e.mode.Store(c.mode) out, err := e.deployAsking(t, "ie") if err == nil || !strings.Contains(out, c.want) { t.Fatalf("err=%v out=%s, want a refusal containing %q", err, out, c.want) } if c.mode == "fail" && !strings.Contains(out, "), so deploy stopped before shipping anything. Try again, or run without --region.") { t.Fatalf("C15b tail missing: %s", out) } if n := e.writes.Load(); n != 0 { t.Fatalf("%d write(s) reached the console before the refusal", n) } if n := e.releases(t); n != 0 { t.Fatalf("%d release object(s) were shipped", n) } }) } } // An older console (a successful /regions without app_default) skips the // check, and the deploy goes on to ship. func TestDeployRegionSkipsAgainstAnOlderConsole(t *testing.T) { e := regionE2EConsole(t, "uk") e.mode.Store("legacy") out, _ := e.deployAsking(t, "ie") if strings.Contains(out, deployRegionMismatch("titogo", "")[:20]) || strings.Contains(out, "deploy stopped before shipping") { t.Fatalf("an older console refused: %s", out) } if e.writes.Load() == 0 || e.releases(t) == 0 { t.Fatalf("deploy never shipped: %s", out) } } // --region naming the home on an app stored as "" (the home) is the same // region, so the deploy ships; naming the stored region on a placed app does // too. func TestDeployRegionNamingTheStoredRegionShips(t *testing.T) { for _, c := range []struct{ name, stored, asked string }{ {"home app, home named", "", "ie"}, {"placed app, its region named", "uk", "uk"}, } { t.Run(c.name, func(t *testing.T) { e := regionE2EConsole(t, c.stored) out, _ := e.deployAsking(t, c.asked) if strings.Contains(out, "already runs in") || strings.Contains(out, "deploy stopped before shipping") { t.Fatalf("the matching region was refused: %s", out) } if e.writes.Load() == 0 || e.releases(t) == 0 { t.Fatalf("deploy never shipped: %s", out) } }) } }