// End-to-end acceptance test for `carlos sidecars` (Task 14): the member // verbs — enable/create/ls/wake/rm — working through the console API with // `carlos auth login` alone, mirroring TestE2EInstancesThroughConsole's // shape (instances_e2e_test.go) exactly: a REAL console.Server over a // DirStore, a device-flow login landing a real bearer token in a temp // credential file, then — with NO store env set — the real run* functions // driven in-process. Ground truth is the DirStore's own manifest and // record objects, plus the `stdout` seam's confirmation lines. package main import ( "context" "net/http" "net/http/httptest" "path/filepath" "strings" "testing" "time" "github.com/carlosframework/platform/internal/console" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) func TestE2ESidecarsThroughConsole(t *testing.T) { useTempCredentials(t) clearDeploymentEnv(t) checkNoRegistry := requireNoRegistryTouch(t) tmp := t.TempDir() relStore := release.NewDirStore(filepath.Join(tmp, "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) } cst, err := console.Open(filepath.Join(tmp, "console.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { cst.Close() }) var srv *console.Server ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv.ServeHTTP(w, r) })) t.Cleanup(ts.Close) srv, err = console.NewServer(console.Config{ Origin: ts.URL, Keymail: "http://keymail.invalid", // never dialled — see e2eDeviceLogin Registry: filepath.Join(tmp, "registry.db"), Releases: relStore, ReleasePub: pub, Key: priv, AppScheme: "http", AppsDomain: "oncarlos.test", }, cst) if err != nil { t.Fatal(err) } const addr = "paul@keymail.dev" if err := cst.AddMember(console.OpsAccountID, addr, console.RoleOwner, time.Now()); err != nil { t.Fatal(err) } if _, err := cst.CreateAppIn(console.OpsAccountID, "titogo", addr, time.Now()); err != nil { t.Fatal(err) } token, expiresAt := e2eDeviceLogin(t, ts.URL, 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) } // --- enable: member-facing manifest write, no domain/host to pin ------- buf := captureStdout(t) runSidecarsEnable([]string{"-app", "titogo", "-account", "ops", "-channel", "beta", "-args", "sidecar run", "-placement", "no-inbound"}) if want := "enabled sidecars for titogo"; !strings.Contains(buf.String(), want) { t.Fatalf("enable stdout = %q, want to contain %q", buf.String(), want) } m, err := provision.GetSidecarManifest(context.Background(), relStore, release.OpsAccount, "titogo") if err != nil { t.Fatalf("manifest after enable: %v", err) } if m.Channel != "beta" || m.Placement != "no-inbound" || len(m.Args) != 2 || m.Args[0] != "sidecar" || m.Args[1] != "run" { t.Fatalf("manifest = %+v, want channel beta, placement no-inbound, args [sidecar run]", m) } // --- create: the derived synthetic host, one label under the account's // sqid (release.OpsAccount == "bab" in this harness, instances_e2e_test.go's // own pin) --------------------------------------------------------------- const wantHost = "bot-acme.titogo.bab.sidecar.internal" buf.Reset() runSidecarsCreate([]string{"-app", "titogo", "-account", "ops", "bot-acme"}) if want := "created sidecar bot-acme (channel beta)"; !strings.Contains(buf.String(), want) { t.Fatalf("create stdout = %q, want %q", buf.String(), want) } rec, err := provision.GetSidecarRecord(context.Background(), relStore, release.OpsAccount, "titogo", "bot-acme") if err != nil { t.Fatalf("record after create: %v", err) } if rec.Channel != "beta" || rec.Name != "bot-acme" || rec.CreatedBy != addr || rec.Host != wantHost { t.Fatalf("record = %+v, want beta/bot-acme created by %s at host %s", rec, addr, wantHost) } // --- ls: short name by default, honest "unknown" version since no box // has ever reported this fresh record ------------------------------------ buf.Reset() runSidecarsList([]string{"-app", "titogo", "-account", "ops"}) shortOut := buf.String() if !strings.Contains(shortOut, "bot-acme") { t.Fatalf("ls stdout = %q, want the short name bot-acme", shortOut) } if strings.Contains(shortOut, wantHost) { t.Fatalf("ls stdout = %q, must NOT show the synthetic host without -long", shortOut) } if !strings.Contains(shortOut, "unknown") { t.Fatalf("ls stdout = %q, want VERSION unknown — the honesty rule: a stamp that overclaims is worse than an absent one", shortOut) } // --- ls -long: the synthetic host, not the short name alone ------------- buf.Reset() runSidecarsList([]string{"-app", "titogo", "-account", "ops", "-long"}) longOut := buf.String() if !strings.Contains(longOut, wantHost) { t.Fatalf("ls -long stdout = %q, want the synthetic host %q", longOut, wantHost) } // --- wake ----------------------------------------------------------- buf.Reset() runSidecarsWake([]string{"-app", "titogo", "-account", "ops", "bot-acme"}) if want := "requested wake for bot-acme"; !strings.Contains(buf.String(), want) { t.Fatalf("wake stdout = %q, want %q", buf.String(), want) } // --- rm: idempotent record retirement ------------------------------ buf.Reset() runSidecarsRm([]string{"-app", "titogo", "-account", "ops", "bot-acme"}) if want := "deleted sidecar bot-acme"; !strings.Contains(buf.String(), want) { t.Fatalf("rm stdout = %q, want %q", buf.String(), want) } if _, err := provision.GetSidecarRecord(context.Background(), relStore, release.OpsAccount, "titogo", "bot-acme"); err == nil { t.Fatalf("record survived rm") } // The whole flow ran through the console: nothing here may have touched // a local registry. checkNoRegistry() }