| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590 |
- // SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
- // SPDX-License-Identifier: Apache-2.0
- package model
- import (
- "net/http"
- "os"
- "testing"
- "github.com/gorilla/mux"
- "miniflux.app/v2/internal/config"
- )
- func TestEnclosure_Html5MimeTypeGivesOriginalMimeType(t *testing.T) {
- enclosure := Enclosure{MimeType: "thing/thisMimeTypeIsNotExpectedToBeReplaced"}
- if enclosure.Html5MimeType() != enclosure.MimeType {
- t.Fatalf(
- "HTML5 MimeType must provide original MimeType if not explicitly Replaced. Got %s ,expected '%s' ",
- enclosure.Html5MimeType(),
- enclosure.MimeType,
- )
- }
- }
- func TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeType(t *testing.T) {
- enclosure := Enclosure{MimeType: "video/m4v"}
- if enclosure.Html5MimeType() != "video/x-m4v" {
- // Solution from this stackoverflow discussion:
- // https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
- // tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
- // https://www.florenceporcel.com/podcast/lfhdu.xml
- t.Fatalf(
- "HTML5 MimeType must be replaced by 'video/x-m4v' when originally video/m4v to ensure playbacks in browsers. Got '%s'",
- enclosure.Html5MimeType(),
- )
- }
- }
- func TestEnclosure_IsAudio(t *testing.T) {
- testCases := []struct {
- name string
- mimeType string
- expected bool
- }{
- {"MP3 audio", "audio/mpeg", true},
- {"WAV audio", "audio/wav", true},
- {"OGG audio", "audio/ogg", true},
- {"Mixed case audio", "Audio/MP3", true},
- {"Video file", "video/mp4", false},
- {"Image file", "image/jpeg", false},
- {"Text file", "text/plain", false},
- {"Empty mime type", "", false},
- {"Audio with extra info", "audio/mpeg; charset=utf-8", true},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- enclosure := &Enclosure{MimeType: tc.mimeType}
- if got := enclosure.IsAudio(); got != tc.expected {
- t.Errorf("IsAudio() = %v, want %v for mime type %s", got, tc.expected, tc.mimeType)
- }
- })
- }
- }
- func TestEnclosure_IsVideo(t *testing.T) {
- testCases := []struct {
- name string
- mimeType string
- expected bool
- }{
- {"MP4 video", "video/mp4", true},
- {"AVI video", "video/avi", true},
- {"WebM video", "video/webm", true},
- {"M4V video", "video/m4v", true},
- {"Mixed case video", "Video/MP4", true},
- {"Audio file", "audio/mpeg", false},
- {"Image file", "image/jpeg", false},
- {"Text file", "text/plain", false},
- {"Empty mime type", "", false},
- {"Video with extra info", "video/mp4; codecs=\"avc1.42E01E\"", true},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- enclosure := &Enclosure{MimeType: tc.mimeType}
- if got := enclosure.IsVideo(); got != tc.expected {
- t.Errorf("IsVideo() = %v, want %v for mime type %s", got, tc.expected, tc.mimeType)
- }
- })
- }
- }
- func TestEnclosure_IsImage(t *testing.T) {
- testCases := []struct {
- name string
- mimeType string
- url string
- expected bool
- }{
- {"JPEG image by mime", "image/jpeg", "http://example.com/file", true},
- {"PNG image by mime", "image/png", "http://example.com/file", true},
- {"GIF image by mime", "image/gif", "http://example.com/file", true},
- {"Mixed case image mime", "Image/JPEG", "http://example.com/file", true},
- {"JPG file extension", "application/octet-stream", "http://example.com/photo.jpg", true},
- {"JPEG file extension", "text/plain", "http://example.com/photo.jpeg", true},
- {"PNG file extension", "unknown/type", "http://example.com/photo.png", true},
- {"GIF file extension", "binary/data", "http://example.com/photo.gif", true},
- {"Mixed case extension", "text/plain", "http://example.com/photo.JPG", true},
- {"Image mime and extension", "image/jpeg", "http://example.com/photo.jpg", true},
- {"Video file", "video/mp4", "http://example.com/video.mp4", false},
- {"Audio file", "audio/mpeg", "http://example.com/audio.mp3", false},
- {"Text file", "text/plain", "http://example.com/file.txt", false},
- {"No extension", "text/plain", "http://example.com/file", false},
- {"Other extension", "text/plain", "http://example.com/file.pdf", false},
- {"Empty values", "", "", false},
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- enclosure := &Enclosure{MimeType: tc.mimeType, URL: tc.url}
- if got := enclosure.IsImage(); got != tc.expected {
- t.Errorf("IsImage() = %v, want %v for mime type %s and URL %s", got, tc.expected, tc.mimeType, tc.url)
- }
- })
- }
- }
- func TestEnclosureList_FindMediaPlayerEnclosure(t *testing.T) {
- testCases := []struct {
- name string
- enclosures EnclosureList
- expectedNil bool
- }{
- {
- name: "Returns first audio enclosure",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- &Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
- },
- expectedNil: false,
- },
- {
- name: "Returns first video enclosure",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- },
- expectedNil: false,
- },
- {
- name: "Skips image enclosure and returns audio",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- },
- expectedNil: false,
- },
- {
- name: "Skips enclosure with empty URL",
- enclosures: EnclosureList{
- &Enclosure{URL: "", MimeType: "audio/mpeg"},
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- },
- expectedNil: false,
- },
- {
- name: "Returns nil for no media enclosures",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
- &Enclosure{URL: "http://example.com/doc.pdf", MimeType: "application/pdf"},
- },
- expectedNil: true,
- },
- {
- name: "Returns nil for empty list",
- enclosures: EnclosureList{},
- expectedNil: true,
- },
- {
- name: "Returns nil for all empty URLs",
- enclosures: EnclosureList{
- &Enclosure{URL: "", MimeType: "audio/mpeg"},
- &Enclosure{URL: "", MimeType: "video/mp4"},
- },
- expectedNil: true,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result := tc.enclosures.FindMediaPlayerEnclosure()
- if tc.expectedNil {
- if result != nil {
- t.Errorf("FindMediaPlayerEnclosure() = %v, want nil", result)
- }
- } else {
- if result == nil {
- t.Errorf("FindMediaPlayerEnclosure() = nil, want non-nil")
- } else if !result.IsAudio() && !result.IsVideo() {
- t.Errorf("FindMediaPlayerEnclosure() returned non-media enclosure: %s", result.MimeType)
- }
- }
- })
- }
- }
- func TestEnclosureList_ContainsAudioOrVideo(t *testing.T) {
- testCases := []struct {
- name string
- enclosures EnclosureList
- expected bool
- }{
- {
- name: "Contains audio",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "audio/mpeg"},
- &Enclosure{MimeType: "image/jpeg"},
- },
- expected: true,
- },
- {
- name: "Contains video",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "image/jpeg"},
- &Enclosure{MimeType: "video/mp4"},
- },
- expected: true,
- },
- {
- name: "Contains both audio and video",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "audio/mpeg"},
- &Enclosure{MimeType: "video/mp4"},
- },
- expected: true,
- },
- {
- name: "Contains only images",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "image/jpeg"},
- &Enclosure{MimeType: "image/png"},
- },
- expected: false,
- },
- {
- name: "Contains only documents",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "application/pdf"},
- &Enclosure{MimeType: "text/plain"},
- },
- expected: false,
- },
- {
- name: "Empty list",
- enclosures: EnclosureList{},
- expected: false,
- },
- {
- name: "Single audio enclosure",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "audio/wav"},
- },
- expected: true,
- },
- {
- name: "Single video enclosure",
- enclosures: EnclosureList{
- &Enclosure{MimeType: "video/webm"},
- },
- expected: true,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- result := tc.enclosures.ContainsAudioOrVideo()
- if result != tc.expected {
- t.Errorf("ContainsAudioOrVideo() = %v, want %v", result, tc.expected)
- }
- })
- }
- }
- func TestEnclosure_ProxifyEnclosureURL(t *testing.T) {
- // Initialize config for testing
- os.Clearenv()
- os.Setenv("BASE_URL", "http://localhost")
- os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
- var err error
- parser := config.NewConfigParser()
- config.Opts, err = parser.ParseEnvironmentVariables()
- if err != nil {
- t.Fatalf(`Config parsing failure: %v`, err)
- }
- router := mux.NewRouter()
- router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
- testCases := []struct {
- name string
- url string
- mimeType string
- mediaProxyOption string
- mediaProxyResourceTypes []string
- expectedURLChanged bool
- }{
- {
- name: "HTTP URL with audio type - proxy mode all",
- url: "http://example.com/audio.mp3",
- mimeType: "audio/mpeg",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: true,
- },
- {
- name: "HTTPS URL with video type - proxy mode all",
- url: "https://example.com/video.mp4",
- mimeType: "video/mp4",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: true,
- },
- {
- name: "HTTP URL with video type - proxy mode http-only",
- url: "http://example.com/video.mp4",
- mimeType: "video/mp4",
- mediaProxyOption: "http-only",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: true,
- },
- {
- name: "HTTPS URL with video type - proxy mode http-only",
- url: "https://example.com/video.mp4",
- mimeType: "video/mp4",
- mediaProxyOption: "http-only",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: false,
- },
- {
- name: "HTTP URL with image type - not in resource types",
- url: "http://example.com/image.jpg",
- mimeType: "image/jpeg",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: false,
- },
- {
- name: "HTTP URL with image type - in resource types",
- url: "http://example.com/image.jpg",
- mimeType: "image/jpeg",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video", "image"},
- expectedURLChanged: true,
- },
- {
- name: "HTTP URL - proxy mode none",
- url: "http://example.com/audio.mp3",
- mimeType: "audio/mpeg",
- mediaProxyOption: "none",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: false,
- },
- {
- name: "Empty URL",
- url: "",
- mimeType: "audio/mpeg",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: false,
- },
- {
- name: "Non-media MIME type",
- url: "http://example.com/doc.pdf",
- mimeType: "application/pdf",
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedURLChanged: false,
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- enclosure := &Enclosure{
- URL: tc.url,
- MimeType: tc.mimeType,
- }
- originalURL := enclosure.URL
- // Call the method
- enclosure.ProxifyEnclosureURL(router, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
- // Check if URL changed as expected
- urlChanged := enclosure.URL != originalURL
- if urlChanged != tc.expectedURLChanged {
- t.Errorf("ProxifyEnclosureURL() URL changed = %v, want %v. Original: %s, New: %s",
- urlChanged, tc.expectedURLChanged, originalURL, enclosure.URL)
- }
- // If URL should have changed, verify it's not empty
- if tc.expectedURLChanged && enclosure.URL == "" {
- t.Error("ProxifyEnclosureURL() resulted in empty URL when proxification was expected")
- }
- // If URL shouldn't have changed, verify it's identical
- if !tc.expectedURLChanged && enclosure.URL != originalURL {
- t.Errorf("ProxifyEnclosureURL() URL changed unexpectedly from %s to %s", originalURL, enclosure.URL)
- }
- })
- }
- }
- func TestEnclosureList_ProxifyEnclosureURL(t *testing.T) {
- // Initialize config for testing
- os.Clearenv()
- os.Setenv("BASE_URL", "http://localhost")
- os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
- var err error
- parser := config.NewConfigParser()
- config.Opts, err = parser.ParseEnvironmentVariables()
- if err != nil {
- t.Fatalf(`Config parsing failure: %v`, err)
- }
- router := mux.NewRouter()
- router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
- testCases := []struct {
- name string
- enclosures EnclosureList
- mediaProxyOption string
- mediaProxyResourceTypes []string
- expectedChangedCount int
- }{
- {
- name: "Mixed enclosures with all proxy mode",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- &Enclosure{URL: "https://example.com/video.mp4", MimeType: "video/mp4"},
- &Enclosure{URL: "http://example.com/image.jpg", MimeType: "image/jpeg"},
- &Enclosure{URL: "http://example.com/doc.pdf", MimeType: "application/pdf"},
- },
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedChangedCount: 2, // audio and video should be proxified
- },
- {
- name: "Mixed enclosures with http-only proxy mode",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- &Enclosure{URL: "https://example.com/video.mp4", MimeType: "video/mp4"},
- &Enclosure{URL: "http://example.com/video2.mp4", MimeType: "video/mp4"},
- },
- mediaProxyOption: "http-only",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedChangedCount: 2, // only HTTP URLs should be proxified
- },
- {
- name: "No media types in resource list",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- &Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
- },
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"image"},
- expectedChangedCount: 0, // no matching resource types
- },
- {
- name: "Proxy mode none",
- enclosures: EnclosureList{
- &Enclosure{URL: "http://example.com/audio.mp3", MimeType: "audio/mpeg"},
- &Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
- },
- mediaProxyOption: "none",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedChangedCount: 0,
- },
- {
- name: "Empty enclosure list",
- enclosures: EnclosureList{},
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedChangedCount: 0,
- },
- {
- name: "Enclosures with empty URLs",
- enclosures: EnclosureList{
- &Enclosure{URL: "", MimeType: "audio/mpeg"},
- &Enclosure{URL: "http://example.com/video.mp4", MimeType: "video/mp4"},
- },
- mediaProxyOption: "all",
- mediaProxyResourceTypes: []string{"audio", "video"},
- expectedChangedCount: 1, // only the non-empty URL should be processed
- },
- }
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- // Store original URLs
- originalURLs := make([]string, len(tc.enclosures))
- for i, enclosure := range tc.enclosures {
- originalURLs[i] = enclosure.URL
- }
- // Call the method
- tc.enclosures.ProxifyEnclosureURL(router, tc.mediaProxyOption, tc.mediaProxyResourceTypes)
- // Count how many URLs actually changed
- changedCount := 0
- for i, enclosure := range tc.enclosures {
- if enclosure.URL != originalURLs[i] {
- changedCount++
- // Verify that changed URLs are not empty (unless they were empty originally)
- if originalURLs[i] != "" && enclosure.URL == "" {
- t.Errorf("Enclosure %d: ProxifyEnclosureURL resulted in empty URL", i)
- }
- }
- }
- if changedCount != tc.expectedChangedCount {
- t.Errorf("ProxifyEnclosureURL() changed %d URLs, want %d", changedCount, tc.expectedChangedCount)
- }
- })
- }
- }
- func TestEnclosure_ProxifyEnclosureURL_EdgeCases(t *testing.T) {
- // Initialize config for testing
- os.Clearenv()
- os.Setenv("BASE_URL", "http://localhost")
- os.Setenv("MEDIA_PROXY_PRIVATE_KEY", "test-private-key")
- var err error
- parser := config.NewConfigParser()
- config.Opts, err = parser.ParseEnvironmentVariables()
- if err != nil {
- t.Fatalf(`Config parsing failure: %v`, err)
- }
- router := mux.NewRouter()
- router.HandleFunc("/proxy/{encodedDigest}/{encodedURL}", func(w http.ResponseWriter, r *http.Request) {}).Name("proxy")
- t.Run("Empty resource types slice", func(t *testing.T) {
- enclosure := &Enclosure{
- URL: "http://example.com/audio.mp3",
- MimeType: "audio/mpeg",
- }
- originalURL := enclosure.URL
- enclosure.ProxifyEnclosureURL(router, "all", []string{})
- // With empty resource types, URL should not change
- if enclosure.URL != originalURL {
- t.Errorf("URL should not change with empty resource types. Original: %s, New: %s", originalURL, enclosure.URL)
- }
- })
- t.Run("Nil resource types slice", func(t *testing.T) {
- enclosure := &Enclosure{
- URL: "http://example.com/audio.mp3",
- MimeType: "audio/mpeg",
- }
- originalURL := enclosure.URL
- enclosure.ProxifyEnclosureURL(router, "all", nil)
- // With nil resource types, URL should not change
- if enclosure.URL != originalURL {
- t.Errorf("URL should not change with nil resource types. Original: %s, New: %s", originalURL, enclosure.URL)
- }
- })
- t.Run("Invalid proxy mode", func(t *testing.T) {
- enclosure := &Enclosure{
- URL: "http://example.com/audio.mp3",
- MimeType: "audio/mpeg",
- }
- originalURL := enclosure.URL
- enclosure.ProxifyEnclosureURL(router, "invalid-mode", []string{"audio"})
- // With invalid proxy mode, the function still proxifies non-HTTPS URLs
- // because shouldProxifyURL defaults to checking URL scheme
- if enclosure.URL == originalURL {
- t.Errorf("URL should change for HTTP URL even with invalid proxy mode. Original: %s, New: %s", originalURL, enclosure.URL)
- }
- })
- }
|