pushover 3.0.3 → 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,206 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Glances do
|
|
4
|
+
subject(:glances) { Pushover::Client.new(token: 'app-token').glances }
|
|
5
|
+
|
|
6
|
+
let(:user) { 'u' * 30 }
|
|
7
|
+
|
|
8
|
+
after { Excon.stubs.clear }
|
|
9
|
+
|
|
10
|
+
describe '#update' do
|
|
11
|
+
context 'with a complete update' do
|
|
12
|
+
let(:request) { {} }
|
|
13
|
+
let(:response) do
|
|
14
|
+
glances.update(
|
|
15
|
+
user: user, device: 'watch-1', title: 'Sales', text: '30', subtext: 'Today', count: -3, percent: 42
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
let(:expected_body) do
|
|
19
|
+
{
|
|
20
|
+
'token' => 'app-token',
|
|
21
|
+
'user' => user,
|
|
22
|
+
'device' => 'watch-1',
|
|
23
|
+
'title' => 'Sales',
|
|
24
|
+
'text' => '30',
|
|
25
|
+
'subtext' => 'Today',
|
|
26
|
+
'count' => -3,
|
|
27
|
+
'percent' => 42
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
before do
|
|
32
|
+
captured_request = request
|
|
33
|
+
Excon.stub(method: :post, path: '/1/glances.json') do |params|
|
|
34
|
+
captured_request.replace(params)
|
|
35
|
+
{
|
|
36
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id'),
|
|
37
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
38
|
+
status: 200
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'posts all fields as JSON' do
|
|
44
|
+
response
|
|
45
|
+
|
|
46
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'uses the JSON content type' do
|
|
50
|
+
response
|
|
51
|
+
|
|
52
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'returns response metadata' do
|
|
56
|
+
expect(response).to have_attributes(status: 1, request: 'request-id', attributes: {})
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'preserves response headers' do
|
|
60
|
+
expect(response.headers).to include('X-Request-Id' => 'header-id')
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'only sends fields being updated and preserves a blank value used to clear data' do
|
|
65
|
+
request = capture_successful_request
|
|
66
|
+
glances.update(user: user, title: '')
|
|
67
|
+
|
|
68
|
+
expect(Oj.strict_load(request[:body])).to eq('token' => 'app-token', 'user' => user, 'title' => '')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'allows numeric fields to be cleared with blank values' do
|
|
72
|
+
request = capture_successful_request
|
|
73
|
+
glances.update(user: user, count: '', percent: '')
|
|
74
|
+
|
|
75
|
+
expect(Oj.strict_load(request[:body])).to include('count' => '', 'percent' => '')
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'accepts a blank device to target all registered widgets' do
|
|
79
|
+
request = capture_successful_request
|
|
80
|
+
glances.update(user: user, device: '', text: 'Garage open')
|
|
81
|
+
|
|
82
|
+
expect(Oj.strict_load(request[:body])).to include('device' => '')
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context 'when Pushover rejects the update' do
|
|
86
|
+
let(:response) { glances.update(user: user, text: 'Garage open') }
|
|
87
|
+
|
|
88
|
+
before do
|
|
89
|
+
Excon.stub(
|
|
90
|
+
{ method: :post, path: '/1/glances.json' },
|
|
91
|
+
{
|
|
92
|
+
body: Oj.dump(
|
|
93
|
+
'status' => 0, 'request' => 'request-id', 'errors' => ['application token is invalid']
|
|
94
|
+
),
|
|
95
|
+
status: 400
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'returns errors from the 400 response' do
|
|
101
|
+
expect(response).to have_attributes(
|
|
102
|
+
status: 0, request: 'request-id', errors: ['application token is invalid']
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
[nil, ''].each do |invalid_user|
|
|
108
|
+
it 'requires a user identifier' do
|
|
109
|
+
expect { glances.update(user: invalid_user, text: 'Garage open') }
|
|
110
|
+
.to raise_error ArgumentError, /user must be supplied/
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
[123, 'u' * 29, 'u' * 31, "#{'u' * 29}-"].each do |invalid_user|
|
|
115
|
+
it 'requires a 30-character alphanumeric user identifier' do
|
|
116
|
+
expect { glances.update(user: invalid_user, text: 'Garage open') }
|
|
117
|
+
.to raise_error ArgumentError, /user must be 30 alphanumeric characters/
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'requires at least one data field' do
|
|
122
|
+
expect { glances.update(user: user, device: 'watch-1') }
|
|
123
|
+
.to raise_error ArgumentError, /at least one glance data field must be supplied/
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it 'treats nil data fields as omitted' do
|
|
127
|
+
expect { glances.update(user: user, text: nil) }
|
|
128
|
+
.to raise_error ArgumentError, /at least one glance data field must be supplied/
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'rejects unsupported parameters' do
|
|
132
|
+
expect { glances.update(user: user, text: 'Garage open', priority: 1) }
|
|
133
|
+
.to raise_error ArgumentError, /unsupported glance parameter: priority/
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
%i[title text subtext].each do |field|
|
|
137
|
+
it "requires #{field} to be a string" do
|
|
138
|
+
expect { glances.update(user: user, field => 123) }
|
|
139
|
+
.to raise_error ArgumentError, /#{field} must be a String/
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
it "limits #{field} to 100 characters" do
|
|
143
|
+
expect { glances.update(user: user, field => 'a' * 101) }
|
|
144
|
+
.to raise_error ArgumentError, /#{field} must be at most 100 characters/
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it 'counts multibyte text limits by characters' do
|
|
149
|
+
stub_success
|
|
150
|
+
|
|
151
|
+
expect { glances.update(user: user, text: 'é' * 100) }.not_to raise_error
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'accepts a negative integer count' do
|
|
155
|
+
stub_success
|
|
156
|
+
|
|
157
|
+
expect { glances.update(user: user, count: -1) }.not_to raise_error
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
['1', 1.5, true].each do |invalid_count|
|
|
161
|
+
it 'requires count to be an integer or blank' do
|
|
162
|
+
expect { glances.update(user: user, count: invalid_count) }
|
|
163
|
+
.to raise_error ArgumentError, /count must be an Integer or blank/
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
[0, 100].each do |valid_percent|
|
|
168
|
+
it "accepts percent #{valid_percent}" do
|
|
169
|
+
stub_success
|
|
170
|
+
|
|
171
|
+
expect { glances.update(user: user, percent: valid_percent) }.not_to raise_error
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
[-1, 101, '50', 50.5].each do |invalid_percent|
|
|
176
|
+
it 'requires percent to be an integer from 0 to 100 or blank' do
|
|
177
|
+
expect { glances.update(user: user, percent: invalid_percent) }
|
|
178
|
+
.to raise_error ArgumentError, /percent must be an Integer from 0 to 100 or blank/
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
[123, 'a' * 26, 'invalid device', 'watch,phone'].each do |invalid_device|
|
|
183
|
+
it 'validates a nonblank device name' do
|
|
184
|
+
expect { glances.update(user: user, device: invalid_device, text: 'Garage open') }
|
|
185
|
+
.to raise_error ArgumentError, /device must be blank or 1 to 25 letters, numbers, underscores, or hyphens/
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def stub_success
|
|
190
|
+
Excon.stub(
|
|
191
|
+
{ method: :post, path: '/1/glances.json' },
|
|
192
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
193
|
+
)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def capture_successful_request
|
|
197
|
+
request = {}
|
|
198
|
+
captured_request = request
|
|
199
|
+
Excon.stub(method: :post, path: '/1/glances.json') do |params|
|
|
200
|
+
captured_request.replace(params)
|
|
201
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
202
|
+
end
|
|
203
|
+
request
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Groups do
|
|
4
|
+
subject(:groups) { Pushover::Client.new(token: 'app-token').groups }
|
|
5
|
+
|
|
6
|
+
let(:group) { 'g' * 30 }
|
|
7
|
+
|
|
8
|
+
after { Excon.stubs.clear }
|
|
9
|
+
|
|
10
|
+
describe '#create' do
|
|
11
|
+
context 'with a group name' do
|
|
12
|
+
let(:request) { {} }
|
|
13
|
+
let(:response) { groups.create(name: 'On-call West') }
|
|
14
|
+
|
|
15
|
+
before do
|
|
16
|
+
captured_request = request
|
|
17
|
+
Excon.stub(method: :post, path: '/1/groups.json') do |params|
|
|
18
|
+
captured_request.replace(params)
|
|
19
|
+
{
|
|
20
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id', 'group' => group),
|
|
21
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
22
|
+
status: 200
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'posts to the group creation endpoint' do
|
|
28
|
+
response
|
|
29
|
+
|
|
30
|
+
expect(request.slice(:method, :path)).to eq(method: :post, path: '/1/groups.json')
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'posts the application token and name as JSON' do
|
|
34
|
+
response
|
|
35
|
+
|
|
36
|
+
expect(Oj.strict_load(request[:body])).to eq('token' => 'app-token', 'name' => 'On-call West')
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'uses the JSON content type' do
|
|
40
|
+
response
|
|
41
|
+
|
|
42
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
it 'returns response metadata' do
|
|
46
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns the new group key' do
|
|
50
|
+
expect(response.attributes).to eq('group' => group)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it 'preserves response headers' do
|
|
54
|
+
expect(response.headers).to include('X-Request-Id' => 'header-id')
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
context 'when Pushover rejects the request' do
|
|
59
|
+
let(:response) { groups.create(name: 'On-call West') }
|
|
60
|
+
|
|
61
|
+
before do
|
|
62
|
+
Excon.stub(
|
|
63
|
+
{ method: :post, path: '/1/groups.json' },
|
|
64
|
+
{
|
|
65
|
+
body: Oj.dump(
|
|
66
|
+
'status' => 0, 'request' => 'request-id', 'errors' => ['application token is invalid']
|
|
67
|
+
),
|
|
68
|
+
status: 400
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it 'returns errors from the 400 response' do
|
|
74
|
+
expect(response).to have_attributes(
|
|
75
|
+
status: 0, request: 'request-id', errors: ['application token is invalid']
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
[nil, ''].each do |invalid_name|
|
|
81
|
+
it 'requires a group name' do
|
|
82
|
+
expect { groups.create(name: invalid_name) }.to raise_error ArgumentError, /name must be supplied/
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
it 'requires the group name to be a string' do
|
|
87
|
+
expect { groups.create(name: 123) }.to raise_error ArgumentError, /name must be a String/
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
describe '#list' do
|
|
92
|
+
context 'when the account owns groups' do
|
|
93
|
+
let(:request) { {} }
|
|
94
|
+
let(:listed_groups) do
|
|
95
|
+
[
|
|
96
|
+
{ 'group' => group, 'name' => 'On-call West' },
|
|
97
|
+
{ 'group' => 'h' * 30, 'name' => 'Operations' }
|
|
98
|
+
]
|
|
99
|
+
end
|
|
100
|
+
let(:response) { groups.list }
|
|
101
|
+
|
|
102
|
+
before do
|
|
103
|
+
captured_request = request
|
|
104
|
+
Excon.stub(
|
|
105
|
+
method: :get,
|
|
106
|
+
path: '/1/groups.json',
|
|
107
|
+
query: { token: 'app-token' },
|
|
108
|
+
body: nil
|
|
109
|
+
) do |params|
|
|
110
|
+
captured_request.replace(params)
|
|
111
|
+
{
|
|
112
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id', 'groups' => listed_groups),
|
|
113
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
114
|
+
status: 200
|
|
115
|
+
}
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it 'gets groups with the application token in the query string' do
|
|
120
|
+
response
|
|
121
|
+
|
|
122
|
+
expect(request.slice(:method, :path, :query)).to eq(
|
|
123
|
+
method: :get, path: '/1/groups.json', query: 'token=app-token'
|
|
124
|
+
)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'does not send a request body' do
|
|
128
|
+
response
|
|
129
|
+
|
|
130
|
+
expect(request).not_to have_key(:body)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'returns response metadata' do
|
|
134
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'returns each group key and name without reordering' do
|
|
138
|
+
expect(response.attributes).to eq('groups' => listed_groups)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
it 'preserves response headers' do
|
|
142
|
+
expect(response.headers).to include('X-Request-Id' => 'header-id')
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
context 'when the account has no groups' do
|
|
147
|
+
before do
|
|
148
|
+
Excon.stub(
|
|
149
|
+
{ method: :get, path: '/1/groups.json', query: { token: 'app-token' }, body: nil },
|
|
150
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id', 'groups' => []), status: 200 }
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'returns an empty groups array' do
|
|
155
|
+
expect(groups.list.attributes).to eq('groups' => [])
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'when Pushover rejects the request' do
|
|
160
|
+
let(:response) { groups.list }
|
|
161
|
+
|
|
162
|
+
before do
|
|
163
|
+
Excon.stub(
|
|
164
|
+
{ method: :get, path: '/1/groups.json', query: { token: 'app-token' }, body: nil },
|
|
165
|
+
{
|
|
166
|
+
body: Oj.dump(
|
|
167
|
+
'status' => 0, 'request' => 'request-id', 'errors' => ['application token is invalid']
|
|
168
|
+
),
|
|
169
|
+
status: 400
|
|
170
|
+
}
|
|
171
|
+
)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
it 'returns errors from the 400 response' do
|
|
175
|
+
expect(response).to have_attributes(
|
|
176
|
+
status: 0, request: 'request-id', errors: ['application token is invalid']
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe '#get' do
|
|
183
|
+
context 'with a valid group' do
|
|
184
|
+
let(:request) { {} }
|
|
185
|
+
let(:group_users) do
|
|
186
|
+
[
|
|
187
|
+
{ 'user' => 'u' * 30, 'device' => nil, 'memo' => 'Jim', 'disabled' => false },
|
|
188
|
+
{
|
|
189
|
+
'user' => 'v' * 30,
|
|
190
|
+
'device' => 'iphone',
|
|
191
|
+
'memo' => 'Mary',
|
|
192
|
+
'disabled' => true,
|
|
193
|
+
'name' => 'Mary Example',
|
|
194
|
+
'email' => 'mary@example.test'
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
end
|
|
198
|
+
let(:group_attributes) { { 'name' => 'On-call West', 'users' => group_users } }
|
|
199
|
+
let(:response) { groups.get(group: group) }
|
|
200
|
+
|
|
201
|
+
before do
|
|
202
|
+
captured_request = request
|
|
203
|
+
Excon.stub(
|
|
204
|
+
method: :get,
|
|
205
|
+
path: "/1/groups/#{group}.json",
|
|
206
|
+
query: { token: 'app-token' },
|
|
207
|
+
body: nil
|
|
208
|
+
) do |params|
|
|
209
|
+
captured_request.replace(params)
|
|
210
|
+
{
|
|
211
|
+
body: Oj.dump({ 'status' => 1, 'request' => 'request-id' }.merge(group_attributes)),
|
|
212
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
213
|
+
status: 200
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
it 'gets the group with the application token in the query string' do
|
|
219
|
+
response
|
|
220
|
+
|
|
221
|
+
expect(request.slice(:method, :path, :query)).to eq(
|
|
222
|
+
method: :get, path: "/1/groups/#{group}.json", query: 'token=app-token'
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
it 'does not send a request body' do
|
|
227
|
+
response
|
|
228
|
+
|
|
229
|
+
expect(request).not_to have_key(:body)
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
it 'returns response metadata' do
|
|
233
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
it 'returns the group name and membership fields without reordering' do
|
|
237
|
+
expect(response.attributes).to eq(group_attributes)
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
it 'preserves response headers' do
|
|
241
|
+
expect(response.headers).to include('X-Request-Id' => 'header-id')
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
context 'when the group has no users' do
|
|
246
|
+
before do
|
|
247
|
+
Excon.stub(
|
|
248
|
+
{ method: :get, path: "/1/groups/#{group}.json", query: { token: 'app-token' }, body: nil },
|
|
249
|
+
{
|
|
250
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id', 'name' => 'Empty Group', 'users' => []),
|
|
251
|
+
status: 200
|
|
252
|
+
}
|
|
253
|
+
)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it 'returns an empty users array' do
|
|
257
|
+
expect(groups.get(group: group).attributes).to eq('name' => 'Empty Group', 'users' => [])
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
context 'when Pushover rejects the request' do
|
|
262
|
+
let(:response) { groups.get(group: group) }
|
|
263
|
+
|
|
264
|
+
before do
|
|
265
|
+
Excon.stub(
|
|
266
|
+
{ method: :get, path: "/1/groups/#{group}.json", query: { token: 'app-token' }, body: nil },
|
|
267
|
+
{
|
|
268
|
+
body: Oj.dump('status' => 0, 'request' => 'request-id', 'errors' => ['group is invalid']),
|
|
269
|
+
status: 400
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
it 'returns errors from the 400 response' do
|
|
275
|
+
expect(response).to have_attributes(status: 0, request: 'request-id', errors: ['group is invalid'])
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
[nil, ''].each do |invalid_group|
|
|
280
|
+
it 'requires a group key' do
|
|
281
|
+
expect { groups.get(group: invalid_group) }.to raise_error ArgumentError, /group must be supplied/
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
['g' * 29, 'g' * 31, "#{'g' * 29}-", 123].each do |invalid_group|
|
|
286
|
+
it 'requires exactly 30 alphanumeric characters' do
|
|
287
|
+
expect { groups.get(group: invalid_group) }
|
|
288
|
+
.to raise_error ArgumentError, /group must be 30 alphanumeric characters/
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
shared_examples 'a group mutation' do |operation, action, arguments, expected_body|
|
|
294
|
+
let(:request) { {} }
|
|
295
|
+
let(:response) { groups.public_send(operation, **arguments) }
|
|
296
|
+
let(:api_response) do
|
|
297
|
+
{
|
|
298
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id'),
|
|
299
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
300
|
+
status: 200
|
|
301
|
+
}
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
before do
|
|
305
|
+
captured_request = request
|
|
306
|
+
Excon.stub(method: :post, path: "/1/groups/#{arguments[:group]}/#{action}.json") do |params|
|
|
307
|
+
captured_request.replace(params)
|
|
308
|
+
api_response
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
it 'posts to the documented endpoint' do
|
|
313
|
+
response
|
|
314
|
+
|
|
315
|
+
expect(request.slice(:method, :path)).to eq(
|
|
316
|
+
method: :post, path: "/1/groups/#{arguments[:group]}/#{action}.json"
|
|
317
|
+
)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'serializes the documented string-key JSON body' do
|
|
321
|
+
response
|
|
322
|
+
|
|
323
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'uses the JSON content type' do
|
|
327
|
+
response
|
|
328
|
+
|
|
329
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
it 'returns response metadata and headers' do
|
|
333
|
+
expect(response).to have_attributes(
|
|
334
|
+
status: 1, request: 'request-id', attributes: {}, headers: include('X-Request-Id' => 'header-id')
|
|
335
|
+
)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
context 'when Pushover rejects the request' do
|
|
339
|
+
let(:api_response) do
|
|
340
|
+
{
|
|
341
|
+
body: Oj.dump('status' => 0, 'request' => 'request-id', 'errors' => ['group operation failed']),
|
|
342
|
+
status: 400
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'returns errors from the 400 response' do
|
|
347
|
+
expect(response).to have_attributes(
|
|
348
|
+
status: 0, request: 'request-id', errors: ['group operation failed']
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
describe '#add_user' do
|
|
355
|
+
it_behaves_like(
|
|
356
|
+
'a group mutation',
|
|
357
|
+
:add_user,
|
|
358
|
+
'add_user',
|
|
359
|
+
{ group: 'g' * 30, user: 'u' * 30, device: 'd' * 25, memo: 'é' * 200 },
|
|
360
|
+
{
|
|
361
|
+
'token' => 'app-token',
|
|
362
|
+
'user' => 'u' * 30,
|
|
363
|
+
'device' => 'd' * 25,
|
|
364
|
+
'memo' => 'é' * 200
|
|
365
|
+
}
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
[
|
|
369
|
+
[{ device: nil, memo: nil }, { 'token' => 'app-token', 'user' => 'u' * 30 }],
|
|
370
|
+
[
|
|
371
|
+
{ device: '', memo: '' },
|
|
372
|
+
{ 'token' => 'app-token', 'user' => 'u' * 30, 'device' => '', 'memo' => '' }
|
|
373
|
+
]
|
|
374
|
+
].each do |optional_fields, expected_body|
|
|
375
|
+
context "with #{optional_fields.values.compact.empty? ? 'nil' : 'blank'} optional fields" do
|
|
376
|
+
let(:request) { {} }
|
|
377
|
+
|
|
378
|
+
before do
|
|
379
|
+
captured_request = request
|
|
380
|
+
Excon.stub(method: :post, path: "/1/groups/#{group}/add_user.json") do |params|
|
|
381
|
+
captured_request.replace(params)
|
|
382
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
383
|
+
end
|
|
384
|
+
groups.add_user(group: group, user: 'u' * 30, **optional_fields)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
it 'distinguishes omitted fields from explicit blanks' do
|
|
388
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
389
|
+
end
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
[nil, ''].each do |invalid_user|
|
|
394
|
+
it 'requires a user key' do
|
|
395
|
+
expect { groups.add_user(group: group, user: invalid_user) }
|
|
396
|
+
.to raise_error ArgumentError, /user must be supplied/
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
['u' * 29, 'u' * 31, "#{'u' * 29}-", 123].each do |invalid_user|
|
|
401
|
+
it 'requires a 30-character alphanumeric user key' do
|
|
402
|
+
expect { groups.add_user(group: group, user: invalid_user) }
|
|
403
|
+
.to raise_error ArgumentError, /user must be 30 alphanumeric characters/
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
['d' * 26, 'invalid device', 'phone,watch', 123].each do |invalid_device|
|
|
408
|
+
it 'requires a blank or valid single device name' do
|
|
409
|
+
expect { groups.add_user(group: group, user: 'u' * 30, device: invalid_device) }
|
|
410
|
+
.to raise_error ArgumentError, /device must be blank or 1 to 25/
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
it 'limits memos to 200 characters' do
|
|
415
|
+
expect { groups.add_user(group: group, user: 'u' * 30, memo: 'é' * 201) }
|
|
416
|
+
.to raise_error ArgumentError, /memo must be at most 200 characters/
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
it 'requires memos to be strings' do
|
|
420
|
+
expect { groups.add_user(group: group, user: 'u' * 30, memo: 123) }
|
|
421
|
+
.to raise_error ArgumentError, /memo must be a String/
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
it 'validates the group key before posting' do
|
|
425
|
+
expect { groups.add_user(group: 'invalid', user: 'u' * 30) }
|
|
426
|
+
.to raise_error ArgumentError, /group must be 30 alphanumeric characters/
|
|
427
|
+
end
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
describe '#remove_user' do
|
|
431
|
+
it_behaves_like(
|
|
432
|
+
'a group mutation',
|
|
433
|
+
:remove_user,
|
|
434
|
+
'remove_user',
|
|
435
|
+
{ group: 'g' * 30, user: 'u' * 30 },
|
|
436
|
+
{ 'token' => 'app-token', 'user' => 'u' * 30 }
|
|
437
|
+
)
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
describe '#disable_user' do
|
|
441
|
+
it_behaves_like(
|
|
442
|
+
'a group mutation',
|
|
443
|
+
:disable_user,
|
|
444
|
+
'disable_user',
|
|
445
|
+
{ group: 'g' * 30, user: 'u' * 30, device: 'phone_1' },
|
|
446
|
+
{ 'token' => 'app-token', 'user' => 'u' * 30, 'device' => 'phone_1' }
|
|
447
|
+
)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
describe '#enable_user' do
|
|
451
|
+
it_behaves_like(
|
|
452
|
+
'a group mutation',
|
|
453
|
+
:enable_user,
|
|
454
|
+
'enable_user',
|
|
455
|
+
{ group: 'g' * 30, user: 'u' * 30, device: '' },
|
|
456
|
+
{ 'token' => 'app-token', 'user' => 'u' * 30, 'device' => '' }
|
|
457
|
+
)
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
describe '#rename' do
|
|
461
|
+
it_behaves_like(
|
|
462
|
+
'a group mutation',
|
|
463
|
+
:rename,
|
|
464
|
+
'rename',
|
|
465
|
+
{ group: 'g' * 30, name: 'Operations' },
|
|
466
|
+
{ 'token' => 'app-token', 'name' => 'Operations' }
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
[nil, ''].each do |invalid_name|
|
|
470
|
+
it 'requires a group name' do
|
|
471
|
+
expect { groups.rename(group: group, name: invalid_name) }
|
|
472
|
+
.to raise_error ArgumentError, /name must be supplied/
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
it 'requires the group name to be a string' do
|
|
477
|
+
expect { groups.rename(group: group, name: 123) }
|
|
478
|
+
.to raise_error ArgumentError, /name must be a String/
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
it 'validates the group key before posting' do
|
|
482
|
+
expect { groups.rename(group: 'invalid', name: 'Operations') }
|
|
483
|
+
.to raise_error ArgumentError, /group must be 30 alphanumeric characters/
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
end
|