pushpad 0.5.1 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 16e079cb71bb9a62497ef67e1d02d8e10ae52ef9
4
- data.tar.gz: 8bd989647419688540a10b16f2ee6400f13176fc
3
+ metadata.gz: 72937e2f8f9f8340e2c8a503a413a13f5cce90cd
4
+ data.tar.gz: b296e7b4007accf9d7d8729b31c1f0180fa465a1
5
5
  SHA512:
6
- metadata.gz: 239f05ffb94da74b1cf932d3200f4e0267e9b41fa53d937b745a919b78500f6aacec685b920c3c3f45032dbb2b791fb368715ea15dfa9a502d5fbd7f6a8646c7
7
- data.tar.gz: 7ec1b731bb3a23a275ffdd76f05cbabc3616a05a1cef98aff83e114918d8f5c41829766363a8add0f288e91af64890d2251a1e93967879e0ee09f06f3c184e2a
6
+ metadata.gz: 85de1abb940f431332c41280c5a47983e93b29999c25877c4eef01e913b2517f208a13804110294d904399f4f348f7899fc10817fa3e95d85436e85a908532fd
7
+ data.tar.gz: 7c3230b40848571ce581498286364eee8e86388519b1cf82df37cfd54bf45074930196752021ec985b09a2072370632cc5c56c629ac7a298f17f7604fd824f2b
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ .bundle
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.2
data/Gemfile CHANGED
@@ -1,2 +1,6 @@
1
1
  source 'https://rubygems.org'
2
2
  gemspec
3
+
4
+ group :test do
5
+ gem 'rake'
6
+ end
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # Pushpad - Web Push Notifications Service
2
+
3
+ [![Build Status](https://travis-ci.org/pushpad/pushpad-ruby.svg?branch=master)](https://travis-ci.org/pushpad/pushpad-ruby)
2
4
 
3
5
  [Pushpad](https://pushpad.xyz) is a service for sending push notifications from your web app. It supports the **Push API** (Chrome, Firefox, Opera) and **APNs** (Safari).
4
6
 
@@ -109,13 +111,83 @@ notification.broadcast
109
111
 
110
112
  If no user with that id has subscribed to push notifications, that id is simply ignored.
111
113
 
112
- The methods above return an hash:
114
+ The methods above return an hash:
113
115
 
114
116
  - `"id"` is the id of the notification on Pushpad
115
117
  - `"scheduled"` is the estimated reach of the notification (i.e. the number of devices to which the notification will be sent, which can be different from the number of users, since a user may receive notifications on multiple devices)
116
118
  - `"uids"` (`deliver_to` only) are the user IDs that will be actually reached by the notification because they are subscribed to your notifications. For example if you send a notification to `['uid1', 'uid2', 'uid3']`, but only `'uid1'` is subscribed, you will get `['uid1']` in response. Note that if a user has unsubscribed after the last notification sent to him, he may still be reported for one time as subscribed (this is due to [the way](http://blog.pushpad.xyz/2016/05/the-push-api-and-its-wild-unsubscription-mechanism/) the W3C Push API works).
117
119
 
120
+ The `id` and `scheduled_count` attribute are also stored on the notification object:
121
+
122
+ ```ruby
123
+
124
+ notification.deliver_to user
125
+
126
+ notification.id # => 1000
127
+ notification.scheduled_count # => 5
128
+ ```
129
+
130
+ ## Getting push notification data
131
+
132
+ You can retrieve data for past notifications:
133
+
134
+ ```ruby
135
+ notification = Pushpad::Notification.find(42)
136
+
137
+ # get basic attributes
138
+ notification.id # => 42
139
+ notification.title # => "Foo Bar",
140
+ notification.body # => "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
141
+ notification.target_url # => "http://example.com",
142
+ notification.ttl # => 604800,
143
+ notification.require_interaction # => false,
144
+ notification.icon_url # => "http://example.com/assets/icon.png",
145
+
146
+ # `created_at` is a `Time` instance
147
+ notification.created_at.utc.to_s # => "2016-07-06 10:09:14 UTC",
148
+
149
+ # get statistics
150
+ notification.scheduled_count # => 1
151
+ notification.successfully_sent_count # => 4,
152
+ notification.opened_count # => 2
153
+ ```
154
+
155
+ Or for mutliple notifications of a project at once:
156
+
157
+ ```ruby
158
+ notifications = Pushpad::Notification.find_all(project_id: 5)
159
+
160
+ # same attributes as for single notification in example above
161
+ notifications[0].id # => 42
162
+ notifications[0].title # => "Foo Bar",
163
+ ```
164
+
165
+ If `Pushpad.project_id` is defined, the `project_id` option can be
166
+ omitted.
167
+
168
+ The REST API paginates the result set. You can pass a `page` parameter
169
+ to get the full list in multiple requests.
170
+
171
+ ```ruby
172
+ notifications = Pushpad::Notification.find_all(project_id: 5, page: 2)
173
+ ```
174
+
175
+ ## Getting subscription count
176
+
177
+ You can retrieve the number of subscriptions for a given project,
178
+ optionally filtered by `tags` or `uids`:
179
+
180
+ ```ruby
181
+ Pushpad::Subscription.count(project_id: 5) # => 100
182
+ Pushpad::Subscription.count(project_id: 5, uids: ['user1']) # => 2
183
+ Pushpad::Subscription.count(project_id: 5, tags: ['sports']) # => 10
184
+ Pushpad::Subscription.count(project_id: 5, tags: 'sports && travel') # => 5
185
+ Pushpad::Subscription.count(project_id: 5, uids: ['user1'], tags: 'sports && travel') # => 1
186
+ ```
187
+
188
+ If `Pushpad.project_id` is defined, the `project_id` option can be
189
+ omitted.
190
+
118
191
  ## License
119
192
 
120
193
  The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
121
-
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+
3
+ RSpec::Core::RakeTask.new
4
+
5
+ task :default => :spec
@@ -0,0 +1,120 @@
1
+ require "json"
2
+ require "time"
3
+
4
+ module Pushpad
5
+ class Notification
6
+ class DeliveryError < RuntimeError
7
+ end
8
+
9
+ class FindError < RuntimeError
10
+ end
11
+
12
+ class ReadonlyError < RuntimeError
13
+ end
14
+
15
+ attr_accessor :body, :title, :target_url, :icon_url, :ttl, :require_interaction
16
+ attr_reader :id, :created_at, :scheduled_count, :successfully_sent_count, :opened_count
17
+
18
+ def initialize(options)
19
+ @id = options[:id]
20
+ @created_at = options[:created_at] && Time.parse(options[:created_at])
21
+ @scheduled_count = options[:scheduled_count]
22
+ @successfully_sent_count = options[:successfully_sent_count]
23
+ @opened_count = options[:opened_count]
24
+
25
+ @body = options[:body]
26
+ @title = options[:title]
27
+ @target_url = options[:target_url]
28
+ @icon_url = options[:icon_url]
29
+ @ttl = options[:ttl]
30
+ @require_interaction = options[:require_interaction]
31
+ end
32
+
33
+ def self.find(id)
34
+ response = Request.get("https://pushpad.xyz/notifications/#{id}")
35
+
36
+ unless response.code == "200"
37
+ raise FindError, "Response #{response.code} #{response.message}: #{response.body}"
38
+ end
39
+
40
+ new(JSON.parse(response.body, symbolize_names: true)).readonly!
41
+ end
42
+
43
+ def self.find_all(options = {})
44
+ project_id = options[:project_id] || Pushpad.project_id
45
+ raise "You must set project_id" unless project_id
46
+
47
+ query_parameters = {}
48
+ query_parameters[:page] = options[:page] if options.key?(:page)
49
+
50
+ response = Request.get("https://pushpad.xyz/projects/#{project_id}/notifications",
51
+ query_parameters: query_parameters)
52
+
53
+ unless response.code == "200"
54
+ raise FindError, "Response #{response.code} #{response.message}: #{response.body}"
55
+ end
56
+
57
+ JSON.parse(response.body, symbolize_names: true).map do |attributes|
58
+ new(attributes).readonly!
59
+ end
60
+ end
61
+
62
+ def readonly!
63
+ @readonly = true
64
+ self
65
+ end
66
+
67
+ def broadcast(options = {})
68
+ deliver req_body(nil, options[:tags]), options
69
+ end
70
+
71
+ def deliver_to(users, options = {})
72
+ uids = if users.respond_to?(:ids)
73
+ users.ids
74
+ elsif users.respond_to?(:collect)
75
+ users.collect {|u| u.respond_to?(:id) ? u.id : u }
76
+ else
77
+ [users.respond_to?(:id) ? users.id : users]
78
+ end
79
+ deliver req_body(uids, options[:tags]), options
80
+ end
81
+
82
+ private
83
+
84
+ def deliver(req_body, options = {})
85
+ if @readonly
86
+ raise(ReadonlyError,
87
+ "Notifications fetched with `find` or `find_all` cannot be delivered again.")
88
+ end
89
+
90
+ project_id = options[:project_id] || Pushpad.project_id
91
+ raise "You must set project_id" unless project_id
92
+
93
+ endpoint = "https://pushpad.xyz/projects/#{project_id}/notifications"
94
+ response = Request.post(endpoint, req_body)
95
+
96
+ unless response.code == "201"
97
+ raise DeliveryError, "Response #{response.code} #{response.message}: #{response.body}"
98
+ end
99
+
100
+ JSON.parse(response.body).tap do |attributes|
101
+ @id = attributes["id"]
102
+ @scheduled_count = attributes["scheduled"]
103
+ end
104
+ end
105
+
106
+ def req_body(uids = nil, tags = nil)
107
+ notification_params = { "body" => self.body }
108
+ notification_params["title"] = self.title if self.title
109
+ notification_params["target_url"] = self.target_url if self.target_url
110
+ notification_params["icon_url"] = self.icon_url if self.icon_url
111
+ notification_params["ttl"] = self.ttl if self.ttl
112
+ notification_params["require_interaction"] = self.require_interaction unless self.require_interaction.nil?
113
+
114
+ body = { "notification" => notification_params }
115
+ body["uids"] = uids if uids
116
+ body["tags"] = tags if tags
117
+ body.to_json
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,56 @@
1
+ require "uri"
2
+ require "net/http"
3
+
4
+ module Pushpad
5
+ module Request
6
+ extend self
7
+
8
+ def head(endpoint, options = {})
9
+ perform(Net::HTTP::Head, endpoint, options)
10
+ end
11
+
12
+ def get(endpoint, options = {})
13
+ perform(Net::HTTP::Get, endpoint, options)
14
+ end
15
+
16
+ def post(endpoint, body, options = {})
17
+ perform(Net::HTTP::Post, endpoint, options) do |request|
18
+ request.body = body
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def perform(method, endpoint, options)
25
+ uri = URI.parse(endpoint)
26
+ request = method.new(path_and_query(uri, options[:query_parameters]), headers)
27
+
28
+ yield request if block_given?
29
+
30
+ https(uri, request)
31
+ end
32
+
33
+ def path_and_query(uri, query_parameters)
34
+ [uri.path, query(query_parameters)].compact.join("?")
35
+ end
36
+
37
+ def query(parameters)
38
+ parameters && !parameters.empty? ? URI.encode_www_form(parameters) : nil
39
+ end
40
+
41
+ def https(uri, request)
42
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |https|
43
+ https.request(request)
44
+ end
45
+ end
46
+
47
+ def headers
48
+ raise "You must set Pushpad.auth_token" unless Pushpad.auth_token
49
+ {
50
+ "Authorization" => %(Token token="#{Pushpad.auth_token}"),
51
+ "Content-Type" => "application/json;charset=UTF-8",
52
+ "Accept" => "application/json"
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,52 @@
1
+ module Pushpad
2
+ class Subscription
3
+ class CountError < RuntimeError
4
+ end
5
+
6
+ def self.count(options = {})
7
+ CountQuery.new(options).perform
8
+ end
9
+
10
+ class CountQuery
11
+ attr_reader :options
12
+
13
+ def initialize(options)
14
+ @options = options
15
+ end
16
+
17
+ def perform
18
+ project_id = options[:project_id] || Pushpad.project_id
19
+ raise "You must set project_id" unless project_id
20
+
21
+ endpoint = "https://pushpad.xyz/projects/#{project_id}/subscriptions"
22
+ response = Request.head(endpoint, query_parameters: query_parameters)
23
+
24
+ unless response.code == "200"
25
+ raise CountError, "Response #{response.code} #{response.message}: #{response.body}"
26
+ end
27
+
28
+ response["X-Total-Count"].to_i
29
+ end
30
+
31
+ private
32
+
33
+ def query_parameters
34
+ [uid_query_parameters, tag_query_parameters].flatten(1)
35
+ end
36
+
37
+ def uid_query_parameters
38
+ options.fetch(:uids, []).map { |uid| ["uids[]", uid] }
39
+ end
40
+
41
+ def tag_query_parameters
42
+ tags = options.fetch(:tags, [])
43
+
44
+ if tags.is_a?(String)
45
+ [["tags", tags]]
46
+ else
47
+ tags.map { |tag| ["tags[]", tag] }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
data/lib/pushpad.rb CHANGED
@@ -1,6 +1,8 @@
1
- require 'net/http'
2
1
  require 'openssl'
3
- require 'json'
2
+
3
+ require "pushpad/request"
4
+ require "pushpad/notification"
5
+ require "pushpad/subscription"
4
6
 
5
7
  module Pushpad
6
8
  @@auth_token = nil
@@ -38,75 +40,4 @@ module Pushpad
38
40
  uid_signature = self.signature_for(uid.to_s)
39
41
  "#{self.path(options)}?uid=#{uid}&uid_signature=#{uid_signature}"
40
42
  end
41
-
42
- class Notification
43
- class DeliveryError < RuntimeError
44
- end
45
-
46
- attr_accessor :body, :title, :target_url, :icon_url, :ttl, :require_interaction
47
-
48
- def initialize(options)
49
- self.body = options[:body]
50
- self.title = options[:title]
51
- self.target_url = options[:target_url]
52
- self.icon_url = options[:icon_url]
53
- self.ttl = options[:ttl]
54
- self.require_interaction = options[:require_interaction]
55
- end
56
-
57
- def broadcast(options = {})
58
- deliver req_body(nil, options[:tags]), options
59
- end
60
-
61
- def deliver_to(users, options = {})
62
- uids = if users.respond_to?(:ids)
63
- users.ids
64
- elsif users.respond_to?(:collect)
65
- users.collect {|u| u.respond_to?(:id) ? u.id : u }
66
- else
67
- [users.respond_to?(:id) ? users.id : users]
68
- end
69
- deliver req_body(uids, options[:tags]), options
70
- end
71
-
72
- private
73
-
74
- def deliver(req_body, options = {})
75
- project_id = options[:project_id] || Pushpad.project_id
76
- raise "You must set project_id" unless project_id
77
- endpoint = "https://pushpad.xyz/projects/#{project_id}/notifications"
78
- uri = URI.parse(endpoint)
79
- https = Net::HTTP.new(uri.host, uri.port)
80
- https.use_ssl = true
81
- req = Net::HTTP::Post.new(uri.path, req_headers)
82
- req.body = req_body
83
- res = https.request(req)
84
- raise DeliveryError, "Response #{res.code} #{res.message}: #{res.body}" unless res.code == '201'
85
- JSON.parse(res.body)
86
- end
87
-
88
- def req_headers
89
- raise "You must set Pushpad.auth_token" unless Pushpad.auth_token
90
- {
91
- 'Authorization' => 'Token token="' + Pushpad.auth_token + '"',
92
- 'Content-Type' => 'application/json;charset=UTF-8',
93
- 'Accept' => 'application/json'
94
- }
95
- end
96
-
97
- def req_body(uids = nil, tags = nil)
98
- notification_params = { "body" => self.body }
99
- notification_params["title"] = self.title if self.title
100
- notification_params["target_url"] = self.target_url if self.target_url
101
- notification_params["icon_url"] = self.icon_url if self.icon_url
102
- notification_params["ttl"] = self.ttl if self.ttl
103
- notification_params["require_interaction"] = self.require_interaction unless self.require_interaction.nil?
104
-
105
- body = { "notification" => notification_params }
106
- body["uids"] = uids if uids
107
- body["tags"] = tags if tags
108
- body.to_json
109
- end
110
- end
111
-
112
43
  end
data/pushpad.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = "pushpad"
3
+ spec.version = '0.6.0'
4
+ spec.authors = ["Pushpad"]
5
+ spec.email = ["support@pushpad.xyz"]
6
+ spec.summary = "Web push notifications for Chrome, Firefox and Safari using Pushpad."
7
+ spec.homepage = "https://pushpad.xyz"
8
+ spec.license = "MIT"
9
+ spec.files = `git ls-files`.split("\n")
10
+ spec.test_files = `git ls-files -- spec/*`.split("\n")
11
+ spec.add_development_dependency "rspec"
12
+ spec.add_development_dependency "webmock"
13
+ end
@@ -0,0 +1,360 @@
1
+ require "spec_helper"
2
+
3
+ module Pushpad
4
+ describe Notification do
5
+ let!(:auth_token) { Pushpad.auth_token = "abc123" }
6
+ let!(:project_id) { Pushpad.project_id = 123 }
7
+ let(:notification) { Pushpad::Notification.new body: "Example message" }
8
+
9
+ def stub_notification_get(attributes)
10
+ stub_request(:get, "https://pushpad.xyz/notifications/#{attributes[:id]}").
11
+ to_return(status: 200, body: attributes.to_json)
12
+ end
13
+
14
+ def stub_failing_notification_get(notification_id)
15
+ stub_request(:get, "https://pushpad.xyz/notifications/#{notification_id}").
16
+ to_return(status: 404)
17
+ end
18
+
19
+ def stub_notifications_get(options)
20
+ stub_request(:get, "https://pushpad.xyz/projects/#{options[:project_id]}/notifications").
21
+ with(query: hash_including(options.fetch(:query, {}))).
22
+ to_return(status: 200, body: options[:list].to_json)
23
+ end
24
+
25
+ def stub_failing_notifications_get(options)
26
+ stub_request(:get, "https://pushpad.xyz/projects/#{options[:project_id]}/notifications").
27
+ to_return(status: 403)
28
+ end
29
+
30
+ def stub_notification_post(project_id, params = {}, response_body = "{}")
31
+ stub_request(:post, "https://pushpad.xyz/projects/#{project_id}/notifications").
32
+ with(body: hash_including(params)).
33
+ to_return(status: 201, body: response_body)
34
+ end
35
+
36
+ def stub_failing_notification_post(project_id)
37
+ stub_request(:post, "https://pushpad.xyz/projects/#{project_id}/notifications").
38
+ to_return(status: 403)
39
+ end
40
+
41
+ describe ".new" do
42
+ it "allows delivering notifications even if an id attribute is supplied" do
43
+ stub_notification_post(project_id)
44
+
45
+ expect {
46
+ Notification.new(id: "ignored").broadcast
47
+ }.not_to raise_error
48
+ end
49
+ end
50
+
51
+ describe ".find" do
52
+ it "returns notification with attributes from json response" do
53
+ attributes = {
54
+ id: 5,
55
+ title: "Foo Bar",
56
+ body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
57
+ target_url: "http://example.com",
58
+ created_at: "2016-07-06T10:09:14.835Z",
59
+ ttl: 604800,
60
+ require_interaction: false,
61
+ icon_url: "https://example.com/assets/icon.png",
62
+ scheduled_count: 2,
63
+ successfully_sent_count: 4,
64
+ opened_count: 1
65
+ }
66
+ stub_notification_get(attributes)
67
+
68
+ notification = Notification.find(5)
69
+
70
+ attributes.delete(:created_at)
71
+ expect(notification).to have_attributes(attributes)
72
+ expect(notification.created_at.utc.to_s).to eq(Time.utc(2016, 7, 6, 10, 9, 14.835).to_s)
73
+ end
74
+
75
+ it "fails with FindError if response status code is not 200" do
76
+ stub_failing_notification_get(5)
77
+
78
+ expect {
79
+ Notification.find(5)
80
+ }.to raise_error(Notification::FindError)
81
+ end
82
+
83
+ it "returns notification that fails with ReadonlyError when calling deliver_to" do
84
+ stub_notification_get(id: 5)
85
+
86
+ notification = Notification.find(5)
87
+
88
+ expect {
89
+ notification.deliver_to(100)
90
+ }.to raise_error(Notification::ReadonlyError)
91
+ end
92
+
93
+ it "returns notification that fails with ReadonlyError when calling broadcast" do
94
+ stub_notification_get(id: 5)
95
+
96
+ notification = Notification.find(5)
97
+
98
+ expect {
99
+ notification.broadcast
100
+ }.to raise_error(Notification::ReadonlyError)
101
+ end
102
+ end
103
+
104
+ describe ".find_all" do
105
+ it "returns notifications of project with attributes from json response" do
106
+ attributes = {
107
+ id: 5,
108
+ title: "Foo Bar",
109
+ body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
110
+ target_url: "http://example.com",
111
+ created_at: "2016-07-06T10:09:14.835Z",
112
+ ttl: 604800,
113
+ require_interaction: false,
114
+ icon_url: "https://example.com/assets/icon.png",
115
+ scheduled_count: 2,
116
+ successfully_sent_count: 4,
117
+ opened_count: 1
118
+ }
119
+ stub_notifications_get(project_id: 10, list: [attributes])
120
+
121
+ notifications = Notification.find_all(project_id: 10)
122
+
123
+ attributes.delete(:created_at)
124
+ expect(notifications[0]).to have_attributes(attributes)
125
+ expect(notifications[0].created_at.utc.to_s).to eq(Time.utc(2016, 7, 6, 10, 9, 14.835).to_s)
126
+ end
127
+
128
+ it "falls back to global project id" do
129
+ attributes = { id: 5 }
130
+ stub_notifications_get(project_id: 10, list: [attributes])
131
+
132
+ Pushpad.project_id = 10
133
+ notifications = Notification.find_all
134
+
135
+ expect(notifications[0]).to have_attributes(attributes)
136
+ end
137
+
138
+ it "fails with helpful error message when project_id is missing" do
139
+ Pushpad.project_id = nil
140
+
141
+ expect {
142
+ Notification.find_all
143
+ }.to raise_error(/must set project_id/)
144
+ end
145
+
146
+ it "allows passing page parameter for pagination" do
147
+ attributes = { id: 5 }
148
+ stub_notifications_get(project_id: 10, list: [attributes], query: { page: "3" })
149
+
150
+ notifications = Notification.find_all(project_id: 10, page: 3)
151
+
152
+ expect(notifications[0]).to have_attributes(attributes)
153
+ end
154
+
155
+ it "fails with FindError if response status code is not 200" do
156
+ stub_failing_notifications_get(project_id: 10)
157
+
158
+ expect {
159
+ Notification.find_all(project_id: 10)
160
+ }.to raise_error(Notification::FindError)
161
+ end
162
+
163
+ it "returns notifications that fail with ReadonlyError when calling deliver_to" do
164
+ stub_notifications_get(project_id: 10, list: [{ id: 5 }])
165
+
166
+ notifications = Notification.find_all(project_id: 10)
167
+
168
+ expect {
169
+ notifications[0].deliver_to(100)
170
+ }.to raise_error(Notification::ReadonlyError)
171
+ end
172
+
173
+ it "returns notifications that fail with ReadonlyError when calling broadcast" do
174
+ stub_notifications_get(project_id: 10, list: [{ id: 5 }])
175
+
176
+ notifications = Notification.find_all(project_id: 10)
177
+
178
+ expect {
179
+ notifications[0].broadcast
180
+ }.to raise_error(Notification::ReadonlyError)
181
+ end
182
+ end
183
+
184
+ shared_examples "delivery method" do |method|
185
+ it "fails with DeliveryError if response status code is not 201" do
186
+ stub_failing_notification_post(project_id)
187
+
188
+ expect {
189
+ method.call(notification)
190
+ }.to raise_error(Notification::DeliveryError)
191
+ end
192
+
193
+ it "sets id and scheduled_count from json response" do
194
+ response_body = {
195
+ "id" => 3,
196
+ "scheduled" => 5
197
+ }.to_json
198
+ stub_notification_post(project_id, {}, response_body)
199
+
200
+ method.call(notification)
201
+
202
+ expect(notification).to have_attributes(id: 3, scheduled_count: 5)
203
+ end
204
+
205
+ it "overrides id on subsequent calls" do
206
+ id_counter = 0
207
+ response_body = lambda do |*|
208
+ id_counter += 1
209
+
210
+ {
211
+ "id" => id_counter,
212
+ "scheduled" => 5
213
+ }.to_json
214
+ end
215
+ stub_notification_post(project_id, {}, response_body)
216
+
217
+ method.call(notification)
218
+ method.call(notification)
219
+ method.call(notification)
220
+
221
+ expect(notification.id).to eq(3)
222
+ end
223
+
224
+ it "returns raw json response" do
225
+ response = {
226
+ "id" => 4,
227
+ "scheduled" => 5,
228
+ "uids" => ["uid0", "uid1"]
229
+ }
230
+ stub_notification_post(project_id, {}, response.to_json)
231
+
232
+ result = method.call(notification)
233
+
234
+ expect(result).to eq(response)
235
+ end
236
+
237
+ it "fails with helpful error message when project_id is missing" do
238
+ Pushpad.project_id = nil
239
+
240
+ expect {
241
+ method.call(notification)
242
+ }.to raise_error(/must set project_id/)
243
+ end
244
+ end
245
+
246
+ describe "#deliver_to" do
247
+ include_examples "delivery method", lambda { |notification|
248
+ notification.deliver_to(100)
249
+ }
250
+
251
+ shared_examples "notification params" do
252
+ it "includes the params in the request" do
253
+ req = stub_notification_post project_id, notification: notification_params
254
+ notification.deliver_to [123, 456]
255
+ expect(req).to have_been_made.once
256
+ end
257
+ end
258
+
259
+ context "a notification with just the required params" do
260
+ let(:notification_params) do
261
+ { body: "Example message" }
262
+ end
263
+ let(:notification) { Pushpad::Notification.new body: notification_params[:body] }
264
+ include_examples "notification params"
265
+ end
266
+
267
+ context "a notification with all the optional params" do
268
+ let(:notification_params) do
269
+ {
270
+ body: "Example message",
271
+ title: "Website Name",
272
+ target_url: "http://example.com",
273
+ icon_url: "http://example.com/assets/icon.png",
274
+ ttl: 604800,
275
+ require_interaction: true
276
+ }
277
+ end
278
+ let(:notification) { Pushpad::Notification.new notification_params }
279
+ include_examples "notification params"
280
+ end
281
+
282
+ context "with a scalar as a param" do
283
+ it "reaches only that uid" do
284
+ req = stub_notification_post project_id, uids: [100]
285
+ notification.deliver_to(100)
286
+ expect(req).to have_been_made.once
287
+ end
288
+ end
289
+
290
+ context "with an array as a param" do
291
+ it "reaches only those uids" do
292
+ req = stub_notification_post project_id, uids: [123, 456]
293
+ notification.deliver_to([123, 456])
294
+ expect(req).to have_been_made.once
295
+ end
296
+ end
297
+
298
+ context "with uids and tags" do
299
+ it "filters audience by uids and tags" do
300
+ req = stub_notification_post project_id, uids: [123, 456], tags: ["tag1"]
301
+ notification.deliver_to([123, 456], tags: ["tag1"])
302
+ expect(req).to have_been_made.once
303
+ end
304
+ end
305
+ end
306
+
307
+ describe "#broadcast" do
308
+ include_examples "delivery method", lambda { |notification|
309
+ notification.broadcast
310
+ }
311
+
312
+ shared_examples "notification params" do
313
+ it "includes the params in the request" do
314
+ req = stub_notification_post project_id, notification: notification_params
315
+ notification.broadcast
316
+ expect(req).to have_been_made.once
317
+ end
318
+ end
319
+
320
+ context "a notification with just the required params" do
321
+ let(:notification_params) do
322
+ { body: "Example message" }
323
+ end
324
+ let(:notification) { Pushpad::Notification.new body: notification_params[:body] }
325
+ include_examples "notification params"
326
+ end
327
+
328
+ context "a notification with all the optional params" do
329
+ let(:notification_params) do
330
+ {
331
+ body: "Example message",
332
+ title: "Website Name",
333
+ target_url: "http://example.com",
334
+ icon_url: "http://example.com/assets/icon.png",
335
+ ttl: 604800,
336
+ require_interaction: true
337
+ }
338
+ end
339
+ let(:notification) { Pushpad::Notification.new notification_params }
340
+ include_examples "notification params"
341
+ end
342
+
343
+ context "without params" do
344
+ it "reaches everyone" do
345
+ req = stub_notification_post project_id, {}
346
+ notification.broadcast
347
+ expect(req).to have_been_made.once
348
+ end
349
+ end
350
+
351
+ context "with tags" do
352
+ it "filters audience by tags" do
353
+ req = stub_notification_post project_id, tags: ["tag1", "tag2"]
354
+ notification.broadcast tags: ["tag1", "tag2"]
355
+ expect(req).to have_been_made.once
356
+ end
357
+ end
358
+ end
359
+ end
360
+ end
@@ -0,0 +1,78 @@
1
+ require "spec_helper"
2
+
3
+ module Pushpad
4
+ describe Request do
5
+ describe ".head" do
6
+ it "passes auth_token in authorization header" do
7
+ stub_request(:any, /pushpad.xyz/)
8
+
9
+ Pushpad.auth_token = "abc123"
10
+ Request.head("https://pushpad.xyz/example")
11
+
12
+ expect(a_request(:head, "https://pushpad.xyz/example").
13
+ with(headers: { "Authorization" => 'Token token="abc123"' })).to have_been_made
14
+ end
15
+
16
+ it "supports passing query parameters" do
17
+ stub_request(:any, /pushpad.xyz/)
18
+
19
+ Pushpad.auth_token = "abc123"
20
+ Request.head("https://pushpad.xyz/example", query_parameters: [["some", "value"]])
21
+
22
+ expect(a_request(:head, "https://pushpad.xyz/example?some=value")).to have_been_made
23
+ end
24
+ end
25
+
26
+ describe ".get" do
27
+ it "passes auth_token in authorization header" do
28
+ stub_request(:any, /pushpad.xyz/)
29
+
30
+ Pushpad.auth_token = "abc123"
31
+ Request.get("https://pushpad.xyz/example")
32
+
33
+ expect(a_request(:get, "https://pushpad.xyz/example").
34
+ with(headers: { "Authorization" => 'Token token="abc123"' })).to have_been_made
35
+ end
36
+
37
+ it "supports passing query parameters" do
38
+ stub_request(:any, /pushpad.xyz/)
39
+
40
+ Pushpad.auth_token = "abc123"
41
+ Request.get("https://pushpad.xyz/example", query_parameters: [["some", "value"]])
42
+
43
+ expect(a_request(:get, "https://pushpad.xyz/example?some=value")).to have_been_made
44
+ end
45
+ end
46
+
47
+ describe ".post" do
48
+ it "passes auth_token in authorization header" do
49
+ stub_request(:any, /pushpad.xyz/)
50
+
51
+ Pushpad.auth_token = "abc123"
52
+ Request.post("https://pushpad.xyz/example", '{"some": "value"}')
53
+
54
+ expect(a_request(:post, "https://pushpad.xyz/example").
55
+ with(headers: { "Authorization" => 'Token token="abc123"' })).to have_been_made
56
+ end
57
+
58
+ it "passes request body" do
59
+ stub_request(:any, /pushpad.xyz/)
60
+
61
+ Pushpad.auth_token = "abc123"
62
+ Request.post("https://pushpad.xyz/example", '{"some": "value"}')
63
+
64
+ expect(a_request(:post, "https://pushpad.xyz/example").
65
+ with(body: '{"some": "value"}')).to have_been_made
66
+ end
67
+
68
+ it "supports passing query parameters" do
69
+ stub_request(:any, /pushpad.xyz/)
70
+
71
+ Pushpad.auth_token = "abc123"
72
+ Request.post("https://pushpad.xyz/example", "{}", query_parameters: [["some", "value"]])
73
+
74
+ expect(a_request(:post, "https://pushpad.xyz/example?some=value")).to have_been_made
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,85 @@
1
+ require "spec_helper"
2
+
3
+ module Pushpad
4
+ describe Subscription do
5
+ def stub_subscriptions_head(options)
6
+ stub_request(:head, "https://pushpad.xyz/projects/#{options[:project_id]}/subscriptions").
7
+ with(query: hash_including(options.fetch(:query, {}))).
8
+ to_return(status: 200,
9
+ headers: { "X-Total-Count" => options.fetch(:total_count, 10) })
10
+ end
11
+
12
+ def stub_failing_subscriptions_head(options)
13
+ stub_request(:head, "https://pushpad.xyz/projects/#{options[:project_id]}/subscriptions").
14
+ to_return(status: 503)
15
+ end
16
+
17
+ describe ".count" do
18
+ it "returns value from X-Total-Count header" do
19
+ stub_subscriptions_head(project_id: 5, total_count: 100)
20
+
21
+ result = Subscription.count(project_id: 5)
22
+
23
+ expect(result).to eq(100)
24
+ end
25
+
26
+ it "falls back to global project_id" do
27
+ request = stub_subscriptions_head(project_id: 5, total_count: 100)
28
+
29
+ Pushpad.project_id = 5
30
+ Subscription.count
31
+
32
+ expect(request).to have_been_made.once
33
+ end
34
+
35
+ it "fails with helpful error message when project_id is missing" do
36
+ Pushpad.project_id = nil
37
+
38
+ expect {
39
+ Subscription.count
40
+ }.to raise_error(/must set project_id/)
41
+ end
42
+
43
+ it "allows passing uids" do
44
+ request = stub_subscriptions_head(project_id: 5, query: { uids: ["uid0", "uid1"] })
45
+
46
+ Subscription.count(project_id: 5, uids: ["uid0", "uid1"])
47
+
48
+ expect(request).to have_been_made.once
49
+ end
50
+
51
+ it "allows passing tags" do
52
+ request = stub_subscriptions_head(project_id: 5, query: { tags: ["sports", "travel"] })
53
+
54
+ Subscription.count(project_id: 5, tags: ["sports", "travel"])
55
+
56
+ expect(request).to have_been_made.once
57
+ end
58
+
59
+ it "allows passing tags as boolean expression" do
60
+ request = stub_subscriptions_head(project_id: 5, query: { tags: "sports || travel" })
61
+
62
+ Subscription.count(project_id: 5, tags: "sports || travel")
63
+
64
+ expect(request).to have_been_made.once
65
+ end
66
+
67
+ it "allows passing tags and uids" do
68
+ request = stub_subscriptions_head(project_id: 5,
69
+ query: { tags: ["sports", "travel"], uids: ["uid0"] })
70
+
71
+ Subscription.count(project_id: 5, tags: ["sports", "travel"], uids: ["uid0"])
72
+
73
+ expect(request).to have_been_made.once
74
+ end
75
+
76
+ it "fails with CountError if response status code is not 200" do
77
+ stub_failing_subscriptions_head(project_id: 5)
78
+
79
+ expect {
80
+ Subscription.count(project_id: 5)
81
+ }.to raise_error(Subscription::CountError)
82
+ end
83
+ end
84
+ end
85
+ end
data/spec/pushpad_spec.rb CHANGED
@@ -1,10 +1,6 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Pushpad do
4
- let!(:auth_token) { Pushpad.auth_token = 'abc123' }
5
- let!(:project_id) { Pushpad.project_id = 123 }
6
- let(:notification) { Pushpad::Notification.new body: "Example message" }
7
-
8
4
  describe "#auth_token=" do
9
5
  it "sets the Pushpad auth token globally" do
10
6
  Pushpad.auth_token = 'abc123'
@@ -25,118 +21,4 @@ describe Pushpad do
25
21
  expect(signature).to eq '27fbe136f5a4aa0b6be74c0e18fa8ce81ad91b60'
26
22
  end
27
23
  end
28
-
29
- def stub_notification_post project_id, params
30
- stub_request(:post, "https://pushpad.xyz/projects/#{project_id}/notifications").
31
- with(body: hash_including(params)).
32
- to_return(status: 201, body: '{}')
33
- end
34
-
35
- describe "#deliver_to" do
36
-
37
- shared_examples 'notification params' do
38
- it "includes the params in the request" do
39
- req = stub_notification_post project_id, notification: notification_params
40
- notification.deliver_to [123, 456]
41
- expect(req).to have_been_made.once
42
- end
43
- end
44
-
45
- context "a notification with just the required params" do
46
- let(:notification_params) do
47
- { body: "Example message" }
48
- end
49
- let(:notification) { Pushpad::Notification.new body: notification_params[:body] }
50
- include_examples 'notification params'
51
- end
52
-
53
- context "a notification with all the optional params" do
54
- let(:notification_params) do
55
- {
56
- body: "Example message",
57
- title: "Website Name",
58
- target_url: "http://example.com",
59
- icon_url: "http://example.com/assets/icon.png",
60
- ttl: 604800,
61
- require_interaction: true
62
- }
63
- end
64
- let(:notification) { Pushpad::Notification.new notification_params }
65
- include_examples 'notification params'
66
- end
67
-
68
- context "with a scalar as a param" do
69
- it "reaches only that uid" do
70
- req = stub_notification_post project_id, uids: [100]
71
- notification.deliver_to(100)
72
- expect(req).to have_been_made.once
73
- end
74
- end
75
-
76
- context "with an array as a param" do
77
- it "reaches only those uids" do
78
- req = stub_notification_post project_id, uids: [123, 456]
79
- notification.deliver_to([123, 456])
80
- expect(req).to have_been_made.once
81
- end
82
- end
83
-
84
- context "with uids and tags" do
85
- it "filters audience by uids and tags" do
86
- req = stub_notification_post project_id, uids: [123, 456], tags: ['tag1']
87
- notification.deliver_to([123, 456], tags: ['tag1'])
88
- expect(req).to have_been_made.once
89
- end
90
- end
91
- end
92
-
93
- describe "#broadcast" do
94
-
95
- shared_examples 'notification params' do
96
- it "includes the params in the request" do
97
- req = stub_notification_post project_id, notification: notification_params
98
- notification.broadcast
99
- expect(req).to have_been_made.once
100
- end
101
- end
102
-
103
- context "a notification with just the required params" do
104
- let(:notification_params) do
105
- { body: "Example message" }
106
- end
107
- let(:notification) { Pushpad::Notification.new body: notification_params[:body] }
108
- include_examples 'notification params'
109
- end
110
-
111
- context "a notification with all the optional params" do
112
- let(:notification_params) do
113
- {
114
- body: "Example message",
115
- title: "Website Name",
116
- target_url: "http://example.com",
117
- icon_url: "http://example.com/assets/icon.png",
118
- ttl: 604800,
119
- require_interaction: true
120
- }
121
- end
122
- let(:notification) { Pushpad::Notification.new notification_params }
123
- include_examples 'notification params'
124
- end
125
-
126
- context "without params" do
127
- it "reaches everyone" do
128
- req = stub_notification_post project_id, {}
129
- notification.broadcast
130
- expect(req).to have_been_made.once
131
- end
132
- end
133
-
134
- context "with tags" do
135
- it "filters audience by tags" do
136
- req = stub_notification_post project_id, tags: ['tag1', 'tag2']
137
- notification.broadcast tags: ['tag1', 'tag2']
138
- expect(req).to have_been_made.once
139
- end
140
- end
141
- end
142
24
  end
metadata CHANGED
@@ -1,41 +1,41 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pushpad
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pushpad
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-01-27 00:00:00.000000000 Z
11
+ date: 2017-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - '>='
18
18
  - !ruby/object:Gem::Version
19
19
  version: '0'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - '>='
25
25
  - !ruby/object:Gem::Version
26
26
  version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: webmock
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ">="
31
+ - - '>='
32
32
  - !ruby/object:Gem::Version
33
33
  version: '0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ">="
38
+ - - '>='
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
41
  description:
@@ -45,10 +45,21 @@ executables: []
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - .gitignore
49
+ - .rspec
50
+ - .travis.yml
48
51
  - Gemfile
49
52
  - LICENSE.txt
50
53
  - README.md
54
+ - Rakefile
51
55
  - lib/pushpad.rb
56
+ - lib/pushpad/notification.rb
57
+ - lib/pushpad/request.rb
58
+ - lib/pushpad/subscription.rb
59
+ - pushpad.gemspec
60
+ - spec/pushpad/notification_spec.rb
61
+ - spec/pushpad/request_spec.rb
62
+ - spec/pushpad/subscription_spec.rb
52
63
  - spec/pushpad_spec.rb
53
64
  - spec/spec_helper.rb
54
65
  homepage: https://pushpad.xyz
@@ -61,20 +72,23 @@ require_paths:
61
72
  - lib
62
73
  required_ruby_version: !ruby/object:Gem::Requirement
63
74
  requirements:
64
- - - ">="
75
+ - - '>='
65
76
  - !ruby/object:Gem::Version
66
77
  version: '0'
67
78
  required_rubygems_version: !ruby/object:Gem::Requirement
68
79
  requirements:
69
- - - ">="
80
+ - - '>='
70
81
  - !ruby/object:Gem::Version
71
82
  version: '0'
72
83
  requirements: []
73
84
  rubyforge_project:
74
- rubygems_version: 2.4.5
85
+ rubygems_version: 2.0.14.1
75
86
  signing_key:
76
87
  specification_version: 4
77
88
  summary: Web push notifications for Chrome, Firefox and Safari using Pushpad.
78
89
  test_files:
79
- - spec/spec_helper.rb
90
+ - spec/pushpad/notification_spec.rb
91
+ - spec/pushpad/request_spec.rb
92
+ - spec/pushpad/subscription_spec.rb
80
93
  - spec/pushpad_spec.rb
94
+ - spec/spec_helper.rb