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.
168 lines
7.3 KiB
Go
168 lines
7.3 KiB
Go
package creator
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestBlockedOwnedCollectionRemainsDueWithoutClearingErrors(t *testing.T) {
|
|
store, accountStore, ctx := openCreatorIntegrationStore(t)
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
var want []string
|
|
for _, tc := range []struct {
|
|
name, works, comments, login, business, runtime string
|
|
due bool
|
|
}{
|
|
{"both_blocked", "blocked", "blocked", "logged_in", "normal", "active", true},
|
|
{"works_blocked", "blocked", "succeeded", "logged_in", "normal", "active", true},
|
|
{"comments_blocked", "succeeded", "blocked", "logged_in", "normal", "active", true},
|
|
{"muted_blocked", "blocked", "blocked", "logged_in", "muted", "active", true},
|
|
{"recently_completed", "succeeded", "succeeded", "logged_in", "normal", "active", false},
|
|
{"requires_login", "blocked", "blocked", "needs_login", "normal", "active", false},
|
|
{"banned", "blocked", "blocked", "logged_in", "banned", "active", false},
|
|
{"paused", "blocked", "blocked", "logged_in", "normal", "paused", false},
|
|
} {
|
|
accountID := createIntegrationAccount(t, ctx, accountStore, strings.ReplaceAll(tc.name, "_", "-"))
|
|
if err := store.EnsureAccountProfile(ctx, accountID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `UPDATE creator_account_profile SET login_status=$2 WHERE account_id=(SELECT id FROM social_account WHERE account_id=$1)`, accountID, tc.login); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `UPDATE creator_account_profile SET business_status=$2 WHERE account_id=(SELECT id FROM social_account WHERE account_id=$1)`, accountID, tc.business); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `UPDATE social_account SET status=$2 WHERE account_id=$1`, accountID, tc.runtime); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for kind, status := range map[string]string{"works": tc.works, "comments": tc.comments} {
|
|
reason := ""
|
|
if status == "blocked" {
|
|
reason = "identity verification unavailable: multiple browser page targets"
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `INSERT INTO creator_collection_checkpoint
|
|
(source_type,source_id,collection_kind,window_start,window_end,status,last_error,last_completed_at)
|
|
VALUES ('owned',$1,$2,$3,$4,$5,$6,$4)`, accountID, kind, now.Add(-24*time.Hour), now, status, reason); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if tc.due {
|
|
want = append(want, accountID)
|
|
}
|
|
}
|
|
got, err := store.ListDueOwnedAccounts(ctx, now, 3600)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sort.Strings(want)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("blocked tasks must be retried only for eligible accounts: got=%v want=%v", got, want)
|
|
}
|
|
for _, accountID := range want {
|
|
status, err := store.GetAccountCollectionStatus(ctx, accountID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, checkpoint := range []AccountCheckpointStatus{status.Works, status.Comments} {
|
|
if checkpoint.Status == "blocked" && checkpoint.LastError == "" {
|
|
t.Fatalf("scheduling must not hide the actual error: %+v", checkpoint)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBlockedCheckpointRetryPreservesWindowCursorAndProgress(t *testing.T) {
|
|
store, _, ctx := openCreatorIntegrationStore(t)
|
|
end := time.Now().UTC().Truncate(time.Second).Add(-time.Hour)
|
|
start := end.Add(-7 * 24 * time.Hour)
|
|
completed := start.Add(-time.Hour)
|
|
for _, kind := range []string{"works", "comments"} {
|
|
t.Run(kind, func(t *testing.T) {
|
|
id := "blocked-progress-" + kind
|
|
cursor := "resume-page-2"
|
|
if kind == "comments" {
|
|
cursor = `{"work_key":"work-1","cursor":"resume-page-2"}`
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `INSERT INTO creator_collection_checkpoint
|
|
(source_type,source_id,collection_kind,window_start,window_end,status,cursor,
|
|
last_error,last_completed_at,public_work_count,public_work_count_in_progress)
|
|
VALUES ('owned',$1,$2,$3,$4,'blocked',$5,'runtime unavailable',$6,77,13)`,
|
|
id, kind, start, end, cursor, completed); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
lease, err := store.beginCheckpoint(ctx, SourceOwned, id, kind, end, end.Add(time.Hour))
|
|
if err != nil || lease == "" {
|
|
t.Fatalf("start retry: lease=%q err=%v", lease, err)
|
|
}
|
|
checkpoint, err := store.checkpoint(ctx, SourceOwned, id, kind)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var publicCount int64
|
|
var gotCompleted time.Time
|
|
if err := store.db.QueryRowContext(ctx, `SELECT public_work_count,last_completed_at FROM creator_collection_checkpoint WHERE source_type='owned' AND source_id=$1 AND collection_kind=$2`, id, kind).Scan(&publicCount, &gotCompleted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if checkpoint.Status != "running" || checkpoint.Cursor != cursor ||
|
|
!checkpoint.WindowStart.Equal(start) || !checkpoint.WindowEnd.Equal(end) ||
|
|
publicCount != 77 || checkpoint.PublicWorkCountInProgress != 13 || !gotCompleted.Equal(completed) {
|
|
t.Fatalf("blocked retry discarded prior progress: %+v", checkpoint)
|
|
}
|
|
if err := store.failCheckpoint(ctx, SourceOwned, id, kind, lease, ErrUnavailable); !errors.Is(err, ErrUnavailable) {
|
|
t.Fatalf("failed retry must retain its real error: %v", err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type blockedRetryCollector struct{ cursor string }
|
|
|
|
func (c *blockedRetryCollector) VerifyIdentity(context.Context, string) error { return nil }
|
|
func (c *blockedRetryCollector) ListWorks(_ context.Context, _, cursor string) (WorkPage, error) {
|
|
c.cursor = cursor
|
|
return WorkPage{}, nil
|
|
}
|
|
func (*blockedRetryCollector) ListTopLevelComments(context.Context, string, string) (CommentPage, error) {
|
|
return CommentPage{}, nil
|
|
}
|
|
|
|
func TestBlockedCollectionBecomesSuccessfulOnlyAfterCollection(t *testing.T) {
|
|
store, accountStore, ctx := openCreatorIntegrationStore(t)
|
|
id := createIntegrationAccount(t, ctx, accountStore, "blocked-retry")
|
|
if err := store.EnsureAccountProfile(ctx, id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.RecordVerifiedLoginResult(ctx, id, "sec_uid_"+id); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
start, end := now.Add(-24*time.Hour), now.Add(-time.Hour)
|
|
if _, err := store.db.ExecContext(ctx, `INSERT INTO creator_collection_checkpoint
|
|
(source_type,source_id,collection_kind,window_start,window_end,status,cursor,last_error,public_work_count_in_progress)
|
|
VALUES ('owned',$1,'works',$2,$3,'blocked','resume-page-2','browser target unavailable',13)`, id, start, end); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
collector := &blockedRetryCollector{}
|
|
report, err := store.CollectSource(ctx, PlatformDouyin, SourceOwned, id, collector, now)
|
|
if err != nil || !report.PaginationComplete || report.PublicWorksSeen != 13 || collector.cursor != "resume-page-2" {
|
|
t.Fatalf("collection must resume rather than restart: report=%+v cursor=%q err=%v", report, collector.cursor, err)
|
|
}
|
|
checkpoint, err := store.checkpoint(ctx, SourceOwned, id, "works")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err := store.GetAccountCollectionStatus(ctx, id)
|
|
if err != nil || status.Works.Status != "succeeded" || status.Works.LastError != "" ||
|
|
status.Works.LastCompletedAt == nil || status.PublicWorkCount == nil || *status.PublicWorkCount != 13 ||
|
|
!checkpoint.WindowStart.Equal(start) || !checkpoint.WindowEnd.Equal(end) {
|
|
t.Fatalf("successful collection must update status and preserve its window: checkpoint=%+v status=%+v err=%v", checkpoint, status, err)
|
|
}
|
|
}
|