hootenanny 0.0.1 → 0.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/.gitignore +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +5 -7
- data/Gemfile.lock +46 -28
- data/app/controllers/hootenanny/notifications_controller.rb +31 -0
- data/app/controllers/hootenanny/parameters.rb +16 -0
- data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
- data/app/models/hootenanny/publish_notification.rb +90 -0
- data/app/models/hootenanny/subscription.rb +72 -23
- data/config/routes.rb +2 -1
- data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
- data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
- data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
- data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
- data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
- data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
- data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
- data/hootenanny.gemspec +6 -2
- data/lib/hootenanny/configuration.rb +43 -0
- data/lib/hootenanny/correspondent.rb +44 -0
- data/lib/hootenanny/errors.rb +42 -1
- data/lib/hootenanny/feed.rb +75 -0
- data/lib/hootenanny/feed/atom_feed.rb +18 -0
- data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
- data/lib/hootenanny/feed/digest_feed.rb +14 -0
- data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
- data/lib/hootenanny/feed/feed_item.rb +30 -0
- data/lib/hootenanny/feed/file.rb +66 -0
- data/lib/hootenanny/feed/json_feed.rb +48 -0
- data/lib/hootenanny/feed/json_feed_item.rb +8 -0
- data/lib/hootenanny/feed/null_feed.rb +27 -0
- data/lib/hootenanny/feed/rss_feed.rb +52 -0
- data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
- data/lib/hootenanny/feed_store.rb +30 -0
- data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
- data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
- data/lib/hootenanny/hub.rb +116 -22
- data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
- data/lib/hootenanny/request.rb +40 -0
- data/lib/hootenanny/request/publish_notification.rb +94 -0
- data/lib/hootenanny/request/subscription.rb +153 -0
- data/lib/hootenanny/subscription_delivery.rb +47 -0
- data/lib/hootenanny/topic.rb +38 -0
- data/lib/hootenanny/topic_synchronizer.rb +71 -0
- data/lib/hootenanny/uri.rb +53 -0
- data/lib/hootenanny/verification.rb +108 -0
- data/lib/hootenanny/version.rb +1 -1
- data/spec/dummy/config/database.yml +12 -6
- data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
- data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
- data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
- data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
- data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
- data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
- data/spec/factories/publish_notification.rb +13 -0
- data/spec/factories/requests/publish_notification.rb +11 -0
- data/spec/factories/requests/subscription.rb +46 -0
- data/spec/factories/subscription.rb +14 -2
- data/spec/factories/verification.rb +15 -0
- data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
- data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
- data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
- data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
- data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
- data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
- data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
- data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
- data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
- data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
- data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
- data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
- data/spec/fixtures/feeds/json/sample_feed.json +30 -0
- data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
- data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
- data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
- data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
- data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
- data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
- data/spec/lib/hootenanny/configuration_spec.rb +7 -0
- data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
- data/spec/lib/hootenanny/errors_spec.rb +21 -0
- data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
- data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
- data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
- data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
- data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
- data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
- data/spec/lib/hootenanny/feed_spec.rb +159 -0
- data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
- data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
- data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
- data/spec/lib/hootenanny/hub_spec.rb +73 -0
- data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
- data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
- data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
- data/spec/lib/hootenanny/request_spec.rb +21 -0
- data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
- data/spec/lib/hootenanny/topic_spec.rb +15 -0
- data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
- data/spec/lib/hootenanny/uri_spec.rb +32 -0
- data/spec/lib/hootenanny/verification_spec.rb +92 -0
- data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
- data/spec/models/hootenanny/subscription_spec.rb +58 -19
- data/spec/support/verification.rb +63 -0
- metadata +231 -14
- data/spec/controllers/hootenanny/hub_spec.rb +0 -15
File without changes
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:rawvoice="http://www.rawvoice.com/rawvoiceRssModule/" version="2.0">
|
2
|
+
<channel>
|
3
|
+
<title>Ruby Rogues</title>
|
4
|
+
<link>http://rubyrogues.com</link>
|
5
|
+
<description>Rubyist.where(:rogue => true)</description>
|
6
|
+
<language>en-US</language>
|
7
|
+
<item>
|
8
|
+
<title>109 RR Extreme Programming with Will Read</title>
|
9
|
+
<link>http://rubyrogues.com/109-rr-extreme-programming-with-will-read/</link>
|
10
|
+
<guid isPermaLink="false">http://rubyrogues.com/?p=1361</guid>
|
11
|
+
<description>description text</description>
|
12
|
+
<enclosure url="http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3" length="73410493" type="audio/mpeg"/>
|
13
|
+
</item>
|
14
|
+
</channel>
|
15
|
+
</rss>
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<rss version="1.0"
|
3
|
+
xmlns:content="http://purl.org/rss/1.0/modules/content/"
|
4
|
+
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
5
|
+
xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
|
6
|
+
xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/">
|
7
|
+
<channel>
|
8
|
+
<title>Feed Title</title>
|
9
|
+
<link>http://feed.com</link>
|
10
|
+
<description>Feed Description</description>
|
11
|
+
</channel>
|
12
|
+
</rss>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:rawvoice="http://www.rawvoice.com/rawvoiceRssModule/" version="2.0">
|
2
|
+
<channel>
|
3
|
+
<title>Ruby Rogues</title>
|
4
|
+
<link>http://rubyrogues.com</link>
|
5
|
+
<description>Rubyist.where(:rogue => true)</description>
|
6
|
+
<language>en-US</language>
|
7
|
+
<item>
|
8
|
+
<title>109 RR Extreme Programming with Will Read</title>
|
9
|
+
<link>http://rubyrogues.com/109-rr-extreme-programming-with-will-read/</link>
|
10
|
+
<guid isPermaLink="false">http://rubyrogues.com/?p=1361</guid>
|
11
|
+
<description>description text</description>
|
12
|
+
<enclosure url="http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3" length="73410493" type="audio/mpeg"/>
|
13
|
+
</item>
|
14
|
+
<item>
|
15
|
+
<title>108 RR Ruby Trends</title>
|
16
|
+
<link>http://rubyrogues.com/108-rr-ruby-trends/</link>
|
17
|
+
<guid isPermaLink="false">http://rubyrogues.com/?p=1354</guid>
|
18
|
+
<description>description text 2</description>
|
19
|
+
<enclosure url="http://traffic.libsyn.com/rubyrogues/RR108CommunityTrends.mp3" length="63807463" type="audio/mpeg"/>
|
20
|
+
</item>
|
21
|
+
</channel>
|
22
|
+
</rss>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:rawvoice="http://www.rawvoice.com/rawvoiceRssModule/" version="2.0">
|
2
|
+
<channel>
|
3
|
+
<title>Javascript Jabber</title>
|
4
|
+
<link>http://javascriptjabber.com</link>
|
5
|
+
<description>Javascripter.where(:jabber => true)</description>
|
6
|
+
<language>en-US</language>
|
7
|
+
<item>
|
8
|
+
<title>1 JS Jabber</title>
|
9
|
+
<link>http://rubyrogues.com/109-rr-extreme-programming-with-will-read/</link>
|
10
|
+
<guid isPermaLink="false">http://rubyrogues.com/?p=1361</guid>
|
11
|
+
<description>description text</description>
|
12
|
+
<enclosure url="http://traffic.libsyn.com/rubyrogues/RR109ExtremeProgramming.mp3" length="73410493" type="audio/mpeg"/>
|
13
|
+
</item>
|
14
|
+
<item>
|
15
|
+
<title>2 JS Jabber</title>
|
16
|
+
<link>http://rubyrogues.com/108-rr-ruby-trends/</link>
|
17
|
+
<guid isPermaLink="false">http://rubyrogues.com/?p=1354</guid>
|
18
|
+
<description>description text 2</description>
|
19
|
+
<enclosure url="http://traffic.libsyn.com/rubyrogues/RR108CommunityTrends.mp3" length="63807463" type="audio/mpeg"/>
|
20
|
+
</item>
|
21
|
+
</channel>
|
22
|
+
</rss>
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'rspectacular/spec_helpers/active_record_basic'
|
2
|
+
require 'hootenanny/correspondent'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
describe Correspondent do
|
6
|
+
it 'can broadcast a topic to all subscribers' do
|
7
|
+
synchronizer = double
|
8
|
+
subscription = Subscription.new(:subscriber => 'http://subscriber.com')
|
9
|
+
|
10
|
+
allow(Subscription).to receive(:to).
|
11
|
+
and_return [subscription]
|
12
|
+
allow(synchronizer).to receive(:sync).
|
13
|
+
and_return(true)
|
14
|
+
|
15
|
+
correspondent = Correspondent.new('http://topic.com',
|
16
|
+
synchronizer: synchronizer)
|
17
|
+
correspondent.broadcast
|
18
|
+
|
19
|
+
expect(synchronizer).to have_received(:sync).
|
20
|
+
with(
|
21
|
+
subscriber: URI.parse('http://subscriber.com'),
|
22
|
+
topic: URI.parse('http://topic.com'),
|
23
|
+
digest_secret: nil)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns true if the broadcast is completely successful' do
|
27
|
+
synchronizer = double
|
28
|
+
subscription1 = Subscription.new( :subscriber => 'http://subscriber1.com',
|
29
|
+
:topic => 'http://topic.com')
|
30
|
+
subscription2 = Subscription.new( :subscriber => 'http://subscriber2.com',
|
31
|
+
:topic => 'http://topic.com')
|
32
|
+
|
33
|
+
allow(Subscription).to receive(:to).
|
34
|
+
and_return [subscription1, subscription2]
|
35
|
+
|
36
|
+
allow(synchronizer).to receive(:sync).
|
37
|
+
with(
|
38
|
+
subscriber: URI.parse('http://subscriber1.com'),
|
39
|
+
topic: URI.parse('http://topic.com'),
|
40
|
+
digest_secret: nil).
|
41
|
+
and_return(true)
|
42
|
+
|
43
|
+
allow(synchronizer).to receive(:sync).
|
44
|
+
with(
|
45
|
+
subscriber: URI.parse('http://subscriber2.com'),
|
46
|
+
topic: URI.parse('http://topic.com'),
|
47
|
+
digest_secret: nil).
|
48
|
+
and_return(true)
|
49
|
+
|
50
|
+
correspondent = Correspondent.new('http://topic.com',
|
51
|
+
synchronizer: synchronizer)
|
52
|
+
|
53
|
+
expect(correspondent.broadcast).to be_a TrueClass
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'returns false if the broadcast is only partially successful' do
|
57
|
+
synchronizer = double
|
58
|
+
subscription1 = Subscription.new( :subscriber => 'http://subscriber1.com',
|
59
|
+
:topic => 'http://topic.com')
|
60
|
+
subscription2 = Subscription.new( :subscriber => 'http://subscriber2.com',
|
61
|
+
:topic => 'http://topic.com')
|
62
|
+
|
63
|
+
allow(Subscription).to receive(:to).
|
64
|
+
and_return [subscription1, subscription2]
|
65
|
+
|
66
|
+
allow(synchronizer).to receive(:sync).
|
67
|
+
with(
|
68
|
+
subscriber: URI.parse('http://subscriber1.com'),
|
69
|
+
topic: URI.parse('http://topic.com'),
|
70
|
+
digest_secret: nil).
|
71
|
+
and_return(true)
|
72
|
+
|
73
|
+
allow(synchronizer).to receive(:sync).
|
74
|
+
with(
|
75
|
+
subscriber: URI.parse('http://subscriber2.com'),
|
76
|
+
topic: URI.parse('http://topic.com'),
|
77
|
+
digest_secret: nil).
|
78
|
+
and_return(false)
|
79
|
+
|
80
|
+
correspondent = Correspondent.new('http://topic.com',
|
81
|
+
synchronizer: synchronizer)
|
82
|
+
|
83
|
+
expect(correspondent.broadcast).to be_a FalseClass
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'loads the object used to synchronize from the global configuration if none is passed in' do
|
87
|
+
allow(Configuration.instance).to receive(:default_synchronizer)
|
88
|
+
|
89
|
+
Correspondent.new('http://topic.com')
|
90
|
+
|
91
|
+
expect(Configuration.instance).to have_received(:default_synchronizer)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/errors'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
describe Error do
|
6
|
+
it 'can wrap an exception with itself' do
|
7
|
+
exception = Exception.new('my exception')
|
8
|
+
exception.set_backtrace(['file_name.rb:1234 line'])
|
9
|
+
|
10
|
+
wrapped_exception = Error.wrap(exception)
|
11
|
+
|
12
|
+
expect(wrapped_exception).to be_an Error
|
13
|
+
expect(wrapped_exception.message).to eql 'my exception'
|
14
|
+
expect(wrapped_exception.backtrace).to eql ['file_name.rb:1234 line']
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'can describe InvalidSchemeErrors properly' do
|
18
|
+
expect(URI::InvalidSchemeError.new.message).to eql 'Only HTTP and HTTPS URIs are allowed.'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/feed/atom_feed'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
class Feed
|
6
|
+
describe AtomFeed do
|
7
|
+
let(:atom_content_path) { ::File.expand_path('./spec/fixtures/feeds/atom/sample_feed.atom') }
|
8
|
+
let(:atom_content) { ::File.read(atom_content_path) }
|
9
|
+
|
10
|
+
it 'can create itself from some Atom' do
|
11
|
+
allow(RSS::Parser).to receive(:parse)
|
12
|
+
.and_return('content')
|
13
|
+
|
14
|
+
adapter = AtomFeed.from_content(atom_content)
|
15
|
+
|
16
|
+
expect(adapter).to be_a Hootenanny::Feed::AtomFeed
|
17
|
+
expect(adapter.content).to eql 'content'
|
18
|
+
expect(RSS::Parser).to have_received(:parse)
|
19
|
+
.with(atom_content)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'throws an error if the content is not parsable' do
|
23
|
+
expect { AtomFeed.from_content('<asdf') }
|
24
|
+
.to raise_error(Hootenanny::Feed::ParseError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can iterate over each of the items in the feed' do
|
28
|
+
adapter = AtomFeed.from_content(atom_content)
|
29
|
+
|
30
|
+
expect(adapter).to have(2).items
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can wrap each item in a AtomFeedItem' do
|
34
|
+
adapter = AtomFeed.from_content(atom_content)
|
35
|
+
|
36
|
+
expect(adapter.items.first).to be_a Hootenanny::Feed::AtomFeedItem
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/feed/digest_feed'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
class Feed
|
6
|
+
describe DigestFeed do
|
7
|
+
let(:digest_content_path) { ::File.expand_path('./spec/fixtures/feeds/digest/sample_feed.digest') }
|
8
|
+
let(:digest_content) { ::File.read(digest_content_path) }
|
9
|
+
|
10
|
+
it 'can create itself from a digest list' do
|
11
|
+
allow(JSON).to receive(:parse)
|
12
|
+
.and_return 'content'
|
13
|
+
|
14
|
+
adapter = DigestFeed.from_content(digest_content)
|
15
|
+
|
16
|
+
expect(adapter).to be_a Hootenanny::Feed::DigestFeed
|
17
|
+
expect(adapter.content).to eql 'content'
|
18
|
+
expect(JSON).to have_received(:parse)
|
19
|
+
.with(digest_content)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'throws an error if the content is not parsable' do
|
23
|
+
expect { DigestFeed.from_content('asdf') }
|
24
|
+
.to raise_error(Hootenanny::Feed::ParseError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can iterate over each of the items in the feed' do
|
28
|
+
adapter = DigestFeed.from_content(digest_content)
|
29
|
+
|
30
|
+
expect(adapter).to have(2).items
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can wrap each item in a DigestFeedItem' do
|
34
|
+
adapter = DigestFeed.from_content(digest_content)
|
35
|
+
|
36
|
+
expect(adapter.items.first).to be_a Hootenanny::Feed::DigestFeedItem
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/feed/feed_item'
|
3
|
+
require 'rss'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Feed
|
7
|
+
describe FeedItem do
|
8
|
+
let(:rss_content_path) { ::File.expand_path('./spec/fixtures/feeds/rss/sample_feed.rss') }
|
9
|
+
let(:rss_content) { ::File.read(rss_content_path) }
|
10
|
+
let(:rss) { RSS::Parser.parse(rss_content) }
|
11
|
+
let(:rss_item) { rss.items.first }
|
12
|
+
|
13
|
+
context 'when the feed item represents an object' do
|
14
|
+
it 'can determine equality based on a digest' do
|
15
|
+
feed_item = FeedItem.new(rss_item)
|
16
|
+
digest = double(:to_digest => '01bfe038abb650189f78891be1581a52a7754a9be5fee7f8feeb0f7d617d3dd6')
|
17
|
+
|
18
|
+
expect(feed_item).to eql digest
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'can determine equality based on another feed item with the same object' do
|
22
|
+
feed_item = FeedItem.new(rss_item)
|
23
|
+
second_feed_item = FeedItem.new(rss_item)
|
24
|
+
|
25
|
+
expect(feed_item).to eql second_feed_item
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'can determine equality based on another feed time with a different object' do
|
29
|
+
feed_item = FeedItem.new(rss_item)
|
30
|
+
second_feed_item = FeedItem.new(rss.items[1])
|
31
|
+
|
32
|
+
expect(feed_item).not_to eql second_feed_item
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'can determine equality for operations that require hashing' do
|
36
|
+
feed_item = [FeedItem.new(rss_item)]
|
37
|
+
second_feed_item = [FeedItem.new(rss_item)]
|
38
|
+
|
39
|
+
expect(feed_item - second_feed_item).to be_empty
|
40
|
+
|
41
|
+
feed_item = [FeedItem.new(rss_item)]
|
42
|
+
second_feed_item = [FeedItem.new(rss.items[1])]
|
43
|
+
|
44
|
+
expect(feed_item - second_feed_item).to eql feed_item
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/feed/file'
|
3
|
+
require 'securerandom'
|
4
|
+
|
5
|
+
module Hootenanny
|
6
|
+
class Feed
|
7
|
+
describe File do
|
8
|
+
it 'can add a string or path onto itself to create a new file' do
|
9
|
+
original_file = File.new('./spec/fixtures/feeds/rss')
|
10
|
+
new_file = original_file << 'sample_feed.rss'
|
11
|
+
|
12
|
+
expect(new_file.object_id).not_to eql original_file.object_id
|
13
|
+
expect(new_file.to_s).to eql './spec/fixtures/feeds/rss/sample_feed.rss'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'can find the first file when the path represents a glob' do
|
17
|
+
file = File.new('./spec/fixtures/feeds/rss/*.rss')
|
18
|
+
|
19
|
+
expect(file.to_s).to match %r{/spec/fixtures/feeds/rss/\w+\.rss}
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'does not create a new path if the passed in value is nil' do
|
23
|
+
file = File.new('./spec/fixtures/feeds/rss')
|
24
|
+
other_file = file << nil
|
25
|
+
|
26
|
+
expect(other_file.to_s).to eql './spec/fixtures/feeds/rss'
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'does not create a new path if the passed in value is blank' do
|
30
|
+
file = File.new('./spec/fixtures/feeds/rss')
|
31
|
+
other_file = file << ''
|
32
|
+
|
33
|
+
expect(other_file.to_s).to eql './spec/fixtures/feeds/rss'
|
34
|
+
expect(other_file.object_id).to eql file.object_id
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'can map a file extension to a type' do
|
38
|
+
file = File.new('./spec/fixtures/feeds/rss/sample_feed.rss')
|
39
|
+
|
40
|
+
expect(file.type).to eql 'RSS'
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can read the file' do
|
44
|
+
file = File.new('./spec/fixtures/feeds/rss/sample_feed.rss')
|
45
|
+
|
46
|
+
expect(file.read).to match /Ruby Rogues/
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'is empty if the file does not exist' do
|
50
|
+
file = File.new('./spec/fixtures/feeds/rss/unknown_feed.rss')
|
51
|
+
|
52
|
+
expect(file.read).to eql ''
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'can write to the file' do
|
56
|
+
filename = "./tmp/#{SecureRandom.hex(4)}/myfile.txt"
|
57
|
+
|
58
|
+
feed_file = File.new(filename)
|
59
|
+
|
60
|
+
feed_file.write('my stuff')
|
61
|
+
|
62
|
+
expect(feed_file.read).to eql 'my stuff'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,128 @@
|
|
1
|
+
require 'rspectacular'
|
2
|
+
require 'hootenanny/feed'
|
3
|
+
|
4
|
+
module Hootenanny
|
5
|
+
class Feed
|
6
|
+
describe JSONFeed do
|
7
|
+
let(:json_content_path) { ::File.expand_path('./spec/fixtures/feeds/json/sample_feed.json') }
|
8
|
+
let(:json_content) { ::File.read(json_content_path) }
|
9
|
+
|
10
|
+
it 'can create itself from some JSON' do
|
11
|
+
allow(JSON).to receive(:parse)
|
12
|
+
.and_return('content')
|
13
|
+
|
14
|
+
adapter = JSONFeed.from_content(json_content)
|
15
|
+
|
16
|
+
expect(adapter).to be_a Hootenanny::Feed::JSONFeed
|
17
|
+
expect(adapter.content).to eql 'content'
|
18
|
+
expect(JSON).to have_received(:parse)
|
19
|
+
.with(json_content)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'throws an error if the content is not parsable' do
|
23
|
+
expect { JSONFeed.from_content('asdf') }
|
24
|
+
.to raise_error(Hootenanny::Feed::ParseError)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'can iterate over each of the items in the feed' do
|
28
|
+
adapter = JSONFeed.from_content(json_content)
|
29
|
+
|
30
|
+
expect(adapter).to have(2).items
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'can wrap each item in a JSONFeedItem' do
|
34
|
+
adapter = JSONFeed.from_content(json_content)
|
35
|
+
|
36
|
+
expect(adapter.items.first).to be_a Hootenanny::Feed::JSONFeedItem
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'can properly set content for itself from a String' do
|
40
|
+
feed = JSONFeed.from_content('{"key": "value", "items": []}')
|
41
|
+
|
42
|
+
expect(feed.to_s).to eql '{"key":"value","items":[]}'
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'if content is not blank, it always returns a String containing items' do
|
46
|
+
feed = JSONFeed.from_content('{"key": "value"}')
|
47
|
+
|
48
|
+
expect(feed.to_s).to eql '{"key":"value","items":[]}'
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'if it is blank' do
|
52
|
+
feed = JSONFeed.from_content('{}')
|
53
|
+
|
54
|
+
expect(feed.to_s).to eql '{"items":[]}'
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'it properly sets items to an empty array even if no items are in the content' do
|
58
|
+
feed = JSONFeed.from_content('{"key": "value"}')
|
59
|
+
|
60
|
+
expect(feed.items).to eql []
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'can properly set content for itself from a String' do
|
64
|
+
feed = JSONFeed.from_content('{"key": "value", "items": []}')
|
65
|
+
|
66
|
+
expect(feed.content).to eql(
|
67
|
+
'key' => 'value',
|
68
|
+
'items' => []
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'can properly set content for itself from a Hash' do
|
73
|
+
feed = JSONFeed.from_content(key: 'value')
|
74
|
+
|
75
|
+
expect(feed.content).to eql(
|
76
|
+
'key' => 'value'
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'raises an error if it does not know how to parse content' do
|
81
|
+
expect { JSONFeed.from_content(2) }.to raise_error Hootenanny::Feed::ParseError
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'can determine the difference between two feeds' do
|
85
|
+
first_content_path = ::File.expand_path('./spec/fixtures/feeds/json/sample_feed.json')
|
86
|
+
first_content = ::File.read(first_content_path)
|
87
|
+
|
88
|
+
second_content_path = ::File.expand_path('./spec/fixtures/feeds/json/feed_with_one_item.json')
|
89
|
+
second_content = ::File.read(second_content_path)
|
90
|
+
|
91
|
+
first_feed = Feed.infer(first_content, type: :json)
|
92
|
+
second_feed = Feed.infer(second_content, type: :json)
|
93
|
+
|
94
|
+
difference_feed = first_feed - second_feed
|
95
|
+
|
96
|
+
expect(difference_feed.items).to have(1).item
|
97
|
+
expect(difference_feed.items).to eql [first_feed.items[1]]
|
98
|
+
expect(difference_feed.content['title']).to eql 'Ruby Rogues'
|
99
|
+
expect(difference_feed.items.first.content['title']).to eql '108 RR Ruby Trends'
|
100
|
+
|
101
|
+
expect(difference_feed.to_s).to include 'Ruby Rogues'
|
102
|
+
expect(difference_feed.to_s).to include '108 RR Ruby Trends'
|
103
|
+
expect(difference_feed.to_s).not_to include '109 RR Extreme Programming with Will Read'
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'can determine the difference between two feeds of differing types' do
|
107
|
+
first_content_path = ::File.expand_path('./spec/fixtures/feeds/json/sample_feed.json')
|
108
|
+
first_content = ::File.read(first_content_path)
|
109
|
+
|
110
|
+
second_content = '{"items": ["1a6ed1752c74c2b7fcc0f43ce39e53f8f220cd85f85e339b06eb75a0f16bbea8"]}'
|
111
|
+
|
112
|
+
first_feed = Feed.infer(first_content, type: :json)
|
113
|
+
second_feed = Feed.infer(second_content, type: :digest)
|
114
|
+
|
115
|
+
difference_feed = first_feed - second_feed
|
116
|
+
|
117
|
+
expect(difference_feed.items).to have(1).item
|
118
|
+
expect(difference_feed.items).to eql [first_feed.items[1]]
|
119
|
+
expect(difference_feed.content['title']).to eql 'Ruby Rogues'
|
120
|
+
expect(difference_feed.items.first.content['title']).to eql '108 RR Ruby Trends'
|
121
|
+
|
122
|
+
expect(difference_feed.to_s).to include 'Ruby Rogues'
|
123
|
+
expect(difference_feed.to_s).to include '108 RR Ruby Trends'
|
124
|
+
expect(difference_feed.to_s).not_to include '109 RR Extreme Programming with Will Read'
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|