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,92 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Sounds do
|
|
4
|
+
subject(:sounds) { Pushover::Client.new(token: 'app-token').sounds }
|
|
5
|
+
|
|
6
|
+
let(:available_sounds) do
|
|
7
|
+
{
|
|
8
|
+
'pushover' => 'Pushover (default)',
|
|
9
|
+
'bike' => 'Bike',
|
|
10
|
+
'custom-alert' => 'Custom Alert'
|
|
11
|
+
}
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
after { Excon.stubs.clear }
|
|
15
|
+
|
|
16
|
+
describe '#get' do
|
|
17
|
+
context 'when Pushover returns available sounds' do
|
|
18
|
+
let(:request) { {} }
|
|
19
|
+
let(:response) { sounds.get }
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
captured_request = request
|
|
23
|
+
Excon.stub(
|
|
24
|
+
method: :get,
|
|
25
|
+
path: '/1/sounds.json',
|
|
26
|
+
query: { token: 'app-token' },
|
|
27
|
+
body: nil
|
|
28
|
+
) do |params|
|
|
29
|
+
captured_request.replace(params)
|
|
30
|
+
{
|
|
31
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id', 'sounds' => available_sounds),
|
|
32
|
+
headers: { 'X-Limit-App-Remaining' => '9' },
|
|
33
|
+
status: 200
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'gets sounds with the application token in the query string' do
|
|
39
|
+
response
|
|
40
|
+
|
|
41
|
+
expect(request.slice(:method, :path, :query)).to eq(
|
|
42
|
+
method: :get, path: '/1/sounds.json', query: 'token=app-token'
|
|
43
|
+
)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it 'does not send a request body' do
|
|
47
|
+
response
|
|
48
|
+
|
|
49
|
+
expect(request).not_to have_key(:body)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'returns response metadata' do
|
|
53
|
+
expect(response).to have_attributes(status: 1, request: 'request-id')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'returns built-in and custom sounds' do
|
|
57
|
+
expect(response.attributes).to eq('sounds' => available_sounds)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'preserves response headers' do
|
|
61
|
+
expect(response.headers).to include('X-Limit-App-Remaining' => '9')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
context 'when Pushover rejects the application token' do
|
|
66
|
+
before do
|
|
67
|
+
Excon.stub(
|
|
68
|
+
{ method: :get, path: '/1/sounds.json', query: { token: 'app-token' }, body: nil },
|
|
69
|
+
{
|
|
70
|
+
body: Oj.dump(
|
|
71
|
+
'status' => 0,
|
|
72
|
+
'request' => 'request-id',
|
|
73
|
+
'errors' => ['application token is invalid'],
|
|
74
|
+
'token' => 'invalid'
|
|
75
|
+
),
|
|
76
|
+
status: 400
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'returns errors from a 4xx response' do
|
|
82
|
+
response = sounds.get
|
|
83
|
+
|
|
84
|
+
expect(response).to have_attributes(status: 0, errors: ['application token is invalid'])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'preserves endpoint-specific error details' do
|
|
88
|
+
expect(sounds.get.attributes).to eq('token' => 'invalid')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Subscriptions do
|
|
4
|
+
subject(:subscriptions) { Pushover::Client.new(token: 'app-token').subscriptions }
|
|
5
|
+
|
|
6
|
+
let(:subscription) { 'Forum-f504h08fhlasdfj' }
|
|
7
|
+
let(:user) { 'u' * 30 }
|
|
8
|
+
|
|
9
|
+
after { Excon.stubs.clear }
|
|
10
|
+
|
|
11
|
+
it 'does not expose import as a public alias' do
|
|
12
|
+
expect(subscriptions).not_to respond_to(:import)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe '#migrate' do
|
|
16
|
+
context 'with all documented fields' do
|
|
17
|
+
let(:request) { {} }
|
|
18
|
+
let(:response) do
|
|
19
|
+
subscriptions.migrate(
|
|
20
|
+
subscription: subscription,
|
|
21
|
+
user: user,
|
|
22
|
+
device_name: 'phone-1',
|
|
23
|
+
sound: 'custom-sound'
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
let(:expected_body) do
|
|
27
|
+
{
|
|
28
|
+
'token' => 'app-token',
|
|
29
|
+
'subscription' => subscription,
|
|
30
|
+
'user' => user,
|
|
31
|
+
'device_name' => 'phone-1',
|
|
32
|
+
'sound' => 'custom-sound'
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
before do
|
|
37
|
+
captured_request = request
|
|
38
|
+
Excon.stub(method: :post, path: '/1/subscriptions/migrate.json') do |params|
|
|
39
|
+
captured_request.replace(params)
|
|
40
|
+
{
|
|
41
|
+
body: Oj.dump(
|
|
42
|
+
'status' => 1,
|
|
43
|
+
'request' => 'request-id',
|
|
44
|
+
'subscribed_user_key' => 's' * 30
|
|
45
|
+
),
|
|
46
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
47
|
+
status: 200
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'posts to the subscription migration endpoint' do
|
|
53
|
+
response
|
|
54
|
+
|
|
55
|
+
expect(request.slice(:method, :path)).to eq(
|
|
56
|
+
method: :post, path: '/1/subscriptions/migrate.json'
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'serializes the application token and migration fields with string keys' do
|
|
61
|
+
response
|
|
62
|
+
|
|
63
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'uses the JSON content type' do
|
|
67
|
+
response
|
|
68
|
+
|
|
69
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
it 'returns response metadata and the subscribed user key' do
|
|
73
|
+
expect(response).to have_attributes(
|
|
74
|
+
status: 1,
|
|
75
|
+
request: 'request-id',
|
|
76
|
+
attributes: { 'subscribed_user_key' => 's' * 30 }
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'preserves response headers' do
|
|
81
|
+
expect(response.headers).to include('X-Request-Id' => 'header-id')
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
context 'without optional fields' do
|
|
86
|
+
let(:request) { {} }
|
|
87
|
+
|
|
88
|
+
before do
|
|
89
|
+
captured_request = request
|
|
90
|
+
Excon.stub(method: :post, path: '/1/subscriptions/migrate.json') do |params|
|
|
91
|
+
captured_request.replace(params)
|
|
92
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
subscriptions.migrate(subscription: subscription, user: user)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it 'omits device_name and sound from the JSON body' do
|
|
99
|
+
expect(Oj.strict_load(request[:body])).to eq(
|
|
100
|
+
'token' => 'app-token', 'subscription' => subscription, 'user' => user
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
context 'with a blank sound' do
|
|
106
|
+
let(:request) { {} }
|
|
107
|
+
|
|
108
|
+
before do
|
|
109
|
+
captured_request = request
|
|
110
|
+
Excon.stub(method: :post, path: '/1/subscriptions/migrate.json') do |params|
|
|
111
|
+
captured_request.replace(params)
|
|
112
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
subscriptions.migrate(subscription: subscription, user: user, sound: '')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
it 'preserves the documented default-sound selection' do
|
|
119
|
+
expect(Oj.strict_load(request[:body])).to include('sound' => '')
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
context 'when Pushover rejects the migration' do
|
|
124
|
+
before do
|
|
125
|
+
Excon.stub(
|
|
126
|
+
{ method: :post, path: '/1/subscriptions/migrate.json' },
|
|
127
|
+
{
|
|
128
|
+
body: Oj.dump(
|
|
129
|
+
'status' => 0,
|
|
130
|
+
'request' => 'request-id',
|
|
131
|
+
'errors' => ['subscription is invalid'],
|
|
132
|
+
'user' => 'invalid'
|
|
133
|
+
),
|
|
134
|
+
status: 400
|
|
135
|
+
}
|
|
136
|
+
)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it 'returns API errors and preserves endpoint-specific details' do
|
|
140
|
+
expect(subscriptions.migrate(subscription: subscription, user: user)).to have_attributes(
|
|
141
|
+
status: 0, request: 'request-id', errors: ['subscription is invalid'], attributes: { 'user' => 'invalid' }
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
[nil, ''].each do |invalid_subscription|
|
|
147
|
+
it 'requires a subscription code' do
|
|
148
|
+
expect { subscriptions.migrate(subscription: invalid_subscription, user: user) }
|
|
149
|
+
.to raise_error ArgumentError, /subscription must be supplied/
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
it 'requires the subscription code to be a string' do
|
|
154
|
+
expect { subscriptions.migrate(subscription: 123, user: user) }
|
|
155
|
+
.to raise_error ArgumentError, /subscription must be a String/
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
[nil, ''].each do |invalid_user|
|
|
159
|
+
it 'requires a user key' do
|
|
160
|
+
expect { subscriptions.migrate(subscription: subscription, user: invalid_user) }
|
|
161
|
+
.to raise_error ArgumentError, /user must be supplied/
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
['u' * 29, 'u' * 31, "#{'u' * 29}-", 123].each do |invalid_user|
|
|
166
|
+
it 'requires a 30-character alphanumeric user key' do
|
|
167
|
+
expect { subscriptions.migrate(subscription: subscription, user: invalid_user) }
|
|
168
|
+
.to raise_error ArgumentError, /user must be 30 alphanumeric characters/
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
['', 'd' * 26, 'invalid device', 'phone,watch', 123].each do |invalid_device|
|
|
173
|
+
it 'validates an optional device name' do
|
|
174
|
+
expect do
|
|
175
|
+
subscriptions.migrate(subscription: subscription, user: user, device_name: invalid_device)
|
|
176
|
+
end.to raise_error ArgumentError, /device_name must be 1 to 25/
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
it 'requires an optional sound to be a string' do
|
|
181
|
+
expect { subscriptions.migrate(subscription: subscription, user: user, sound: 123) }
|
|
182
|
+
.to raise_error ArgumentError, /sound must be a String/
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe Pushover::Teams do
|
|
4
|
+
subject(:teams) { Pushover::Client.new(token: 'team-token').teams }
|
|
5
|
+
|
|
6
|
+
after { Excon.stubs.clear }
|
|
7
|
+
|
|
8
|
+
describe '#get' do
|
|
9
|
+
context 'when Pushover returns team information' do
|
|
10
|
+
let(:request) { {} }
|
|
11
|
+
let(:response) { teams.get }
|
|
12
|
+
let(:team_attributes) do
|
|
13
|
+
{
|
|
14
|
+
'name' => 'Operations',
|
|
15
|
+
'users' => [
|
|
16
|
+
{
|
|
17
|
+
'id' => 'member-id',
|
|
18
|
+
'name' => 'Team Member',
|
|
19
|
+
'email' => 'member@example.test',
|
|
20
|
+
'administrator' => true,
|
|
21
|
+
'devices' => [
|
|
22
|
+
{
|
|
23
|
+
'name' => 'iphone',
|
|
24
|
+
'os' => 'iOS',
|
|
25
|
+
'os_version' => '26',
|
|
26
|
+
'enabled' => true,
|
|
27
|
+
'administratively_disabled' => false
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
before do
|
|
36
|
+
captured_request = request
|
|
37
|
+
Excon.stub(
|
|
38
|
+
method: :get,
|
|
39
|
+
path: '/1/teams.json',
|
|
40
|
+
query: { token: 'team-token' },
|
|
41
|
+
body: nil
|
|
42
|
+
) do |params|
|
|
43
|
+
captured_request.replace(params)
|
|
44
|
+
{
|
|
45
|
+
body: Oj.dump({ 'status' => 1, 'request' => 'request-id' }.merge(team_attributes)),
|
|
46
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
47
|
+
status: 200
|
|
48
|
+
}
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'gets the team with the team token in the query string' do
|
|
53
|
+
response
|
|
54
|
+
|
|
55
|
+
expect(request.slice(:method, :path, :query)).to eq(
|
|
56
|
+
method: :get, path: '/1/teams.json', query: 'token=team-token'
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'does not send a request body' do
|
|
61
|
+
response
|
|
62
|
+
|
|
63
|
+
expect(request).not_to have_key(:body)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'preserves the team, users, and nested devices' do
|
|
67
|
+
expect(response).to have_attributes(
|
|
68
|
+
status: 1, request: 'request-id', headers: include('X-Request-Id' => 'header-id'), attributes: team_attributes
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
context 'when Pushover rejects the team token' do
|
|
74
|
+
before do
|
|
75
|
+
Excon.stub(
|
|
76
|
+
{ method: :get, path: '/1/teams.json', query: { token: 'team-token' }, body: nil },
|
|
77
|
+
{
|
|
78
|
+
body: Oj.dump('status' => 0, 'request' => 'request-id', 'errors' => ['team token is invalid']),
|
|
79
|
+
status: 400
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
it 'returns errors from the 400 response' do
|
|
85
|
+
expect(teams.get).to have_attributes(
|
|
86
|
+
status: 0, request: 'request-id', errors: ['team token is invalid']
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
describe '#add_user' do
|
|
93
|
+
context 'with all documented fields' do
|
|
94
|
+
let(:request) { {} }
|
|
95
|
+
let(:expected_body) do
|
|
96
|
+
{
|
|
97
|
+
'token' => 'team-token',
|
|
98
|
+
'email' => 'member@example.test',
|
|
99
|
+
'name' => 'Team Member',
|
|
100
|
+
'password' => 'temporary password',
|
|
101
|
+
'instant' => 'true',
|
|
102
|
+
'admin' => 'true',
|
|
103
|
+
'group' => 'On-call'
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
let(:response) do
|
|
107
|
+
teams.add_user(
|
|
108
|
+
email: 'member@example.test',
|
|
109
|
+
name: 'Team Member',
|
|
110
|
+
password: 'temporary password',
|
|
111
|
+
instant: true,
|
|
112
|
+
admin: true,
|
|
113
|
+
group: 'On-call'
|
|
114
|
+
)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
before do
|
|
118
|
+
captured_request = request
|
|
119
|
+
Excon.stub(method: :post, path: '/1/teams/add_user.json') do |params|
|
|
120
|
+
captured_request.replace(params)
|
|
121
|
+
{
|
|
122
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id'),
|
|
123
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
124
|
+
status: 200
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
it 'posts to the add-user endpoint' do
|
|
130
|
+
response
|
|
131
|
+
|
|
132
|
+
expect(request.slice(:method, :path)).to eq(
|
|
133
|
+
method: :post, path: '/1/teams/add_user.json'
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'serializes string-key JSON and literal true flag values' do
|
|
138
|
+
response
|
|
139
|
+
|
|
140
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'uses the JSON content type' do
|
|
144
|
+
response
|
|
145
|
+
|
|
146
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'returns response metadata and headers' do
|
|
150
|
+
expect(response).to have_attributes(
|
|
151
|
+
status: 1, request: 'request-id', attributes: {}, headers: include('X-Request-Id' => 'header-id')
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
context 'with only an e-mail address' do
|
|
157
|
+
let(:request) { {} }
|
|
158
|
+
|
|
159
|
+
before do
|
|
160
|
+
captured_request = request
|
|
161
|
+
Excon.stub(method: :post, path: '/1/teams/add_user.json') do |params|
|
|
162
|
+
captured_request.replace(params)
|
|
163
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
teams.add_user(email: 'member@example.test')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'omits optional fields' do
|
|
170
|
+
expect(Oj.strict_load(request[:body])).to eq(
|
|
171
|
+
'token' => 'team-token', 'email' => 'member@example.test'
|
|
172
|
+
)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
context 'with blank text fields and disabled flags' do
|
|
177
|
+
let(:request) { {} }
|
|
178
|
+
let(:expected_body) do
|
|
179
|
+
{
|
|
180
|
+
'token' => 'team-token',
|
|
181
|
+
'email' => 'member@example.test',
|
|
182
|
+
'name' => '',
|
|
183
|
+
'password' => '',
|
|
184
|
+
'group' => ''
|
|
185
|
+
}
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
before do
|
|
189
|
+
captured_request = request
|
|
190
|
+
Excon.stub(method: :post, path: '/1/teams/add_user.json') do |params|
|
|
191
|
+
captured_request.replace(params)
|
|
192
|
+
{ body: Oj.dump('status' => 1, 'request' => 'request-id'), status: 200 }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
teams.add_user(
|
|
196
|
+
email: 'member@example.test', name: '', password: '', instant: false, admin: false, group: ''
|
|
197
|
+
)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
it 'preserves blank strings and omits false flags' do
|
|
201
|
+
expect(Oj.strict_load(request[:body])).to eq(expected_body)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
context 'when Pushover rejects the request' do
|
|
206
|
+
before do
|
|
207
|
+
Excon.stub(
|
|
208
|
+
{ method: :post, path: '/1/teams/add_user.json' },
|
|
209
|
+
{
|
|
210
|
+
body: Oj.dump('status' => 0, 'request' => 'request-id', 'errors' => ['user could not be added']),
|
|
211
|
+
status: 400
|
|
212
|
+
}
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it 'returns errors from the 400 response' do
|
|
217
|
+
expect(teams.add_user(email: 'member@example.test')).to have_attributes(
|
|
218
|
+
status: 0, request: 'request-id', errors: ['user could not be added']
|
|
219
|
+
)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
[nil, '', ' ', 123].each do |invalid_email|
|
|
224
|
+
it 'requires a nonblank e-mail string' do
|
|
225
|
+
expect { teams.add_user(email: invalid_email) }
|
|
226
|
+
.to raise_error ArgumentError, /email must be a nonblank String/
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
%i[name password group].each do |field|
|
|
231
|
+
it "requires #{field} to be a string when supplied" do
|
|
232
|
+
expect { teams.add_user(email: 'member@example.test', field => 123) }
|
|
233
|
+
.to raise_error ArgumentError, /#{field} must be a String/
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
%i[instant admin].each do |field|
|
|
238
|
+
it "requires #{field} to be boolean when supplied" do
|
|
239
|
+
expect { teams.add_user(email: 'member@example.test', field => 'true') }
|
|
240
|
+
.to raise_error ArgumentError, /#{field} must be true or false/
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
it 'rejects unsupported fields' do
|
|
245
|
+
expect { teams.add_user(email: 'member@example.test', role: 'owner') }
|
|
246
|
+
.to raise_error ArgumentError, /unsupported team user parameter: role/
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
shared_examples 'a team e-mail mutation' do |operation, path|
|
|
251
|
+
let(:request) { {} }
|
|
252
|
+
let(:response) { teams.public_send(operation, email: 'member@example.test') }
|
|
253
|
+
let(:api_response) do
|
|
254
|
+
{
|
|
255
|
+
body: Oj.dump('status' => 1, 'request' => 'request-id'),
|
|
256
|
+
headers: { 'X-Request-Id' => 'header-id' },
|
|
257
|
+
status: 200
|
|
258
|
+
}
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
before do
|
|
262
|
+
captured_request = request
|
|
263
|
+
Excon.stub(method: :post, path: path) do |params|
|
|
264
|
+
captured_request.replace(params)
|
|
265
|
+
api_response
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'posts to the documented endpoint' do
|
|
270
|
+
response
|
|
271
|
+
|
|
272
|
+
expect(request.slice(:method, :path)).to eq(method: :post, path: path)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
it 'serializes the team token and e-mail with string keys' do
|
|
276
|
+
response
|
|
277
|
+
|
|
278
|
+
expect(Oj.strict_load(request[:body])).to eq(
|
|
279
|
+
'token' => 'team-token', 'email' => 'member@example.test'
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
it 'uses the JSON content type' do
|
|
284
|
+
response
|
|
285
|
+
|
|
286
|
+
expect(request[:headers]).to include('Content-Type' => 'application/json')
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
it 'returns response metadata and headers' do
|
|
290
|
+
expect(response).to have_attributes(
|
|
291
|
+
status: 1, request: 'request-id', attributes: {}, headers: include('X-Request-Id' => 'header-id')
|
|
292
|
+
)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
context 'when Pushover rejects the request' do
|
|
296
|
+
let(:api_response) do
|
|
297
|
+
{
|
|
298
|
+
body: Oj.dump('status' => 0, 'request' => 'request-id', 'errors' => ['team operation failed']),
|
|
299
|
+
status: 400
|
|
300
|
+
}
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
it 'returns errors from the 400 response' do
|
|
304
|
+
expect(response).to have_attributes(
|
|
305
|
+
status: 0, request: 'request-id', errors: ['team operation failed']
|
|
306
|
+
)
|
|
307
|
+
end
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
[nil, '', ' ', 123].each do |invalid_email|
|
|
311
|
+
it 'requires a nonblank e-mail string' do
|
|
312
|
+
expect { teams.public_send(operation, email: invalid_email) }
|
|
313
|
+
.to raise_error ArgumentError, /email must be a nonblank String/
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
describe '#revoke_invitation' do
|
|
319
|
+
it_behaves_like 'a team e-mail mutation', :revoke_invitation, '/1/teams/revoke_invitation.json'
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
describe '#remove_user' do
|
|
323
|
+
it_behaves_like 'a team e-mail mutation', :remove_user, '/1/teams/remove_user.json'
|
|
324
|
+
end
|
|
325
|
+
end
|