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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +28 -0
  3. data/.gitignore +1 -0
  4. data/.rubocop.yml +11 -24
  5. data/.ruby-version +1 -0
  6. data/CONVENTIONS.md +19 -0
  7. data/Gemfile +1 -2
  8. data/Guardfile +3 -3
  9. data/README.md +236 -44
  10. data/Rakefile +9 -59
  11. data/exe/pushover +6 -5
  12. data/lib/pushover/client.rb +53 -0
  13. data/lib/pushover/glances.rb +86 -0
  14. data/lib/pushover/groups.rb +137 -0
  15. data/lib/pushover/licenses.rb +67 -0
  16. data/lib/pushover/limits.rb +18 -0
  17. data/lib/pushover/message.rb +29 -32
  18. data/lib/pushover/message_encryption.rb +48 -0
  19. data/lib/pushover/message_validator.rb +167 -0
  20. data/lib/pushover/messages.rb +20 -0
  21. data/lib/pushover/receipt.rb +23 -10
  22. data/lib/pushover/receipts.rb +60 -0
  23. data/lib/pushover/response.rb +8 -3
  24. data/lib/pushover/sounds.rb +18 -0
  25. data/lib/pushover/subscriptions.rb +48 -0
  26. data/lib/pushover/teams.rb +93 -0
  27. data/lib/pushover/users.rb +37 -0
  28. data/lib/pushover/version.rb +1 -1
  29. data/lib/pushover.rb +14 -1
  30. data/pushover.gemspec +9 -7
  31. data/spec/gem/specification_spec.rb +19 -0
  32. data/spec/pushover/client_spec.rb +149 -0
  33. data/spec/pushover/glances_spec.rb +206 -0
  34. data/spec/pushover/groups_spec.rb +486 -0
  35. data/spec/pushover/licenses_spec.rb +238 -0
  36. data/spec/pushover/limits_spec.rb +92 -0
  37. data/spec/pushover/message_spec.rb +146 -26
  38. data/spec/pushover/messages_spec.rb +199 -0
  39. data/spec/pushover/receipt_spec.rb +119 -11
  40. data/spec/pushover/receipts_spec.rb +232 -0
  41. data/spec/pushover/response_spec.rb +21 -16
  42. data/spec/pushover/sounds_spec.rb +92 -0
  43. data/spec/pushover/subscriptions_spec.rb +185 -0
  44. data/spec/pushover/teams_spec.rb +325 -0
  45. data/spec/pushover/users_spec.rb +115 -0
  46. data/spec/spec_helper.rb +2 -3
  47. metadata +54 -18
  48. 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