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.
Files changed (112) hide show
  1. data/.gitignore +1 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +5 -7
  4. data/Gemfile.lock +46 -28
  5. data/app/controllers/hootenanny/notifications_controller.rb +31 -0
  6. data/app/controllers/hootenanny/parameters.rb +16 -0
  7. data/app/controllers/hootenanny/subscriptions_controller.rb +24 -3
  8. data/app/models/hootenanny/publish_notification.rb +90 -0
  9. data/app/models/hootenanny/subscription.rb +72 -23
  10. data/config/routes.rb +2 -1
  11. data/db/migrate/20130607182642_add_started_at_and_lease_duration_to_subscriptions.rb +15 -0
  12. data/db/migrate/20130608225621_add_hmac_secret_to_subscription.rb +5 -0
  13. data/db/migrate/20130611235218_add_publish_notifications.rb +9 -0
  14. data/db/migrate/20130612153138_add_timestamps_to_publish_notification.rb +5 -0
  15. data/db/migrate/20130705200729_add_processed_flag_to_publish_notifications.rb +6 -0
  16. data/db/migrate/20130711061329_switch_publish_notification_process_state_from_boolean_to_string.rb +11 -0
  17. data/db/migrate/20130711061558_add_index_to_notifications_state.rb +5 -0
  18. data/hootenanny.gemspec +6 -2
  19. data/lib/hootenanny/configuration.rb +43 -0
  20. data/lib/hootenanny/correspondent.rb +44 -0
  21. data/lib/hootenanny/errors.rb +42 -1
  22. data/lib/hootenanny/feed.rb +75 -0
  23. data/lib/hootenanny/feed/atom_feed.rb +18 -0
  24. data/lib/hootenanny/feed/atom_feed_item.rb +8 -0
  25. data/lib/hootenanny/feed/digest_feed.rb +14 -0
  26. data/lib/hootenanny/feed/digest_feed_item.rb +11 -0
  27. data/lib/hootenanny/feed/feed_item.rb +30 -0
  28. data/lib/hootenanny/feed/file.rb +66 -0
  29. data/lib/hootenanny/feed/json_feed.rb +48 -0
  30. data/lib/hootenanny/feed/json_feed_item.rb +8 -0
  31. data/lib/hootenanny/feed/null_feed.rb +27 -0
  32. data/lib/hootenanny/feed/rss_feed.rb +52 -0
  33. data/lib/hootenanny/feed/rss_feed_item.rb +8 -0
  34. data/lib/hootenanny/feed_store.rb +30 -0
  35. data/lib/hootenanny/feed_store/file_feed_store.rb +55 -0
  36. data/lib/hootenanny/feed_store/web_feed_store.rb +42 -0
  37. data/lib/hootenanny/hub.rb +116 -22
  38. data/lib/hootenanny/publish_notification_expiration_policy.rb +17 -0
  39. data/lib/hootenanny/request.rb +40 -0
  40. data/lib/hootenanny/request/publish_notification.rb +94 -0
  41. data/lib/hootenanny/request/subscription.rb +153 -0
  42. data/lib/hootenanny/subscription_delivery.rb +47 -0
  43. data/lib/hootenanny/topic.rb +38 -0
  44. data/lib/hootenanny/topic_synchronizer.rb +71 -0
  45. data/lib/hootenanny/uri.rb +53 -0
  46. data/lib/hootenanny/verification.rb +108 -0
  47. data/lib/hootenanny/version.rb +1 -1
  48. data/spec/dummy/config/database.yml +12 -6
  49. data/spec/dummy/db/migrate/20130607183149_add_started_at_and_lease_duration_to_subscriptions.hootenanny.rb +16 -0
  50. data/spec/dummy/db/migrate/20130608231253_add_hmac_secret_to_subscription.hootenanny.rb +6 -0
  51. data/spec/dummy/db/migrate/20130611235546_add_publish_notifications.hootenanny.rb +10 -0
  52. data/spec/dummy/db/migrate/20130612153353_add_timestamps_to_publish_notification.hootenanny.rb +6 -0
  53. data/spec/dummy/db/migrate/20130705200832_add_processed_flag_to_publish_notifications.hootenanny.rb +7 -0
  54. data/spec/dummy/db/migrate/20130711061518_switch_publish_notification_process_state_from_boolean_to_string.hootenanny.rb +12 -0
  55. data/spec/dummy/db/migrate/20130711061629_add_index_to_notifications_state.hootenanny.rb +6 -0
  56. data/spec/factories/publish_notification.rb +13 -0
  57. data/spec/factories/requests/publish_notification.rb +11 -0
  58. data/spec/factories/requests/subscription.rb +46 -0
  59. data/spec/factories/subscription.rb +14 -2
  60. data/spec/factories/verification.rb +15 -0
  61. data/spec/features/publishers/can_notify_the_hub_of_content_updates_spec.rb +58 -0
  62. data/spec/features/subscribers/are_protected_from_unwarranted_subscriptions_spec.rb +59 -0
  63. data/spec/features/subscribers/can_receive_distributions_of_topic_content_spec.rb +175 -0
  64. data/spec/features/subscribers/can_subscribe_to_a_topic_spec.rb +76 -7
  65. data/spec/fixtures/feeds/atom/97d79220f68b4bf27.atom +0 -0
  66. data/spec/fixtures/feeds/atom/sample_feed.atom +51 -0
  67. data/spec/fixtures/feeds/digest/97d79220f68b4bf27.digest +1 -0
  68. data/spec/fixtures/feeds/digest/complete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +6 -0
  69. data/spec/fixtures/feeds/digest/incomplete_broadcasted_items/5b187098da59f077f/97d79220f68b4bf27.digest +5 -0
  70. data/spec/fixtures/feeds/digest/sample_feed.digest +6 -0
  71. data/spec/fixtures/feeds/json/97d79220f68b4bf27.json +1 -0
  72. data/spec/fixtures/feeds/json/feed_with_one_item.json +21 -0
  73. data/spec/fixtures/feeds/json/sample_feed.json +30 -0
  74. data/spec/fixtures/feeds/rss/5b187098da59f077f/97d79220f68b4bf27.rss +21 -0
  75. data/spec/fixtures/feeds/rss/97d79220f68b4bf27.rss +0 -0
  76. data/spec/fixtures/feeds/rss/feed_with_one_item.rss +15 -0
  77. data/spec/fixtures/feeds/rss/minimal_feed.rss +12 -0
  78. data/spec/fixtures/feeds/rss/sample_feed.rss +22 -0
  79. data/spec/fixtures/feeds/rss/sample_feed_2.rss +22 -0
  80. data/spec/lib/hootenanny/configuration_spec.rb +7 -0
  81. data/spec/lib/hootenanny/correspondent_spec.rb +94 -0
  82. data/spec/lib/hootenanny/errors_spec.rb +21 -0
  83. data/spec/lib/hootenanny/feed/atom_feed_item_spec.rb +9 -0
  84. data/spec/lib/hootenanny/feed/atom_feed_spec.rb +40 -0
  85. data/spec/lib/hootenanny/feed/digest_feed_item_spec.rb +9 -0
  86. data/spec/lib/hootenanny/feed/digest_feed_spec.rb +40 -0
  87. data/spec/lib/hootenanny/feed/feed_item_spec.rb +49 -0
  88. data/spec/lib/hootenanny/feed/file_spec.rb +66 -0
  89. data/spec/lib/hootenanny/feed/json_feed_item_spec.rb +9 -0
  90. data/spec/lib/hootenanny/feed/json_feed_spec.rb +128 -0
  91. data/spec/lib/hootenanny/feed/null_feed_spec.rb +9 -0
  92. data/spec/lib/hootenanny/feed/rss_feed_item_spec.rb +9 -0
  93. data/spec/lib/hootenanny/feed/rss_feed_spec.rb +143 -0
  94. data/spec/lib/hootenanny/feed_spec.rb +159 -0
  95. data/spec/lib/hootenanny/feed_store/file_feed_store_spec.rb +58 -0
  96. data/spec/lib/hootenanny/feed_store/web_feed_store_spec.rb +47 -0
  97. data/spec/lib/hootenanny/feed_store_spec.rb +27 -0
  98. data/spec/lib/hootenanny/hub_spec.rb +73 -0
  99. data/spec/lib/hootenanny/publish_notification_expiration_policy_spec.rb +35 -0
  100. data/spec/lib/hootenanny/request/publish_notification_spec.rb +43 -0
  101. data/spec/lib/hootenanny/request/subscription_spec.rb +89 -0
  102. data/spec/lib/hootenanny/request_spec.rb +21 -0
  103. data/spec/lib/hootenanny/subscription_delivery_spec.rb +54 -0
  104. data/spec/lib/hootenanny/topic_spec.rb +15 -0
  105. data/spec/lib/hootenanny/topic_synchronizer_spec.rb +98 -0
  106. data/spec/lib/hootenanny/uri_spec.rb +32 -0
  107. data/spec/lib/hootenanny/verification_spec.rb +92 -0
  108. data/spec/models/hootenanny/publish_notification_spec.rb +55 -0
  109. data/spec/models/hootenanny/subscription_spec.rb +58 -19
  110. data/spec/support/verification.rb +63 -0
  111. metadata +231 -14
  112. data/spec/controllers/hootenanny/hub_spec.rb +0 -15
@@ -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,7 @@
1
+ require 'rspectacular'
2
+ require 'hootenanny/configuration'
3
+
4
+ module Hootenanny
5
+ describe Configuration do
6
+ end
7
+ end
@@ -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,9 @@
1
+ require 'rspectacular'
2
+ require 'hootenanny/feed/atom_feed_item'
3
+
4
+ module Hootenanny
5
+ class Feed
6
+ describe AtomFeedItem do
7
+ end
8
+ end
9
+ 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,9 @@
1
+ require 'rspectacular'
2
+ require 'hootenanny/feed/digest_feed_item'
3
+
4
+ module Hootenanny
5
+ class Feed
6
+ describe DigestFeedItem do
7
+ end
8
+ end
9
+ 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,9 @@
1
+ require 'rspectacular'
2
+ require 'hootenanny/feed/json_feed_item'
3
+
4
+ module Hootenanny
5
+ class Feed
6
+ describe JSONFeedItem do
7
+ end
8
+ end
9
+ 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