package api import ( "context" "net/http" "net/http/httptest" "os" "path/filepath" "testing" hub "git.ipao.vip/rogee/creator-hub/internal/environment" douyin "git.ipao.vip/rogee/creator-hub/internal/platform/douyin" ) func testRunnableEnvironment() hub.EnvironmentContext { return hub.EnvironmentContext{ Env: hub.Env{Alias: "account-1"}, AccountID: "account-1", AccountStatus: "active", AuthorizationStatus: "authorized", BindingID: "binding-1", RuntimeID: "runtime-1", RuntimeInstanceID: "runtime-instance-1", RuntimeNetworkID: "network-1", Exit: hub.NetworkExit{ID: "exit-1", HealthStatus: "healthy"}, BindingVersion: 1, } } func TestPurgeAccountProfileTreatsMissingRuntimeAsIdempotent(t *testing.T) { environment := testRunnableEnvironment() for _, status := range []int{http.StatusNoContent, http.StatusNotFound} { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete || r.URL.Path != "/v1/browsers/account-1" { t.Fatalf("purge request = %s %s", r.Method, r.URL.Path) } w.WriteHeader(status) })) if err := purgeAccountProfile(context.Background(), hub.Gateway{Endpoint: server.URL}, environment); err != nil { t.Errorf("status %d: %v", status, err) } server.Close() } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadGateway) _, _ = w.Write([]byte("gateway failure")) })) if err := purgeAccountProfile(context.Background(), hub.Gateway{Endpoint: server.URL}, environment); err == nil { t.Fatal("gateway rejection was reported as successful") } server.Close() if err := purgeAccountProfile(context.Background(), hub.Gateway{Endpoint: "http://127.0.0.1:1"}, environment); err == nil { t.Fatal("unreachable gateway was reported as successful") } } func TestCreatorGatewayBrowserOperationsValidateResponses(t *testing.T) { response := map[string]string{ "identity": `{"uid":"123"}`, "messages": `{"status":"succeeded","history_source":"live","history_cursor":"next","account_uid":"123","messages":[]}`, "get": `{"status":200,"body":"ok"}`, "resolve": `{"url":"https://www.douyin.com/video/1"}`, "media": `{"status":200,"content_type":"video/mp4","body_base64":"aGk="}`, "login-qr": `{"content_type":"image/png","body_base64":"aGk=","qr_detected":true}`, } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { key := "" switch r.URL.Path { case "/v1/browsers/account-1/douyin/identity": key = "identity" case "/v1/browsers/account-1/douyin/messages": key = "messages" case "/v1/browsers/account-1/douyin/get": key = "get" case "/v1/browsers/account-1/douyin/resolve": key = "resolve" case "/v1/browsers/account-1/douyin/media": key = "media" case "/v1/browsers/account-1/douyin/login-qr": key = "login-qr" default: http.NotFound(w, r) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(response[key])) })) defer server.Close() environment := testRunnableEnvironment() browser := creatorGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL}, environment: environment} if uid, err := browser.Identity(context.Background(), "douyin-uid"); err != nil || uid != "123" { t.Fatalf("identity = %q, %v", uid, err) } history, err := browser.MessageHistory(context.Background(), "123", "456", "", 20) if err != nil || history.HistoryCursor != "next" { t.Fatalf("history = %#v, %v", history, err) } if got, err := browser.Get(context.Background(), "https://www.douyin.com/video/1"); err != nil || got.Status != http.StatusOK || string(got.Body) != "ok" || got.Challenge != douyin.ChallengeNone { t.Fatalf("get = %#v, %v", got, err) } if got, err := browser.Resolve(context.Background(), "https://www.douyin.com/video/1"); err != nil || got == "" { t.Fatalf("resolve = %q, %v", got, err) } destination := filepath.Join(t.TempDir(), "video.mp4") if err := browser.Media(context.Background(), "https://www.douyin.com/video/1", destination); err != nil { t.Fatal(err) } if data, err := os.ReadFile(destination); err != nil || string(data) != "hi" { t.Fatalf("media = %q, %v", data, err) } douyinBrowser := douyinGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL, Token: "test-token"}, environment: environment} qr, err := douyinBrowser.LoginQR(context.Background()) if err != nil || qr.ContentType != "image/png" || !qr.QRDetected { t.Fatalf("login QR = %#v, %v", qr, err) } if got, err := douyinBrowser.Get(context.Background(), "https://www.douyin.com/video/1"); err != nil || got.Status != http.StatusOK { t.Fatalf("douyin get = %#v, %v", got, err) } if _, err := douyinBrowser.request(); err != nil { t.Fatal(err) } } func TestWriteCreatorMediaRejectsInvalidAndPublishesAtomically(t *testing.T) { if err := writeCreatorMedia("", []byte("data")); err == nil { t.Fatal("empty destination was accepted") } if err := writeCreatorMedia(filepath.Join(t.TempDir(), "video"), nil); err == nil { t.Fatal("empty media was accepted") } if err := writeCreatorMedia(filepath.Join(t.TempDir(), "video"), make([]byte, maxCreatorMediaBytes+1)); err == nil { t.Fatal("oversized media was accepted") } if err := writeCreatorMedia(filepath.Join(t.TempDir(), "missing", "video"), []byte("data")); err == nil { t.Fatal("unwritable media destination was accepted") } destination := filepath.Join(t.TempDir(), "video") if err := writeCreatorMedia(destination, []byte("data")); err != nil { t.Fatal(err) } data, err := os.ReadFile(destination) if err != nil || string(data) != "data" { t.Fatalf("published media = %q, err = %v", data, err) } } func TestDouyinWorkKeyFromURLValidatesCanonicalVideoURLs(t *testing.T) { for _, testCase := range []struct { value string want string valid bool }{ {value: "https://www.douyin.com/video/123", want: "123", valid: true}, {value: "https://www.douyin.com/video/", valid: false}, {value: "https://www.douyin.com/video/0", valid: false}, {value: "https://v.douyin.com/video/123", valid: false}, {value: "https://www.douyin.com/user/123", valid: false}, {value: "https://www.douyin.com/video/abc", valid: false}, } { got, err := douyinWorkKeyFromURL(testCase.value) if testCase.valid && (err != nil || got != testCase.want) { t.Errorf("douyinWorkKeyFromURL(%q) = %q, %v", testCase.value, got, err) } if !testCase.valid && err == nil { t.Errorf("douyinWorkKeyFromURL(%q) was accepted", testCase.value) } } } func TestCreatorGatewayBrowserRejectsMalformedGatewayData(t *testing.T) { response := `{"uid":""}` status := http.StatusOK server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(status) _, _ = w.Write([]byte(response)) })) defer server.Close() environment := testRunnableEnvironment() browser := creatorGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL}, environment: environment} if _, err := browser.Identity(context.Background(), "expected"); err == nil { t.Fatal("empty identity was accepted") } status = http.StatusBadGateway if _, err := browser.Identity(context.Background(), "expected"); err == nil { t.Fatal("rejected identity was accepted") } status = http.StatusBadGateway if _, err := browser.MessageHistory(context.Background(), "expected", "target", "", 1); err == nil { t.Fatal("rejected message history was accepted") } if _, err := browser.Get(context.Background(), "target"); err == nil { t.Fatal("rejected browser request was accepted") } if _, err := browser.Resolve(context.Background(), "target"); err == nil { t.Fatal("rejected share resolution was accepted") } if err := browser.Media(context.Background(), "target", filepath.Join(t.TempDir(), "video")); err == nil { t.Fatal("rejected media request was accepted") } douyinBrowser := douyinGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL, Token: "test-token"}, environment: environment} if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("rejected login QR request was accepted") } if _, err := douyinBrowser.Get(context.Background(), "target"); err == nil { t.Fatal("rejected native browser request was accepted") } status = http.StatusOK for _, args := range [][4]any{ {"", "target", "", 1}, {"expected", "", "", 1}, {"expected", "target", "cursor", 0}, {"expected", "target", "cursor", 201}, } { if _, err := browser.MessageHistory(context.Background(), args[0].(string), args[1].(string), args[2].(string), args[3].(int)); err == nil { t.Fatalf("invalid history args %#v were accepted", args) } } response = `{` if _, err := browser.Get(context.Background(), "target"); err == nil { t.Fatal("malformed get response was accepted") } if _, err := browser.Resolve(context.Background(), "target"); err == nil { t.Fatal("malformed resolve response was accepted") } if err := browser.Media(context.Background(), "target", filepath.Join(t.TempDir(), "video")); err == nil { t.Fatal("malformed media response was accepted") } if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("malformed login QR response was accepted") } invalid := environment invalid.RuntimeID = "" if _, err := (douyinGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL}, environment: invalid}).request(); err == nil { t.Fatal("non-running environment was accepted") } response = `{"status":200,"body":"ok","challenge":"unexpected"}` if _, err := douyinBrowser.Get(context.Background(), "target"); err == nil { t.Fatal("invalid challenge was accepted") } response = `{"content_type":"text/plain","body_base64":"aGk="}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("non-image login QR was accepted") } response = `{"content_type":"image/png","body_base64":"!!!"}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("invalid login QR base64 was accepted") } response = `{"content_type":"image/png","body_base64":""}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("empty login QR was accepted") } response = `{"status":200,"content_type":"text/plain","body_base64":"aGk="}` if err := browser.Media(context.Background(), "target", filepath.Join(t.TempDir(), "video")); err == nil { t.Fatal("non-video media was accepted") } response = `{"status":200,"content_type":"video/mp4","body_base64":"!!!"}` if err := browser.Media(context.Background(), "target", filepath.Join(t.TempDir(), "video")); err == nil { t.Fatal("invalid media base64 was accepted") } }