package api import ( "context" "encoding/base64" "errors" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "git.ipao.vip/rogee/creator-hub/internal/creator" hub "git.ipao.vip/rogee/creator-hub/internal/environment" "github.com/gofiber/fiber/v3" ) func TestCreatorHelperBranches(t *testing.T) { if got := flattenActionEvidence(map[string]string{}, "evidence", "text"); got != 1 { t.Fatalf("flatten string count = %d", got) } values := map[string]string{} if got := flattenActionEvidence(values, "evidence", map[string]any{"nested": "value", "empty": "", "number": 1}); got != 1 || values["evidence.nested"] != "value" { t.Fatalf("flatten map = %d, %#v", got, values) } if got := flattenActionEvidence(values, "evidence", []any{"ignored"}); got != 0 { t.Fatalf("flatten unsupported count = %d", got) } if token, err := materialClaimToken(); err != nil || len(token) != 32 { t.Fatalf("material claim token = %q, %v", token, err) } if _, err := creatorMaterialHasAudio(context.Background(), "/does/not/exist"); err == nil { t.Fatal("missing media must not report audio") } mediaPath := filepath.Join(t.TempDir(), "media.bin") if err := writeCreatorMedia(mediaPath, []byte("media")); err != nil { t.Fatal(err) } if data, err := os.ReadFile(mediaPath); err != nil || string(data) != "media" || !fileExists(mediaPath) { t.Fatalf("media write: %q %v", data, err) } if err := os.WriteFile(filepath.Join(filepath.Dir(mediaPath), "empty"), nil, 0o600); err != nil { t.Fatal(err) } if fileExists(filepath.Join(filepath.Dir(mediaPath), "empty")) || fileExists("/does/not/exist") { t.Fatal("empty or missing media reported as existing") } for _, input := range [][2]string{{"", ""}, {"unsupported", "model"}, {"whisper", "model"}} { if _, err := transcribeCreatorAudio(context.Background(), "/does/not/exist", input[0], input[1]); err == nil { t.Fatalf("invalid transcription config accepted: %v", input) } } if err := validateTranscriptionBinary("/does/not/exist"); err == nil { t.Fatal("missing transcription binary accepted") } if _, err := processCreatorMaterial(context.Background(), nil, nil, nil, "../escape"); !errors.Is(err, creator.ErrInvalid) { t.Fatalf("invalid material path = %v", err) } if _, err := verifyCreatorPlatformIdentity(context.Background(), "unsupported", hub.Gateway{}, hub.EnvironmentContext{}, "key"); !errors.Is(err, creator.ErrUnavailable) { t.Fatalf("unsupported identity platform = %v", err) } if _, _, err := newCreatorCollector(context.Background(), creator.PlatformDouyin, hub.Gateway{}, hub.EnvironmentContext{}, "", "target", "", creator.SourceCompetitor, "id"); !errors.Is(err, creator.ErrInvalid) { t.Fatalf("missing collector key = %v", err) } if _, err := decodeXiaohongshuResponse([]byte("not-json")); err == nil { t.Fatal("malformed Xiaohongshu response must fail") } if response, err := decodeXiaohongshuResponse([]byte(`{"status":200,"body":"ok","challenge":""}`)); err != nil || response.Status != 200 || string(response.Body) != "ok" { t.Fatalf("decode Xiaohongshu response = %+v, %v", response, err) } if _, err := decodeBase64("not-base64"); err == nil { t.Fatal("invalid base64 must fail") } encoded := base64.StdEncoding.EncodeToString([]byte("media")) if data, err := decodeBase64(encoded); err != nil || string(data) != "media" { t.Fatalf("decode media = %q, %v", data, err) } if err := validateXiaohongshuSource("", ""); err == nil { t.Fatal("empty Xiaohongshu source must fail") } if err := validateXiaohongshuCompetitor(creator.CompetitorInput{Platform: creator.PlatformDouyin}); err != nil { t.Fatal(err) } } func TestCreatorPageQueryValidation(t *testing.T) { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { page, pageSize, enabled, err := creatorPageQuery(c) if err != nil { return creatorError(c, err) } return c.JSON(map[string]any{"page": page, "page_size": pageSize, "enabled": enabled}) }) for _, query := range []string{"", "?page=2&page_size=10&enabled=false"} { response, err := app.Test(httptest.NewRequest(http.MethodGet, "/"+query, nil)) if err != nil || response.StatusCode != http.StatusOK { t.Fatalf("valid query %s: %d %v", query, response.StatusCode, err) } } for _, query := range []string{"?page=bad"} { response, err := app.Test(httptest.NewRequest(http.MethodGet, "/"+query, nil)) if err != nil || response.StatusCode != http.StatusBadRequest { t.Fatalf("invalid query %s: %d %v", query, response.StatusCode, err) } } } func TestCreatorControlPlaneGuards(t *testing.T) { ctx := context.Background() if _, err := persistDouyinMessageHistory(ctx, nil, creator.Conversation{Platform: creator.PlatformXiaohongshu}, "uid", nil); !errors.Is(err, creator.ErrInvalid) { t.Fatalf("invalid history store/platform = %v", err) } conversation := creator.Conversation{Platform: creator.PlatformDouyin, AccountID: "account", PeerUID: "peer"} cases := []douyinHistoryMessage{ {}, {ServerID: "id", SenderUID: "sender", Content: []byte("not-json")}, {ServerID: "id", SenderUID: "sender", CreatedAt: "not-a-time"}, } for _, item := range cases { if _, err := persistDouyinMessageHistory(ctx, nil, conversation, "account", []douyinHistoryMessage{item}); !errors.Is(err, creator.ErrInvalid) { t.Fatalf("invalid history item %v = %v", item, err) } } if _, err := (creatorGatewayActionExecutor{}).Execute(ctx, creator.ActionRequest{}); !errors.Is(err, creator.ErrUnavailable) { t.Fatalf("empty action executor = %v", err) } if err := (creatorMaterialDownloader{}).Download(ctx, creator.Work{Platform: "unsupported"}, "/tmp/media"); !errors.Is(err, creator.ErrUnavailable) { t.Fatalf("unsupported material platform = %v", err) } if _, err := newXiaohongshuReadCollector(ctx, nil, nil, nil, "", creator.SourceOwned, "id"); !errors.Is(err, creator.ErrUnavailable) { t.Fatalf("empty Xiaohongshu collector = %v", err) } if listenerBoundaryPointer(time.Time{}) != nil { t.Fatal("zero listener boundary should be nil") } } func TestCreatorSchedulerAndPreviewGuards(t *testing.T) { ctx := context.Background() checks := []struct { name string call func() error }{ {"verify account", func() error { _, err := verifyCreatorAccount(ctx, nil, nil, nil, "account"); return err }}, {"login QR", func() error { _, err := creatorLoginQRCode(ctx, nil, nil, nil, "account"); return err }}, {"preview competitor", func() error { _, err := previewDouyinCompetitor(ctx, nil, nil, nil, "account", creator.CompetitorInput{}) return err }}, {"sync competitor", func() error { _, err := syncCreatorCompetitor(ctx, nil, nil, nil, "competitor", "account"); return err }}, {"sync due competitor", func() error { _, err := syncCreatorCompetitorDue(ctx, nil, nil, nil, "competitor", "account") return err }}, {"sync owned", func() error { return syncCreatorOwned(ctx, nil, nil, nil, "account", time.Now()) }}, {"select collection account", func() error { _, err := creatorCollectionAccount(ctx, nil, nil, nil, creator.PlatformDouyin) return err }}, } for _, check := range checks { if err := check.call(); err == nil { t.Fatalf("%s unexpectedly succeeded", check.name) } } } func TestCreatorGatewayBrowserHistoryAndMedia(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch { case strings.HasSuffix(r.URL.Path, "/douyin/messages"): _, _ = w.Write([]byte(`{"status":"succeeded","account_uid":"account","history_source":"douyin","messages":[]}`)) case strings.HasSuffix(r.URL.Path, "/douyin/media"): _, _ = w.Write([]byte(`{"status":200,"content_type":"video/mp4","body_base64":"` + base64.StdEncoding.EncodeToString([]byte("video")) + `"}`)) default: http.NotFound(w, r) } })) defer server.Close() browser := creatorGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL, Token: "token"}, environment: hub.EnvironmentContext{Env: hub.Env{Alias: "browser"}, RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-dddddddddddddddddddddddddddddddd", BindingVersion: 1}} history, err := browser.MessageHistory(context.Background(), "account", "peer", "", 10) if err != nil || history.Status != "succeeded" || history.HistorySource != "douyin" { t.Fatalf("history = %+v, %v", history, err) } if _, err := browser.MessageHistory(context.Background(), "", "peer", "", 10); err == nil { t.Fatal("empty history identity must fail") } path := filepath.Join(t.TempDir(), "media.mp4") if err := browser.Media(context.Background(), "https://video.example/media", path); err != nil { t.Fatal(err) } if data, err := os.ReadFile(path); err != nil || string(data) != "video" { t.Fatalf("media output = %q, %v", data, err) } } func TestXiaohongshuGatewayMedia(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if !strings.HasSuffix(r.URL.Path, "/xiaohongshu/media") { http.NotFound(w, r) return } _, _ = w.Write([]byte(`{"status":200,"content_type":"image/jpeg","body_base64":"` + base64.StdEncoding.EncodeToString([]byte("image")) + `"}`)) })) defer server.Close() browser := xiaohongshuGatewayBrowser{gateway: hub.Gateway{Endpoint: server.URL, Token: "token"}, environment: hub.EnvironmentContext{Env: hub.Env{Alias: "browser"}, AccountID: "account", AccountStatus: "active", AuthorizationStatus: "authorized", BindingID: "binding", RuntimeInstanceID: "instance", RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-dddddddddddddddddddddddddddddddd", BindingVersion: 1, Exit: hub.NetworkExit{ID: "exit", HealthStatus: "healthy"}}} data, contentType, err := browser.Media(context.Background(), "https://www.xiaohongshu.com/explore/abc") if err != nil || string(data) != "image" || contentType != "image/jpeg" { t.Fatalf("media = %q, %q, %v", data, contentType, err) } } func TestCreatorGatewayBrowserIdentity(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || !strings.Contains(r.URL.Path, "/douyin/identity") { t.Fatalf("unexpected identity request: %s %s", r.Method, r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"uid":"verified-uid"}`)) })) defer server.Close() browser := creatorGatewayBrowser{ gateway: hub.Gateway{Endpoint: server.URL, Token: "token"}, environment: hub.EnvironmentContext{Env: hub.Env{Alias: "browser"}, RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-dddddddddddddddddddddddddddddddd", BindingVersion: 1}, } uid, err := browser.Identity(context.Background(), "expected-key") if err != nil || uid != "verified-uid" { t.Fatalf("identity = %q, %v", uid, err) } }