pushpad 0.5.1 → 0.6.0

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