hootenanny 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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