package api import ( "context" "fmt" "net/http" "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", RuntimeID: "runtime-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} { cleanup := startFakeChannelGateway(t, int64(70+status%10), fmt.Sprintf("purge-key-%d", status), map[string]string{ "DELETE /v1/browsers/account-1": fmt.Sprintf("%d {}", status), }) if err := purgeAccountProfile(context.Background(), hub.Gateway{ID: int64(70 + status%10), Name: "gw"}, environment); err != nil { t.Errorf("status %d: %v", status, err) } cleanup() } failureCleanup := startFakeChannelGateway(t, 80, "purge-key-0080", map[string]string{ "DELETE /v1/browsers/account-1": "502 gateway failure", }) if err := purgeAccountProfile(context.Background(), hub.Gateway{ID: 80, Name: "gw"}, environment); err == nil { t.Fatal("gateway rejection was reported as successful") } failureCleanup() if err := purgeAccountProfile(context.Background(), hub.Gateway{ID: 999, Name: "gw-missing"}, 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}`, } cleanup := startFakeChannelGateway(t, 81, "browser-ops-key-00081", map[string]string{ "POST /v1/browsers/account-1/douyin/identity": response["identity"], "POST /v1/browsers/account-1/douyin/messages": response["messages"], "POST /v1/browsers/account-1/douyin/get": response["get"], "POST /v1/browsers/account-1/douyin/resolve": response["resolve"], "POST /v1/browsers/account-1/douyin/media": response["media"], "POST /v1/browsers/account-1/douyin/login-qr": response["login-qr"], }) defer cleanup() environment := testRunnableEnvironment() browser := creatorGatewayBrowser{gateway: hub.Gateway{ID: 81, Name: "gw"}, 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{ID: 81, Name: "gw"}, 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) { environment := testRunnableEnvironment() malformedRoutes := map[string]string{ "POST /v1/browsers/account-1/douyin/identity": `{"uid":""}`, "POST /v1/browsers/account-1/douyin/get": "502 " + `{"error":"rejected"}`, "POST /v1/browsers/account-1/douyin/resolve": "502 " + `{"error":"rejected"}`, "POST /v1/browsers/account-1/douyin/media": "502 " + `{"error":"rejected"}`, "POST /v1/browsers/account-1/douyin/messages": "502 " + `{"error":"rejected"}`, "POST /v1/browsers/account-1/douyin/login-qr": "502 " + `{"error":"rejected"}`, } cleanup := startFakeChannelGateway(t, 82, "browser-malformed-key-082", malformedRoutes) defer cleanup() browser := creatorGatewayBrowser{gateway: hub.Gateway{ID: 82, Name: "gw"}, environment: environment} if _, err := browser.Identity(context.Background(), "expected"); err == nil { t.Fatal("empty identity was accepted") } 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{ID: 82, Name: "gw"}, 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") } 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) } } malformedRoutes["POST /v1/browsers/account-1/douyin/get"] = "200 `{`" malformedRoutes["POST /v1/browsers/account-1/douyin/resolve"] = "200 `{`" malformedRoutes["POST /v1/browsers/account-1/douyin/media"] = "200 `{`" malformedRoutes["POST /v1/browsers/account-1/douyin/login-qr"] = "200 `{`" 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{ID: 82, Name: "gw"}, environment: invalid}).request(); err == nil { t.Fatal("non-running environment was accepted") } malformedRoutes["POST /v1/browsers/account-1/douyin/get"] = `{"status":200,"body":"ok","challenge":"unexpected"}` if _, err := douyinBrowser.Get(context.Background(), "target"); err == nil { t.Fatal("invalid challenge was accepted") } malformedRoutes["POST /v1/browsers/account-1/douyin/login-qr"] = `{"content_type":"text/plain","body_base64":"aGk="}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("non-image login QR was accepted") } malformedRoutes["POST /v1/browsers/account-1/douyin/login-qr"] = `{"content_type":"image/png","body_base64":"!!!"}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("invalid login QR base64 was accepted") } malformedRoutes["POST /v1/browsers/account-1/douyin/login-qr"] = `{"content_type":"image/png","body_base64":""}` if _, err := douyinBrowser.LoginQR(context.Background()); err == nil { t.Fatal("empty login QR was accepted") } malformedRoutes["POST /v1/browsers/account-1/douyin/media"] = `{"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") } malformedRoutes["POST /v1/browsers/account-1/douyin/media"] = `{"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") } }