From 1504f6f4734f90bd7bc828bae491ef14c697243d Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sun, 10 May 2026 18:11:41 +0200 Subject: [PATCH] test: add coverage for allanime client and playback service - test decodeSourceURL, detectStreamType, detectEmbedType - test buildStreamSource, buildSourceReferences - test decryptTobeparsed, isLikelyM3U8, isLikelyMP4 - test rankSources, normalizeQuality, parseQualityValue - test qualityMatches, sourceQualityPriority --- api/playback/allanime_client_test.go | 453 ++++++++++++++++++++++++ api/playback/service_ranking_test.go | 491 +++++++++++++++++++++++++++ 2 files changed, 944 insertions(+) create mode 100644 api/playback/allanime_client_test.go create mode 100644 api/playback/service_ranking_test.go diff --git a/api/playback/allanime_client_test.go b/api/playback/allanime_client_test.go new file mode 100644 index 0000000..ad95a65 --- /dev/null +++ b/api/playback/allanime_client_test.go @@ -0,0 +1,453 @@ +package playback + +import ( + "context" + "crypto/aes" + "encoding/json" + "testing" +) + +func TestDecodeSourceURL(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + encoded string + want string + }{ + { + name: "empty returns empty", + encoded: "", + want: "", + }, + { + name: "with double prefix stripped", + encoded: "--example.com/video.mp4", + want: "example.com/video.mp4", + }, + { + name: "hex substitution", + encoded: "7aexample", + want: "Bexample", + }, + { + name: "mixed substitution", + encoded: "79url7a01", + want: "AurlB9", + }, + { + name: "clock replacement", + encoded: "/clock", + want: "/clock.json", + }, + { + name: "no clock replacement if already json", + encoded: "/clock.json", + want: "/clock.json", + }, + { + name: "complex url", + encoded: "--79stream7acom", + want: "AstreamBcom", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := decodeSourceURL(tt.encoded) + if got != tt.want { + t.Errorf("decodeSourceURL(%q) = %q, want %q", tt.encoded, got, tt.want) + } + }) + } +} + +func TestDetectStreamType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + url string + wantType string + }{ + { + name: "m3u8 extension", + url: "https://example.com/video.m3u8", + wantType: "m3u8", + }, + { + name: "master m3u8", + url: "https://example.com/master.m3u8", + wantType: "m3u8", + }, + { + name: "mp4 extension", + url: "https://example.com/video.mp4", + wantType: "mp4", + }, + { + name: "unknown", + url: "https://example.com/video.avi", + wantType: "unknown", + }, + { + name: "empty returns unknown", + url: "", + wantType: "unknown", + }, + { + name: "case insensitive - M3U8", + url: "https://example.com/MASTER.M3U8", + wantType: "m3u8", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := detectStreamType(tt.url) + if got != tt.wantType { + t.Errorf("detectStreamType(%q) = %q, want %q", tt.url, got, tt.wantType) + } + }) + } +} + +func TestDetectEmbedType(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + url string + wantType string + }{ + { + name: "streamwish", + url: "https://streamwish.com/e/abc123", + wantType: "embed", + }, + { + name: "streamsb", + url: "https://streamsb.com/e/abc123", + wantType: "embed", + }, + { + name: "mp4upload", + url: "https://mp4upload.com/e/abc123", + wantType: "embed", + }, + { + name: "ok.ru", + url: "https://ok.ru/video/123", + wantType: "embed", + }, + { + name: "gogoplay", + url: "https://gogoplay.io/embed/123", + wantType: "embed", + }, + { + name: "streamlare", + url: "https://streamlare.com/e/abc", + wantType: "embed", + }, + { + name: "unknown host", + url: "https://unknown.com/video", + wantType: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := detectEmbedType(tt.url) + if got != tt.wantType { + t.Errorf("detectEmbedType(%q) = %q, want %q", tt.url, got, tt.wantType) + } + }) + } +} + +func TestBuildStreamSource(t *testing.T) { + t.Parallel() + + t.Run("constructs with correct defaults", func(t *testing.T) { + got := buildStreamSource("https://example.com/video.mp4", "mp4", "test-provider") + + if got.URL != "https://example.com/video.mp4" { + t.Errorf("URL = %q, want %q", got.URL, "https://example.com/video.mp4") + } + if got.Provider != "test-provider" { + t.Errorf("Provider = %q, want %q", got.Provider, "test-provider") + } + if got.Type != "mp4" { + t.Errorf("Type = %q, want %q", got.Type, "mp4") + } + if got.Referer != allAnimeReferer { + t.Errorf("Referer = %q, want %q", got.Referer, allAnimeReferer) + } + }) +} + +func TestBuildSourceReferences(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + rawURLs []any + wantRefs []sourceReference + }{ + { + name: "empty returns empty", + rawURLs: nil, + wantRefs: nil, + }, + { + name: "filters empty URLs", + rawURLs: []any{ + map[string]any{"sourceUrl": "", "sourceName": "test"}, + map[string]any{"sourceUrl": "https://example.com/v.mp4", "sourceName": "default"}, + }, + wantRefs: []sourceReference{ + {URL: "https://example.com/v.mp4", Name: "default"}, + }, + }, + { + name: "deduplicates URLs", + rawURLs: []any{ + map[string]any{"sourceUrl": "https://example.com/v.mp4", "sourceName": "test"}, + map[string]any{"sourceUrl": "https://example.com/v.mp4", "sourceName": "test2"}, + }, + wantRefs: []sourceReference{ + {URL: "https://example.com/v.mp4", Name: "test"}, + }, + }, + { + name: "prioritizes default provider", + rawURLs: []any{ + map[string]any{"sourceUrl": "https://a.com/v.mp4", "sourceName": "fallback"}, + map[string]any{"sourceUrl": "https://b.com/v.mp4", "sourceName": "default"}, + map[string]any{"sourceUrl": "https://c.com/v.mp4", "sourceName": "yt-mp4"}, + }, + wantRefs: []sourceReference{ + {URL: "https://b.com/v.mp4", Name: "default"}, + {URL: "https://c.com/v.mp4", Name: "yt-mp4"}, + {URL: "https://a.com/v.mp4", Name: "fallback"}, + }, + }, + { + name: "skips invalid map entries", + rawURLs: []any{ + "invalid", + 123, + map[string]any{"sourceUrl": "https://example.com/v.mp4"}, + }, + wantRefs: []sourceReference{ + {URL: "https://example.com/v.mp4", Name: ""}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := buildSourceReferences(tt.rawURLs) + + if len(got) != len(tt.wantRefs) { + t.Errorf("got %d refs, want %d", len(got), len(tt.wantRefs)) + return + } + + for i, want := range tt.wantRefs { + if got[i].URL != want.URL { + t.Errorf("ref[%d].URL = %q, want %q", i, got[i].URL, want.URL) + } + if got[i].Name != want.Name { + t.Errorf("ref[%d].Name = %q, want %q", i, got[i].Name, want.Name) + } + } + }) + } +} + +func TestBuildSourceReferencesOrder(t *testing.T) { + t.Parallel() + + rawURLs := []any{ + map[string]any{"sourceUrl": "https://s.com/v.mp4", "sourceName": "s-mp4"}, + map[string]any{"sourceUrl": "https://default.com/v.mp4", "sourceName": "default"}, + map[string]any{"sourceUrl": "https://luf.com/v.mp4", "sourceName": "luf-mp4"}, + map[string]any{"sourceUrl": "https://yt.com/v.mp4", "sourceName": "yt-mp4"}, + } + + got := buildSourceReferences(rawURLs) + + wantOrder := []string{"default", "yt-mp4", "s-mp4", "luf-mp4"} + if len(got) != len(wantOrder) { + t.Fatalf("got %d refs, want %d", len(got), len(wantOrder)) + } + + for i, wantName := range wantOrder { + if got[i].Name != wantName { + t.Errorf("ref[%d].Name = %q, want %q (priority order: default > yt-mp4 > s-mp4 > luf-mp4)", i, got[i].Name, wantName) + } + } +} + +func TestIsLikelyM3U8(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + input []byte + want bool + }{ + { + name: "valid m3u8", + input: []byte("#EXTM3U\n#EXT-X-VERSION:3"), + want: true, + }, + { + name: "with leading spaces", + input: []byte(" #EXTM3U\n"), + want: true, + }, + { + name: "empty", + input: []byte{}, + want: false, + }, + { + name: "not m3u8", + input: []byte(" m3u8 > unknown > embed)", ranked[0].source.Type) + } + if ranked[1].source.Type != "m3u8" { + t.Errorf("ranked[1] = %q, want m3u8", ranked[1].source.Type) + } +} + +func TestRankSourcesWithQuality(t *testing.T) { + t.Parallel() + + sources := []StreamSource{ + {URL: "https://a.com/v.mp4", Quality: "1080p", Type: "mp4"}, + {URL: "https://b.com/v.mp4", Quality: "720p", Type: "mp4"}, + {URL: "https://c.com/v.mp4", Quality: "480p", Type: "mp4"}, + } + + ranked, err := rankSources(sources, "1080p") + if err != nil { + t.Fatalf("rankSources() error = %v", err) + } + + if ranked[0].source.Quality != "1080p" { + t.Errorf("ranked[0].Quality = %q, want 1080p", ranked[0].source.Quality) + } +} + +func TestNormalizeQuality(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + quality string + wantNorm string + }{ + { + name: "empty returns best", + quality: "", + wantNorm: "best", + }, + { + name: "lowercase best", + quality: "BEST", + wantNorm: "best", + }, + { + name: "with spaces", + quality: " 720p ", + wantNorm: "720p", + }, + { + name: "worst", + quality: "worst", + wantNorm: "worst", + }, + { + name: "specific quality", + quality: "1080p", + wantNorm: "1080p", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := normalizeQuality(tt.quality) + if got != tt.wantNorm { + t.Errorf("normalizeQuality(%q) = %q, want %q", tt.quality, got, tt.wantNorm) + } + }) + } +} + +func TestParseQualityValue(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + quality string + want int + }{ + { + name: "auto returns 240", + quality: "auto", + want: 240, + }, + { + name: "1080p extracts 1080", + quality: "1080p", + want: 1080, + }, + { + name: "720 extracts 720", + quality: "720", + want: 720, + }, + { + name: "fhd is treated as fhd", + quality: "fhd", + want: 0, + }, + { + name: "empty returns 0", + quality: "", + want: 0, + }, + { + name: "multiple digits stops at first non-digit", + quality: "1080p60fps", + want: 1080, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := parseQualityValue(tt.quality) + if got != tt.want { + t.Errorf("parseQualityValue(%q) = %d, want %d", tt.quality, got, tt.want) + } + }) + } +} + +func TestQualityMatches(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + source string + target string + want bool + }{ + { + name: "exact match", + source: "1080p", + target: "1080p", + want: true, + }, + { + name: "target in source", + source: "1920x1080", + target: "1080", + want: true, + }, + { + name: "digit match", + source: "1080p", + target: "1080", + want: true, + }, + { + name: "no match", + source: "720p", + target: "1080", + want: false, + }, + { + name: "empty source returns false", + source: "", + target: "1080", + want: false, + }, + { + name: "empty target returns true (empty always contained)", + source: "1080p", + target: "", + want: true, + }, + { + name: "auto doesn't match specific", + source: "auto", + target: "1080", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := qualityMatches(tt.source, tt.target) + if got != tt.want { + t.Errorf("qualityMatches(%q, %q) = %v, want %v", tt.source, tt.target, got, tt.want) + } + }) + } +} + +func TestSourceQualityPriority(t *testing.T) { + t.Parallel() + +tests := []struct { + name string + source string + target string + wantMin int + }{ + { + name: "best mode favors higher quality", + source: "1080p", + target: "best", + wantMin: 1080, + }, + { + name: "worst mode penalizes higher quality", + source: "1080p", + target: "worst", + wantMin: -2000, + }, + { + name: "exact match gets bonus", + source: "1080p", + target: "1080p", + wantMin: 2000, + }, + { + name: "close match gets penalty but positive score", + source: "1080p", + target: "720p", + wantMin: 500, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := sourceQualityPriority(tt.source, tt.target) + + if tt.wantMin != 0 && got < tt.wantMin { + t.Errorf("sourceQualityPriority(%q, %q) = %d, want >= %d", tt.source, tt.target, got, tt.wantMin) + } + }) + } +} + +func TestSourceTypePriorityLookup(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + sourceType string + want int + }{ + { + name: "mp4 priority", + sourceType: "mp4", + want: 500, + }, + { + name: "m3u8 priority", + sourceType: "m3u8", + want: 450, + }, + { + name: "unknown uses fallback", + sourceType: "unknown", + want: 300, + }, + { + name: "embed fallback", + sourceType: "embed", + want: 100, + }, + { + name: "unrecognized uses fallback", + sourceType: "video", + want: 200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := lookupPriority(sourceTypePriority, tt.sourceType, 200) + if got != tt.want { + t.Errorf("lookupPriority(sourceTypePriority, %q, 200) = %d, want %d", tt.sourceType, got, tt.want) + } + }) + } +} + +func TestProviderPriorityLookup(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + provider string + want int + }{ + { + name: "s-mp4", + provider: "s-mp4", + want: 120, + }, + { + name: "default", + provider: "default", + want: 115, + }, + { + name: "yt-mp4", + provider: "yt-mp4", + want: 100, + }, + { + name: "unknown uses fallback", + provider: "unknown", + want: 60, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := lookupPriority(providerPriority, tt.provider, 60) + if got != tt.want { + t.Errorf("lookupPriority(providerPriority, %q, 60) = %d, want %d", tt.provider, got, tt.want) + } + }) + } +} + +func TestNormalizeSourceTypeFromProbe(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + source StreamSource + contentType string + wantType string + }{ + { + name: "video/mp4 normalizes to mp4", + source: StreamSource{Type: "unknown"}, + contentType: "video/mp4", + wantType: "mp4", + }, + { + name: "application/octet-stream unchanged", + source: StreamSource{Type: "mp4"}, + contentType: "application/octet-stream", + wantType: "mp4", + }, + { + name: "mpegurl normalizes to m3u8", + source: StreamSource{Type: "unknown"}, + contentType: "application/vnd.apple.mpegurl", + wantType: "m3u8", + }, + { + name: "video/mpegurl", + source: StreamSource{Type: "unknown"}, + contentType: "video/mpegurl", + wantType: "m3u8", + }, + { + name: "case insensitive", + source: StreamSource{Type: "unknown"}, + contentType: "VIDEO/MP4", + wantType: "mp4", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := normalizeSourceTypeFromProbe(tt.source, tt.contentType) + if got.Type != tt.wantType { + t.Errorf("normalizeSourceTypeFromProbe().Type = %q, want %q", got.Type, tt.wantType) + } + }) + } +} + +func TestExtractDigits(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + want string + }{ + { + name: "extracts digits", + value: "1080p", + want: "1080", + }, + { + name: "empty if no digits", + value: "p", + want: "", + }, + { + name: "stops at non-digit after digits", + value: "720p60", + want: "720", + }, + { + name: "multiple non-digit does not break", + value: "abc123def", + want: "123", + }, + { + name: "all digits", + value: "1080", + want: "1080", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := extractDigits(tt.value) + if got != tt.want { + t.Errorf("extractDigits(%q) = %q, want %q", tt.value, got, tt.want) + } + }) + } +} \ No newline at end of file