// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
package rewrite // import "miniflux.app/v2/internal/reader/rewrite"
import (
"os"
"reflect"
"strings"
"testing"
"miniflux.app/v2/internal/config"
"miniflux.app/v2/internal/model"
)
func TestParseRules(t *testing.T) {
rulesText := `add_dynamic_image,replace("article/(.*).svg"|"article/$1.png"),remove(".spam, .ads:not(.keep)")`
expected := []rule{
{name: "add_dynamic_image"},
{name: "replace", args: []string{"article/(.*).svg", "article/$1.png"}},
{name: "remove", args: []string{".spam, .ads:not(.keep)"}},
}
actual := parseRules(rulesText)
if !reflect.DeepEqual(expected, actual) {
t.Errorf(`Parsed rules do not match expected rules: got %v instead of %v`, actual, expected)
}
}
func TestReplaceTextLinks(t *testing.T) {
scenarios := map[string]string{
`This is a link to example.org`: `This is a link to example.org`,
`This is a link to ftp://example.org`: `This is a link to ftp://example.org`,
`This is a link to www.example.org`: `This is a link to www.example.org`,
`This is a link to http://example.org`: `This is a link to http://example.org`,
`This is a link to http://example.org, end of sentence.`: `This is a link to http://example.org, end of sentence.`,
`This is a link to https://example.org`: `This is a link to https://example.org`,
`This is a link to https://www.example.org/path/to?q=s`: `This is a link to https://www.example.org/path/to?q=s`,
`This is a link to https://example.org/index#hash-tag, http://example.org/.`: `This is a link to https://example.org/index#hash-tag, http://example.org/.`,
}
for input, expected := range scenarios {
actual := replaceTextLinks(input)
if actual != expected {
t.Errorf(`Unexpected link replacement, got "%s" instead of "%s"`, actual, expected)
}
}
}
func TestRewriteWithNoMatchingRule(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `Some text.`,
}
testEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `Some text.`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteYoutubeVideoLink(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: ` Your problem is so terrible, I worry that, if I help you, I risk drawing the attention of whatever god of technology inflicted it on you. <foo>
Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteYoutubeShortLink(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
Title: `A title`,
Content: `
Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteIncorrectYoutubeLink(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{
URL: "https://www.youtube.com/some-page",
Title: `A title`,
Content: `Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/some-page",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteYoutubeLinkAndCustomEmbedURL(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `
Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteYoutubeVideoLinkUsingInvidious(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `
Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, `add_youtube_video_using_invidious_player`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteYoutubeShortLinkUsingInvidious(t *testing.T) {
config.Opts = config.NewOptions()
controlEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
Title: `A title`,
Content: `
Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/shorts/1LUWKWZkPjo",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, `add_youtube_video_using_invidious_player`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestAddYoutubeVideoFromId(t *testing.T) {
config.Opts = config.NewOptions()
scenarios := map[string]string{
// Test with single YouTube ID
`Some content with youtube ID `: `
Some content with youtube ID `,
// Test with multiple YouTube IDs
`Content with youtube_id: "dQw4w9WgXcQ" and youtube_id: "jNQXAC9IVRw"`: `
Content with youtube_id: "dQw4w9WgXcQ" and youtube_id: "jNQXAC9IVRw"`,
// Test with YouTube ID using equals sign
`Some content with youtube_id = "dQw4w9WgXcQ"`: `
Some content with youtube_id = "dQw4w9WgXcQ"`,
// Test with spaces around delimiters
`Some content with youtube_id : "dQw4w9WgXcQ"`: `
Some content with youtube_id : "dQw4w9WgXcQ"`,
// Test with YouTube ID without quotes (regex requires quotes)
`Some content with youtube_id: dQw4w9WgXcQ and more`: `Some content with youtube_id: dQw4w9WgXcQ and more`,
// Test with no YouTube ID
`Some regular content without any video ID`: `Some regular content without any video ID`,
// Test with invalid YouTube ID (wrong length)
`Some content with youtube_id: "invalid"`: `Some content with youtube_id: "invalid"`,
// Test with empty content
``: ``,
}
for input, expected := range scenarios {
actual := addYoutubeVideoFromId(input)
if actual != expected {
t.Errorf(`addYoutubeVideoFromId test failed for input "%s"`, input)
t.Errorf(`Expected: "%s"`, expected)
t.Errorf(`Actual: "%s"`, actual)
}
}
}
func TestAddYoutubeVideoFromIdWithCustomEmbedURL(t *testing.T) {
os.Clearenv()
os.Setenv("YOUTUBE_EMBED_URL_OVERRIDE", "https://invidious.custom/embed/")
var err error
parser := config.NewParser()
config.Opts, err = parser.ParseEnvironmentVariables()
if err != nil {
t.Fatalf(`Parsing failure: %v`, err)
}
input := `Some content with youtube_id: "dQw4w9WgXcQ"`
expected := `
Some content with youtube_id: "dQw4w9WgXcQ"`
actual := addYoutubeVideoFromId(input)
if actual != expected {
t.Errorf(`addYoutubeVideoFromId with custom embed URL failed`)
t.Errorf(`Expected: "%s"`, expected)
t.Errorf(`Actual: "%s"`, actual)
}
}
func TestAddInvidiousVideo(t *testing.T) {
scenarios := map[string][]string{
// Test with various Invidious instances
"https://invidious.io/watch?v=dQw4w9WgXcQ": {
"Some video content",
`
Some video content`,
},
"https://yewtu.be/watch?v=jNQXAC9IVRw": {
"Another video description",
`
Another video description`,
},
"http://invidious.snopyta.org/watch?v=dQw4w9WgXcQ": {
"HTTP instance test",
`
HTTP instance test`,
},
"https://youtube.com/watch?v=dQw4w9WgXcQ": {
"YouTube URL (also matches regex)",
`
YouTube URL (also matches regex)`,
},
"https://example.org/watch?v=dQw4w9WgXcQ": {
"Any domain with watch pattern",
`
Any domain with watch pattern`,
},
// Test with query parameters
"https://invidious.io/watch?v=dQw4w9WgXcQ&t=30s": {
"Video with timestamp",
`
Video with timestamp`,
},
// Test with more complex query parameters
"https://invidious.io/watch?v=dQw4w9WgXcQ&t=30s&autoplay=1": {
"Video with multiple parameters",
`
Video with multiple parameters`,
},
// Test with non-matching URLs (should return content unchanged)
"https://invidious.io/": {
"Invidious homepage",
"Invidious homepage",
},
"https://invidious.io/some-other-page": {
"Other page",
"Other page",
},
"https://invidious.io/search?q=test": {
"Search page",
"Search page",
},
// Test with empty content
"https://empty.invidious.io/watch?v=dQw4w9WgXcQ": {
"",
`
`,
},
}
for entryURL, testData := range scenarios {
entryContent := testData[0]
expected := testData[1]
actual := addInvidiousVideo(entryURL, entryContent)
if actual != expected {
t.Errorf(`addInvidiousVideo test failed for URL "%s" and content "%s"`, entryURL, entryContent)
t.Errorf(`Expected: "%s"`, expected)
t.Errorf(`Actual: "%s"`, actual)
}
}
}
func TestRewriteWithInexistingCustomRule(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `Video Description`,
}
testEntry := &model.Entry{
URL: "https://www.youtube.com/watch?v=1234",
Title: `A title`,
Content: `Video Description`,
}
ApplyContentRewriteRules(testEntry, `some rule`)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLink(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: ``,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkHtmlInjection(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `
`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkAndImageNoTitle(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `
`,
}
testEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `
`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdLinkAndNoImage(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `test`,
}
testEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `test`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithXkcdAndNoImage(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `test`,
}
testEntry := &model.Entry{
URL: "https://xkcd.com/1912/",
Title: `A title`,
Content: `test`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteMailtoLink(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://www.qwantz.com/",
Title: `A title`,
Content: `contact [blah blah]`,
}
testEntry := &model.Entry{
URL: "https://www.qwantz.com/",
Title: `A title`,
Content: `contact`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithPDFLink(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://example.org/document.pdf",
Title: `A title`,
Content: `PDF
test`,
}
testEntry := &model.Entry{
URL: "https://example.org/document.pdf",
Title: `A title`,
Content: `test`,
}
ApplyContentRewriteRules(testEntry, ``)
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithNoLazyImage(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: ``,
}
testEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `
`,
}
ApplyContentRewriteRules(testEntry, "add_dynamic_image")
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithLazyImage(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `
`,
}
testEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `
`,
}
ApplyContentRewriteRules(testEntry, "add_dynamic_image")
if !reflect.DeepEqual(testEntry, controlEntry) {
t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry)
}
}
func TestRewriteWithLazyDivImage(t *testing.T) {
controlEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `
`,
}
testEntry := &model.Entry{
URL: "https://example.org/article",
Title: `A title`,
Content: `
Test
Hello World!
Test
`, } testEntry := &model.Entry{ URL: "https://example.org/article", Title: `A title`, Content: `Test
|
Article URL: https://example.org/article
Comments URL: https://news.ycombinator.com/item?id=37620043
Points: 23
# Comments: 38
`, } controlEntry := &model.Entry{ URL: "https://example.org/article", Title: `A title`, Content: `Article URL: https://example.org/article
Comments URL: https://news.ycombinator.com/item?id=37620043 Open with HACK
Points: 23
# Comments: 38
`, } ApplyContentRewriteRules(testEntry, `add_hn_links_using_hack`) if !reflect.DeepEqual(testEntry, controlEntry) { t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) } } func TestAddHackerNewsLinksUsingOpener(t *testing.T) { testEntry := &model.Entry{ URL: "https://example.org/article", Title: `A title`, Content: `Article URL: https://example.org/article
Comments URL: https://news.ycombinator.com/item?id=37620043
Points: 23
# Comments: 38
`, } controlEntry := &model.Entry{ URL: "https://example.org/article", Title: `A title`, Content: `Article URL: https://example.org/article
Comments URL: https://news.ycombinator.com/item?id=37620043 Open with Opener
Points: 23
# Comments: 38
`, } ApplyContentRewriteRules(testEntry, `add_hn_links_using_opener`) if !reflect.DeepEqual(testEntry, controlEntry) { t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) } } func TestAddImageTitle(t *testing.T) { testEntry := &model.Entry{ URL: "https://example.org/article", Title: `A title`, Content: `pouf
pouf
pouf
pouf
"onerror=alert(1) a="
"onerror=alert(1) a="
;"onerror=alert(1) a=;"
This separates the two cards
This separates the two cards
Example Article 2 - Example`, } ApplyContentRewriteRules(testEntry, `fix_ghost_cards`) if !reflect.DeepEqual(testEntry, controlEntry) { t.Errorf(`Not expected output: got "%+v" instead of "%+v"`, testEntry, controlEntry) } }