|
|
@@ -0,0 +1,297 @@
|
|
|
+// 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 (
|
|
|
+ "testing"
|
|
|
+
|
|
|
+ "miniflux.app/v2/internal/model"
|
|
|
+)
|
|
|
+
|
|
|
+func TestRewriteEntryURL(t *testing.T) {
|
|
|
+ scenarios := []struct {
|
|
|
+ name string
|
|
|
+ feed *model.Feed
|
|
|
+ entry *model.Entry
|
|
|
+ expectedURL string
|
|
|
+ description string
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "NoRewriteRules",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: "",
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when no rewrite rules are specified",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "EmptyRewriteRules",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: " ",
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when rewrite rules are empty/whitespace",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ValidRewriteRule",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/article/(.+)"|"https://example.com/full-article/$1")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/full-article/123",
|
|
|
+ description: "Should rewrite URL according to the regex pattern",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ComplexRegexRewrite",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://news.ycombinator.com/rss",
|
|
|
+ UrlRewriteRules: `rewrite("^https://news\.ycombinator\.com/item\?id=(.+)"|"https://hn.algolia.com/api/v1/items/$1")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://news.ycombinator.com/item?id=12345",
|
|
|
+ },
|
|
|
+ expectedURL: "https://hn.algolia.com/api/v1/items/12345",
|
|
|
+ description: "Should handle complex regex patterns with escaped characters",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "NoMatchingPattern",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://different.com/(.+)"|"https://rewritten.com/$1")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when regex pattern doesn't match",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "InvalidRegexPattern",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/[invalid"|"https://rewritten.com/$1")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when regex pattern is invalid",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "MalformedRewriteRule",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("invalid format")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when rewrite rule format is malformed",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "MultipleGroups",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/([^/]+)/article/(.+)"|"https://example.com/full/$1/story/$2")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/tech/article/ai-news",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/full/tech/story/ai-news",
|
|
|
+ description: "Should handle multiple capture groups in regex",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "URLWithSpecialCharacters",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/(.+)"|"https://proxy.example.com/$1")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/test?param=value&other=123#section",
|
|
|
+ },
|
|
|
+ expectedURL: "https://proxy.example.com/article/test?param=value&other=123#section",
|
|
|
+ description: "Should handle URLs with query parameters and fragments",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ReplaceWithStaticURL",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/(.+)"|"https://static.example.com/reader")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://static.example.com/reader",
|
|
|
+ description: "Should replace with static URL when no capture groups are used in replacement",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "EmptyReplacementString",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/(.+)"|"x")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "x",
|
|
|
+ description: "Should replace with specified string",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "EmptyReplacementNotSupported",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/(.+)"|"")`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when replacement is empty string (not supported by regex pattern)",
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "InvalidRewriteRuleFormat",
|
|
|
+ feed: &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `not-a-rewrite-rule`,
|
|
|
+ },
|
|
|
+ entry: &model.Entry{
|
|
|
+ URL: "https://example.com/article/123",
|
|
|
+ },
|
|
|
+ expectedURL: "https://example.com/article/123",
|
|
|
+ description: "Should return original URL when rewrite rule doesn't match expected format",
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, scenario := range scenarios {
|
|
|
+ t.Run(scenario.name, func(t *testing.T) {
|
|
|
+ result := RewriteEntryURL(scenario.feed, scenario.entry)
|
|
|
+ if result != scenario.expectedURL {
|
|
|
+ t.Errorf("Expected URL %q, got %q. Description: %s", scenario.expectedURL, result, scenario.description)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func TestRewriteEntryURLWithNilValues(t *testing.T) {
|
|
|
+ t.Run("NilFeed", func(t *testing.T) {
|
|
|
+ entry := &model.Entry{URL: "https://example.com/article/123"}
|
|
|
+
|
|
|
+ // This should panic or handle gracefully - let's see what happens
|
|
|
+ defer func() {
|
|
|
+ if r := recover(); r == nil {
|
|
|
+ t.Error("Expected panic when feed is nil, but function completed normally")
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ RewriteEntryURL(nil, entry)
|
|
|
+ })
|
|
|
+
|
|
|
+ t.Run("NilEntry", func(t *testing.T) {
|
|
|
+ feed := &model.Feed{
|
|
|
+ ID: 1,
|
|
|
+ FeedURL: "https://example.com/feed.xml",
|
|
|
+ UrlRewriteRules: `rewrite("^https://example.com/(.+)"|"https://rewritten.com/$1")`,
|
|
|
+ }
|
|
|
+
|
|
|
+ // This should panic or handle gracefully - let's see what happens
|
|
|
+ defer func() {
|
|
|
+ if r := recover(); r == nil {
|
|
|
+ t.Error("Expected panic when entry is nil, but function completed normally")
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ RewriteEntryURL(feed, nil)
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+func TestCustomReplaceRuleRegex(t *testing.T) {
|
|
|
+ scenarios := []struct {
|
|
|
+ name string
|
|
|
+ input string
|
|
|
+ expected []string
|
|
|
+ matches bool
|
|
|
+ }{
|
|
|
+ {
|
|
|
+ name: "ValidRule",
|
|
|
+ input: `rewrite("^https://example.com/(.+)"|"https://rewritten.com/$1")`,
|
|
|
+ expected: []string{`rewrite("^https://example.com/(.+)"|"https://rewritten.com/$1")`, `^https://example.com/(.+)`, `https://rewritten.com/$1`},
|
|
|
+ matches: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "ValidRuleWithEscapedCharacters",
|
|
|
+ input: `rewrite("^https://news\\.ycombinator\\.com/item\\?id=(.+)"|"https://hn.algolia.com/api/v1/items/$1")`,
|
|
|
+ expected: []string{`rewrite("^https://news\\.ycombinator\\.com/item\\?id=(.+)"|"https://hn.algolia.com/api/v1/items/$1")`, `^https://news\\.ycombinator\\.com/item\\?id=(.+)`, `https://hn.algolia.com/api/v1/items/$1`},
|
|
|
+ matches: true,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "InvalidFormat",
|
|
|
+ input: `rewrite("invalid")`,
|
|
|
+ expected: nil,
|
|
|
+ matches: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "EmptyString",
|
|
|
+ input: ``,
|
|
|
+ expected: nil,
|
|
|
+ matches: false,
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: "RandomText",
|
|
|
+ input: `some random text`,
|
|
|
+ expected: nil,
|
|
|
+ matches: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, scenario := range scenarios {
|
|
|
+ t.Run(scenario.name, func(t *testing.T) {
|
|
|
+ parts := customReplaceRuleRegex.FindStringSubmatch(scenario.input)
|
|
|
+
|
|
|
+ if scenario.matches {
|
|
|
+ if len(parts) < 3 {
|
|
|
+ t.Errorf("Expected regex to match and return at least 3 parts, got %d parts: %v", len(parts), parts)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Check the full match and captured groups
|
|
|
+ if parts[0] != scenario.expected[0] {
|
|
|
+ t.Errorf("Expected full match %q, got %q", scenario.expected[0], parts[0])
|
|
|
+ }
|
|
|
+ if parts[1] != scenario.expected[1] {
|
|
|
+ t.Errorf("Expected first capture group %q, got %q", scenario.expected[1], parts[1])
|
|
|
+ }
|
|
|
+ if parts[2] != scenario.expected[2] {
|
|
|
+ t.Errorf("Expected second capture group %q, got %q", scenario.expected[2], parts[2])
|
|
|
+ }
|
|
|
+ } else if len(parts) >= 3 {
|
|
|
+ t.Errorf("Expected regex not to match, but got %d parts: %v", len(parts), parts)
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+}
|