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.
- checksums.yaml +4 -4
- data/LICENSE +79 -7
- data/README.md +321 -0
- data/Rakefile +134 -136
- data/lib/simple-rss.rb +541 -154
- data/simple-rss.gemspec +5 -6
- data/test/base/array_tags_test.rb +37 -0
- data/test/base/base_test.rb +76 -77
- data/test/base/empty_tag_test.rb +56 -0
- data/test/base/encoding_test.rb +87 -0
- data/test/base/enumerable_test.rb +101 -0
- data/test/base/feed_attributes_test.rb +26 -0
- data/test/base/fetch_test.rb +117 -0
- data/test/base/hash_xml_serialization_test.rb +142 -0
- data/test/base/item_attributes_test.rb +26 -0
- data/test/base/json_serialization_test.rb +81 -0
- data/test/data/atom_with_entry_attrs.xml +13 -0
- data/test/data/atom_with_feed_attrs.xml +13 -0
- data/test/data/media_rss.xml +465 -0
- data/test/data/rss20_utf8.xml +61 -0
- data/test/data/rss20_with_channel_attrs.xml +13 -0
- data/test/data/rss20_with_item_attrs.xml +13 -0
- data/test/test_helper.rb +10 -3
- metadata +21 -11
- data/README.markdown +0 -47
- data/install.rb +0 -40
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.
|
|
4
|
-
s.
|
|
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 = "
|
|
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 = ["
|
|
12
|
-
s.
|
|
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
|
data/test/base/base_test.rb
CHANGED
|
@@ -1,83 +1,82 @@
|
|
|
1
|
-
|
|
2
|
-
require 'test_helper'
|
|
1
|
+
require "test_helper"
|
|
3
2
|
class BaseTest < Test::Unit::TestCase
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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&from=rss", @rss09.items.first.link
|
|
28
|
+
assert_equal "http://books.slashdot.org/article.pl?sid=05/08/29/1319236&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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|