Files
cmsp/app_video_test.go
T

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")
}
})
}
}