331 lines
9.0 KiB
Go
331 lines
9.0 KiB
Go
package job
|
|
|
|
import (
|
|
"net/url"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"91porn-server/common/hevcpull"
|
|
"91porn-server/common/laosiji"
|
|
"91porn-server/models/v/vidmod"
|
|
"91porn-server/skd/skdg"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
)
|
|
|
|
func TestBuildH265AppTranscodeM3u8URL(t *testing.T) {
|
|
const pullSecret = "test-pull-secret-strong-32-bytes!!"
|
|
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
expiresAt := now.Add(h265PullURLTTL)
|
|
tests := []struct {
|
|
name string
|
|
base string
|
|
source string
|
|
wantPath string
|
|
}{
|
|
{
|
|
name: "normalizes slashes",
|
|
base: " https://app.example.com/ ",
|
|
source: " /video/demo/index.m3u8 ",
|
|
wantPath: "/api/app/vid/transcode/m3u8/video/demo/index.m3u8",
|
|
},
|
|
{name: "empty base", source: "video.m3u8"},
|
|
{name: "empty source", base: "https://app.example.com"},
|
|
{name: "base without scheme", base: "app.example.com", source: "video.m3u8"},
|
|
{name: "plain http is forbidden", base: "http://app.example.com", source: "video.m3u8"},
|
|
{name: "unsupported scheme", base: "ftp://app.example.com", source: "video.m3u8"},
|
|
{name: "base with path", base: "https://app.example.com/prefix", source: "video.m3u8"},
|
|
{name: "base with query", base: "https://app.example.com?token=x", source: "video.m3u8"},
|
|
{name: "source traversal", base: "https://app.example.com", source: "video/../index.m3u8"},
|
|
{name: "absolute source", base: "https://app.example.com", source: "https://cdn.example/index.m3u8"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := buildH265AppTranscodeM3u8URL(tt.base, tt.source, pullSecret, expiresAt)
|
|
if tt.wantPath == "" {
|
|
if got != "" {
|
|
t.Fatalf("buildH265AppTranscodeM3u8URL() = %q, want empty", got)
|
|
}
|
|
return
|
|
}
|
|
parsed, err := url.Parse(got)
|
|
if err != nil {
|
|
t.Fatalf("parse signed URL: %v", err)
|
|
}
|
|
if parsed.Path != tt.wantPath {
|
|
t.Fatalf("signed URL path = %q, want %q", parsed.Path, tt.wantPath)
|
|
}
|
|
if err = hevcpull.VerifyURL(parsed, pullSecret, now); err != nil {
|
|
t.Fatalf("signed URL did not verify: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHevcAppTranscodeM3u8URLUsesStableClaimTime(t *testing.T) {
|
|
oldConf := skdg.Conf
|
|
t.Cleanup(func() {
|
|
skdg.Conf = oldConf
|
|
})
|
|
claimedAt := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
|
|
skdg.Conf = nil
|
|
if got := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt); got != "" {
|
|
t.Fatalf("nil config returned %q", got)
|
|
}
|
|
|
|
skdg.Conf = &skdg.GlobalConfig{}
|
|
skdg.Conf.Url.AppApiUrl = "https://app.example.com"
|
|
skdg.Conf.Hevc.PullSecret = "test-pull-secret-strong-32-bytes!!"
|
|
first := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt)
|
|
second := hevcAppTranscodeM3u8URL("video.m3u8", claimedAt)
|
|
if first == "" || first != second {
|
|
t.Fatalf("signed URL must be stable for one claim: first=%q second=%q", first, second)
|
|
}
|
|
parsed, err := url.Parse(first)
|
|
if err != nil {
|
|
t.Fatalf("parse signed URL: %v", err)
|
|
}
|
|
if !strings.HasPrefix(parsed.Path, "/api/app/vid/transcode/m3u8/") {
|
|
t.Fatalf("unexpected signed URL path: %s", parsed.Path)
|
|
}
|
|
if err = hevcpull.VerifyURL(parsed, skdg.Conf.Hevc.PullSecret, claimedAt); err != nil {
|
|
t.Fatalf("signed URL did not verify: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestH265CloudFileIDIsIndependentFromSignedPullURL(t *testing.T) {
|
|
video := &vidmod.VideoModel{
|
|
ID: primitive.NewObjectID(),
|
|
SourceURL: " /sp/movie/index.m3u8 ",
|
|
H265FailCount: 1,
|
|
H265PendingAt: time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
first := h265CloudFileID(video)
|
|
if first == "" {
|
|
t.Fatal("empty cloud file ID")
|
|
}
|
|
|
|
video.H265PendingAt = video.H265PendingAt.Add(time.Hour)
|
|
video.SourceURL = "/sp/movie/index.m3u8"
|
|
if got := h265CloudFileID(video); got != first {
|
|
t.Fatalf("equivalent source or claim time changed cloud file ID: %q != %q", got, first)
|
|
}
|
|
|
|
video.SourceURL = "/sp/other/index.m3u8"
|
|
if got := h265CloudFileID(video); got == first {
|
|
t.Fatal("different source reused cloud file ID")
|
|
}
|
|
video.SourceURL = "/sp/movie/index.m3u8"
|
|
video.H265FailCount++
|
|
if got := h265CloudFileID(video); got == first {
|
|
t.Fatal("new retry attempt reused cloud file ID")
|
|
}
|
|
}
|
|
|
|
func TestH265TranscodingJobEnabledRequiresEnvironmentSwitchAndConfig(t *testing.T) {
|
|
oldConf := skdg.Conf
|
|
oldLSJConfig := laosiji.Config{
|
|
AppID: laosiji.Appid,
|
|
APIKey: laosiji.APIKey,
|
|
APIUrl: laosiji.APIUrl,
|
|
ImageYuan: laosiji.IMAGEYUAN,
|
|
NoticeURL: laosiji.NoticeURL,
|
|
}
|
|
t.Cleanup(func() {
|
|
skdg.Conf = oldConf
|
|
laosiji.InitConfig(oldLSJConfig)
|
|
})
|
|
|
|
skdg.Conf = &skdg.GlobalConfig{}
|
|
skdg.Conf.Base.Env = "prod"
|
|
skdg.Conf.Url.AppApiUrl = "https://app.example.com"
|
|
laosiji.InitConfig(laosiji.Config{})
|
|
if h265TranscodingJobEnabled() {
|
|
t.Fatal("job enabled without laosiji credentials")
|
|
}
|
|
|
|
laosiji.InitConfig(laosiji.Config{
|
|
AppID: "test-app",
|
|
APIKey: "test-key",
|
|
APIUrl: "https://laosiji.example.com",
|
|
})
|
|
skdg.Conf.Base.Env = "test"
|
|
if h265TranscodingJobEnabled() {
|
|
t.Fatal("job enabled in test without explicit switch")
|
|
}
|
|
|
|
enabled := true
|
|
skdg.Conf.Hevc.EnableTranscode = &enabled
|
|
if h265TranscodingJobEnabled() {
|
|
t.Fatal("job enabled in test without hevc.pullSecret")
|
|
}
|
|
|
|
skdg.Conf.Hevc.PullSecret = "test-pull-secret-strong-32-bytes!!"
|
|
skdg.Conf.Url.AppApiUrl = ""
|
|
if h265TranscodingJobEnabled() {
|
|
t.Fatal("job enabled without appApiUrl")
|
|
}
|
|
|
|
skdg.Conf.Url.AppApiUrl = "https://app.example.com"
|
|
if !h265TranscodingJobEnabled() {
|
|
t.Fatal("job disabled in test with explicit switch and complete configuration")
|
|
}
|
|
|
|
skdg.Conf.Base.Env = "prod"
|
|
skdg.Conf.Hevc.EnableTranscode = nil
|
|
if !h265TranscodingJobEnabled() {
|
|
t.Fatal("job disabled with complete production configuration")
|
|
}
|
|
|
|
disabled := false
|
|
skdg.Conf.Hevc.EnableTranscode = &disabled
|
|
if h265TranscodingJobEnabled() {
|
|
t.Fatal("job enabled after explicit production kill switch")
|
|
}
|
|
}
|
|
|
|
func TestH265PlayableURL(t *testing.T) {
|
|
video := &vidmod.VideoModel{SourceURL: "sp/source/index.m3u8"}
|
|
|
|
if got := h265PlayableURL(video, laosiji.TranscodeResult{
|
|
Done: true,
|
|
NoNeed: true,
|
|
HevcURL: "https://app.example.com/api/app/vid/h5/light/m3u8/sp/source/index.m3u8",
|
|
}); got != video.SourceURL {
|
|
t.Fatalf("no-need result returned %q, want original source %q", got, video.SourceURL)
|
|
}
|
|
|
|
if got := h265PlayableURL(video, laosiji.TranscodeResult{
|
|
Done: true,
|
|
HevcURL: "https://cdn.example.com/m3m/transcoded/index.m3u8",
|
|
}); got != "laosiji/m3m/transcoded/index.m3u8" {
|
|
t.Fatalf("transcoded result returned %q", got)
|
|
}
|
|
|
|
if got := h265PlayableURL(nil, laosiji.TranscodeResult{Done: true, NoNeed: true}); got != "" {
|
|
t.Fatalf("nil video no-need result returned %q", got)
|
|
}
|
|
}
|
|
|
|
func TestH265PendingTimedOut(t *testing.T) {
|
|
now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC)
|
|
tests := []struct {
|
|
name string
|
|
video *vidmod.VideoModel
|
|
want bool
|
|
}{
|
|
{name: "nil video"},
|
|
{
|
|
name: "not pending",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusQueued,
|
|
H265PendingAt: now.Add(-25 * time.Hour),
|
|
},
|
|
},
|
|
{
|
|
name: "missing claim time",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusPending,
|
|
},
|
|
},
|
|
{
|
|
name: "still within timeout",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusPending,
|
|
H265PendingAt: now.Add(-h265PendingTimeout + time.Second),
|
|
},
|
|
},
|
|
{
|
|
name: "at timeout",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusPending,
|
|
H265PendingAt: now.Add(-h265PendingTimeout),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "past timeout",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusPending,
|
|
H265PendingAt: now.Add(-h265PendingTimeout - time.Second),
|
|
},
|
|
want: true,
|
|
},
|
|
{
|
|
name: "future timestamp",
|
|
video: &vidmod.VideoModel{
|
|
H265Status: vidmod.H265StatusPending,
|
|
H265PendingAt: now.Add(time.Second),
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := h265PendingTimedOut(tt.video, now); got != tt.want {
|
|
t.Fatalf("h265PendingTimedOut() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHomeH265QueryMatchesAppHomepageSorts(t *testing.T) {
|
|
for _, sortType := range []int{1, 2, 3, 7, 9} {
|
|
filter := homeH265Filter("module-id", sortType)
|
|
if _, exists := filter["reviewAt"]; exists {
|
|
t.Fatalf("sort %d unexpectedly limits reviewAt: %#v", sortType, filter["reviewAt"])
|
|
}
|
|
}
|
|
|
|
tests := []struct {
|
|
sortType int
|
|
want bson.D
|
|
}{
|
|
{
|
|
sortType: 1,
|
|
want: bson.D{{Key: "reviewAt", Value: -1}},
|
|
},
|
|
{
|
|
sortType: 2,
|
|
want: bson.D{
|
|
{Key: "liaoBaTopSort", Value: -1},
|
|
{Key: "likeCount", Value: -1},
|
|
{Key: "reviewAt", Value: -1},
|
|
},
|
|
},
|
|
{
|
|
sortType: 3,
|
|
want: bson.D{
|
|
{Key: "playCount", Value: -1},
|
|
{Key: "reviewAt", Value: -1},
|
|
},
|
|
},
|
|
{
|
|
sortType: 7,
|
|
want: bson.D{
|
|
{Key: "collectCount", Value: -1},
|
|
{Key: "reviewAt", Value: -1},
|
|
},
|
|
},
|
|
{
|
|
sortType: 9,
|
|
want: bson.D{
|
|
{Key: "hot", Value: -1},
|
|
{Key: "reviewAt", Value: -1},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
got := homeH265Options(tt.sortType).Sort
|
|
if !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("sort %d = %#v, want %#v", tt.sortType, got, tt.want)
|
|
}
|
|
}
|
|
}
|