package console import ( "net/http" "reflect" "sort" "testing" "time" "github.com/carlosframework/platform/internal/svcrole" ) // deployDoors is spec §6, verbatim. A door added later is refused to a // deploy credential unless someone adds it here on purpose. var deployDoors = []string{ "GET /api/cli/apps", "GET /api/cli/apps/{account}/{app}/channels", "POST /api/cli/apps/{account}/{app}/releases", "GET /api/cli/apps/{account}/{app}/releases", "POST /api/cli/apps/{account}/{app}/promote", "POST /api/cli/apps/{account}/{app}/rollback", "GET /api/cli/apps/{account}/{app}/tags", "GET /api/cli/apps/{account}/{app}/tags/{version}", "GET /api/cli/apps/{account}/{app}/version-target", "GET /api/cli/apps/{account}/{app}/pipeline", "GET /api/cli/apps/{account}/{app}/pending", "GET /api/cli/apps/{account}/{app}/instances", // R6: carlos deploy "GET /api/cli/apps/{account}/{app}/regions", // R6: carlos deploy --region } func TestDeployReachesExactlyItsDoors(t *testing.T) { s := newConsoleFixture(t).srv var got []string for pattern, set := range s.cliRoles { if set.Permits(svcrole.Deploy) { got = append(got, pattern) } } sort.Strings(got) want := append([]string(nil), deployDoors...) sort.Strings(want) if !reflect.DeepEqual(got, want) { t.Fatalf("deploy reaches:\n got %q\nwant %q", got, want) } } // Review Focus 2: the app binding holds for deploy like any service. func TestDeployIsBoundToItsApp(t *testing.T) { fx := newConsoleFixture(t) for _, app := range []string{"hello", "other"} { if _, err := fx.st.CreateAppIn(OpsAccountID, app, "paul@keymail.dev", time.Now()); err != nil { t.Fatal(err) } } tok, err := fx.st.NewServicePrincipalScoped(OpsAccountID, "ci", "hello", svcrole.Deploy, []string{"edge"}, nil, "paul@keymail.dev", time.Now()) if err != nil { t.Fatal(err) } // Sibling app: deploy reaches "other" (exists) but its token is bound to "hello" statusA, bodyA := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/other/promote", map[string]string{"version": "v1", "channel": "edge"}, tok) if statusA != http.StatusNotFound { t.Fatalf("deploy on a sibling app = %d, want 404", statusA) } // Nonexistent app: deploy reaches "nosuchapp" (does not exist) on the same account statusB, bodyB := postRaw(t, fx.ts.URL+"/api/cli/apps/bab/nosuchapp/promote", map[string]string{"version": "v1", "channel": "edge"}, tok) if statusB != http.StatusNotFound { t.Fatalf("deploy on a nonexistent app = %d, want 404", statusB) } // Both refusals must be byte-identical to avoid leaking whether "other" exists if bodyA != bodyB { t.Fatalf("app-binding 404 is not identical to nonexistent-app 404:\n(sibling) %q\n(nonexistent) %q", bodyA, bodyB) } // And a door outside §6 is the role gate's 403, not a 404. status, out := postJSON(t, fx.ts.URL+"/api/cli/apps/bab/hello/delete", map[string]string{}, tok) if status != http.StatusForbidden { t.Fatalf("deploy on trash = %d %v, want 403", status, out) } }