virtuous 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +26 -0
  3. data/.gitignore +5 -0
  4. data/.reek.yml +36 -0
  5. data/.rubocop.yml +87 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +18 -0
  9. data/Gemfile +17 -0
  10. data/LICENSE +21 -0
  11. data/README.md +54 -0
  12. data/Rakefile +24 -0
  13. data/lib/virtuous/client/contact.rb +220 -0
  14. data/lib/virtuous/client/contact_address.rb +78 -0
  15. data/lib/virtuous/client/gift.rb +394 -0
  16. data/lib/virtuous/client/gift_designation.rb +59 -0
  17. data/lib/virtuous/client/individual.rb +125 -0
  18. data/lib/virtuous/client/recurring_gift.rb +86 -0
  19. data/lib/virtuous/client.rb +272 -0
  20. data/lib/virtuous/error.rb +54 -0
  21. data/lib/virtuous/helpers/hash_helper.rb +28 -0
  22. data/lib/virtuous/helpers/string_helper.rb +31 -0
  23. data/lib/virtuous/parse_oj.rb +24 -0
  24. data/lib/virtuous/version.rb +5 -0
  25. data/lib/virtuous.rb +12 -0
  26. data/logo/virtuous.svg +1 -0
  27. data/spec/spec_helper.rb +25 -0
  28. data/spec/support/client_factory.rb +10 -0
  29. data/spec/support/fixtures/contact.json +112 -0
  30. data/spec/support/fixtures/contact_address.json +20 -0
  31. data/spec/support/fixtures/contact_addresses.json +42 -0
  32. data/spec/support/fixtures/contact_gifts.json +80 -0
  33. data/spec/support/fixtures/gift.json +55 -0
  34. data/spec/support/fixtures/gift_designation_query_options.json +2701 -0
  35. data/spec/support/fixtures/gift_designations.json +175 -0
  36. data/spec/support/fixtures/gifts.json +112 -0
  37. data/spec/support/fixtures/import.json +0 -0
  38. data/spec/support/fixtures/individual.json +46 -0
  39. data/spec/support/fixtures/recurring_gift.json +26 -0
  40. data/spec/support/fixtures_helper.rb +5 -0
  41. data/spec/support/virtuous_mock.rb +101 -0
  42. data/spec/virtuous/client_spec.rb +270 -0
  43. data/spec/virtuous/error_spec.rb +74 -0
  44. data/spec/virtuous/resources/contact_address_spec.rb +75 -0
  45. data/spec/virtuous/resources/contact_spec.rb +137 -0
  46. data/spec/virtuous/resources/gift_designation_spec.rb +70 -0
  47. data/spec/virtuous/resources/gift_spec.rb +249 -0
  48. data/spec/virtuous/resources/individual_spec.rb +95 -0
  49. data/spec/virtuous/resources/recurring_gift_spec.rb +67 -0
  50. data/spec/virtuous_spec.rb +7 -0
  51. data/virtuous.gemspec +25 -0
  52. metadata +121 -0
@@ -0,0 +1,270 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Virtuous::Client do
4
+ let(:attrs) { { api_key: 'test_api_key' } }
5
+ let(:attrs_without_authentication) { { api_key: nil } }
6
+
7
+ subject(:client) { described_class.new(**attrs) }
8
+
9
+ shared_examples 'param request' do |verb|
10
+ let(:endpoint) { "#{verb}_test" }
11
+ let(:url) { "https://api.virtuoussoftware.com/#{endpoint}" }
12
+
13
+ before do
14
+ stub_request(:any, /_test/).to_return(body: response_body.to_json)
15
+ end
16
+
17
+ it 'requests at `path` argument' do
18
+ client.public_send(verb, endpoint)
19
+
20
+ expect(WebMock).to have_requested(verb, url)
21
+ end
22
+
23
+ it 'passes request parameters' do
24
+ client.public_send(verb, endpoint, request_params)
25
+
26
+ expect(WebMock).to have_requested(verb, url).with(query: request_params)
27
+ end
28
+
29
+ it 'returns parsed response body' do
30
+ expect(client.public_send(verb, endpoint)).to eq(response_body)
31
+ end
32
+ end
33
+
34
+ shared_examples 'body request' do |verb|
35
+ let(:body) { { 'test' => 123 } }
36
+ let(:endpoint) { "#{verb}_test" }
37
+ let(:url) { "https://api.virtuoussoftware.com/#{endpoint}" }
38
+
39
+ let(:do_request) { client.public_send(verb, endpoint, body) }
40
+
41
+ before do
42
+ stub_request(:any, /_test/).to_return(body: body.to_json)
43
+ end
44
+
45
+ it 'requests at `path` argument' do
46
+ do_request
47
+
48
+ expect(WebMock).to have_requested(verb, url)
49
+ end
50
+
51
+ it 'passes request body' do
52
+ do_request
53
+
54
+ expect(WebMock)
55
+ .to have_requested(verb, url)
56
+ .with(body: body)
57
+ end
58
+
59
+ it 'returns parsed response body' do
60
+ expect(do_request).to eq(body)
61
+ end
62
+ end
63
+
64
+ shared_examples 'api key auth' do
65
+ let(:path) { '/api/Contact/1' }
66
+ let(:url) { "https://api.virtuoussoftware.com#{path}" }
67
+
68
+ it 'doesn\'t request an access token' do
69
+ client.get(path)
70
+
71
+ expect(WebMock).not_to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
72
+ end
73
+
74
+ it 'uses the key in requests' do
75
+ client.get(path)
76
+
77
+ expect(WebMock).to have_requested(:get, url)
78
+ .with(headers: { 'Authorization' => "Bearer #{api_key}" })
79
+ end
80
+ end
81
+
82
+ shared_examples 'valid access token auth' do
83
+ let(:path) { '/api/Contact/1' }
84
+ let(:url) { "https://api.virtuoussoftware.com#{path}" }
85
+
86
+ it 'doesn\'t request an access token' do
87
+ client.get(path)
88
+
89
+ expect(WebMock).not_to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
90
+ end
91
+
92
+ it 'uses the access token in requests' do
93
+ client.get(path)
94
+
95
+ expect(WebMock).to have_requested(:get, url)
96
+ .with(headers: { 'Authorization' => "Bearer #{access_token}" })
97
+ end
98
+ end
99
+
100
+ shared_examples 'expired access token auth' do
101
+ let(:path) { '/api/Contact/1' }
102
+ let(:url) { "https://api.virtuoussoftware.com#{path}" }
103
+
104
+ it 'doesn\'t request an access token before doing a request' do
105
+ client
106
+
107
+ expect(WebMock).not_to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
108
+ end
109
+
110
+ it 'requests a new access token before doing a request' do
111
+ client.get(path)
112
+
113
+ body = URI.encode_www_form({ grant_type: 'refresh_token', refresh_token: refresh_token })
114
+
115
+ expect(WebMock).to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
116
+ .with(body: body)
117
+
118
+ expect(client.refresh_token).to eq('new_refresh_token')
119
+ expect(client.access_token).to eq('new_access_token')
120
+ expect(client.expires_at).to be > Time.now
121
+ end
122
+
123
+ it 'doesn\'t request an access token a second time' do
124
+ client.get(path)
125
+
126
+ WebMock.reset_executed_requests!
127
+
128
+ client.get(path)
129
+
130
+ expect(WebMock).not_to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
131
+ end
132
+
133
+ it 'uses the new access token in requests' do
134
+ client.get(path)
135
+
136
+ expect(WebMock).to have_requested(:get, url)
137
+ .with(headers: { 'Authorization' => 'Bearer new_access_token' })
138
+ end
139
+ end
140
+
141
+ context 'with api key' do
142
+ let(:api_key) { 'test_api_key' }
143
+ let(:attrs) { { api_key: api_key } }
144
+
145
+ it_behaves_like 'api key auth'
146
+ end
147
+
148
+ context 'with access token' do
149
+ let(:access_token) { 'test_access_token' }
150
+ let(:attrs) { { access_token: access_token } }
151
+
152
+ it_behaves_like 'valid access token auth'
153
+ end
154
+
155
+ context 'with non expired access token' do
156
+ let(:access_token) { 'test_access_token' }
157
+ let(:refresh_token) { 'test_refresh_token' }
158
+ let(:attrs) do
159
+ { access_token: access_token, refresh_token: refresh_token, expires_at: Time.now + 1_295_999 }
160
+ end
161
+
162
+ it_behaves_like 'valid access token auth'
163
+ end
164
+
165
+ context 'with expired access token' do
166
+ let(:access_token) { 'test_access_token' }
167
+ let(:refresh_token) { 'test_refresh_token' }
168
+ let(:attrs) do
169
+ {
170
+ access_token: access_token, refresh_token: refresh_token, expires_at: Time.new(2023, 12, 11)
171
+ }
172
+ end
173
+
174
+ it_behaves_like 'expired access token auth'
175
+ end
176
+
177
+ context 'with no access token' do
178
+ let(:refresh_token) { 'test_refresh_token' }
179
+ let(:attrs) { { refresh_token: refresh_token } }
180
+
181
+ it_behaves_like 'expired access token auth'
182
+ end
183
+
184
+ context 'with password auth' do
185
+ let(:attrs) { {} }
186
+ let(:access_token) { 'new_access_token' }
187
+
188
+ before :each do
189
+ client.authenticate('user@email.com', 'password')
190
+ WebMock.reset_executed_requests!
191
+ end
192
+
193
+ it_behaves_like 'valid access token auth'
194
+ end
195
+
196
+ describe '#authenticate(email, password, otp = nil)' do
197
+ let(:email) { 'user@email.com' }
198
+ let(:password) { 'test_password' }
199
+ let(:attrs) { {} }
200
+
201
+ it 'sends a request to retrieve a token' do
202
+ client.authenticate(email, password)
203
+
204
+ body = URI.encode_www_form({ grant_type: 'password', username: email, password: password })
205
+
206
+ expect(WebMock).to have_requested(:post, 'https://api.virtuoussoftware.com/Token')
207
+ .with(body: body)
208
+ end
209
+
210
+ it 'sets the new token' do
211
+ client.authenticate(email, password)
212
+
213
+ expect(client.refresh_token).to eq('new_refresh_token')
214
+ expect(client.access_token).to eq('new_access_token')
215
+ expect(client.expires_at).to be > Time.now
216
+ end
217
+
218
+ it 'returns the new token values' do
219
+ response = client.authenticate(email, password)
220
+
221
+ expect(response[:refresh_token]).to eq(client.refresh_token)
222
+ expect(response[:access_token]).to eq(client.access_token)
223
+ expect(response[:expires_at]).to eq(client.expires_at)
224
+ end
225
+
226
+ context 'with otp' do
227
+ let(:email) { 'otp@user.com' }
228
+
229
+ it 'returns requires_otp if otp is not set' do
230
+ response = client.authenticate(email, password)
231
+
232
+ expect(response[:requires_otp]).to eq(true)
233
+ end
234
+
235
+ it 'sets the new token' do
236
+ client.authenticate(email, password, '111111')
237
+
238
+ expect(client.refresh_token).to eq('new_refresh_token')
239
+ expect(client.access_token).to eq('new_access_token')
240
+ expect(client.expires_at).to be > Time.now
241
+ end
242
+ end
243
+ end
244
+
245
+ describe '#get(path, options = {})' do
246
+ let(:request_params) { { 'test' => 123 } }
247
+ let(:response_body) { request_params }
248
+
249
+ include_examples 'param request', :get
250
+ end
251
+
252
+ describe '#delete(path, options = {})' do
253
+ let(:request_params) { { 'test' => 123 } }
254
+ let(:response_body) { {} }
255
+
256
+ include_examples 'param request', :delete
257
+ end
258
+
259
+ describe '#patch(path, options = {})' do
260
+ include_examples 'body request', :patch
261
+ end
262
+
263
+ describe '#post(path, options = {})' do
264
+ include_examples 'body request', :post
265
+ end
266
+
267
+ describe '#put(path, options = {})' do
268
+ include_examples 'body request', :put
269
+ end
270
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'rack/utils'
3
+
4
+ CODES = Rack::Utils::HTTP_STATUS_CODES.keys.freeze
5
+
6
+ RSpec.describe Virtuous::Error do
7
+ let(:client) do
8
+ Virtuous::Client.new(
9
+ base_url: 'http://some-virtuous-uri.com',
10
+ api_key: 'test_api_key'
11
+ )
12
+ end
13
+
14
+ def stub(code, body)
15
+ stub_request(:any, /virtuous/).to_return(status: code, body: body)
16
+ end
17
+
18
+ def expect_failure(code, body)
19
+ url = 'http://some-virtuous-uri.com/anything'
20
+
21
+ expect do
22
+ client.get('anything')
23
+ end.to raise_error(Virtuous::Error, /#{code}: #{url} #{body}/)
24
+ end
25
+
26
+ def expect_success
27
+ expect { client.get('anything') }.to_not raise_error
28
+ end
29
+
30
+ context 'informational responses' do
31
+ CODES.grep(100..199).each do |code|
32
+ it "does not raise error for #{code}" do
33
+ stub(code, '{}')
34
+ expect_success
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'success responses' do
40
+ CODES.grep(200..299).each do |code|
41
+ it "does not raise error for #{code}" do
42
+ stub(code, '{}')
43
+ expect_success
44
+ end
45
+ end
46
+ end
47
+
48
+ context 'redirection responses' do
49
+ CODES.grep(300..399).each do |code|
50
+ it "does not raise error for #{code}" do
51
+ stub(code, '')
52
+ expect_success
53
+ end
54
+ end
55
+ end
56
+
57
+ context 'client error responses' do
58
+ CODES.grep(400..499).each do |code|
59
+ it "raises exception for #{code}" do
60
+ stub(code, 'test client error')
61
+ expect_failure(code, 'test client error')
62
+ end
63
+ end
64
+ end
65
+
66
+ context 'server error responses' do
67
+ CODES.grep(500..599).each do |code|
68
+ it "raises exception for #{code}" do
69
+ stub(code, 'test client error')
70
+ expect_failure(code, 'test client error')
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Virtuous::Client::ContactAddress, type: :model do
4
+ include_context 'resource specs'
5
+
6
+ describe '#get_contact_addresses(contact_id)' do
7
+ subject(:resource) { client.get_contact_addresses(1) }
8
+
9
+ it 'returns a list of addresses' do
10
+ expect(subject).to be_a(Array)
11
+ end
12
+
13
+ it 'passes options' do
14
+ expect(client).to receive(:get)
15
+ .with('api/ContactAddress/ByContact/1')
16
+ .and_call_original
17
+ resource
18
+ end
19
+ end
20
+
21
+ describe '#create_contact_address(data)' do
22
+ subject(:resource) do
23
+ client.create_contact_address(
24
+ contact_id: 1, label: 'Home address', address1: '324 Frank Island',
25
+ address2: 'Apt. 366', city: 'Antonioborough', state: 'Massachusetts', postal: '27516',
26
+ country: 'USA'
27
+ )
28
+ end
29
+
30
+ it 'passes options' do
31
+ expect(client).to receive(:post)
32
+ .with(
33
+ 'api/ContactAddress',
34
+ {
35
+ 'contactId' => 1, 'label' => 'Home address', 'address1' => '324 Frank Island',
36
+ 'address2' => 'Apt. 366', 'city' => 'Antonioborough', 'state' => 'Massachusetts',
37
+ 'postal' => '27516', 'country' => 'USA'
38
+ }
39
+ )
40
+ .and_call_original
41
+ resource
42
+ end
43
+
44
+ it 'returns a hash' do
45
+ expect(resource).to be_a(Hash)
46
+ end
47
+ end
48
+
49
+ describe '#update_contact_address(id, data)' do
50
+ subject(:resource) do
51
+ client.update_contact_address(
52
+ 1, label: 'Home address', address1: '324 Frank Island', address2: 'Apt. 366',
53
+ city: 'Antonioborough', state: 'Massachusetts', postal: '27516', country: 'USA'
54
+ )
55
+ end
56
+
57
+ it 'passes options' do
58
+ expect(client).to receive(:put)
59
+ .with(
60
+ 'api/ContactAddress/1',
61
+ {
62
+ 'label' => 'Home address', 'address1' => '324 Frank Island', 'address2' => 'Apt. 366',
63
+ 'city' => 'Antonioborough', 'state' => 'Massachusetts', 'postal' => '27516',
64
+ 'country' => 'USA'
65
+ }
66
+ )
67
+ .and_call_original
68
+ resource
69
+ end
70
+
71
+ it 'returns a hash' do
72
+ expect(resource).to be_a(Hash)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,137 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Virtuous::Client::Contact, type: :model do
4
+ include_context 'resource specs'
5
+
6
+ describe '#find_contact_by_email(email)' do
7
+ it 'returns a hash' do
8
+ expect(client.find_contact_by_email('email@test.com')).to be_a(Hash)
9
+ end
10
+
11
+ it 'queries contacts' do
12
+ expect(client).to receive(:get).with('api/Contact/Find',
13
+ { email: 'email@test.com' }).and_call_original
14
+
15
+ resource = client.find_contact_by_email('email@test.com')
16
+
17
+ expect(resource[:id]).to eq(1)
18
+ end
19
+ end
20
+
21
+ describe '#get_contact(id)' do
22
+ it 'returns a hash' do
23
+ expect(client.get_contact(1)).to be_a(Hash)
24
+ end
25
+
26
+ it 'queries contacts' do
27
+ expect(client).to receive(:get).with('api/Contact/1').and_call_original
28
+
29
+ resource = client.get_contact(1)
30
+
31
+ expect(resource[:id]).to eq(1)
32
+ end
33
+ end
34
+
35
+ describe '#import_contact(data)' do
36
+ subject(:resource) do
37
+ client.import_contact(
38
+ reference_source: 'Test source',
39
+ reference_id: 123,
40
+ contact_type: 'Organization',
41
+ name: 'Test Org',
42
+ title: 'Mr',
43
+ first_name: 'Test',
44
+ last_name: 'Individual',
45
+ email: 'test_individual@email.com'
46
+ )
47
+ end
48
+
49
+ it 'passes options' do
50
+ expect(client).to receive(:post)
51
+ .with(
52
+ 'api/Contact/Transaction',
53
+ {
54
+ 'referenceSource' => 'Test source',
55
+ 'referenceId' => 123,
56
+ 'contactType' => 'Organization',
57
+ 'name' => 'Test Org',
58
+ 'title' => 'Mr',
59
+ 'firstName' => 'Test',
60
+ 'lastName' => 'Individual',
61
+ 'email' => 'test_individual@email.com'
62
+ }
63
+ )
64
+ .and_call_original
65
+ resource
66
+ end
67
+ end
68
+
69
+ describe '#create_contact(data)' do
70
+ subject(:resource) do
71
+ client.create_contact(
72
+ reference_source: 'Test source',
73
+ reference_id: 123,
74
+ contact_type: 'Organization',
75
+ name: 'Test Org',
76
+ contact_individuals: [{
77
+ first_name: 'Test',
78
+ last_name: 'Individual'
79
+ }]
80
+ )
81
+ end
82
+
83
+ it 'passes options' do
84
+ expect(client).to receive(:post)
85
+ .with(
86
+ 'api/Contact',
87
+ {
88
+ 'referenceSource' => 'Test source',
89
+ 'referenceId' => 123,
90
+ 'contactType' => 'Organization',
91
+ 'name' => 'Test Org',
92
+ 'contactIndividuals' => [{
93
+ 'firstName' => 'Test',
94
+ 'lastName' => 'Individual'
95
+ }]
96
+ }
97
+ )
98
+ .and_call_original
99
+ resource
100
+ end
101
+
102
+ it 'returns a hash' do
103
+ expect(resource).to be_a(Hash)
104
+ end
105
+ end
106
+
107
+ describe '#update_contact(id, data)' do
108
+ subject(:resource) do
109
+ client.update_contact(
110
+ 1,
111
+ reference_source: 'Test source',
112
+ reference_id: 123,
113
+ contact_type: 'Organization',
114
+ name: 'Test Org'
115
+ )
116
+ end
117
+
118
+ it 'passes options' do
119
+ expect(client).to receive(:put)
120
+ .with(
121
+ 'api/Contact/1',
122
+ {
123
+ 'referenceSource' => 'Test source',
124
+ 'referenceId' => 123,
125
+ 'contactType' => 'Organization',
126
+ 'name' => 'Test Org'
127
+ }
128
+ )
129
+ .and_call_original
130
+ resource
131
+ end
132
+
133
+ it 'returns a hash' do
134
+ expect(resource).to be_a(Hash)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Virtuous::Client::Individual, type: :model do
4
+ include_context 'resource specs'
5
+
6
+ describe '#gift_designation_query_options' do
7
+ it 'returns a hash' do
8
+ expect(client.gift_designation_query_options).to be_a(Hash)
9
+ end
10
+
11
+ it 'queries options' do
12
+ expect(client).to receive(:get).with('api/GiftDesignation/QueryOptions').and_call_original
13
+
14
+ response = client.gift_designation_query_options
15
+
16
+ expect(response).to have_key(:options)
17
+ end
18
+ end
19
+
20
+ describe '#query_gift_designations(**options)' do
21
+ it 'returns a list of designations' do
22
+ response = client.query_gift_designations
23
+ expect(response).to be_a(Hash)
24
+
25
+ expect(response).to have_key(:list)
26
+ expect(response).to have_key(:total)
27
+ end
28
+
29
+ it 'sends take as a query param' do
30
+ expect(client).to receive(:post).with(URI('api/GiftDesignation/Query?take=12'),
31
+ {}).and_call_original
32
+
33
+ client.query_gift_designations(take: 12)
34
+ end
35
+
36
+ it 'sends skip as a query param' do
37
+ expect(client).to receive(:post).with(URI('api/GiftDesignation/Query?skip=12'),
38
+ {}).and_call_original
39
+
40
+ client.query_gift_designations(skip: 12)
41
+ end
42
+
43
+ it 'sends sort_by in the body' do
44
+ expect(client).to receive(:post).with(URI('api/GiftDesignation/Query'), { 'sortBy' => 'Id' })
45
+ .and_call_original
46
+
47
+ client.query_gift_designations(sort_by: 'Id')
48
+ end
49
+
50
+ it 'sends descending in the body' do
51
+ expect(client).to receive(:post).with(
52
+ URI('api/GiftDesignation/Query'), { 'descending' => true }
53
+ ).and_call_original
54
+
55
+ client.query_gift_designations(descending: true)
56
+ end
57
+
58
+ it 'sends conditions in the body' do
59
+ expect(client).to receive(:post).with(
60
+ URI('api/GiftDesignation/Query'), { 'groups' => [{ 'conditions' => [{
61
+ 'parameter' => 'Gift Id', 'operator' => 'Is', 'value' => 102
62
+ }] }] }
63
+ ).and_call_original
64
+
65
+ client.query_gift_designations(conditions: [{
66
+ parameter: 'Gift Id', operator: 'Is', value: 102
67
+ }])
68
+ end
69
+ end
70
+ end