package api import ( "context" "encoding/json" "net/http" "net/http/httptest" "os" "strings" "testing" "git.ipao.vip/rogee/creator-hub/internal/creator" hub "git.ipao.vip/rogee/creator-hub/internal/environment" "github.com/gofiber/fiber/v3" ) func TestPendingEnvironmentLoginRoutesKeepBrowserIdentity(t *testing.T) { raw := os.Getenv("CREATORHUB_POSTGRES_TEST_URL") if raw == "" { t.Skip("set CREATORHUB_POSTGRES_TEST_URL") } dbURL := isolatedControlPlaneDatabaseURL(t, raw) ctx := context.Background() hs, err := hub.Open(ctx, dbURL) if err != nil { t.Fatal(err) } defer hs.Close() cs, err := creator.Open(ctx, dbURL) if err != nil { t.Fatal(err) } defer cs.Close() alias := "" running := false logged := false creates := 0 gateway := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") runtime := map[string]any{"id": "login-runtime", "alias": alias, "state": "running", "binding_version": 1, "network_id": "login-network", "node_id": "login-node", "proxy_ready": true} switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/browsers": items := []map[string]any{} if running { items = append(items, runtime) } json.NewEncoder(w).Encode(items) case r.Method == http.MethodGet && r.URL.Path == "/v1/browsers/"+alias: if !running { w.WriteHeader(http.StatusNotFound) return } json.NewEncoder(w).Encode(runtime) case r.Method == http.MethodPost && r.URL.Path == "/v1/browsers": var body map[string]any if err := json.NewDecoder(r.Body).Decode(&body); err != nil { t.Error(err) } if body["profile_id"] != alias || body["alias"] != alias { t.Errorf("unstable browser identity: %#v", body) } running = true creates++ w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(runtime) case strings.HasSuffix(r.URL.Path, "/douyin/identity"): if !logged { json.NewEncoder(w).Encode(map[string]string{"status": "manual_login", "reason": "awaiting_login"}) return } json.NewEncoder(w).Encode(creator.PlatformIdentity{UID: "99491952055", Nickname: "平台昵称", AvatarURL: "https://example.com/avatar.jpg", DouyinNumber: "1004291301", SecUID: "MS4w-test"}) case strings.HasSuffix(r.URL.Path, "/douyin/login-qr"): json.NewEncoder(w).Encode(map[string]any{"content_type": "image/png", "body_base64": "aGk=", "qr_detected": true}) default: t.Errorf("unexpected native request %s %s", r.Method, r.URL.Path) http.NotFound(w, r) } })) defer gateway.Close() if _, err := hs.CreateGateway(ctx, "login-gw", gateway.URL, "test-token-login"); err != nil { t.Fatal(err) } env, err := hs.CreateStandaloneEnv(ctx, "login-gw", hub.Fingerprint{}) if err != nil { t.Fatal(err) } alias = env.Alias app := fiber.New() registerEnvironmentLoginRoutes(app, cs, hs) route := "/api/creator/environments/" + alias for _, suffix := range []string{"verify", "login-qr"} { response := do(app, http.MethodPost, route+"/"+suffix, `{}`) if response.Code != http.StatusOK { t.Fatalf("%s: %d %s", suffix, response.Code, response.Body.String()) } } pending, err := hs.ListPendingEnvironments(ctx) if err != nil || len(pending) != 1 || pending[0].ID != env.ID || env.ID < 1 { t.Fatalf("pending browser created account or lost its ordering ID: %v %v", pending, err) } logged = true response := do(app, http.MethodPost, route+"/verify", `{}`) if response.Code != http.StatusOK { t.Fatalf("verify: %d %s", response.Code, response.Body.String()) } var result creator.LoginResult if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil || result.AccountID == "" || result.Status != "logged_in" { t.Fatalf("login response: %#v %v", result, err) } bound, err := hs.GetEnvironmentContext(ctx, alias) if err != nil || bound.AccountID != result.AccountID || bound.RuntimeID != "login-runtime" || bound.ProfileID != alias || bound.Fingerprint.Seed != env.Fingerprint.Seed || bound.ID != env.ID { t.Fatalf("binding changed browser: %#v %v", bound, err) } response = do(app, http.MethodPost, route+"/verify", `{}`) if response.Code != http.StatusOK || creates != 1 { t.Fatalf("repeat login recreated browser: creates=%d %d %s", creates, response.Code, response.Body.String()) } }