simple-rss 2.0.0 → 2.2.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/README.md +321 -0
- data/lib/simple-rss.rb +490 -4
- data/simple-rss.gemspec +4 -4
- data/test/base/enumerable_test.rb +101 -0
- data/test/base/feed_merging_and_diffing_test.rb +140 -0
- data/test/base/fetch_integration_test.rb +25 -0
- data/test/base/fetch_test.rb +90 -0
- data/test/base/filtering_and_validation_test.rb +187 -0
- data/test/base/hash_xml_serialization_test.rb +142 -0
- data/test/base/json_serialization_test.rb +81 -0
- data/test/base/media_and_enclosure_helpers_test.rb +84 -0
- metadata +13 -5
- data/README.markdown +0 -47
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class FeedMergingAndDiffingTest < Test::Unit::TestCase
|
|
4
|
+
def setup
|
|
5
|
+
@feed_one = SimpleRSS.parse <<~XML
|
|
6
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
7
|
+
<rss version="2.0">
|
|
8
|
+
<channel>
|
|
9
|
+
<title>Feed One</title>
|
|
10
|
+
<item>
|
|
11
|
+
<guid>shared-guid</guid>
|
|
12
|
+
<title>Shared (older)</title>
|
|
13
|
+
<pubDate>Mon, 01 Jan 2024 10:00:00 UTC</pubDate>
|
|
14
|
+
</item>
|
|
15
|
+
<item>
|
|
16
|
+
<guid>one-guid</guid>
|
|
17
|
+
<title>Only One</title>
|
|
18
|
+
<pubDate>Mon, 01 Jan 2024 11:00:00 UTC</pubDate>
|
|
19
|
+
</item>
|
|
20
|
+
<item>
|
|
21
|
+
<title>Unidentified One</title>
|
|
22
|
+
</item>
|
|
23
|
+
</channel>
|
|
24
|
+
</rss>
|
|
25
|
+
XML
|
|
26
|
+
|
|
27
|
+
@feed_two = SimpleRSS.parse <<~XML
|
|
28
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
29
|
+
<rss version="2.0">
|
|
30
|
+
<channel>
|
|
31
|
+
<title>Feed Two</title>
|
|
32
|
+
<item>
|
|
33
|
+
<guid>shared-guid</guid>
|
|
34
|
+
<title>Shared (newer)</title>
|
|
35
|
+
<pubDate>Mon, 01 Jan 2024 12:00:00 UTC</pubDate>
|
|
36
|
+
</item>
|
|
37
|
+
<item>
|
|
38
|
+
<link>https://example.com/two-only</link>
|
|
39
|
+
<title>Only Two</title>
|
|
40
|
+
<pubDate>Mon, 01 Jan 2024 13:00:00 UTC</pubDate>
|
|
41
|
+
</item>
|
|
42
|
+
<item>
|
|
43
|
+
<title>Unidentified Two</title>
|
|
44
|
+
</item>
|
|
45
|
+
</channel>
|
|
46
|
+
</rss>
|
|
47
|
+
XML
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def test_merge_dedupes_and_sorts_newest_first
|
|
51
|
+
merged = @feed_one.merge(@feed_two)
|
|
52
|
+
titles = merged.map { |item| item[:title] }
|
|
53
|
+
|
|
54
|
+
assert_equal [
|
|
55
|
+
"Only Two",
|
|
56
|
+
"Shared (newer)",
|
|
57
|
+
"Only One",
|
|
58
|
+
"Unidentified One",
|
|
59
|
+
"Unidentified Two"
|
|
60
|
+
], titles
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def test_class_merge_combines_multiple_feeds
|
|
64
|
+
merged = SimpleRSS.merge(@feed_one, @feed_two)
|
|
65
|
+
|
|
66
|
+
assert_equal 5, merged.size
|
|
67
|
+
assert_equal "Only Two", merged.first[:title]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_diff_reports_added_and_removed_items
|
|
71
|
+
old_feed = SimpleRSS.parse <<~XML
|
|
72
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
73
|
+
<rss version="2.0">
|
|
74
|
+
<channel>
|
|
75
|
+
<title>Old Feed</title>
|
|
76
|
+
<item>
|
|
77
|
+
<guid>stay</guid>
|
|
78
|
+
<title>Stay</title>
|
|
79
|
+
</item>
|
|
80
|
+
<item>
|
|
81
|
+
<guid>remove</guid>
|
|
82
|
+
<title>Remove</title>
|
|
83
|
+
</item>
|
|
84
|
+
</channel>
|
|
85
|
+
</rss>
|
|
86
|
+
XML
|
|
87
|
+
|
|
88
|
+
new_feed = SimpleRSS.parse <<~XML
|
|
89
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
90
|
+
<rss version="2.0">
|
|
91
|
+
<channel>
|
|
92
|
+
<title>New Feed</title>
|
|
93
|
+
<item>
|
|
94
|
+
<guid>stay</guid>
|
|
95
|
+
<title>Stay</title>
|
|
96
|
+
</item>
|
|
97
|
+
<item>
|
|
98
|
+
<guid>add</guid>
|
|
99
|
+
<title>Add</title>
|
|
100
|
+
</item>
|
|
101
|
+
</channel>
|
|
102
|
+
</rss>
|
|
103
|
+
XML
|
|
104
|
+
|
|
105
|
+
diff = old_feed.diff(new_feed)
|
|
106
|
+
|
|
107
|
+
assert_equal(["Add"], diff[:added].map { |item| item[:title] })
|
|
108
|
+
assert_equal(["Remove"], diff[:removed].map { |item| item[:title] })
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def test_dedupe_mutates_items_and_keeps_unidentified_entries
|
|
112
|
+
feed = SimpleRSS.parse <<~XML
|
|
113
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
114
|
+
<rss version="2.0">
|
|
115
|
+
<channel>
|
|
116
|
+
<title>Dedupe Feed</title>
|
|
117
|
+
<item>
|
|
118
|
+
<guid>duplicate</guid>
|
|
119
|
+
<title>First duplicate</title>
|
|
120
|
+
</item>
|
|
121
|
+
<item>
|
|
122
|
+
<guid>duplicate</guid>
|
|
123
|
+
<title>Second duplicate</title>
|
|
124
|
+
</item>
|
|
125
|
+
<item>
|
|
126
|
+
<title>Unidentified One</title>
|
|
127
|
+
</item>
|
|
128
|
+
<item>
|
|
129
|
+
<title>Unidentified Two</title>
|
|
130
|
+
</item>
|
|
131
|
+
</channel>
|
|
132
|
+
</rss>
|
|
133
|
+
XML
|
|
134
|
+
|
|
135
|
+
result = feed.dedupe
|
|
136
|
+
|
|
137
|
+
assert_same feed, result
|
|
138
|
+
assert_equal(["First duplicate", "Unidentified One", "Unidentified Two"], feed.items.map { |item| item[:title] })
|
|
139
|
+
end
|
|
140
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
# Integration tests that require network access
|
|
4
|
+
# These are skipped by default, run with NETWORK_TESTS=1
|
|
5
|
+
class FetchIntegrationTest < Test::Unit::TestCase
|
|
6
|
+
def test_fetch_real_feed
|
|
7
|
+
omit unless ENV["NETWORK_TESTS"]
|
|
8
|
+
rss = SimpleRSS.fetch("https://feeds.bbci.co.uk/news/rss.xml", timeout: 10)
|
|
9
|
+
assert_kind_of SimpleRSS, rss
|
|
10
|
+
assert rss.title
|
|
11
|
+
assert rss.items.any?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def test_fetch_stores_caching_headers
|
|
15
|
+
omit unless ENV["NETWORK_TESTS"]
|
|
16
|
+
rss = SimpleRSS.fetch("https://feeds.bbci.co.uk/news/rss.xml", timeout: 10)
|
|
17
|
+
assert(rss.etag || rss.last_modified, "Expected ETag or Last-Modified header")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_fetch_follows_redirect
|
|
21
|
+
omit unless ENV["NETWORK_TESTS"]
|
|
22
|
+
rss = SimpleRSS.fetch("https://github.com/cardmagic/simple-rss/commits/master.atom", timeout: 10)
|
|
23
|
+
assert_kind_of SimpleRSS, rss
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
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
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class FilteringAndValidationTest < 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
|
+
@atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom.xml")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_feed_type_for_known_formats
|
|
11
|
+
assert_equal :rss1, @rss09.feed_type
|
|
12
|
+
assert_equal :rss2, @rss20.feed_type
|
|
13
|
+
assert_equal :atom, @atom.feed_type
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_feed_type_unknown_for_non_standard_feed
|
|
17
|
+
feed = SimpleRSS.parse <<~XML
|
|
18
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
19
|
+
<feed>
|
|
20
|
+
<title>Unknown Feed</title>
|
|
21
|
+
<entry>
|
|
22
|
+
<title>Post</title>
|
|
23
|
+
</entry>
|
|
24
|
+
</feed>
|
|
25
|
+
XML
|
|
26
|
+
|
|
27
|
+
assert_equal :unknown, feed.feed_type
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def test_class_valid_returns_true_for_well_formed_feed
|
|
31
|
+
xml = <<~XML
|
|
32
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<rss version="2.0">
|
|
34
|
+
<channel>
|
|
35
|
+
<title>Valid Feed</title>
|
|
36
|
+
<link>http://example.com</link>
|
|
37
|
+
<item>
|
|
38
|
+
<title>Post</title>
|
|
39
|
+
</item>
|
|
40
|
+
</channel>
|
|
41
|
+
</rss>
|
|
42
|
+
XML
|
|
43
|
+
|
|
44
|
+
assert_equal true, SimpleRSS.valid?(xml)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_class_valid_returns_false_for_invalid_feed
|
|
48
|
+
invalid_xml = open(File.dirname(__FILE__) + "/../data/not-rss.xml").read
|
|
49
|
+
|
|
50
|
+
assert_equal false, SimpleRSS.valid?(invalid_xml)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def test_class_valid_returns_false_when_source_read_fails
|
|
54
|
+
unreadable_source = Object.new
|
|
55
|
+
unreadable_source.define_singleton_method(:read) do
|
|
56
|
+
raise IOError, "stream closed"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
assert_equal false, SimpleRSS.valid?(unreadable_source)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def test_instance_valid_requires_metadata_and_items
|
|
63
|
+
valid_feed = SimpleRSS.parse <<~XML
|
|
64
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
65
|
+
<rss version="2.0">
|
|
66
|
+
<channel>
|
|
67
|
+
<title>Valid Feed</title>
|
|
68
|
+
<item>
|
|
69
|
+
<title>Post</title>
|
|
70
|
+
</item>
|
|
71
|
+
</channel>
|
|
72
|
+
</rss>
|
|
73
|
+
XML
|
|
74
|
+
|
|
75
|
+
invalid_feed = SimpleRSS.parse <<~XML
|
|
76
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
77
|
+
<rss version="2.0">
|
|
78
|
+
<channel>
|
|
79
|
+
<description>No title and no link</description>
|
|
80
|
+
<item>
|
|
81
|
+
<description>Body only</description>
|
|
82
|
+
</item>
|
|
83
|
+
</channel>
|
|
84
|
+
</rss>
|
|
85
|
+
XML
|
|
86
|
+
|
|
87
|
+
empty_feed = SimpleRSS.parse <<~XML
|
|
88
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
89
|
+
<rss version="2.0">
|
|
90
|
+
<channel>
|
|
91
|
+
<title>No Items</title>
|
|
92
|
+
</channel>
|
|
93
|
+
</rss>
|
|
94
|
+
XML
|
|
95
|
+
|
|
96
|
+
assert_equal true, valid_feed.valid?
|
|
97
|
+
assert_equal false, invalid_feed.valid?
|
|
98
|
+
assert_equal false, empty_feed.valid?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def test_items_since_filters_by_date
|
|
102
|
+
threshold = Time.parse("Wed Aug 24 13:30:00 UTC 2005")
|
|
103
|
+
|
|
104
|
+
filtered = @rss20.items_since(threshold)
|
|
105
|
+
|
|
106
|
+
assert_equal 1, filtered.size
|
|
107
|
+
assert_operator filtered.first[:pubDate], :>, threshold
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def test_items_by_category_matches_strings_and_arrays
|
|
111
|
+
feed_with_string_category = SimpleRSS.parse <<~XML
|
|
112
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
+
<rss version="2.0">
|
|
114
|
+
<channel>
|
|
115
|
+
<title>String Category Feed</title>
|
|
116
|
+
<item>
|
|
117
|
+
<title>Ruby News</title>
|
|
118
|
+
<category>Technology</category>
|
|
119
|
+
</item>
|
|
120
|
+
<item>
|
|
121
|
+
<title>Sports News</title>
|
|
122
|
+
<category>Sports</category>
|
|
123
|
+
</item>
|
|
124
|
+
</channel>
|
|
125
|
+
</rss>
|
|
126
|
+
XML
|
|
127
|
+
|
|
128
|
+
feed_with_array_category = SimpleRSS.parse(
|
|
129
|
+
<<~XML,
|
|
130
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
131
|
+
<rss version="2.0">
|
|
132
|
+
<channel>
|
|
133
|
+
<title>Array Category Feed</title>
|
|
134
|
+
<item>
|
|
135
|
+
<title>Dev Update</title>
|
|
136
|
+
<category>Technology</category>
|
|
137
|
+
<category>Ruby</category>
|
|
138
|
+
</item>
|
|
139
|
+
</channel>
|
|
140
|
+
</rss>
|
|
141
|
+
XML
|
|
142
|
+
array_tags: [:category]
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
string_results = feed_with_string_category.items_by_category("tech")
|
|
146
|
+
array_results = feed_with_array_category.items_by_category("ruby")
|
|
147
|
+
|
|
148
|
+
assert_equal 1, string_results.size
|
|
149
|
+
assert_equal "Ruby News", string_results.first[:title]
|
|
150
|
+
assert_equal 1, array_results.size
|
|
151
|
+
assert_equal "Dev Update", array_results.first[:title]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def test_search_matches_title_description_summary_and_content
|
|
155
|
+
feed = SimpleRSS.parse <<~XML
|
|
156
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
157
|
+
<rss version="2.0">
|
|
158
|
+
<channel>
|
|
159
|
+
<title>Search Feed</title>
|
|
160
|
+
<item>
|
|
161
|
+
<title>Ruby Patterns</title>
|
|
162
|
+
<description>Language design</description>
|
|
163
|
+
</item>
|
|
164
|
+
<item>
|
|
165
|
+
<title>Other Topic</title>
|
|
166
|
+
<description>Talks about BREAKING updates</description>
|
|
167
|
+
</item>
|
|
168
|
+
<item>
|
|
169
|
+
<title>Third Topic</title>
|
|
170
|
+
<summary>A quick ruby summary</summary>
|
|
171
|
+
</item>
|
|
172
|
+
<item>
|
|
173
|
+
<title>Fourth Topic</title>
|
|
174
|
+
<content>Deep dive into Ruby internals</content>
|
|
175
|
+
</item>
|
|
176
|
+
</channel>
|
|
177
|
+
</rss>
|
|
178
|
+
XML
|
|
179
|
+
|
|
180
|
+
ruby_results = feed.search("ruby")
|
|
181
|
+
breaking_results = feed.search("breaking")
|
|
182
|
+
|
|
183
|
+
assert_equal 3, ruby_results.size
|
|
184
|
+
assert_equal 1, breaking_results.size
|
|
185
|
+
assert_equal "Other Topic", breaking_results.first[:title]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class HashXmlSerializationTest < 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
|
+
# to_hash tests
|
|
10
|
+
|
|
11
|
+
def test_to_hash_returns_hash
|
|
12
|
+
result = @rss20.to_hash
|
|
13
|
+
assert_kind_of Hash, result
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def test_to_hash_includes_feed_title
|
|
17
|
+
result = @rss20.to_hash
|
|
18
|
+
assert_equal "Technoblog", result[:title]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_to_hash_includes_items
|
|
22
|
+
result = @rss20.to_hash
|
|
23
|
+
assert_kind_of Array, result[:items]
|
|
24
|
+
assert_equal 10, result[:items].size
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_to_hash_is_alias_for_as_json
|
|
28
|
+
assert_equal @rss20.as_json, @rss20.to_hash
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# to_xml RSS 2.0 tests
|
|
32
|
+
|
|
33
|
+
def test_to_xml_returns_string
|
|
34
|
+
result = @rss20.to_xml
|
|
35
|
+
assert_kind_of String, result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def test_to_xml_default_format_is_rss2
|
|
39
|
+
result = @rss20.to_xml
|
|
40
|
+
assert_match(/<rss version="2.0">/, result)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_to_xml_rss2_has_xml_declaration
|
|
44
|
+
result = @rss20.to_xml(format: :rss2)
|
|
45
|
+
assert_match(/^<\?xml version="1.0" encoding="UTF-8"\?>/, result)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_to_xml_rss2_has_channel
|
|
49
|
+
result = @rss20.to_xml(format: :rss2)
|
|
50
|
+
assert_match(/<channel>/, result)
|
|
51
|
+
assert_match(%r{</channel>}, result)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_to_xml_rss2_has_title
|
|
55
|
+
result = @rss20.to_xml(format: :rss2)
|
|
56
|
+
assert_match(%r{<title>Technoblog</title>}, result)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_to_xml_rss2_has_link
|
|
60
|
+
result = @rss20.to_xml(format: :rss2)
|
|
61
|
+
assert_match(%r{<link>http://tech.rufy.com</link>}, result)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_to_xml_rss2_has_items
|
|
65
|
+
result = @rss20.to_xml(format: :rss2)
|
|
66
|
+
assert_match(/<item>/, result)
|
|
67
|
+
assert_match(%r{</item>}, result)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_to_xml_rss2_item_has_title
|
|
71
|
+
result = @rss20.to_xml(format: :rss2)
|
|
72
|
+
assert_match(/<item>\n<title>/, result)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_to_xml_rss2_item_has_guid
|
|
76
|
+
result = @rss20.to_xml(format: :rss2)
|
|
77
|
+
assert_match(%r{<guid>http://tech.rufy.com/entry/\d+</guid>}, result)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def test_to_xml_rss2_escapes_special_characters
|
|
81
|
+
rss = SimpleRSS.parse('<rss version="2.0"><channel><title>Test & Title</title><item><title>Item <1></title></item></channel></rss>')
|
|
82
|
+
result = rss.to_xml(format: :rss2)
|
|
83
|
+
assert_match(/&/, result)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# to_xml Atom tests
|
|
87
|
+
|
|
88
|
+
def test_to_xml_atom_format
|
|
89
|
+
result = @atom.to_xml(format: :atom)
|
|
90
|
+
assert_match(%r{<feed xmlns="http://www.w3.org/2005/Atom">}, result)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def test_to_xml_atom_has_title
|
|
94
|
+
result = @atom.to_xml(format: :atom)
|
|
95
|
+
assert_match(%r{<title>dive into mark</title>}, result)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def test_to_xml_atom_has_link
|
|
99
|
+
result = @atom.to_xml(format: :atom)
|
|
100
|
+
assert_match(%r{<link href="http://example.org/" rel="alternate"/>}, result)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_to_xml_atom_has_entries
|
|
104
|
+
result = @atom.to_xml(format: :atom)
|
|
105
|
+
assert_match(/<entry>/, result)
|
|
106
|
+
assert_match(%r{</entry>}, result)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_to_xml_atom_entry_has_title
|
|
110
|
+
result = @atom.to_xml(format: :atom)
|
|
111
|
+
assert_match(%r{<entry>\n<title>Atom draft-07 snapshot</title>}, result)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Error handling
|
|
115
|
+
|
|
116
|
+
def test_to_xml_raises_on_unknown_format
|
|
117
|
+
assert_raise(ArgumentError) { @rss20.to_xml(format: :unknown) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def test_to_xml_error_message_includes_supported_formats
|
|
121
|
+
error = assert_raise(ArgumentError) { @rss20.to_xml(format: :foo) }
|
|
122
|
+
assert_match(/Supported: :rss2, :atom/, error.message)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Round-trip tests
|
|
126
|
+
|
|
127
|
+
def test_to_xml_rss2_can_be_reparsed
|
|
128
|
+
xml = @rss20.to_xml(format: :rss2)
|
|
129
|
+
reparsed = SimpleRSS.parse(xml)
|
|
130
|
+
|
|
131
|
+
assert_equal "Technoblog", reparsed.title
|
|
132
|
+
assert_equal 10, reparsed.items.size
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def test_to_xml_atom_can_be_reparsed
|
|
136
|
+
xml = @atom.to_xml(format: :atom)
|
|
137
|
+
reparsed = SimpleRSS.parse(xml)
|
|
138
|
+
|
|
139
|
+
assert_equal "dive into mark", reparsed.title
|
|
140
|
+
assert_equal 1, reparsed.items.size
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
require "json"
|
|
3
|
+
|
|
4
|
+
class JsonSerializationTest < Test::Unit::TestCase
|
|
5
|
+
def setup
|
|
6
|
+
@rss20 = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/rss20.xml")
|
|
7
|
+
@atom = SimpleRSS.parse open(File.dirname(__FILE__) + "/../data/atom.xml")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def test_as_json_returns_hash
|
|
11
|
+
result = @rss20.as_json
|
|
12
|
+
assert_kind_of Hash, result
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def test_as_json_includes_feed_title
|
|
16
|
+
result = @rss20.as_json
|
|
17
|
+
assert_equal "Technoblog", result[:title]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def test_as_json_includes_feed_link
|
|
21
|
+
result = @rss20.as_json
|
|
22
|
+
assert_equal "http://tech.rufy.com", result[:link]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def test_as_json_includes_items
|
|
26
|
+
result = @rss20.as_json
|
|
27
|
+
assert_kind_of Array, result[:items]
|
|
28
|
+
assert_equal 10, result[:items].size
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def test_as_json_items_have_title
|
|
32
|
+
result = @rss20.as_json
|
|
33
|
+
assert result[:items].first[:title].include?("some_string.starts_with?")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_as_json_converts_time_to_iso8601
|
|
37
|
+
result = @rss20.as_json
|
|
38
|
+
pub_date = result[:items].first[:pubDate]
|
|
39
|
+
assert_kind_of String, pub_date
|
|
40
|
+
assert_match(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/, pub_date)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_as_json_works_with_atom
|
|
44
|
+
result = @atom.as_json
|
|
45
|
+
assert_equal "dive into mark", result[:title]
|
|
46
|
+
assert_equal 1, result[:items].size
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def test_to_json_returns_string
|
|
50
|
+
result = @rss20.to_json
|
|
51
|
+
assert_kind_of String, result
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_to_json_is_valid_json
|
|
55
|
+
result = @rss20.to_json
|
|
56
|
+
parsed = JSON.parse(result)
|
|
57
|
+
assert_kind_of Hash, parsed
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_to_json_roundtrip
|
|
61
|
+
json_string = @rss20.to_json
|
|
62
|
+
parsed = JSON.parse(json_string, symbolize_names: true)
|
|
63
|
+
|
|
64
|
+
assert_equal "Technoblog", parsed[:title]
|
|
65
|
+
assert_equal 10, parsed[:items].size
|
|
66
|
+
assert parsed[:items].first[:title].include?("some_string.starts_with?")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_as_json_excludes_nil_feed_tags
|
|
70
|
+
result = @rss20.as_json
|
|
71
|
+
# Feed tags that weren't in the source shouldn't appear
|
|
72
|
+
refute result.key?(:subtitle)
|
|
73
|
+
refute result.key?(:id)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def test_as_json_accepts_options_parameter
|
|
77
|
+
# Should not raise, even if options aren't used yet
|
|
78
|
+
result = @rss20.as_json(only: [:title])
|
|
79
|
+
assert_kind_of Hash, result
|
|
80
|
+
end
|
|
81
|
+
end
|