273 lines
9.6 KiB
Go
273 lines
9.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"cmsp/internal/store"
|
|
"cmsp/internal/taobao"
|
|
"cmsp/internal/task"
|
|
)
|
|
|
|
type videoTestPage struct {
|
|
url string
|
|
navigations []string
|
|
blocked string
|
|
deepBlocked bool
|
|
missingCookie bool
|
|
evaluateErr bool
|
|
}
|
|
|
|
func (p *videoTestPage) Navigate(url string, _ time.Duration) error {
|
|
p.url = url
|
|
p.navigations = append(p.navigations, url)
|
|
return nil
|
|
}
|
|
func (p *videoTestPage) CookieNames(string) (map[string]bool, error) {
|
|
if p.missingCookie {
|
|
return map[string]bool{}, nil
|
|
}
|
|
return map[string]bool{"_m_h5_tk": true, "_m_h5_tk_enc": true, "_tb_token_": true, "tracknick": true}, nil
|
|
}
|
|
func (p *videoTestPage) Evaluate(string) (string, error) {
|
|
if p.evaluateErr {
|
|
return "", errors.New("fictional page read failure")
|
|
}
|
|
text := "normal fictional page"
|
|
if strings.Contains(p.url, "item.htm") || p.deepBlocked {
|
|
text = p.blocked
|
|
}
|
|
raw, _ := json.Marshal(map[string]any{"url": p.url, "title": "fictional page", "text": text, "urls": []string{}})
|
|
return string(raw), nil
|
|
}
|
|
|
|
func newVideoTestApp(t *testing.T) *App {
|
|
t.Helper()
|
|
a := NewApp()
|
|
a.cfg.Download.WaitSecondsMin = 0
|
|
a.cfg.Download.WaitSecondsMax = 0
|
|
a.cfg.Download.VideoDir = t.TempDir()
|
|
db, err := store.Open(filepath.Join(t.TempDir(), "fictional.db"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
a.db = db
|
|
if err := db.UpsertProducts([]store.Product{{ID: "current", ItemID: "fictional-item", MainImage: "https://example.invalid/image.jpg"}, {ID: "remaining"}}, "fictional-time"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.UpdateProductStatus("current", store.VideoFound, store.DownloadFailed, store.UploadDone, "fictional previous error"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.ReplaceVideos("current", []store.Video{{Status: store.VideoStatusUploaded, LocalPath: "fictional-path", RemoteURL: "https://example.invalid/remote.mp4"}}, "fictional-time"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return a
|
|
}
|
|
|
|
func TestRiskStopPreservesSQLiteProductVideosAndRemainingQueue(t *testing.T) {
|
|
for _, stage := range []string{"deep", "search", "detail", "suspected"} {
|
|
t.Run(stage, func(t *testing.T) {
|
|
a := newVideoTestApp(t)
|
|
before, _, _ := a.db.GetProduct("current")
|
|
remaining, _, _ := a.db.GetProduct("remaining")
|
|
videos, _ := a.db.ListVideos("current")
|
|
page := &videoTestPage{blocked: "系统检测到当前访问存在异常", deepBlocked: stage == "deep"}
|
|
if stage == "suspected" {
|
|
page.blocked = ""
|
|
}
|
|
searches := 0
|
|
search := func() ([]taobao.SimilarItem, error) {
|
|
searches++
|
|
if stage == "search" {
|
|
return nil, taobao.ErrAccessBlocked
|
|
}
|
|
return []taobao.SimilarItem{{ItemID: "1", URL: "https://detail.tmall.com/item.htm?ns=1&id=1&skuId=fictional"}, {ItemID: "2"}}, nil
|
|
}
|
|
finished := make(chan task.Progress, 1)
|
|
guard := task.NewEmptyRiskGuard(1)
|
|
runner := task.NewRunner(task.Options{
|
|
Load: func(_ context.Context, id string) (task.Product, error) { return task.Product{ID: id}, nil },
|
|
Prepare: func(ctx context.Context, p task.Product) (task.Work, error) {
|
|
product, _, _ := a.db.GetProduct(p.ID)
|
|
_, work, err := a.prepareVideoFetchOnPage(ctx, product, page, search, guard, make(taobao.DetailCache), &taobao.LoginGuard{})
|
|
return work, err
|
|
},
|
|
OnFinished: func(p task.Progress) { finished <- p },
|
|
})
|
|
if err := runner.Start(context.Background(), []string{"current", "remaining"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case progress := <-finished:
|
|
want := task.StopReasonRiskBlocked
|
|
if stage == "suspected" {
|
|
want = task.StopReasonRiskSuspected
|
|
}
|
|
if progress.State != task.StateLoginRequired || progress.StopReason != want || progress.Done != 0 || progress.Failed != 0 {
|
|
t.Fatalf("wrong stop: %+v", progress)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("batch failed to stop")
|
|
}
|
|
after, _, _ := a.db.GetProduct("current")
|
|
remainingAfter, _, _ := a.db.GetProduct("remaining")
|
|
videosAfter, _ := a.db.ListVideos("current")
|
|
if !reflect.DeepEqual(before, after) || !reflect.DeepEqual(remaining, remainingAfter) || !reflect.DeepEqual(videos, videosAfter) {
|
|
t.Fatal("risk stop altered SQLite state")
|
|
}
|
|
if stage == "deep" && searches != 0 {
|
|
t.Fatal("search followed failed deep guard")
|
|
}
|
|
if len(page.navigations) > 2 {
|
|
t.Fatal("continued candidate or next product navigation")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBatchDeepChecksOnceAndCandidatesReuseWithoutLoginNavigation(t *testing.T) {
|
|
a := newVideoTestApp(t)
|
|
product, _, _ := a.db.GetProduct("current")
|
|
page := &videoTestPage{}
|
|
cache := make(taobao.DetailCache)
|
|
loginGuard := &taobao.LoginGuard{}
|
|
search := func() ([]taobao.SimilarItem, error) { return []taobao.SimilarItem{{ItemID: "1"}, {ItemID: "2"}}, nil }
|
|
for i := 0; i < 2; i++ {
|
|
if i == 1 {
|
|
product, _, _ = a.db.GetProduct("remaining")
|
|
}
|
|
_, work, err := a.prepareVideoFetchOnPage(context.Background(), product, page, search, task.NewEmptyRiskGuard(8), cache, loginGuard)
|
|
if err != nil || !work.Skipped {
|
|
t.Fatalf("normal empty detail: %v", err)
|
|
}
|
|
}
|
|
deep, detail := 0, 0
|
|
for _, url := range page.navigations {
|
|
if strings.Contains(url, "my_itaobao") {
|
|
deep++
|
|
} else {
|
|
detail++
|
|
}
|
|
}
|
|
if deep != 1 || detail != 2 {
|
|
t.Fatalf("incorrect deep/candidate navigation counts: %d/%d", deep, detail)
|
|
}
|
|
}
|
|
|
|
func TestSingleProductUsesSuccessfulLocalVideoWithoutChrome(t *testing.T) {
|
|
a := newVideoTestApp(t)
|
|
file := filepath.Join(a.cfg.Download.VideoDir, "fictional.mp4")
|
|
if err := os.WriteFile(file, []byte("fictional file"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.db.ReplaceVideos("current", []store.Video{{Status: store.VideoStatusDownloaded, LocalPath: file}}, "fictional-time"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := a.db.UpdateProductStatus("current", store.VideoFound, store.DownloadDone, store.UploadDone, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
a.cfg.Taobao.ChromePath = "does-not-exist"
|
|
result, err := a.FetchVideosForProduct("current")
|
|
if err != nil || result.DownloadedCount != 1 || result.Directory != a.cfg.Download.VideoDir {
|
|
t.Fatalf("local reuse failed: %+v %v", result, err)
|
|
}
|
|
}
|
|
|
|
func TestCancelledPreparationPreservesProduct(t *testing.T) {
|
|
a := newVideoTestApp(t)
|
|
product, _, _ := a.db.GetProduct("current")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
_, _, err := a.prepareVideoFetchOnPage(ctx, product, &videoTestPage{}, func() ([]taobao.SimilarItem, error) { t.Fatal("cancelled search called"); return nil, nil }, nil, nil, &taobao.LoginGuard{})
|
|
if !errors.Is(err, context.Canceled) {
|
|
t.Fatal(err)
|
|
}
|
|
after, _, _ := a.db.GetProduct("current")
|
|
if !reflect.DeepEqual(product, after) {
|
|
t.Fatal("cancel changed state")
|
|
}
|
|
}
|
|
|
|
func TestSecondProductCurrentGuardStopsBeforeSearchAndPreservesSQLite(t *testing.T) {
|
|
for _, scenario := range []string{"login", "punish", "risk", "cookie", "read"} {
|
|
t.Run(scenario, func(t *testing.T) {
|
|
a := newVideoTestApp(t)
|
|
if err := a.db.UpsertProducts([]store.Product{{ID: "first", ItemID: "fictional-first"}}, "fictional-time"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
before, _, _ := a.db.GetProduct("current")
|
|
remaining, _, _ := a.db.GetProduct("remaining")
|
|
videos, _ := a.db.ListVideos("current")
|
|
page := &videoTestPage{}
|
|
loginGuard := &taobao.LoginGuard{}
|
|
cache := make(taobao.DetailCache)
|
|
searches := 0
|
|
finished := make(chan task.Progress, 1)
|
|
runner := task.NewRunner(task.Options{
|
|
Load: func(_ context.Context, id string) (task.Product, error) { return task.Product{ID: id}, nil },
|
|
Prepare: func(ctx context.Context, p task.Product) (task.Work, error) {
|
|
if p.ID == "current" {
|
|
switch scenario {
|
|
case "login":
|
|
page.url = "https://login.taobao.com/member/login.jhtml"
|
|
case "punish":
|
|
page.url = "https://item.taobao.com/punish"
|
|
case "risk":
|
|
page.url = "https://item.taobao.com/item.htm?id=prior"
|
|
page.blocked = "系统检测到当前访问存在异常"
|
|
case "cookie":
|
|
page.missingCookie = true
|
|
case "read":
|
|
page.evaluateErr = true
|
|
}
|
|
}
|
|
product, _, _ := a.db.GetProduct(p.ID)
|
|
_, work, err := a.prepareVideoFetchOnPage(ctx, product, page, func() ([]taobao.SimilarItem, error) {
|
|
searches++
|
|
return []taobao.SimilarItem{{ItemID: "fictional-candidate"}}, nil
|
|
}, task.NewEmptyRiskGuard(8), cache, loginGuard)
|
|
return work, err
|
|
},
|
|
OnFinished: func(p task.Progress) { finished <- p },
|
|
})
|
|
if err := runner.Start(context.Background(), []string{"first", "current", "remaining"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
select {
|
|
case progress := <-finished:
|
|
if progress.State != task.StateLoginRequired || progress.Done != 1 || progress.Failed != 0 || progress.Skipped != 1 {
|
|
t.Fatalf("wrong stop: %+v", progress)
|
|
}
|
|
if (scenario == "risk" || scenario == "punish") && progress.StopReason != task.StopReasonRiskBlocked {
|
|
t.Fatal("lost risk stop reason")
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("second product guard failed to stop")
|
|
}
|
|
deep := 0
|
|
for _, u := range page.navigations {
|
|
if strings.Contains(u, "my_itaobao") {
|
|
deep++
|
|
}
|
|
}
|
|
if deep != 1 || searches != 1 {
|
|
t.Fatalf("guard navigated or searched after failure: deep=%d search=%d", deep, searches)
|
|
}
|
|
after, _, _ := a.db.GetProduct("current")
|
|
remainingAfter, _, _ := a.db.GetProduct("remaining")
|
|
videosAfter, _ := a.db.ListVideos("current")
|
|
if !reflect.DeepEqual(before, after) || !reflect.DeepEqual(remaining, remainingAfter) || !reflect.DeepEqual(videos, videosAfter) {
|
|
t.Fatal("current guard failure changed states")
|
|
}
|
|
})
|
|
}
|
|
}
|