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