236 lines
10 KiB
Go
236 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.ipao.vip/rogee/creator-hub/internal/creator"
|
|
"git.ipao.vip/rogee/creator-hub/internal/hub"
|
|
"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: "runtime", RuntimeNetworkID: "network", 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: "runtime", RuntimeNetworkID: "network", 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: "runtime", RuntimeNetworkID: "network", BindingVersion: 1},
|
|
}
|
|
uid, err := browser.Identity(context.Background(), "expected-key")
|
|
if err != nil || uid != "verified-uid" {
|
|
t.Fatalf("identity = %q, %v", uid, err)
|
|
}
|
|
}
|