package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "regexp" "strings" "testing" hub "git.ipao.vip/rogee/creator-hub/internal/environment" ) func TestAnonymousBrowserUsesIndependentProfile(t *testing.T) { databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if databaseURL == "" { t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage") } for _, withTemplate := range []bool{false, true} { name := "without_template" if withTemplate { name = "with_template" } t.Run(name, func(t *testing.T) { ctx := context.Background() store, err := hub.Open(ctx, isolatedControlPlaneDatabaseURL(t, databaseURL)) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = store.Close() }) var payloads []struct { Alias string `json:"alias"` ProfileID string `json:"profile_id"` } var purgedProfiles []string profilePattern := regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$`) gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case r.Method == http.MethodPost && r.URL.Path == "/v1/browsers": var payload struct { Alias string `json:"alias"` ProfileID string `json:"profile_id"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Error(err) w.WriteHeader(http.StatusBadRequest) return } payloads = append(payloads, payload) if !profilePattern.MatchString(payload.ProfileID) { w.WriteHeader(http.StatusBadRequest) _, _ = w.Write([]byte(`{"detail":"profile_id is invalid"}`)) return } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(map[string]any{ "id": strings.Repeat("a", 64), "network_id": "native-" + strings.Repeat("b", 32), "alias": payload.Alias, "binding_version": 1, "state": "running", }) case r.Method == http.MethodGet && r.URL.Path == "/v1/browsers": _, _ = w.Write([]byte(`[]`)) case r.Method == http.MethodDelete && strings.HasPrefix(r.URL.Path, "/v1/browsers/"): var payload struct { PurgeProfile bool `json:"purge_profile"` } if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { t.Error(err) } if !payload.PurgeProfile { t.Error("anonymous cleanup must purge its temporary profile") } // The gateway purges the profile saved in the runtime selected by alias. alias := strings.TrimPrefix(r.URL.Path, "/v1/browsers/") for _, created := range payloads { if created.Alias == alias { purgedProfiles = append(purgedProfiles, created.ProfileID) break } } w.WriteHeader(http.StatusNoContent) default: http.NotFound(w, r) } })) defer gateway.Close() if _, err := store.CreateGateway(ctx, "gw-profile-test", gateway.URL, "unit-test-gateway-token"); err != nil { t.Fatal(err) } var template hub.EnvironmentContext if withTemplate { template, err = store.CreateStandaloneEnv(ctx, "gw-profile-test", hub.Fingerprint{}, 4096) if err != nil { t.Fatal(err) } } for i := 0; i < 2; i++ { lease, err := newAnonymousBrowser(ctx, store) if err != nil { t.Fatalf("anonymous browser creation failed: %v", err) } if err := lease.close(); err != nil { t.Fatal(err) } payload := payloads[i] if payload.ProfileID != payload.Alias || !strings.HasPrefix(payload.ProfileID, "anon-") { t.Fatalf("anonymous profile must use its own alias: %+v", payload) } if purgedProfiles[i] != payload.ProfileID { t.Fatalf("cleanup purged another profile: got %q want %q", purgedProfiles[i], payload.ProfileID) } if withTemplate && payload.ProfileID == template.ProfileID { t.Fatal("anonymous browser reused an existing environment profile") } } if payloads[0].ProfileID == payloads[1].ProfileID { t.Fatal("anonymous browsers must not share a profile") } if withTemplate { stored, err := store.GetEnvironmentContext(ctx, template.Alias) if err != nil { t.Fatal(err) } if stored.ProfileID != template.ProfileID || stored.Fingerprint.Seed != template.Fingerprint.Seed { t.Fatal("anonymous browser changed the existing environment identity") } } }) } }