virtuous 0.0.1

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 (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