package main import ( "bufio" "bytes" "context" "database/sql" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "os" "os/exec" "path/filepath" "strings" "syscall" "testing" "time" "git.ipao.vip/rogee/creator-hub/internal/hub" "git.ipao.vip/rogee/creator-hub/internal/phasea" "git.ipao.vip/rogee/creator-hub/internal/taskstate" "github.com/gofiber/fiber/v3" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) func TestExecuteContextStopsOnSIGTERM(t *testing.T) { if os.Getenv("CREATORHUB_SIGTERM_HELPER") == "1" { command := &cobra.Command{Use: "shutdown-test", RunE: func(command *cobra.Command, _ []string) error { _, _ = os.Stdout.WriteString("ready\n") <-command.Context().Done() _, _ = os.Stdout.WriteString("stopped\n") return nil }} if err := execute(command); err != nil { t.Fatal(err) } return } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() process := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestExecuteContextStopsOnSIGTERM$") process.Env = append(os.Environ(), "CREATORHUB_SIGTERM_HELPER=1") stdout, err := process.StdoutPipe() if err != nil { t.Fatal(err) } if err := process.Start(); err != nil { t.Fatal(err) } scanner := bufio.NewScanner(stdout) if !scanner.Scan() || scanner.Text() != "ready" { t.Fatalf("helper did not become ready: %q err=%v", scanner.Text(), scanner.Err()) } if err := process.Process.Signal(syscall.SIGTERM); err != nil { t.Fatal(err) } if !scanner.Scan() || scanner.Text() != "stopped" { t.Fatalf("helper did not finish cleanup: %q err=%v", scanner.Text(), scanner.Err()) } if err := process.Wait(); err != nil || ctx.Err() != nil { t.Fatalf("process did not exit gracefully after SIGTERM: wait=%v context=%v", err, ctx.Err()) } } func TestLoadConfigRejectsInvalidDatabase(t *testing.T) { t.Setenv("CONTROL_PLANE_USERNAME", "operator") t.Setenv("CONTROL_PLANE_PASSWORD", "unit-test-password") t.Setenv("DATABASE_URL", "file:///tmp/creatorhub.db") if _, err := loadConfig(); err == nil { t.Fatal("expected non-Postgres database URL to be rejected") } } func TestLoadConfigRequiresControlPlaneCredentials(t *testing.T) { t.Setenv("CONTROL_PLANE_USERNAME", "") t.Setenv("CONTROL_PLANE_PASSWORD", "") if _, err := loadConfig(); err == nil { t.Fatal("expected missing control-plane credentials to be rejected") } } func TestLoadConfigPasswordLength(t *testing.T) { t.Setenv("CONTROL_PLANE_USERNAME", "operator") t.Setenv("CREATORHUB_CREDENTIAL_MASTER_KEY", "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") for _, test := range []struct { password string wantErr bool }{ {"12345", true}, {"123456", false}, } { t.Run(test.password, func(t *testing.T) { t.Setenv("CONTROL_PLANE_PASSWORD", test.password) _, err := loadConfig() if (err != nil) != test.wantErr { t.Fatalf("loadConfig() error = %v, wantErr %v", err, test.wantErr) } }) } } func TestLoadConfigRequiresCredentialMasterKey(t *testing.T) { t.Setenv("CONTROL_PLANE_USERNAME", "operator") t.Setenv("CONTROL_PLANE_PASSWORD", "unit-test-password") for _, key := range []string{"", "not-base64", "c2hvcnQ="} { t.Setenv("CREATORHUB_CREDENTIAL_MASTER_KEY", key) if _, err := loadConfig(); err == nil { t.Fatalf("accepted invalid credential master key %q", key) } } } func TestControlPlaneAuthentication(t *testing.T) { logger := logrus.StandardLogger() previousOutput := logger.Out var logs bytes.Buffer logrus.SetOutput(&logs) t.Cleanup(func() { logrus.SetOutput(previousOutput) }) directory := t.TempDir() if err := os.WriteFile(filepath.Join(directory, "index.html"), []byte("index"), 0o600); err != nil { t.Fatal(err) } app := newHandler(directory, "operator", "unit-test-password") health, err := app.Test(httptest.NewRequest(http.MethodGet, "/healthz", nil)) if err != nil || health.StatusCode != http.StatusNoContent { t.Fatalf("health check must remain public: status=%d err=%v", health.StatusCode, err) } health.Body.Close() for _, path := range []string{ "/", "/api/phase-a/accounts", "/api/browsers", "/api/network-exits", "/api/phase-a/tasks", "/api/phase-a/audit", } { request := httptest.NewRequest(http.MethodGet, path, nil) response, err := app.Test(request) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != http.StatusUnauthorized || response.Header.Get("WWW-Authenticate") == "" { t.Fatalf("GET %s was not protected: status=%d", path, response.StatusCode) } } for name, test := range map[string]struct { user, password string want int }{ "valid": {"operator", "unit-test-password", http.StatusOK}, "wrong user": {"other", "unit-test-password", http.StatusUnauthorized}, "wrong password": {"operator", "credential-must-not-be-logged", http.StatusUnauthorized}, } { t.Run(name, func(t *testing.T) { request := httptest.NewRequest(http.MethodGet, "/", nil) request.SetBasicAuth(test.user, test.password) response, err := app.Test(request) if err != nil { t.Fatal(err) } response.Body.Close() if response.StatusCode != test.want { t.Fatalf("status=%d want=%d", response.StatusCode, test.want) } }) } if strings.Contains(logs.String(), "credential-must-not-be-logged") { t.Fatalf("authentication credential reached logs: %s", logs.String()) } } type controlPlaneRouteCase struct { method, pattern, path, body string wantAuthenticatedStatus int } type testCredentialBridge struct { values map[string]string } func (bridge *testCredentialBridge) Store(ctx context.Context, _ phasea.CredentialReference, key, value string) error { if err := ctx.Err(); err != nil { return err } bridge.values[key] = value return nil } func (bridge *testCredentialBridge) Delete(ctx context.Context, _ phasea.CredentialReference, key string) error { if err := ctx.Err(); err != nil { return err } delete(bridge.values, key) return nil } func TestControlPlaneRouteRegistrationMatrix(t *testing.T) { app := fiber.New() app.Use(authenticate("operator", "unit-test-password")) registerHubWithNetwork(app, nil, nil, nil) registerPhaseA(app, nil, nil, nil) routes := controlPlaneRouteMatrix() assertControlPlaneRouteMatrix(t, app, routes) for _, route := range routes { if response := do(app, route.method, route.path, route.body); response.Code != http.StatusUnauthorized { t.Fatalf("registered route %s %s returned %d without credentials, want %d", route.method, route.path, response.Code, http.StatusUnauthorized) } } if response := do(app, http.MethodPost, "/api/not-registered", ""); response.Code != http.StatusUnauthorized { t.Fatalf("unregistered route returned %d without credentials, want %d", response.Code, http.StatusUnauthorized) } } func TestControlPlaneRegisteredRouteMatrix(t *testing.T) { databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if databaseURL == "" { t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run control-plane route coverage") } databaseURL = isolatedControlPlaneDatabaseURL(t, databaseURL) ctx := context.Background() phaseAStore, err := phasea.Open(ctx, databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = phaseAStore.Close() }) hubStore, err := hub.Open(ctx, databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = hubStore.Close() }) webDirectory := t.TempDir() if err := os.WriteFile(filepath.Join(webDirectory, "index.html"), []byte("index"), 0o600); err != nil { t.Fatal(err) } app := newHandlerWithStores(webDirectory, "operator", "unit-test-password", phaseAStore, hubStore) routes := controlPlaneRouteMatrix() assertControlPlaneRouteMatrix(t, app, routes) for _, route := range routes { route := route t.Run(route.method+" "+route.pattern, func(t *testing.T) { if response := do(app, route.method, route.path, route.body); response.Code != http.StatusUnauthorized { t.Fatalf("unauthenticated %s %s returned %d, want %d", route.method, route.path, response.Code, http.StatusUnauthorized) } response := do(app, route.method, route.path, route.body, "operator", "unit-test-password") if response.Code != route.wantAuthenticatedStatus { t.Fatalf("authenticated %s %s returned %d, want %d: %s", route.method, route.path, response.Code, route.wantAuthenticatedStatus, response.Body.String()) } }) } for _, route := range []struct { method, path string want int }{ {http.MethodPost, "/api/not-registered", http.StatusMethodNotAllowed}, {http.MethodDelete, "/api/not-registered", http.StatusMethodNotAllowed}, } { t.Run("unregistered "+route.method, func(t *testing.T) { if response := do(app, route.method, route.path, ""); response.Code != http.StatusUnauthorized { t.Fatalf("unauthenticated unregistered %s %s returned %d, want %d", route.method, route.path, response.Code, http.StatusUnauthorized) } if response := do(app, route.method, route.path, "", "operator", "unit-test-password"); response.Code != route.want { t.Fatalf("authenticated unregistered %s %s returned %d, want %d", route.method, route.path, response.Code, route.want) } }) } } func assertControlPlaneRouteMatrix(t *testing.T, app *fiber.App, routes []controlPlaneRouteCase) { t.Helper() expected := make(map[string]struct{}, len(routes)) for _, route := range routes { key := controlPlaneRouteKey(route.method, route.pattern) if _, exists := expected[key]; exists { t.Fatalf("duplicate route matrix entry: %s", key) } expected[key] = struct{}{} } actual := registeredControlPlaneRoutes(app) for key := range expected { if _, exists := actual[key]; !exists { t.Errorf("route matrix is missing registered route: %s", key) } } for key := range actual { if _, exists := expected[key]; !exists { t.Errorf("registered API route is missing from route matrix: %s", key) } } } func controlPlaneRouteKey(method, path string) string { return method + " " + path } func registeredControlPlaneRoutes(app *fiber.App) map[string]struct{} { routes := map[string]struct{}{} for _, methodRoutes := range app.Stack() { for _, route := range methodRoutes { if !strings.HasPrefix(route.Path, "/api/") { continue } switch route.Method { case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete: routes[controlPlaneRouteKey(route.Method, route.Path)] = struct{}{} } } } return routes } func controlPlaneRouteMatrix() []controlPlaneRouteCase { return []controlPlaneRouteCase{ {http.MethodGet, "/api/browsers", "/api/browsers", "", http.StatusOK}, {http.MethodGet, "/api/browsers/:alias", "/api/browsers/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/browsers", "/api/browsers", "", http.StatusBadRequest}, {http.MethodPost, "/api/browsers/:alias/:action", "/api/browsers/missing/start", "", http.StatusNotFound}, {http.MethodDelete, "/api/browsers/:alias", "/api/browsers/missing", "", http.StatusNotFound}, {http.MethodGet, "/api/network-exits", "/api/network-exits", "", http.StatusOK}, {http.MethodGet, "/api/network-exits/:id", "/api/network-exits/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/network-exits", "/api/network-exits", "", http.StatusBadRequest}, {http.MethodPost, "/api/network-exits/:id/check", "/api/network-exits/missing/check", "", http.StatusNotFound}, {http.MethodPost, "/api/network-exits/:id/disable", "/api/network-exits/missing/disable", "", http.StatusNotFound}, {http.MethodGet, "/api/browser-images", "/api/browser-images", "", http.StatusOK}, {http.MethodPost, "/api/browser-images", "/api/browser-images", "", http.StatusBadRequest}, {http.MethodPut, "/api/browser-images/:version", "/api/browser-images/999.0.0", `{"image_ref":"registry.example/browser:missing"}`, http.StatusNotFound}, {http.MethodDelete, "/api/browser-images/:version", "/api/browser-images/999.0.0", "", http.StatusNotFound}, {http.MethodGet, "/api/gateways", "/api/gateways", "", http.StatusOK}, {http.MethodPost, "/api/gateways", "/api/gateways", "", http.StatusBadRequest}, {http.MethodPut, "/api/gateways/:name", "/api/gateways/missing", `{"name":"gw-missing","endpoint":"http://gw-missing:8081","token":""}`, http.StatusNotFound}, {http.MethodDelete, "/api/gateways/:name", "/api/gateways/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts", "/api/phase-a/accounts", "", http.StatusBadRequest}, {http.MethodGet, "/api/phase-a/accounts", "/api/phase-a/accounts", "", http.StatusOK}, {http.MethodGet, "/api/phase-a/accounts/:id", "/api/phase-a/accounts/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts/:id/pause", "/api/phase-a/accounts/missing/pause", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts/:id/resume", "/api/phase-a/accounts/missing/resume", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/accounts/:id/revoke", "/api/phase-a/accounts/missing/revoke", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/drafts", "/api/phase-a/drafts", "", http.StatusBadRequest}, {http.MethodGet, "/api/phase-a/drafts", "/api/phase-a/drafts", "", http.StatusOK}, {http.MethodGet, "/api/phase-a/drafts/:id", "/api/phase-a/drafts/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/confirmations", "/api/phase-a/confirmations", "", http.StatusBadRequest}, {http.MethodGet, "/api/phase-a/confirmations", "/api/phase-a/confirmations", "", http.StatusOK}, {http.MethodGet, "/api/phase-a/confirmations/:id", "/api/phase-a/confirmations/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/tasks", "/api/phase-a/tasks", "", http.StatusBadRequest}, {http.MethodGet, "/api/phase-a/tasks", "/api/phase-a/tasks", "", http.StatusOK}, {http.MethodGet, "/api/phase-a/tasks/:id", "/api/phase-a/tasks/missing", "", http.StatusNotFound}, {http.MethodGet, "/api/phase-a/attempts/:id", "/api/phase-a/attempts/missing", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/tasks/:id/cancel", "/api/phase-a/tasks/missing/cancel", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/tasks/:id/verify", "/api/phase-a/tasks/missing/verify", "", http.StatusBadRequest}, {http.MethodPost, "/api/phase-a/tasks/:id/resume", "/api/phase-a/tasks/missing/resume", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/tasks/:id/finish", "/api/phase-a/tasks/missing/finish", "", http.StatusNotFound}, {http.MethodPost, "/api/phase-a/mock/execute", "/api/phase-a/mock/execute", "", http.StatusBadRequest}, {http.MethodGet, "/api/phase-a/audit", "/api/phase-a/audit", "", http.StatusOK}, } } func TestOperatorNotificationFiltersAndRedacts(t *testing.T) { previousLevel := logrus.GetLevel() t.Cleanup(func() { logrus.SetLevel(previousLevel) }) var output bytes.Buffer notify := newAttentionNotifier(&output) notify(taskstate.Transition{State: "succeeded", ReasonCode: "task_succeeded", AccountID: "account-a", TaskID: "task-a"}) if output.Len() != 0 { t.Fatalf("successful task emitted a notification: %s", output.String()) } for _, level := range []logrus.Level{logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel} { for _, test := range []struct{ state, reason string }{ {"policy_hold", "account_paused"}, {"needs_confirmation", "exit_unhealthy"}, } { output.Reset() logrus.SetLevel(level) notify(taskstate.Transition{State: test.state, ReasonCode: test.reason, AccountID: "account-a", TaskID: "task-a"}) var entry map[string]any if err := json.Unmarshal(output.Bytes(), &entry); err != nil { t.Fatal(err) } if entry["event_type"] != test.state || entry["reason_code"] != test.reason || entry["notification_channel"] != "structured_log" || entry["account_id"] != "account-a" || entry["task_id"] != "task-a" { t.Fatalf("unexpected notification at LOG_LEVEL=%s: %#v", level, entry) } for _, forbidden := range []string{"password", "authorization", "credential", "token", "secret"} { if strings.Contains(strings.ToLower(output.String()), forbidden) { t.Fatalf("notification contained sensitive field %q: %s", forbidden, output.String()) } } } } } func TestPhaseAAccountRequestRejectsUnknownFields(t *testing.T) { for name, body := range map[string]string{ "client id": `{"id":"account-a","name":"账号 A","platform":"douyin","platform_account_key":"a","tags":[],"cookies":"sid=value"}`, "authorization kind": `{"name":"账号 A","platform":"douyin","platform_account_key":"a","tags":[],"cookies":"sid=value","authorization_kind":"owned"}`, "credential reference": `{"name":"账号 A","platform":"douyin","platform_account_key":"a","tags":[],"cookies":"sid=value","credential_reference":{}}`, } { t.Run(name, func(t *testing.T) { app := fiber.New() app.Post("/", func(c fiber.Ctx) error { var input accountRequest if err := decodePhaseA(c, &input); err != nil { return phaseAError(c, err) } return c.SendStatus(http.StatusNoContent) }) response, err := app.Test(httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body))) if err != nil { t.Fatal(err) } defer response.Body.Close() if response.StatusCode != http.StatusBadRequest { t.Fatalf("expected sensitive or unknown field to be rejected, got %d", response.StatusCode) } }) } } func TestPhaseAAccountHTTPWorkflowRedactsSecrets(t *testing.T) { databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if databaseURL == "" { t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage") } ctx := context.Background() databaseURL = isolatedControlPlaneDatabaseURL(t, databaseURL) store, err := phasea.Open(ctx, databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) hubStore, err := hub.Open(ctx, databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = hubStore.Close() }) webDirectory := t.TempDir() if err := os.WriteFile(filepath.Join(webDirectory, "index.html"), []byte("index"), 0o600); err != nil { t.Fatal(err) } protectedApp := newHandlerWithStores(webDirectory, "operator", "unit-test-password", store, hubStore) for _, route := range []struct{ method, path string }{ {http.MethodGet, "/api/phase-a/accounts"}, {http.MethodPost, "/api/phase-a/accounts"}, {http.MethodPost, "/api/phase-a/accounts/account-http/pause"}, {http.MethodGet, "/api/phase-a/tasks"}, {http.MethodPost, "/api/phase-a/mock/execute"}, {http.MethodGet, "/api/phase-a/audit"}, {http.MethodGet, "/api/browsers"}, {http.MethodPost, "/api/browsers/account-http/start"}, {http.MethodDelete, "/api/browsers/account-http"}, {http.MethodPut, "/api/browser-images/1"}, {http.MethodPost, "/api/network-exits/exit-http/check"}, {http.MethodPost, "/api/network-exits/exit-http/disable"}, } { if response := do(protectedApp, route.method, route.path, ""); response.Code != http.StatusUnauthorized { t.Fatalf("unauthenticated %s %s returned %d", route.method, route.path, response.Code) } } for _, path := range []string{"/api/phase-a/accounts", "/api/browsers", "/api/phase-a/audit"} { if response := do(protectedApp, http.MethodGet, path, "", "operator", "unit-test-password"); response.Code != http.StatusOK { t.Fatalf("authenticated GET %s returned %d: %s", path, response.Code, response.Body.String()) } } app := fiber.New() app.Use(authenticate("operator", "unit-test-password")) credentials := &testCredentialBridge{values: map[string]string{}} registerPhaseA(app, store, nil, credentials) cookieHeader := "sessionid=phase-http-secret; token=value" accountBody := `{"name":"Phase HTTP","platform":"douyin","platform_account_key":"phase-http","tags":["测试"],"cookies":"` + cookieHeader + `"}` request := func(method, path, body string, wantStatus int) *httptest.ResponseRecorder { t.Helper() response := do(app, method, path, body, "operator", "unit-test-password") if response.Code != wantStatus { t.Fatalf("%s %s returned %d, want %d: %s", method, path, response.Code, wantStatus, response.Body.String()) } if strings.Contains(response.Body.String(), cookieHeader) { t.Fatalf("%s %s leaked cookies: %s", method, path, response.Body.String()) } return response } created := request(http.MethodPost, "/api/phase-a/accounts", accountBody, http.StatusCreated) var account phasea.Account if err := json.Unmarshal(created.Body.Bytes(), &account); err != nil || account.ID == "" || account.Name != "Phase HTTP" || account.RuntimeStatus != "paused" { t.Fatalf("unexpected account response: %#v err=%v", account, err) } if credentials.values["creatorhub/"+account.ID+"/cookies"] != cookieHeader { t.Fatalf("cookies were not stored through the credential bridge: %#v", credentials.values) } request(http.MethodPost, "/api/phase-a/accounts", accountBody, http.StatusConflict) request(http.MethodPost, "/api/phase-a/accounts", `{"name":"Bad","platform":"douyin","platform_account_key":"bad","tags":[],"cookies":"sessionid"}`, http.StatusBadRequest) request(http.MethodPost, "/api/phase-a/accounts", strings.TrimSuffix(accountBody, "}")+`,"password":"secret"}`, http.StatusBadRequest) request(http.MethodGet, "/api/phase-a/accounts", "", http.StatusOK) request(http.MethodGet, "/api/phase-a/accounts/"+account.ID, "", http.StatusOK) db, err := sql.Open("pgx", databaseURL) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.Close() }) assertControlPlaneDatabaseCount(t, db, `SELECT count(*) FROM credential_reference WHERE id = '`+account.ID+`-cookies' AND provider = 'os_keyring'`, 1) assertControlPlaneDatabaseCount(t, db, `SELECT count(*) FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'social_account' AND column_name = 'cookies'`, 0) for _, failure := range []struct { name string configure func(*persistentCredentialBridge) }{ {name: "chmod", configure: func(bridge *persistentCredentialBridge) { bridge.chmod = func(*os.File, os.FileMode) error { return errors.New("chmod unavailable") } }}, {name: "directory-sync", configure: func(bridge *persistentCredentialBridge) { calls := 0 bridge.syncDirectory = func(path string) error { calls++ if calls == 1 { return errors.New("directory sync unavailable") } return syncCredentialDirectory(path) } }}, } { failureDirectory := t.TempDir() failureCredentials, err := newPersistentCredentialBridge(failureDirectory, []byte("0123456789abcdef0123456789abcdef")) if err != nil { t.Fatal(err) } failure.configure(failureCredentials) failureApp := fiber.New() failureApp.Use(authenticate("operator", "unit-test-password")) registerPhaseA(failureApp, store, nil, failureCredentials) platformKey := "phase-http-store-" + failure.name failureBody := `{"name":"Store failure","platform":"douyin","platform_account_key":"` + platformKey + `","tags":[],"cookies":"sessionid=value"}` failureResponse := do(failureApp, http.MethodPost, "/api/phase-a/accounts", failureBody, "operator", "unit-test-password") if failureResponse.Code != http.StatusInternalServerError { t.Fatalf("%s failure returned %d, want %d: %s", failure.name, failureResponse.Code, http.StatusInternalServerError, failureResponse.Body.String()) } var failedAccounts int if err := db.QueryRowContext(ctx, `SELECT count(*) FROM social_account WHERE platform_account_key = $1`, platformKey).Scan(&failedAccounts); err != nil || failedAccounts != 0 { t.Fatalf("%s failure persisted %d accounts: %v", failure.name, failedAccounts, err) } entries, err := os.ReadDir(failureDirectory) if err != nil || len(entries) != 0 { t.Fatalf("%s failure left ciphertext: entries=%v err=%v", failure.name, entries, err) } } if _, err := db.ExecContext(ctx, ` INSERT INTO gateway (name, endpoint, token) VALUES ('phase-http', 'http://127.0.0.1:8081', 'phase-http-gateway-token'); INSERT INTO browser_image (version, image_ref) VALUES ('1', 'example/browser:1'); INSERT INTO browser_env (alias, name, gateway_name, image_version, fingerprint) VALUES ('account-http', 'Phase HTTP', 'phase-http', '1', '{"seed":1}'); INSERT INTO network_exit (id, protocol, host, port, health_status) VALUES ('exit-http', 'socks5', '127.0.0.1', 1080, 'healthy')`); err != nil { t.Fatal(err) } if _, err := db.ExecContext(ctx, `INSERT INTO environment_binding (id, account_id, browser_env_alias, network_exit_id) VALUES ('binding-http', $1, 'account-http', 'exit-http')`, account.ID); err != nil { t.Fatal(err) } request(http.MethodPost, "/api/phase-a/accounts/"+account.ID+"/resume", "", http.StatusNoContent) request(http.MethodPost, "/api/phase-a/accounts/"+account.ID+"/pause", "", http.StatusNoContent) request(http.MethodPost, "/api/phase-a/accounts/"+account.ID+"/resume", "", http.StatusNoContent) request(http.MethodPost, "/api/phase-a/accounts/"+account.ID+"/revoke", "", http.StatusNoContent) blocked := request(http.MethodPost, "/api/phase-a/accounts/"+account.ID+"/resume", "", http.StatusConflict) if !strings.Contains(blocked.Body.String(), `"reason_code":"account_revoked"`) { t.Fatalf("revoked resume did not return a stable conflict reason: %s", blocked.Body.String()) } } func TestPhaseAErrorRedactsInternalDetails(t *testing.T) { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { return phaseAError(c, errors.New("database exposed a secret value")) }) response, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) if err != nil { t.Fatal(err) } defer response.Body.Close() body, _ := io.ReadAll(response.Body) if response.StatusCode != http.StatusInternalServerError || strings.Contains(string(body), "secret") || !strings.Contains(string(body), "phase A operation failed") { t.Fatalf("internal error was not redacted: status=%d body=%s", response.StatusCode, body) } } func TestSPAServesFileAndIndexFallback(t *testing.T) { directory := t.TempDir() files := map[string]string{ "index.html": "index", "app.js": "asset", "hello world.js": "space", "应用.js": "unicode", } for name, body := range files { if err := os.WriteFile(filepath.Join(directory, name), []byte(body), 0o600); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(directory, "..", "outside-secret.txt"), []byte("secret"), 0o600); err != nil { t.Fatal(err) } app := newHandler(directory, "operator", "unit-test-password") for path, want := range map[string]string{ "/app.js": "asset", "/hello%20world.js": "space", "/%E5%BA%94%E7%94%A8.js": "unicode", "/%2e%2e%2foutside-secret.txt": "index", "/dashboard": "index", } { request := httptest.NewRequest(http.MethodGet, path, nil) request.SetBasicAuth("operator", "unit-test-password") response, err := app.Test(request) if err != nil { t.Fatal(err) } body, readErr := io.ReadAll(response.Body) response.Body.Close() if readErr != nil || response.StatusCode != http.StatusOK || string(body) != want { t.Fatalf("GET %s: status=%d body=%q err=%v", path, response.StatusCode, body, readErr) } } } func TestStartupLogIncludesListenAddress(t *testing.T) { logger := logrus.StandardLogger() previousOutput, previousFormatter, previousLevel := logger.Out, logger.Formatter, logger.Level t.Cleanup(func() { logrus.SetOutput(previousOutput) logrus.SetFormatter(previousFormatter) logrus.SetLevel(previousLevel) }) var output bytes.Buffer logrus.SetOutput(&output) logrus.SetFormatter(&logrus.JSONFormatter{}) logrus.SetLevel(logrus.InfoLevel) logStartup(config{listenAddr: ":8080"}) var entry map[string]any if err := json.Unmarshal(output.Bytes(), &entry); err != nil { t.Fatal(err) } if entry["listen_addr"] != ":8080" || entry["service"] != "control-plane" { t.Fatalf("unexpected startup log: %#v", entry) } }