douyin-release-gate / verify (push) Failing after 18m54s
Add Douyin notification polling, event details, and manual multi-account private messaging. Refine environment memory settings, account operations, login collection recovery, and message UI; update tests and documentation.
206 lines
8.0 KiB
Go
206 lines
8.0 KiB
Go
package creator
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type ownedHistoryCollector struct {
|
|
pages map[string]WorkPage
|
|
workCursors []string
|
|
commentWorks []string
|
|
excludedWorkKeys []string
|
|
workErr error
|
|
}
|
|
|
|
func (c *ownedHistoryCollector) ListWorks(_ context.Context, _, cursor string) (WorkPage, error) {
|
|
c.workCursors = append(c.workCursors, cursor)
|
|
if c.workErr != nil {
|
|
return WorkPage{}, c.workErr
|
|
}
|
|
page, ok := c.pages[cursor]
|
|
if !ok {
|
|
return WorkPage{}, fmt.Errorf("unexpected cursor %q", cursor)
|
|
}
|
|
if cursor == "" {
|
|
page.ExcludedWorkKeys = append(page.ExcludedWorkKeys, c.excludedWorkKeys...)
|
|
}
|
|
return page, nil
|
|
}
|
|
|
|
func (c *ownedHistoryCollector) ListTopLevelComments(_ context.Context, workKey, _ string) (CommentPage, error) {
|
|
c.commentWorks = append(c.commentWorks, workKey)
|
|
return CommentPage{}, nil
|
|
}
|
|
|
|
func TestOwnedCollectionFirstPageFailurePreservesExistingVisibility(t *testing.T) {
|
|
store, accounts, ctx := openCreatorIntegrationStore(t)
|
|
accountID := createIntegrationAccount(t, ctx, accounts, "visibility"+fmt.Sprint(time.Now().UnixNano()))
|
|
if err := store.EnsureAccountProfile(ctx, accountID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
published := now.Add(-time.Hour)
|
|
work, _, err := store.UpsertWork(ctx, WorkInput{
|
|
Platform: PlatformDouyin, WorkKey: "owned-before-failed-refresh", SourceType: SourceOwned,
|
|
SourceID: accountID, Title: "已采集作品", PublishedAt: &published, PublishedAtStatus: "verified",
|
|
}, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
collector := &ownedHistoryCollector{workErr: fmt.Errorf("HTTP 409: browser alias is busy")}
|
|
if _, err := store.CollectSource(ctx, PlatformDouyin, SourceOwned, accountID, collector, now); err == nil {
|
|
t.Fatal("expected the works-page request to fail")
|
|
}
|
|
|
|
var isPublic bool
|
|
if err := store.db.QueryRowContext(ctx, `SELECT is_public FROM creator_work WHERE work_id = $1`, work.ID).Scan(&isPublic); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !isPublic {
|
|
t.Fatal("a failed first page must not hide previously collected public works")
|
|
}
|
|
}
|
|
|
|
func TestCreatorPostgresOwnedCollectionIncludesHistoryAndProgress(t *testing.T) {
|
|
store, accounts, ctx := openCreatorIntegrationStore(t)
|
|
accountID := createIntegrationAccount(t, ctx, accounts, fmt.Sprint(time.Now().UnixNano()))
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
settings, err := store.GetSettings(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := now.Add(-time.Duration(settings.LookbackDays+365) * 24 * time.Hour)
|
|
recent := now.Add(-time.Hour)
|
|
pages := map[string]WorkPage{"": {NextCursor: "history", HasMore: true}}
|
|
for i := 0; i < 28; i++ {
|
|
published := old
|
|
if i == 0 {
|
|
published = recent
|
|
}
|
|
work := WorkInput{WorkKey: fmt.Sprintf("owned-history-%02d", i), Title: fmt.Sprintf("作品 %d", i), PublishedAt: &published, PublishedAtStatus: "verified"}
|
|
cursor := "history"
|
|
if i < 14 {
|
|
cursor = ""
|
|
}
|
|
page := pages[cursor]
|
|
page.Items = append(page.Items, work)
|
|
pages[cursor] = page
|
|
}
|
|
privatePublished := recent
|
|
privateWork, _, err := store.UpsertWork(ctx, WorkInput{Platform: PlatformDouyin, SourceType: SourceOwned, SourceID: accountID, WorkKey: "owned-history-private", Title: "private", PublishedAt: &privatePublished, PublishedAtStatus: "verified"}, now)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collector := &ownedHistoryCollector{pages: pages, excludedWorkKeys: []string{privateWork.WorkKey}}
|
|
profileTotal := int64(30)
|
|
if err := store.RecordAccountMetric(ctx, AccountMetricInput{AccountID: accountID, CollectedAt: now.Add(-time.Minute), AwemeCount: &profileTotal}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The profile aweme_count remains separate from the strict public-work total.
|
|
followers := int64(100)
|
|
if err := store.RecordAccountMetric(ctx, AccountMetricInput{AccountID: accountID, CollectedAt: now, FollowerCount: &followers}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
report, err := store.CollectSource(ctx, PlatformDouyin, SourceOwned, accountID, collector, now)
|
|
if err != nil || !report.PaginationComplete || report.WorksSeen != 29 || report.WorksSaved != 28 {
|
|
t.Fatalf("full history collection: report=%+v err=%v", report, err)
|
|
}
|
|
if len(collector.workCursors) != 2 || collector.workCursors[1] != "history" {
|
|
t.Fatalf("history pagination: %v", collector.workCursors)
|
|
}
|
|
// History is listed, but comments remain limited to the configured window.
|
|
if len(collector.commentWorks) != 1 || collector.commentWorks[0] != "owned-history-00" {
|
|
t.Fatalf("historical comments should not be scheduled: %v", collector.commentWorks)
|
|
}
|
|
first, err := store.ListWorksPage(ctx, WorkFilter{SourceType: SourceOwned, SourceID: accountID}, 1, 25)
|
|
if err != nil || len(first.Data) != 25 || first.Total != 28 || !first.HasNext {
|
|
t.Fatalf("first history page: page=%+v err=%v", first, err)
|
|
}
|
|
second, err := store.ListWorksPage(ctx, WorkFilter{SourceType: SourceOwned, SourceID: accountID}, 2, 25)
|
|
if err != nil || len(second.Data) != 3 || second.Total != 28 || second.HasNext {
|
|
t.Fatalf("last history page: page=%+v err=%v", second, err)
|
|
}
|
|
status, err := store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Read JSON fields to guard the public collection-count API contract.
|
|
publicTotal := int64(28)
|
|
assertCollectionCounts(t, status, 28, &publicTotal)
|
|
views, err := store.ListAccountMonitorViews(ctx)
|
|
if err != nil || len(views) != 1 || views[0].WorkCount != 28 || views[0].PublicWorkCount == nil || *views[0].PublicWorkCount != publicTotal {
|
|
t.Fatalf("list/detail count consistency: views=%+v err=%v", views, err)
|
|
}
|
|
// A full subsequent run must not count duplicate works twice.
|
|
if _, err := store.CollectSource(ctx, PlatformDouyin, SourceOwned, accountID, collector, now.Add(time.Second)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err = store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertCollectionCounts(t, status, 28, &publicTotal)
|
|
var retainedPrivate, visiblePrivate int
|
|
if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*), COUNT(*) FILTER (WHERE is_public) FROM creator_work WHERE work_id = $1`, privateWork.ID).Scan(&retainedPrivate, &visiblePrivate); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if retainedPrivate != 1 || visiblePrivate != 0 {
|
|
t.Fatalf("previously collected non-public work must remain hidden, not deleted: rows=%d visible=%d", retainedPrivate, visiblePrivate)
|
|
}
|
|
}
|
|
|
|
func assertCollectionCounts(t *testing.T, status AccountCollectionStatus, collected int64, total *int64) {
|
|
t.Helper()
|
|
data, err := json.Marshal(status)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
if err := json.Unmarshal(data, &fields); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(fields["work_count"]) != fmt.Sprint(collected) {
|
|
t.Fatalf("collected count: status=%s want=%d", data, collected)
|
|
}
|
|
want := "null"
|
|
if total != nil {
|
|
want = fmt.Sprint(*total)
|
|
}
|
|
if string(fields["public_work_count"]) != want {
|
|
t.Fatalf("platform total: status=%s want=%s", data, want)
|
|
}
|
|
}
|
|
|
|
func TestCreatorPostgresCollectionProgressDistinguishesUnknownAndZero(t *testing.T) {
|
|
store, accounts, ctx := openCreatorIntegrationStore(t)
|
|
accountID := createIntegrationAccount(t, ctx, accounts, fmt.Sprint(time.Now().UnixNano()))
|
|
status, err := store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil || status.Works.Status != "pending" {
|
|
t.Fatalf("pending status: %+v err=%v", status, err)
|
|
}
|
|
assertCollectionCounts(t, status, 0, nil)
|
|
zero := int64(0)
|
|
if err := store.RecordAccountMetric(ctx, AccountMetricInput{AccountID: accountID, CollectedAt: time.Now().UTC(), AwemeCount: &zero}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err = store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertCollectionCounts(t, status, 0, nil)
|
|
collector := &ownedHistoryCollector{pages: map[string]WorkPage{"": {}}}
|
|
if _, err := store.CollectSource(ctx, PlatformDouyin, SourceOwned, accountID, collector, time.Now().UTC()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err = store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assertCollectionCounts(t, status, 0, &zero)
|
|
}
|