simple-rss 1.3.3 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/simple-rss.gemspec CHANGED
@@ -1,15 +1,14 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "simple-rss"
3
- s.version = "1.3.3"
4
- s.version = "#{s.version}-alpha-#{ENV['TRAVIS_BUILD_NUMBER']}" if ENV['TRAVIS']
5
- s.date = "2015-08-17"
3
+ s.version = "2.1.0"
4
+ s.date = "2025-12-29"
6
5
  s.summary = "A simple, flexible, extensible, and liberal RSS and Atom reader for Ruby. It is designed to be backwards compatible with the standard RSS parser, but will never do RSS generation."
7
6
  s.email = "lucas@rufy.com"
8
- s.homepage = "http://github.com/cardmagic/simple-rss"
7
+ s.homepage = "https://github.com/cardmagic/simple-rss"
9
8
  s.description = "A simple, flexible, extensible, and liberal RSS and Atom reader for Ruby. It is designed to be backwards compatible with the standard RSS parser, but will never do RSS generation."
10
9
  s.authors = ["Lucas Carlson"]
11
- s.files = ["install.rb", "lib", "lib/simple-rss.rb", "LICENSE", "Rakefile", "README.markdown", "simple-rss.gemspec", "test", "test/base", "test/base/base_test.rb", "test/data", "test/data/atom.xml", "test/data/not-rss.xml", "test/data/rss09.rdf", "test/data/rss20.xml", "test/test_helper.rb"]
12
- s.rubyforge_project = 'simple-rss'
10
+ s.files = Dir["lib/**/*", "test/**/*", "LICENSE", "README.md", "Rakefile", "simple-rss.gemspec"]
11
+ s.required_ruby_version = ">= 3.1"
13
12
  s.add_development_dependency "rake"
14
13
  s.add_development_dependency "rdoc"
15
14
  s.add_development_dependency "test-unit"
@@ -0,0 +1,37 @@
1
+ require "test_helper"
2
+
3
+ class ArrayTagsTest < Test::Unit::TestCase
4
+ def setup
5
+ @rss20 = SimpleRSS.parse(
6
+ open(File.dirname(__FILE__) + "/../data/rss20.xml"),
7
+ array_tags: [:category]
8
+ )
9
+ @rss20_no_array = SimpleRSS.parse(
10
+ open(File.dirname(__FILE__) + "/../data/rss20.xml")
11
+ )
12
+ end
13
+
14
+ def test_array_tag_returns_array
15
+ assert_kind_of Array, @rss20.items.first.category
16
+ end
17
+
18
+ def test_array_tag_contains_all_values
19
+ categories = @rss20.items.first.category
20
+ assert_equal 2, categories.size
21
+ assert_includes categories, "Programming"
22
+ assert_includes categories, "Ruby"
23
+ end
24
+
25
+ def test_single_value_still_returns_array
26
+ # Item with only one category should still return an array
27
+ categories = @rss20.items[2].category
28
+ assert_kind_of Array, categories
29
+ assert_equal 1, categories.size
30
+ assert_equal ["General"], categories
31
+ end
32
+
33
+ def test_without_array_tags_returns_string
34
+ # Default behavior should return just the first/last match as a string
35
+ assert_kind_of String, @rss20_no_array.items.first.category
36
+ end
37
+ end
@@ -1,83 +1,82 @@
1
- # -*- coding: utf-8 -*-
2
- require 'test_helper'
1
+ require "test_helper"
3
2
  class BaseTest < Test::Unit::TestCase
4
- def setup
5
- @rss09 = SimpleRSS.parse open(File.dirname(__FILE__) + '/../data/rss09.rdf')
6
- @rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + '/../data/rss20.xml')
7
- @rss20_utf8 = SimpleRSS.parse open(File.dirname(__FILE__) + '/../data/rss20_utf8.xml')
8
- @media_rss = SimpleRSS.parse open(File.dirname(__FILE__) + '/../data/media_rss.xml')
9
- @atom = SimpleRSS.parse open(File.dirname(__FILE__) + '/../data/atom.xml')
10
- end
11
-
12
- def test_channel
13
- assert_equal @rss09, @rss09.channel
14
- assert_equal @rss20, @rss20.channel
15
- assert_equal @atom, @atom.feed
16
- end
17
-
18
- def test_items
19
- assert_kind_of Array, @rss09.items
20
- assert_kind_of Array, @rss20.items
21
- assert_kind_of Array, @atom.entries
22
- end
23
-
24
- def test_rss09
25
- assert_equal 10, @rss09.items.size
26
- assert_equal "Slashdot", @rss09.title
27
- assert_equal "http://slashdot.org/", @rss09.channel.link
28
- assert_equal "http://books.slashdot.org/article.pl?sid=05/08/29/1319236&amp;from=rss", @rss09.items.first.link
29
- assert_equal "http://books.slashdot.org/article.pl?sid=05/08/29/1319236&amp;from=rss", @rss09.items.first[:link]
3
+ def setup
4
+ @rss09 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss09.rdf")
5
+ @rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20.xml")
6
+ @rss20_utf8 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20_utf8.xml")
7
+ @media_rss = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/media_rss.xml")
8
+ @atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom.xml")
9
+ end
10
+
11
+ def test_channel
12
+ assert_equal @rss09, @rss09.channel
13
+ assert_equal @rss20, @rss20.channel
14
+ assert_equal @atom, @atom.feed
15
+ end
16
+
17
+ def test_items
18
+ assert_kind_of Array, @rss09.items
19
+ assert_kind_of Array, @rss20.items
20
+ assert_kind_of Array, @atom.entries
21
+ end
22
+
23
+ def test_rss09
24
+ assert_equal 10, @rss09.items.size
25
+ assert_equal "Slashdot", @rss09.title
26
+ assert_equal "http://slashdot.org/", @rss09.channel.link
27
+ assert_equal "http://books.slashdot.org/article.pl?sid=05/08/29/1319236&amp;from=rss", @rss09.items.first.link
28
+ assert_equal "http://books.slashdot.org/article.pl?sid=05/08/29/1319236&amp;from=rss", @rss09.items.first[:link]
30
29
  assert_equal Time.parse("Wed Aug 24 13:33:34 UTC 2005"), @rss20.items.first.pubDate
31
30
  assert_equal Time.parse("Fri Sep 09 02:52:31 PDT 2005"), @rss09.channel.dc_date
32
- end
31
+ end
32
+
33
+ def test_media_rss
34
+ assert_equal 20, @media_rss.items.size
35
+ assert_equal "Uploads from herval", @media_rss.title
36
+ assert_equal "http://www.flickr.com/photos/herval/", @media_rss.channel.link
37
+ assert_equal "http://www.flickr.com/photos/herval/4671960608/", @media_rss.items.first.link
38
+ assert_equal "http://www.flickr.com/photos/herval/4671960608/", @media_rss.items.first[:link]
39
+ assert_equal "http://farm5.static.flickr.com/4040/4671960608_10cb945d5c_o.jpg", @media_rss.items.first.media_content_url
40
+ assert_equal "image/jpeg", @media_rss.items.first.media_content_type
41
+ assert_equal "3168", @media_rss.items.first.media_content_height
42
+ assert_equal "4752", @media_rss.items.first.media_content_width
43
+ assert_equal "Woof?", @media_rss.items.first.media_title
44
+ assert_equal "http://farm5.static.flickr.com/4040/4671960608_954d2297bc_s.jpg", @media_rss.items.first.media_thumbnail_url
45
+ assert_equal "75", @media_rss.items.first.media_thumbnail_height
46
+ assert_equal "75", @media_rss.items.first.media_thumbnail_width
47
+ assert_equal "herval", @media_rss.items.first.media_credit
48
+ assert_equal "photographer", @media_rss.items.first.media_credit_role
49
+ assert_equal "pets frodo", @media_rss.items.first.media_category
50
+ assert_equal "urn:flickr:tags", @media_rss.items.first.media_category_scheme
51
+ end
52
+
53
+ def test_rss20
54
+ assert_equal 10, @rss20.items.size
55
+ assert_equal "Technoblog", @rss20.title
56
+ assert_equal "http://tech.rufy.com", @rss20.channel.link
57
+ assert_equal "http://feeds.feedburner.com/rufytech?m=68", @rss20.items.first.link
58
+ assert_equal "http://feeds.feedburner.com/rufytech?m=68", @rss20.items.first[:link]
59
+ assert_equal "This is an XML content feed. It is intended to be viewed in a newsreader or syndicated to another site.", @rss20.channel.feedburner_browserFriendly
60
+ end
61
+
62
+ def test_atom
63
+ assert_equal 1, @atom.entries.size
64
+ assert_equal "dive into mark", @atom.title
65
+ assert_equal "http://example.org/", @atom.feed.link
66
+ assert_equal "http://example.org/2005/04/02/atom", @atom.entries.first.link
67
+ assert_equal "http://example.org/2005/04/02/atom", @atom.entries.first[:link]
68
+ end
33
69
 
34
- def test_media_rss
35
- assert_equal 20, @media_rss.items.size
36
- assert_equal "Uploads from herval", @media_rss.title
37
- assert_equal "http://www.flickr.com/photos/herval/", @media_rss.channel.link
38
- assert_equal "http://www.flickr.com/photos/herval/4671960608/", @media_rss.items.first.link
39
- assert_equal "http://www.flickr.com/photos/herval/4671960608/", @media_rss.items.first[:link]
40
- assert_equal "http://farm5.static.flickr.com/4040/4671960608_10cb945d5c_o.jpg", @media_rss.items.first.media_content_url
41
- assert_equal "image/jpeg", @media_rss.items.first.media_content_type
42
- assert_equal "3168", @media_rss.items.first.media_content_height
43
- assert_equal "4752", @media_rss.items.first.media_content_width
44
- assert_equal "Woof?", @media_rss.items.first.media_title
45
- assert_equal "http://farm5.static.flickr.com/4040/4671960608_954d2297bc_s.jpg", @media_rss.items.first.media_thumbnail_url
46
- assert_equal "75", @media_rss.items.first.media_thumbnail_height
47
- assert_equal "75", @media_rss.items.first.media_thumbnail_width
48
- assert_equal "herval", @media_rss.items.first.media_credit
49
- assert_equal "photographer", @media_rss.items.first.media_credit_role
50
- assert_equal "pets frodo", @media_rss.items.first.media_category
51
- assert_equal "urn:flickr:tags", @media_rss.items.first.media_category_scheme
52
- end
53
-
54
- def test_rss20
55
- assert_equal 10, @rss20.items.size
56
- assert_equal "Technoblog", @rss20.title
57
- assert_equal "http://tech.rufy.com", @rss20.channel.link
58
- assert_equal "http://feeds.feedburner.com/rufytech?m=68", @rss20.items.first.link
59
- assert_equal "http://feeds.feedburner.com/rufytech?m=68", @rss20.items.first[:link]
60
- assert_equal "This is an XML content feed. It is intended to be viewed in a newsreader or syndicated to another site.", @rss20.channel.feedburner_browserFriendly
61
- end
62
-
63
- def test_atom
64
- assert_equal 1, @atom.entries.size
65
- assert_equal "dive into mark", @atom.title
66
- assert_equal "http://example.org/", @atom.feed.link
67
- assert_equal "http://example.org/2005/04/02/atom", @atom.entries.first.link
68
- assert_equal "http://example.org/2005/04/02/atom", @atom.entries.first[:link]
69
- end
70
-
71
- def test_bad_feed
72
- assert_raise(SimpleRSSError) { SimpleRSS.parse(open(File.dirname(__FILE__) + '/../data/not-rss.xml')) }
73
- end
70
+ def test_bad_feed
71
+ assert_raise(SimpleRSSError) { SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/not-rss.xml")) }
72
+ end
74
73
 
75
- def test_rss_utf8
76
- assert_equal 2, @rss20_utf8.items.size
77
- assert_equal "SC5 Blog", @rss20_utf8.title
78
- assert_equal Encoding::UTF_8, @rss20_utf8.title.encoding
79
- item = @rss20_utf8.items.first
80
- assert_equal "Mitä asiakkaamme ajattelevat meistä?", item.title
81
- assert_equal Encoding::UTF_8, item.title.encoding
82
- end
74
+ def test_rss_utf8
75
+ assert_equal 2, @rss20_utf8.items.size
76
+ assert_equal "SC5 Blog", @rss20_utf8.title
77
+ assert_equal Encoding::UTF_8, @rss20_utf8.title.encoding
78
+ item = @rss20_utf8.items.first
79
+ assert_equal "Mitä asiakkaamme ajattelevat meistä?", item.title
80
+ assert_equal Encoding::UTF_8, item.title.encoding
81
+ end
83
82
  end
@@ -0,0 +1,56 @@
1
+ require_relative "../test_helper"
2
+ require "timeout"
3
+
4
+ class EmptyTagTest < Test::Unit::TestCase
5
+ def setup
6
+ @rss20 = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
7
+ end
8
+
9
+ def test_empty_item_tag_does_not_hang
10
+ # Reproduces issue #16: 100% cpu and hanging process on blank item tag
11
+ # Adding an empty tag should not cause regex catastrophic backtracking
12
+ original_tags = SimpleRSS.item_tags.dup
13
+
14
+ begin
15
+ SimpleRSS.item_tags << :""
16
+
17
+ # This should complete quickly, not hang
18
+ Timeout.timeout(5) do
19
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
20
+ assert_not_nil rss.items
21
+ end
22
+ ensure
23
+ SimpleRSS.item_tags.replace(original_tags)
24
+ end
25
+ end
26
+
27
+ def test_blank_item_tag_does_not_hang
28
+ original_tags = SimpleRSS.item_tags.dup
29
+
30
+ begin
31
+ SimpleRSS.item_tags << :" "
32
+
33
+ Timeout.timeout(5) do
34
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
35
+ assert_not_nil rss.items
36
+ end
37
+ ensure
38
+ SimpleRSS.item_tags.replace(original_tags)
39
+ end
40
+ end
41
+
42
+ def test_empty_feed_tag_does_not_hang
43
+ original_tags = SimpleRSS.feed_tags.dup
44
+
45
+ begin
46
+ SimpleRSS.feed_tags << :""
47
+
48
+ Timeout.timeout(5) do
49
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
50
+ assert_not_nil rss.channel
51
+ end
52
+ ensure
53
+ SimpleRSS.feed_tags.replace(original_tags)
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,87 @@
1
+ require_relative "../test_helper"
2
+
3
+ class EncodingTest < Test::Unit::TestCase
4
+ def test_strings_are_utf8_encoded
5
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
6
+
7
+ assert_equal Encoding::UTF_8, rss.title.encoding
8
+ assert_equal Encoding::UTF_8, rss.link.encoding
9
+ assert_equal Encoding::UTF_8, rss.description.encoding
10
+ end
11
+
12
+ def test_strings_without_percent_are_utf8_encoded
13
+ # Issue #28: strings without '%' were returning ASCII-8BIT
14
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20.xml"))
15
+
16
+ rss.items.each do |item|
17
+ assert_equal Encoding::UTF_8, item.title.encoding, "Item title should be UTF-8"
18
+ assert_equal Encoding::UTF_8, item.link.encoding, "Item link should be UTF-8"
19
+ end
20
+ end
21
+
22
+ def test_utf8_content_preserved
23
+ rss = SimpleRSS.parse(open(File.dirname(__FILE__) + "/../data/rss20_utf8.xml"))
24
+
25
+ assert_equal Encoding::UTF_8, rss.title.encoding
26
+ # Verify UTF-8 characters are preserved
27
+ assert(rss.items.any? { |item| item.title && item.title.encoding == Encoding::UTF_8 })
28
+ end
29
+
30
+ def test_ascii_8bit_source_normalized_to_utf8
31
+ # Issue #28: when source is ASCII-8BIT, output should still be UTF-8
32
+ xml = <<~XML.b # .b forces ASCII-8BIT encoding
33
+ <?xml version="1.0"?>
34
+ <rss version="2.0">
35
+ <channel>
36
+ <title>Test Feed</title>
37
+ <link>http://example.com</link>
38
+ <description>A test feed</description>
39
+ <item>
40
+ <title>Test Item</title>
41
+ <link>http://example.com/item</link>
42
+ </item>
43
+ </channel>
44
+ </rss>
45
+ XML
46
+
47
+ assert_equal Encoding::ASCII_8BIT, xml.encoding, "Source should be ASCII-8BIT"
48
+
49
+ rss = SimpleRSS.parse(xml)
50
+
51
+ assert_equal Encoding::UTF_8, rss.title.encoding, "Title should be UTF-8"
52
+ assert_equal Encoding::UTF_8, rss.link.encoding, "Link should be UTF-8"
53
+ assert_equal Encoding::UTF_8, rss.items.first.title.encoding, "Item title should be UTF-8"
54
+ end
55
+
56
+ def test_consistent_encoding_with_and_without_percent
57
+ # Issue #28: CGI.unescape returns UTF-8, but without '%' we got ASCII-8BIT
58
+ xml_with_percent = <<~XML.b
59
+ <?xml version="1.0"?>
60
+ <rss version="2.0">
61
+ <channel>
62
+ <title>Test%20Feed</title>
63
+ <link>http://example.com</link>
64
+ <description>Test</description>
65
+ </channel>
66
+ </rss>
67
+ XML
68
+
69
+ xml_without_percent = <<~XML.b
70
+ <?xml version="1.0"?>
71
+ <rss version="2.0">
72
+ <channel>
73
+ <title>Test Feed</title>
74
+ <link>http://example.com</link>
75
+ <description>Test</description>
76
+ </channel>
77
+ </rss>
78
+ XML
79
+
80
+ rss_with = SimpleRSS.parse(xml_with_percent)
81
+ rss_without = SimpleRSS.parse(xml_without_percent)
82
+
83
+ assert_equal rss_with.title.encoding, rss_without.title.encoding,
84
+ "Encoding should be consistent regardless of '%' in content"
85
+ assert_equal Encoding::UTF_8, rss_without.title.encoding
86
+ end
87
+ end
@@ -0,0 +1,101 @@
1
+ require "test_helper"
2
+
3
+ class EnumerableTest < Test::Unit::TestCase
4
+ def setup
5
+ @rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20.xml")
6
+ @atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom.xml")
7
+ end
8
+
9
+ def test_includes_enumerable
10
+ assert_includes SimpleRSS.included_modules, Enumerable
11
+ end
12
+
13
+ def test_each_iterates_over_items
14
+ titles = @rss20.map { |item| item[:title] }
15
+ assert_equal @rss20.items.map { |i| i[:title] }, titles
16
+ end
17
+
18
+ def test_each_returns_enumerator_without_block
19
+ enumerator = @rss20.each
20
+ assert_kind_of Enumerator, enumerator
21
+ assert_equal @rss20.items.size, enumerator.count
22
+ end
23
+
24
+ def test_each_returns_self_with_block
25
+ count = 0
26
+ result = @rss20.each { |_item| count += 1 }
27
+ assert_equal @rss20, result
28
+ assert_equal @rss20.items.size, count
29
+ end
30
+
31
+ def test_enumerable_map
32
+ titles = @rss20.map { |item| item[:title] }
33
+ assert_equal @rss20.items.map { |i| i[:title] }, titles
34
+ end
35
+
36
+ def test_enumerable_select
37
+ items_with_link = @rss20.select { |item| item[:link] }
38
+ assert_equal @rss20.items.select { |i| i[:link] }, items_with_link
39
+ end
40
+
41
+ def test_enumerable_first
42
+ assert_equal @rss20.items.first, @rss20.first
43
+ assert_equal @rss20.items.first(3), @rss20.first(3)
44
+ end
45
+
46
+ def test_enumerable_count
47
+ assert_equal @rss20.items.size, @rss20.count
48
+ end
49
+
50
+ def test_index_accessor
51
+ assert_equal @rss20.items[0], @rss20[0]
52
+ assert_equal @rss20.items[5], @rss20[5]
53
+ assert_equal @rss20.items[-1], @rss20[-1]
54
+ end
55
+
56
+ def test_index_accessor_out_of_bounds
57
+ assert_nil @rss20[100]
58
+ end
59
+
60
+ def test_latest_returns_sorted_items
61
+ latest = @rss20.latest(3)
62
+ assert_equal 3, latest.size
63
+
64
+ dates = latest.map { |item| item[:pubDate] }
65
+ assert_equal dates, dates.sort.reverse
66
+ end
67
+
68
+ def test_latest_default_count
69
+ latest = @rss20.latest
70
+ assert latest.size <= 10
71
+ end
72
+
73
+ def test_latest_with_atom_uses_updated
74
+ latest = @atom.latest(1)
75
+ assert_equal 1, latest.size
76
+ end
77
+
78
+ def test_latest_handles_missing_dates
79
+ rss_with_missing_dates = SimpleRSS.parse <<~RSS
80
+ <?xml version="1.0" encoding="UTF-8"?>
81
+ <rss version="2.0">
82
+ <channel>
83
+ <title>Test Feed</title>
84
+ <link>http://example.com</link>
85
+ <item>
86
+ <title>No Date</title>
87
+ </item>
88
+ <item>
89
+ <title>Has Date</title>
90
+ <pubDate>Wed, 24 Aug 2005 13:33:34 GMT</pubDate>
91
+ </item>
92
+ </channel>
93
+ </rss>
94
+ RSS
95
+
96
+ latest = rss_with_missing_dates.latest(2)
97
+ assert_equal 2, latest.size
98
+ assert_equal "Has Date", latest.first[:title]
99
+ assert_equal "No Date", latest.last[:title]
100
+ end
101
+ end
@@ -0,0 +1,26 @@
1
+ require "test_helper"
2
+
3
+ class FeedAttributesTest < Test::Unit::TestCase
4
+ def setup
5
+ # Add feed attribute tags before parsing
6
+ SimpleRSS.feed_tags << :"channel#custom:version"
7
+ SimpleRSS.feed_tags << :"feed#app:id"
8
+
9
+ @rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20_with_channel_attrs.xml")
10
+ @atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom_with_feed_attrs.xml")
11
+ end
12
+
13
+ def teardown
14
+ # Clean up added tags
15
+ SimpleRSS.feed_tags.delete(:"channel#custom:version")
16
+ SimpleRSS.feed_tags.delete(:"feed#app:id")
17
+ end
18
+
19
+ def test_rss20_channel_attribute
20
+ assert_equal "2.0", @rss20.channel_custom_version
21
+ end
22
+
23
+ def test_atom_feed_attribute
24
+ assert_equal "12345", @atom.feed_app_id
25
+ end
26
+ end
@@ -0,0 +1,117 @@
1
+ require "test_helper"
2
+ require "net/http"
3
+
4
+ class FetchTest < Test::Unit::TestCase
5
+ def setup
6
+ @sample_feed = File.read(File.dirname(__FILE__) + "/../data/rss20.xml")
7
+ end
8
+
9
+ # Test attr_readers exist and default to nil for parsed feeds
10
+
11
+ def test_etag_attr_reader_exists
12
+ rss = SimpleRSS.parse(@sample_feed)
13
+ assert_respond_to rss, :etag
14
+ end
15
+
16
+ def test_last_modified_attr_reader_exists
17
+ rss = SimpleRSS.parse(@sample_feed)
18
+ assert_respond_to rss, :last_modified
19
+ end
20
+
21
+ def test_etag_nil_for_parsed_feed
22
+ rss = SimpleRSS.parse(@sample_feed)
23
+ assert_nil rss.etag
24
+ end
25
+
26
+ def test_last_modified_nil_for_parsed_feed
27
+ rss = SimpleRSS.parse(@sample_feed)
28
+ assert_nil rss.last_modified
29
+ end
30
+
31
+ # Test fetch class method exists
32
+
33
+ def test_fetch_class_method_exists
34
+ assert_respond_to SimpleRSS, :fetch
35
+ end
36
+
37
+ # Test fetch with invalid URL raises error
38
+
39
+ def test_fetch_raises_on_invalid_host
40
+ # Socket::ResolutionError was added in Ruby 3.3, use SocketError for older versions
41
+ expected_errors = [SocketError, Errno::ECONNREFUSED, SimpleRSSError]
42
+ expected_errors << Socket::ResolutionError if defined?(Socket::ResolutionError)
43
+ assert_raise(*expected_errors) do
44
+ SimpleRSS.fetch("http://this-host-does-not-exist-12345.invalid/feed.xml", timeout: 1)
45
+ end
46
+ end
47
+
48
+ # Test fetch options are accepted
49
+
50
+ def test_fetch_accepts_etag_option
51
+ # Just verify it doesn't raise an ArgumentError
52
+ assert_nothing_raised do
53
+ SimpleRSS.fetch("http://localhost:1/feed.xml", etag: '"abc123"', timeout: 0.1)
54
+ rescue StandardError
55
+ # Expected - connection will fail
56
+ end
57
+ end
58
+
59
+ def test_fetch_accepts_last_modified_option
60
+ assert_nothing_raised do
61
+ SimpleRSS.fetch("http://localhost:1/feed.xml", last_modified: "Wed, 21 Oct 2015 07:28:00 GMT", timeout: 0.1)
62
+ rescue StandardError
63
+ # Expected - connection will fail
64
+ end
65
+ end
66
+
67
+ def test_fetch_accepts_headers_option
68
+ assert_nothing_raised do
69
+ SimpleRSS.fetch("http://localhost:1/feed.xml", headers: { "X-Custom" => "test" }, timeout: 0.1)
70
+ rescue StandardError
71
+ # Expected - connection will fail
72
+ end
73
+ end
74
+
75
+ def test_fetch_accepts_timeout_option
76
+ assert_nothing_raised do
77
+ SimpleRSS.fetch("http://localhost:1/feed.xml", timeout: 0.1)
78
+ rescue StandardError
79
+ # Expected - connection will fail
80
+ end
81
+ end
82
+
83
+ def test_fetch_accepts_follow_redirects_option
84
+ assert_nothing_raised do
85
+ SimpleRSS.fetch("http://localhost:1/feed.xml", follow_redirects: false, timeout: 0.1)
86
+ rescue StandardError
87
+ # Expected - connection will fail
88
+ end
89
+ end
90
+ end
91
+
92
+ # Integration tests that require network access
93
+ # These are skipped by default, run with NETWORK_TESTS=1
94
+ class FetchIntegrationTest < Test::Unit::TestCase
95
+ def test_fetch_real_feed
96
+ omit unless ENV["NETWORK_TESTS"]
97
+ # Use a reliable, long-lived RSS feed
98
+ rss = SimpleRSS.fetch("https://feeds.bbci.co.uk/news/rss.xml", timeout: 10)
99
+ assert_kind_of SimpleRSS, rss
100
+ assert rss.title
101
+ assert rss.items.any?
102
+ end
103
+
104
+ def test_fetch_stores_caching_headers
105
+ omit unless ENV["NETWORK_TESTS"]
106
+ rss = SimpleRSS.fetch("https://feeds.bbci.co.uk/news/rss.xml", timeout: 10)
107
+ # At least one of these should be present for most feeds
108
+ assert(rss.etag || rss.last_modified, "Expected ETag or Last-Modified header")
109
+ end
110
+
111
+ def test_fetch_follows_redirect
112
+ omit unless ENV["NETWORK_TESTS"]
113
+ # GitHub raw URLs often redirect
114
+ rss = SimpleRSS.fetch("https://github.com/cardmagic/simple-rss/commits/master.atom", timeout: 10)
115
+ assert_kind_of SimpleRSS, rss
116
+ end
117
+ end