package api import ( "context" "errors" "fmt" "net/http" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v3" "git.ipao.vip/rogee/creator-hub/internal/creator" hub "git.ipao.vip/rogee/creator-hub/internal/environment" ) func TestDecodeCreatorRejectsUnknownAndTrailingJSON(t *testing.T) { app := fiber.New() app.Post("/", func(c fiber.Ctx) error { var value struct { Name string `json:"name"` } if err := decodeCreator(c, &value); err != nil { return fiber.ErrBadRequest } return c.SendStatus(http.StatusNoContent) }) valid := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(`{"name":"ok"}`)) valid.Header.Set("Content-Type", "application/json") response, err := app.Test(valid) if err != nil || response.StatusCode != http.StatusNoContent { t.Fatalf("valid creator JSON status = %d, err = %v", response.StatusCode, err) } for _, body := range []string{`{"unknown":true}`, `{"name":"ok"}{"name":"extra"}`} { request := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(body)) request.Header.Set("Content-Type", "application/json") response, err := app.Test(request) if err != nil || response.StatusCode != http.StatusBadRequest { t.Errorf("invalid creator JSON %q status = %d, err = %v", body, response.StatusCode, err) } } } func TestCreatorErrorMapsDomainErrorsAndReasons(t *testing.T) { for _, testCase := range []struct { err error status int }{ {err: creator.ErrInvalid, status: http.StatusBadRequest}, {err: creator.ErrConflict, status: http.StatusConflict}, {err: creator.ErrNotFound, status: http.StatusNotFound}, {err: creator.ErrUnavailable, status: http.StatusServiceUnavailable}, {err: creator.ErrUncertain, status: http.StatusConflict}, {err: errors.New("unexpected"), status: http.StatusInternalServerError}, } { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { return creatorError(c, testCase.err) }) response, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) if err != nil || response.StatusCode != testCase.status { t.Errorf("creator error %v status = %d, err = %v", testCase.err, response.StatusCode, err) } } app := fiber.New() app.Get("/", func(c fiber.Ctx) error { return creatorError(c, fmt.Errorf("%w: gateway stopped", creator.ErrUnavailable)) }) response, err := app.Test(httptest.NewRequest(http.MethodGet, "/", nil)) if err != nil || response.StatusCode != http.StatusServiceUnavailable { t.Fatalf("unavailable reason status = %d, err = %v", response.StatusCode, err) } } func TestCreatorPageQueryRejectsNonNumericValues(t *testing.T) { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { _, _, _, err := creatorPageQuery(c) if err != nil { return fiber.ErrBadRequest } return c.SendStatus(http.StatusNoContent) }) for _, query := range []string{"page=bad", "page_size=bad"} { response, err := app.Test(httptest.NewRequest(http.MethodGet, "/?"+query, nil)) if err != nil || response.StatusCode != http.StatusBadRequest { t.Errorf("invalid page query %q status = %d, err = %v", query, response.StatusCode, err) } } } func TestWorkFilterParsesNumericAndTimeFilters(t *testing.T) { app := fiber.New() app.Get("/", func(c fiber.Ctx) error { filter, err := workFilter(c) if err != nil { return fiber.ErrBadRequest } return c.JSON(filter) }) request := httptest.NewRequest(http.MethodGet, "/?platform=douyin&source_id=source-1&source_type=owned&published_at_status=published&min_likes=1&min_comments=2&min_shares=3&published_after=2026-09-17T00:00:00Z&published_before=2026-09-18T00:00:00Z", nil) response, err := app.Test(request) if err != nil || response.StatusCode != http.StatusOK { t.Fatalf("valid work filter status = %d, err = %v", response.StatusCode, err) } for _, query := range []string{"min_likes=bad", "min_comments=bad", "min_shares=bad", "published_after=bad", "published_before=bad"} { response, err := app.Test(httptest.NewRequest(http.MethodGet, "/?"+query, nil)) if err != nil || response.StatusCode != http.StatusBadRequest { t.Errorf("invalid work filter %q status = %d, err = %v", query, response.StatusCode, err) } } } type accountEnvironmentTestStore struct { environment hub.EnvironmentContext err error } func (s accountEnvironmentTestStore) GetEnvironmentContextForAccount(context.Context, string) (hub.EnvironmentContext, error) { return s.environment, s.err } func TestCreatorSchedulerAndAccountEnvironmentFailWithoutStores(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() runCreatorScheduler(ctx, nil, nil, nil) if _, found, err := accountEnvironment(context.Background(), nil, "account-1"); !errors.Is(err, creator.ErrUnavailable) || found { t.Fatalf("nil account environment store = found=%v err=%v", found, err) } if _, found, err := accountEnvironment(context.Background(), accountEnvironmentTestStore{err: hub.ErrNotFound}, "account-1"); err != nil || found { t.Fatalf("missing account environment = found=%v err=%v", found, err) } sentinel := errors.New("database failed") if _, found, err := accountEnvironment(context.Background(), accountEnvironmentTestStore{err: sentinel}, "account-1"); !errors.Is(err, sentinel) || found { t.Fatalf("account environment error = found=%v err=%v", found, err) } environment := testRunnableEnvironment() got, found, err := accountEnvironment(context.Background(), accountEnvironmentTestStore{environment: environment}, "account-1") if err != nil || !found || got.RuntimeID != environment.RuntimeID { t.Fatalf("account environment = %#v, found=%v, err=%v", got, found, err) } } func TestCreatorPreviewAndLifecycleHelpersRejectUnavailableDependencies(t *testing.T) { if _, err := previewCompetitorShare(context.Background(), nil, nil, nil, "account-1", "", "not-a-url"); err == nil { t.Fatal("invalid competitor share URL was accepted") } if _, err := previewCompetitorShare(context.Background(), nil, nil, nil, "account-1", creator.PlatformDouyin, "https://www.douyin.com/video/123"); err == nil { t.Fatal("unavailable douyin preview dependency was accepted") } if _, err := previewCompetitorShare(context.Background(), nil, nil, nil, "account-1", "", "https://www.douyin.com/video/123"); err == nil { t.Fatal("unavailable Douyin preview was reported as successful") } if _, err := previewCompetitorShare(context.Background(), nil, nil, nil, "account-1", "", "https://www.xiaohongshu.com/explore/123"); err == nil { t.Fatal("xiaohongshu share URL was accepted after platform retirement") } if got := (competitorSharePreview{Platform: creator.PlatformDouyin, PlatformAccountKey: "uid", Nickname: "name", AvatarURL: "avatar", HomepageURL: "home"}).input(); got.PlatformAccountKey != "uid" || got.Nickname != "name" { t.Fatalf("preview input = %#v", got) } if _, err := newDouyinAccountBrowser(context.Background(), nil, nil, nil, "account-1"); err == nil { t.Fatal("missing Douyin dependencies were accepted") } if _, err := verifyCreatorAccount(context.Background(), nil, nil, nil, "account-1"); err == nil { t.Fatal("missing account verification dependencies were accepted") } if _, err := creatorLoginQRCode(context.Background(), nil, nil, nil, "account-1"); err == nil { t.Fatal("missing login QR dependencies were accepted") } if err := startCreatorEnvironment(context.Background(), nil, hub.EnvironmentContext{}); err == nil { t.Fatal("missing runtime store was accepted") } if _, err := creatorCollectionAccount(context.Background(), nil, nil, nil, creator.PlatformDouyin); err == nil { t.Fatal("missing collection dependencies were accepted") } }