pushover 3.0.2 → 4.0.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 +4 -4
- data/.github/workflows/ci.yml +28 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +11 -24
- data/.ruby-version +1 -0
- data/CONVENTIONS.md +19 -0
- data/Gemfile +1 -2
- data/Guardfile +3 -3
- data/README.md +236 -44
- data/Rakefile +9 -59
- data/exe/pushover +6 -5
- data/lib/pushover/client.rb +53 -0
- data/lib/pushover/glances.rb +86 -0
- data/lib/pushover/groups.rb +137 -0
- data/lib/pushover/licenses.rb +67 -0
- data/lib/pushover/limits.rb +18 -0
- data/lib/pushover/message.rb +29 -32
- data/lib/pushover/message_encryption.rb +48 -0
- data/lib/pushover/message_validator.rb +167 -0
- data/lib/pushover/messages.rb +20 -0
- data/lib/pushover/receipt.rb +23 -10
- data/lib/pushover/receipts.rb +60 -0
- data/lib/pushover/response.rb +8 -3
- data/lib/pushover/sounds.rb +18 -0
- data/lib/pushover/subscriptions.rb +48 -0
- data/lib/pushover/teams.rb +93 -0
- data/lib/pushover/users.rb +37 -0
- data/lib/pushover/version.rb +1 -1
- data/lib/pushover.rb +14 -1
- data/pushover.gemspec +9 -7
- data/spec/gem/specification_spec.rb +19 -0
- data/spec/pushover/client_spec.rb +149 -0
- data/spec/pushover/glances_spec.rb +206 -0
- data/spec/pushover/groups_spec.rb +486 -0
- data/spec/pushover/licenses_spec.rb +238 -0
- data/spec/pushover/limits_spec.rb +92 -0
- data/spec/pushover/message_spec.rb +146 -26
- data/spec/pushover/messages_spec.rb +199 -0
- data/spec/pushover/receipt_spec.rb +119 -11
- data/spec/pushover/receipts_spec.rb +232 -0
- data/spec/pushover/response_spec.rb +21 -16
- data/spec/pushover/sounds_spec.rb +92 -0
- data/spec/pushover/subscriptions_spec.rb +185 -0
- data/spec/pushover/teams_spec.rb +325 -0
- data/spec/pushover/users_spec.rb +115 -0
- data/spec/spec_helper.rb +2 -3
- metadata +54 -18
- data/.travis.yml +0 -8
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
require 'base64'
|
|
2
|
+
require 'spec_helper'
|
|
3
|
+
|
|
4
|
+
describe Pushover::Messages do
|
|
5
|
+
subject(:messages) { Pushover::Client.new(token: 'app-token').messages }
|
|
6
|
+
|
|
7
|
+
after { Excon.stubs.clear }
|
|
8
|
+
|
|
9
|
+
describe '#create' do
|
|
10
|
+
context 'with a valid message' do
|
|
11
|
+
let(:request) { {} }
|
|
12
|
+
let(:response) { messages.create(user: 'user-key', message: 'Hello', title: 'Greeting', html: true) }
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
captured_request = request
|
|
16
|
+
Excon.stub(method: :post, path: '/1/messages.json') do |params|
|
|
17
|
+
captured_request.replace(params)
|
|
18
|
+
{ body: Oj.dump(status: 1, request: 'request-id'), headers: { 'X-Limit-App-Remaining' => '9' }, status: 200 }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'posts all fields as JSON' do
|
|
23
|
+
response
|
|
24
|
+
|
|
25
|
+
expect(Oj.strict_load(request[:body])).to eq(
|
|
26
|
+
'token' => 'app-token', 'user' => 'user-key', 'message' => 'Hello', 'title' => 'Greeting', 'html' => 1
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
it 'uses the JSON content type' do
|
|
31
|
+
response
|
|
32
|
+
|
|
33
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it 'returns a response' do
|
|
37
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'preserves response headers' do
|
|
41
|
+
expect(response.headers).to include('X-Limit-App-Remaining' => '9')
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns Pushover errors from a 4xx response' do
|
|
46
|
+
Excon.stub(method: :post, path: '/1/messages.json') do |_params|
|
|
47
|
+
{ body: Oj.dump(status: 0, errors: ['user identifier is invalid']), status: 400 }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
response = messages.create(user: 'invalid', message: 'Hello')
|
|
51
|
+
|
|
52
|
+
expect(response).to have_attributes(status: 0, errors: ['user identifier is invalid'])
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def stub_success
|
|
56
|
+
Excon.stub(
|
|
57
|
+
{ method: :post, path: '/1/messages.json' },
|
|
58
|
+
{ body: Oj.dump(status: 1, request: 'request-id'), status: 200 }
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
%i[user message].each do |parameter|
|
|
63
|
+
it "requires #{parameter}" do
|
|
64
|
+
params = { user: 'user-key', message: 'Hello' }
|
|
65
|
+
params[parameter] = ''
|
|
66
|
+
|
|
67
|
+
expect { messages.create(**params) }.to raise_error ArgumentError, /#{parameter} must be supplied/
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
{
|
|
72
|
+
message: 1024,
|
|
73
|
+
title: 250,
|
|
74
|
+
url: 512,
|
|
75
|
+
url_title: 100
|
|
76
|
+
}.each do |parameter, maximum|
|
|
77
|
+
it "limits #{parameter} to #{maximum} characters" do
|
|
78
|
+
params = { user: 'user-key', message: 'Hello', parameter => 'a' * (maximum + 1) }
|
|
79
|
+
|
|
80
|
+
expect { messages.create(**params) }.to raise_error ArgumentError, /#{parameter} must be at most #{maximum} characters/
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'limits one request to 50 users' do
|
|
85
|
+
users = Array.new(51, 'user-key').join(',')
|
|
86
|
+
|
|
87
|
+
expect { messages.create(user: users, message: 'Hello') }.to raise_error ArgumentError, /at most 50 users/
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'validates device names' do
|
|
91
|
+
expect do
|
|
92
|
+
messages.create(user: 'user-key', message: 'Hello', device: 'not a valid device')
|
|
93
|
+
end.to raise_error ArgumentError, /device contains an invalid name/
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
it 'accepts only documented priorities' do
|
|
97
|
+
expect do
|
|
98
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 3)
|
|
99
|
+
end.to raise_error ArgumentError, /priority must be one of/
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'requires retry and expire for emergency priority' do
|
|
103
|
+
expect do
|
|
104
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 2)
|
|
105
|
+
end.to raise_error ArgumentError, /retry and expire must be supplied/
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it 'requires emergency retries of at least 30 seconds' do
|
|
109
|
+
expect do
|
|
110
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 2, retry: 29, expire: 10_801)
|
|
111
|
+
end.to raise_error ArgumentError, /retry must be at least 30 seconds/
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'limits emergency expiration to 10800 seconds' do
|
|
115
|
+
expect do
|
|
116
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 2, retry: 30, expire: 10_801)
|
|
117
|
+
end.to raise_error ArgumentError, /expire must be at most 10800 seconds/
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'rejects emergency-only fields for other priorities' do
|
|
121
|
+
expect do
|
|
122
|
+
messages.create(user: 'user-key', message: 'Hello', callback: 'https://example.test/callback')
|
|
123
|
+
end.to raise_error ArgumentError, /callback is only valid with priority 2/
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'requires an HTTP or HTTPS callback URL' do
|
|
127
|
+
expect do
|
|
128
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 2, retry: 30, expire: 60, callback: 'file:///tmp/callback')
|
|
129
|
+
end.to raise_error ArgumentError, /callback must be an HTTP or HTTPS URL/
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'rejects a malformed callback URL' do
|
|
133
|
+
expect do
|
|
134
|
+
messages.create(user: 'user-key', message: 'Hello', priority: 2, retry: 30, expire: 60, callback: 'http://[')
|
|
135
|
+
end.to raise_error ArgumentError, /callback must be an HTTP or HTTPS URL/
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'does not allow HTML and monospace formatting together' do
|
|
139
|
+
expect do
|
|
140
|
+
messages.create(user: 'user-key', message: 'Hello', html: true, monospace: true)
|
|
141
|
+
end.to raise_error ArgumentError, /html and monospace cannot both be enabled/
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'requires a positive TTL' do
|
|
145
|
+
expect do
|
|
146
|
+
messages.create(user: 'user-key', message: 'Hello', ttl: 0)
|
|
147
|
+
end.to raise_error ArgumentError, /ttl must be a positive integer/
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'requires valid Base64 attachment data' do
|
|
151
|
+
expect do
|
|
152
|
+
messages.create(user: 'user-key', message: 'Hello', attachment_base64: 'not-base64', attachment_type: 'image/png')
|
|
153
|
+
end.to raise_error ArgumentError, /attachment_base64 must be valid Base64/
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'limits decoded attachments to 5 MiB' do
|
|
157
|
+
oversized = Base64.strict_encode64('a' * 5_242_881)
|
|
158
|
+
|
|
159
|
+
expect do
|
|
160
|
+
messages.create(user: 'user-key', message: 'Hello', attachment_base64: oversized, attachment_type: 'image/png')
|
|
161
|
+
end.to raise_error ArgumentError, /attachment must not exceed 5242880 bytes/
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
it 'requires attachment data and MIME type together' do
|
|
165
|
+
expect do
|
|
166
|
+
messages.create(user: 'user-key', message: 'Hello', attachment_base64: Base64.strict_encode64('image'))
|
|
167
|
+
end.to raise_error ArgumentError, /attachment_type must be supplied/
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
context 'with end-to-end encryption' do
|
|
171
|
+
let(:request) { {} }
|
|
172
|
+
let(:key) { '00' * 32 }
|
|
173
|
+
|
|
174
|
+
before do
|
|
175
|
+
captured_request = request
|
|
176
|
+
allow(SecureRandom).to receive(:random_bytes).and_return("\x01" * 16)
|
|
177
|
+
Excon.stub(method: :post, path: '/1/messages.json') do |params|
|
|
178
|
+
captured_request.replace(Oj.strict_load(params[:body]))
|
|
179
|
+
{ body: Oj.dump(status: 1, request: 'request-id'), status: 200 }
|
|
180
|
+
end
|
|
181
|
+
messages.create(user: 'user-key', message: 'secret', title: 'private', encryption_key: key)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'uses the documented deterministic wire format' do
|
|
185
|
+
expect(request.slice('encrypted', 'message', 'title')).to eq(
|
|
186
|
+
'encrypted' => 1,
|
|
187
|
+
'message' => 'AQEBAQEBAQEBAQEBAQEBAU0FqlkYIHEPzHs0NklFY9QT8ZekMQAM/J6iku1rmOJcEcm1SgT5ASVxoQW5SUFLnyWhe0y1Tp7q0YmS2iTpk4k=',
|
|
188
|
+
'title' => 'AQEBAQEBAQEBAQEBAQEBAWWaD5zPUESFUncs7Hm+DBMTqpHx+n5zEHeQcvJiY+/LX/xd2rJVGXcosjBT23uEQ1TagEXt9wehyXozQxxYguw='
|
|
189
|
+
)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'requires a 64-character hexadecimal encryption key' do
|
|
194
|
+
expect do
|
|
195
|
+
messages.create(user: 'user-key', message: 'Hello', encryption_key: 'invalid')
|
|
196
|
+
end.to raise_error ArgumentError, /encryption_key must be 64 hexadecimal characters/
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -1,24 +1,132 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
require 'rbconfig'
|
|
1
3
|
require 'spec_helper'
|
|
2
4
|
|
|
3
5
|
describe Pushover::Receipt do
|
|
4
|
-
|
|
5
|
-
it { is_expected.to have_attributes(token: a_kind_of(String).or(be_nil)) }
|
|
6
|
+
subject(:legacy_receipt) { described_class.new(receipt: receipt, token: token) }
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
let(:receipt) { 'r' * 30 }
|
|
9
|
+
let(:token) { 'app-token' }
|
|
10
|
+
let(:standalone_script) do
|
|
11
|
+
<<~'RUBY'
|
|
12
|
+
require 'excon'
|
|
13
|
+
Excon.defaults[:mock] = true
|
|
14
|
+
require 'pushover/receipt'
|
|
15
|
+
receipt = 'r' * 30
|
|
16
|
+
Excon.stub(method: :get, path: "/1/receipts/#{receipt}.json", query: { token: 'app-token' }) do
|
|
17
|
+
{ body: Oj.dump('status' => 1), status: 200 }
|
|
18
|
+
end
|
|
19
|
+
abort unless Pushover::Receipt.new(token: 'app-token', receipt: receipt).get.status == 1
|
|
20
|
+
RUBY
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
after { Excon.stubs.clear }
|
|
24
|
+
|
|
25
|
+
it 'supports the standalone legacy require path' do
|
|
26
|
+
_stdout, stderr, status = Open3.capture3(RbConfig.ruby, '-Ilib', '-e', standalone_script)
|
|
27
|
+
|
|
28
|
+
expect(status).to be_success, stderr
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'uses an explicit compatibility class' do
|
|
32
|
+
expect(described_class.superclass).to eq(Object)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'accepts the legacy keyword form' do
|
|
36
|
+
instance = described_class.new(receipt: receipt, token: token)
|
|
37
|
+
|
|
38
|
+
expect(instance).to have_attributes(receipt: receipt, token: token)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'accepts a single Hash with string keys' do
|
|
42
|
+
instance = described_class.new('receipt' => receipt, 'token' => token)
|
|
43
|
+
|
|
44
|
+
expect(instance).to have_attributes(receipt: receipt, token: token)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
it 'keeps the legacy members writable' do
|
|
48
|
+
legacy_receipt.receipt = 'a' * 30
|
|
8
49
|
|
|
9
|
-
|
|
10
|
-
|
|
50
|
+
expect(legacy_receipt.receipt).to eq('a' * 30)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'rejects unknown members' do
|
|
54
|
+
expect { described_class.new(unknown: 'value') }.to raise_error ArgumentError
|
|
55
|
+
end
|
|
11
56
|
|
|
12
|
-
|
|
13
|
-
|
|
57
|
+
it 'rejects non-Hash positional attributes' do
|
|
58
|
+
[nil, false, 1, 'invalid'].each do |attributes|
|
|
59
|
+
expect { described_class.new(attributes) }.to raise_error ArgumentError, /attributes must be supplied as a Hash/
|
|
14
60
|
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it { is_expected.to respond_to(:get).with(0).arguments }
|
|
15
64
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
before { params.delete param }
|
|
65
|
+
describe '#get' do
|
|
66
|
+
before { allow(Warning).to receive(:warn) }
|
|
19
67
|
|
|
20
|
-
|
|
68
|
+
context 'with a valid receipt' do
|
|
69
|
+
let(:request) { {} }
|
|
70
|
+
let(:response) { legacy_receipt.get }
|
|
71
|
+
|
|
72
|
+
before do
|
|
73
|
+
captured_request = request
|
|
74
|
+
Excon.stub(
|
|
75
|
+
method: :get,
|
|
76
|
+
path: "/1/receipts/#{receipt}.json",
|
|
77
|
+
query: { token: token },
|
|
78
|
+
body: nil
|
|
79
|
+
) do |request_params|
|
|
80
|
+
captured_request.replace(request_params)
|
|
81
|
+
{
|
|
82
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id', 'acknowledged' => 0),
|
|
83
|
+
headers: { 'X-Limit-App-Remaining' => '9' },
|
|
84
|
+
status: 200
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it 'delegates to the shared receipt endpoint' do
|
|
90
|
+
response
|
|
91
|
+
|
|
92
|
+
expect(request.slice(:method, :path, :query)).to eq(
|
|
93
|
+
method: :get, path: "/1/receipts/#{receipt}.json", query: "token=#{token}"
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'does not send a request body' do
|
|
98
|
+
response
|
|
99
|
+
|
|
100
|
+
expect(request).not_to have_key(:body)
|
|
21
101
|
end
|
|
102
|
+
|
|
103
|
+
it 'returns the shared response' do
|
|
104
|
+
expect(response).to have_attributes(status: 1, request: 'request-id', attributes: { 'acknowledged' => 0 })
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'emits the exact migration warning once' do
|
|
108
|
+
response
|
|
109
|
+
|
|
110
|
+
expect(Warning).to have_received(:warn).with(
|
|
111
|
+
a_string_including('Pushover::Receipt#get is deprecated; use Pushover::Client.new(token: ...).receipts.get(receipt: ...)'),
|
|
112
|
+
category: nil
|
|
113
|
+
).once
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
%i[receipt token].each do |parameter|
|
|
118
|
+
it "preserves the legacy missing-#{parameter} error" do
|
|
119
|
+
instance = described_class.new(receipt: receipt, token: token)
|
|
120
|
+
instance.public_send("#{parameter}=", nil)
|
|
121
|
+
|
|
122
|
+
expect { instance.get }.to raise_error RuntimeError, "#{parameter} must be supplied"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'applies current receipt validation after the legacy presence checks' do
|
|
127
|
+
instance = described_class.new(receipt: 'receipt', token: token)
|
|
128
|
+
|
|
129
|
+
expect { instance.get }.to raise_error ArgumentError, /receipt must be 30 alphanumeric characters/
|
|
22
130
|
end
|
|
23
131
|
end
|
|
24
132
|
end
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Receipts do
|
|
4
|
+
subject(:receipts) { Pushover::Client.new(token: 'app-token').receipts }
|
|
5
|
+
|
|
6
|
+
let(:receipt) { 'a' * 30 }
|
|
7
|
+
let(:receipt_attributes) do
|
|
8
|
+
{
|
|
9
|
+
'acknowledged' => 1,
|
|
10
|
+
'acknowledged_at' => 1_723_990_000,
|
|
11
|
+
'acknowledged_by' => 'user-key',
|
|
12
|
+
'acknowledged_by_device' => 'phone',
|
|
13
|
+
'last_delivered_at' => 1_723_989_900,
|
|
14
|
+
'expired' => 0,
|
|
15
|
+
'expires_at' => 1_723_993_600,
|
|
16
|
+
'called_back' => 1,
|
|
17
|
+
'called_back_at' => 1_723_990_001
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
let(:success_body) { { 'status' => 1, 'request' => 'request-id' }.merge(receipt_attributes) }
|
|
21
|
+
|
|
22
|
+
after { Excon.stubs.clear }
|
|
23
|
+
|
|
24
|
+
describe '#get' do
|
|
25
|
+
context 'with a valid receipt' do
|
|
26
|
+
let(:request) { {} }
|
|
27
|
+
let(:response) { receipts.get(receipt: receipt) }
|
|
28
|
+
|
|
29
|
+
before do
|
|
30
|
+
captured_request = request
|
|
31
|
+
Excon.stub(
|
|
32
|
+
method: :get,
|
|
33
|
+
path: "/1/receipts/#{receipt}.json",
|
|
34
|
+
query: { token: 'app-token' },
|
|
35
|
+
body: nil
|
|
36
|
+
) do |params|
|
|
37
|
+
captured_request.replace(params)
|
|
38
|
+
{
|
|
39
|
+
body: Oj.dump(success_body),
|
|
40
|
+
headers: { 'X-Limit-App-Remaining' => '9' },
|
|
41
|
+
status: 200
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'gets the receipt with the application token in the query string' do
|
|
47
|
+
response
|
|
48
|
+
|
|
49
|
+
expect(request.slice(:method, :path, :query)).to eq(method: :get, path: "/1/receipts/#{receipt}.json", query: 'token=app-token')
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'does not send a request body' do
|
|
53
|
+
response
|
|
54
|
+
|
|
55
|
+
expect(request).not_to have_key(:body)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'returns the response metadata' do
|
|
59
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
it 'returns the receipt status fields' do
|
|
63
|
+
expect(response.attributes).to eq(receipt_attributes)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'preserves response headers' do
|
|
67
|
+
expect(response.headers).to include('X-Limit-App-Remaining' => '9')
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'when Pushover rejects the request' do
|
|
72
|
+
before do
|
|
73
|
+
Excon.stub(
|
|
74
|
+
{ method: :get, path: "/1/receipts/#{receipt}.json", query: { token: 'app-token' }, body: nil },
|
|
75
|
+
{ body: Oj.dump('status' => 0, 'errors' => ['receipt is invalid']), status: 400 }
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it 'returns errors from a 4xx response' do
|
|
80
|
+
response = receipts.get(receipt: receipt)
|
|
81
|
+
|
|
82
|
+
expect(response).to have_attributes(status: 0, errors: ['receipt is invalid'])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'requires a receipt' do
|
|
87
|
+
expect { receipts.get(receipt: '') }.to raise_error ArgumentError, /receipt must be supplied/
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
['a' * 29, 'a' * 31, "#{'a' * 29}-"].each do |invalid_receipt|
|
|
91
|
+
it 'requires exactly 30 alphanumeric characters' do
|
|
92
|
+
expect { receipts.get(receipt: invalid_receipt) }
|
|
93
|
+
.to raise_error ArgumentError, /receipt must be 30 alphanumeric characters/
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
describe '#cancel' do
|
|
99
|
+
context 'with a valid receipt' do
|
|
100
|
+
let(:request) { {} }
|
|
101
|
+
let(:response) { receipts.cancel(receipt: receipt) }
|
|
102
|
+
|
|
103
|
+
before do
|
|
104
|
+
captured_request = request
|
|
105
|
+
Excon.stub(method: :post, path: "/1/receipts/#{receipt}/cancel.json") do |params|
|
|
106
|
+
captured_request.replace(params)
|
|
107
|
+
{
|
|
108
|
+
body: Oj.dump(status: 1, request: 'request-id'),
|
|
109
|
+
headers: { 'X-Limit-App-Remaining' => '9' },
|
|
110
|
+
status: 200
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
it 'posts to the receipt cancellation endpoint' do
|
|
116
|
+
response
|
|
117
|
+
|
|
118
|
+
expect(request.slice(:method, :path)).to eq(method: :post, path: "/1/receipts/#{receipt}/cancel.json")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'sends the application token as JSON' do
|
|
122
|
+
response
|
|
123
|
+
|
|
124
|
+
expect(Oj.strict_load(request[:body])).to eq('token' => 'app-token')
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'uses the JSON content type' do
|
|
128
|
+
response
|
|
129
|
+
|
|
130
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'returns the response metadata' do
|
|
134
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'preserves response headers' do
|
|
138
|
+
expect(response.headers).to include('X-Limit-App-Remaining' => '9')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
context 'when Pushover rejects the request' do
|
|
143
|
+
before do
|
|
144
|
+
Excon.stub(method: :post, path: "/1/receipts/#{receipt}/cancel.json") do |_params|
|
|
145
|
+
{ body: Oj.dump(status: 0, errors: ['receipt is invalid']), status: 400 }
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'returns errors from a 4xx response' do
|
|
150
|
+
response = receipts.cancel(receipt: receipt)
|
|
151
|
+
|
|
152
|
+
expect(response).to have_attributes(status: 0, errors: ['receipt is invalid'])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'requires a receipt' do
|
|
157
|
+
expect { receipts.cancel(receipt: '') }.to raise_error ArgumentError, /receipt must be supplied/
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
['a' * 29, 'a' * 31, "#{'a' * 29}-"].each do |invalid_receipt|
|
|
161
|
+
it 'requires exactly 30 alphanumeric characters' do
|
|
162
|
+
expect { receipts.cancel(receipt: invalid_receipt) }
|
|
163
|
+
.to raise_error ArgumentError, /receipt must be 30 alphanumeric characters/
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
describe '#cancel_by_tag' do
|
|
169
|
+
context 'with a valid tag' do
|
|
170
|
+
let(:request) { {} }
|
|
171
|
+
let(:response) { receipts.cancel_by_tag(tag: 'l=chicago/ops room') }
|
|
172
|
+
|
|
173
|
+
before do
|
|
174
|
+
captured_request = request
|
|
175
|
+
Excon.stub(method: :post, path: '/1/receipts/cancel_by_tag/l%3Dchicago%2Fops%20room.json') do |params|
|
|
176
|
+
captured_request.replace(params)
|
|
177
|
+
{
|
|
178
|
+
body: Oj.dump(status: 1, request: 'request-id'),
|
|
179
|
+
headers: { 'X-Limit-App-Remaining' => '9' },
|
|
180
|
+
status: 200
|
|
181
|
+
}
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'posts with the tag encoded as one path segment' do
|
|
186
|
+
response
|
|
187
|
+
|
|
188
|
+
expect(request.slice(:method, :path)).to eq(
|
|
189
|
+
method: :post, path: '/1/receipts/cancel_by_tag/l%3Dchicago%2Fops%20room.json'
|
|
190
|
+
)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it 'sends the application token as JSON' do
|
|
194
|
+
response
|
|
195
|
+
|
|
196
|
+
expect(Oj.strict_load(request[:body])).to eq('token' => 'app-token')
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
it 'uses the JSON content type' do
|
|
200
|
+
response
|
|
201
|
+
|
|
202
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
it 'returns the response metadata' do
|
|
206
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
it 'preserves response headers' do
|
|
210
|
+
expect(response.headers).to include('X-Limit-App-Remaining' => '9')
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
context 'when Pushover rejects the request' do
|
|
215
|
+
before do
|
|
216
|
+
Excon.stub(method: :post, path: '/1/receipts/cancel_by_tag/l%3Dchicago%2Fops%20room.json') do |_params|
|
|
217
|
+
{ body: Oj.dump(status: 0, errors: ['tag is invalid']), status: 400 }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
it 'returns errors from a 4xx response' do
|
|
222
|
+
response = receipts.cancel_by_tag(tag: 'l=chicago/ops room')
|
|
223
|
+
|
|
224
|
+
expect(response).to have_attributes(status: 0, errors: ['tag is invalid'])
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'requires a tag' do
|
|
229
|
+
expect { receipts.cancel_by_tag(tag: '') }.to raise_error ArgumentError, /tag must be supplied/
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
|
-
describe Pushover do
|
|
4
|
-
|
|
5
|
-
let(:excon_response) { Excon::Response.new body: '{}', headers: {} }
|
|
3
|
+
describe Pushover::Response do
|
|
4
|
+
let(:excon_response) { Excon::Response.new body: '{}', headers: {} }
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
it { is_expected.to have_attributes(status: a_kind_of(String).or(be_nil)) }
|
|
7
|
+
it { is_expected.to have_attributes(request: a_kind_of(String).or(be_nil)) }
|
|
8
|
+
it { is_expected.to have_attributes(errors: a_kind_of(Array).or(a_kind_of(Hash)).or(be_nil)) }
|
|
9
|
+
it { is_expected.to have_attributes(receipt: a_kind_of(String).or(be_nil)) }
|
|
10
|
+
it { is_expected.to have_attributes(headers: a_kind_of(Hash).or(be_nil)) }
|
|
11
|
+
it { is_expected.to have_attributes(attributes: a_kind_of(Hash).or(be_nil)) }
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
it { expect(described_class).to respond_to(:create_from_excon_response).with(1).argument }
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
15
|
+
describe "#to_s" do
|
|
16
|
+
it "is expected to be purty." do
|
|
17
|
+
expect(described_class.create_from_excon_response(excon_response).to_s).to include('status')
|
|
20
18
|
end
|
|
21
19
|
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
it 'formats structured errors' do
|
|
21
|
+
excon_response.body = Oj.dump('status' => 0, 'errors' => { 'token' => ['has no license credits'] })
|
|
22
|
+
|
|
23
|
+
expect(described_class.create_from_excon_response(excon_response).to_s)
|
|
24
|
+
.to include('errors: {"token"=>["has no license credits"]}')
|
|
24
25
|
end
|
|
25
26
|
end
|
|
27
|
+
|
|
28
|
+
describe "::create_from_excon_response" do
|
|
29
|
+
it { expect(described_class.create_from_excon_response(excon_response)).to be_a described_class }
|
|
30
|
+
end
|
|
26
31
|
end
|