yodlicious 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a1c24f5be0b2cd579d65a6e4729d7023716a82e
4
- data.tar.gz: d540dbae32b123cdff4a6a77a222a2fedf8a8c7e
3
+ metadata.gz: bc007491e579889f0318b9f292d786eced0cf8bb
4
+ data.tar.gz: 8349f9cace5145555c1c12a61eca28fbc708e2c0
5
5
  SHA512:
6
- metadata.gz: 0b003b8a7e0a7b6cfa4611ea324665a8108da6f6bd4b1ba35394b5ba768b229701a438fc40791b07b555af357b163f37ff4e858eaa6844c272c9ef638e74374a
7
- data.tar.gz: 0c7b8cb612bb4c4a50dced5363fba4f8c7e94ea5f74d320584ab96746ecf0df2c6d34cecb0747ec96880b5ea2156a05501a3f55e5bf9bd385d67ba9ddad9490e
6
+ metadata.gz: 6baf54a65da2268c40a0d3543f2c2fb5ffcca19753a7e58e1ee328a93999e563e0a465b770b221b6f333148e52411fdf2efba2aaade036601d153785262b1966
7
+ data.tar.gz: 6c5dcc17e8df67652690bcc51d30c2870edc0310846e773fba2adf8bef8af998ce7665ccf08ba18eda0be690a2b0732a536077e82172364aae9e11f40299dc57
data/.gitignore CHANGED
@@ -12,3 +12,4 @@
12
12
  *.o
13
13
  *.a
14
14
  mkmf.log
15
+ .env
data/Gemfile CHANGED
@@ -7,4 +7,5 @@ gem 'rspec-its'
7
7
  gem 'faraday', '0.9.1'
8
8
  gem 'faraday_middleware', '~>0.9.1'
9
9
  gem 'socksify', '~>1.6.0'
10
+ gem 'dotenv'
10
11
 
data/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # Yodlicious 0.0.1_alpha [ ![Codeship Status for liftforward/yodlicious](https://codeship.com/projects/71603f00-9393-0132-dcd0-1a9a253548c0/status?branch=master)](https://codeship.com/projects/62288)
1
+ ![image of yodlicious](https://github.com/liftforward/yodlicious/blob/master/yodlicious.png)
2
2
 
3
- Yodlicisous is a ruby gem wrapping the Yodlee REST(ish) API. We had to build this for our integration with Yodlee which was somewhat more painfull than it should have been so we figured we share to be a good neighbor.
3
+ # Yodlicious 0.0.1 [ ![Codeship Status for liftforward/yodlicious](https://codeship.com/projects/71603f00-9393-0132-dcd0-1a9a253548c0/status?branch=master)](https://codeship.com/projects/62288)
4
+
5
+ Yodlicisous is a ruby gem wrapping the Yodlee REST(ish) API. We had to build this for our integration with Yodlee which was somewhat more painful than it should have been so we figured we share to be a good neighbor.
4
6
 
5
7
  ## Installation
6
8
 
@@ -37,6 +39,7 @@ yodlee_api = Yodlicious::YodleeApi.new(config)
37
39
 
38
40
  ```
39
41
  When in a Rails app it can be more convenient to use a global default configuration. To use global defaults:
42
+
40
43
  ```ruby
41
44
  #/<myproject>/config/initializers/yodlicious.rb
42
45
  require 'yodlicious'
@@ -50,12 +53,14 @@ Yodlicious::Config.cobranded_password = ENV['YODLEE_COBRANDED_PASSWORD']
50
53
  Yodlicious::Config.logger = Rails.logger
51
54
  ```
52
55
  and wherever you want to use the api simply create a new one and it will pickup the global defaults.
56
+
53
57
  ```ruby
54
58
  yodlee_api = Yodlicious::YodleeApi.new
55
59
  ```
56
60
  If for any reason you need to, you can pass a hash into the constructor and it will use any provided hash values over the defaults. Note this is done on each value not the entire hash.
57
61
 
58
62
  You can also update an existing instances of the YodleeApi's configuration with the configure method. For example:
63
+
59
64
  ```ruby
60
65
 
61
66
  yodlee_api = Yodlicious::YodleeApi.new { base_url: 'http://yodlee.com/blablabla' }
@@ -65,6 +70,7 @@ yodlee_api.configure { base_url: 'https://secure.yodlee.com/blablabla }
65
70
  puts yodlee_api.base_url
66
71
  ```
67
72
  will output
73
+
68
74
  ```
69
75
  https://secure.yodlee.com/blablabla
70
76
  ```
@@ -86,7 +92,7 @@ yodlee_api = Yodlicious::YodleeApi.new(config)
86
92
 
87
93
  ## Working with the API
88
94
 
89
- The Yodlee Api responses are somewhat veried (especially the errors) and as such we build Yodlicious as a pretty thin layer around their request/response model. We didn't attempt to map all their JSON responses into models or anything fancy like that. Instead we simply created a method for each API endpoint which takes the required parameters and return a response object. That said, Response object does provide some conveniences to make up for the inconsisten deliver of errors from Yodlee's APIs.
95
+ The Yodlee Api responses are somewhat varied (especially the errors) and as such we build Yodlicious as a pretty thin layer around their request/response model. We didn't attempt to map all their JSON responses into models or anything fancy like that. Instead we simply created a method for each API endpoint which takes the required parameters and return a response object. That said, Response object does provide some conveniences to make up for the inconsistent delivery of errors from Yodlee's APIs.
90
96
 
91
97
  ### Starting your cobranded session
92
98
 
@@ -99,16 +105,51 @@ pry(main)> response.success?
99
105
  => true
100
106
  ```
101
107
  As you probably suspect the call to cobranded_login wraps the ```/authenticate/coblogin``` endpoint call. If this is a success the YodleeApi instance will cache the cobranded session id yodlee returned and use it on all subsequent api calls. You can also access this value if desired with YodleeApi#cobranded_session_token.
108
+
102
109
  ```
103
110
  pry(main)> yodlee_api.cobranded_session_token
104
111
  => "12162013_1:a0b1ac3e32a2e656f8f5bd21de23ae1721ffd9dab8bee9f29811f5959bbf102f16c98354eba252bb030dc96e267bd2489a40562f18e09ee8ba9038d19280cc43"
105
112
  ```
106
113
  At this point something has probably gone wrong for you and you want to see the response json from ```/authenticate/coblogin```. To do this simply use ```response#body```.
114
+
107
115
  ```
108
116
  pry(main)> response.body
109
117
  => {"Error"=>[{"errorDetail"=>"Invalid Cobrand Credentials"}]}
110
118
  ```
111
119
 
120
+ ### Starting a user session
121
+
122
+ Once the cobranded session is active a number of API endpoints will work however most of the interesting ones require you to register or login under a user account. It is within these accounts that you can add the user's bank accounts and whatnot to aggregate their financial data. There are 3 methods offered to allow you to #register_user, #login_user, or do either #login_or_register_user. After executing any of these the user session will be started and the user's session token will be cached in the YodleeApi instance and used on subsequent calls to api endpoints. As with all api calls if the call was not successful you'll need to look at the body of the response to determine what went wrong.
123
+
124
+ ### Registering a new user
125
+
126
+ ```
127
+ pry(main)> response = yodlee_api.register_user 'my-username', 'my-password123', 'my-email@my-domain.com'
128
+ pry(main)> yodlee_api.user_session_token
129
+ => "12162013_1:69761d51a4010e6382ccb49b854513dbccad0f835a873d37884b68826acefaa5b8d41b634f4cc83d97d86e7df861f70860a4e4d8a3f08d5b5440eae504af5f19"
130
+ ```
131
+
132
+ ### Login existing user
133
+
134
+ ```
135
+ pry(main)> response = yodlee_api.user_login 'my-username', 'my-password123'
136
+ pry(main)> yodlee_api.user_session_token
137
+ => "12162013_1:69761d51a4010e6382ccb49b854513dbccad0f835a873d37884b68826acefaa5b8d41b634f4cc83d97d86e7df861f70860a4e4d8a3f08d5b5440eae504af5f19"
138
+ ```
139
+
140
+ ### Convenience for doing both
141
+
142
+ In case you have a situation where you don't know if the user is already registered there is #login_or_register_user
143
+
144
+ ```
145
+ pry(main)> response = yodlee_api.login_or_register_user 'my-username', 'my-password123', 'my-email@my-domain.com'
146
+ pry(main)> yodlee_api.user_session_token
147
+ => "12162013_1:69761d51a4010e6382ccb49b854513dbccad0f835a873d37884b68826acefaa5b8d41b634f4cc83d97d86e7df861f70860a4e4d8a3f08d5b5440eae504af5f19"
148
+ ```
149
+ ### other API methods
150
+
151
+ TODO
152
+
112
153
  ## Contributing
113
154
 
114
155
  1. Fork it ( https://github.com/liftforward/yodlicious/fork )
@@ -120,6 +161,7 @@ pry(main)> response.body
120
161
  ### Running the integration suite
121
162
 
122
163
  To run the Yodlicious integration tests you'll need an approved yodlee account. This is more than the one offered here [https://devnow.yodlee.com/user/register]. (Some of the integration suite will work against the devnow APIs but not all. On my todo list is to separate them out to make testing easier.) The integration suite expects these values to be set in the following environment variables:
164
+
123
165
  ```
124
166
  YODLEE_BASE_URL="https://consolidatedsdk.yodlee.com/yodsoap/srest/my-cobranded-path/v1.0"
125
167
  YODLEE_COBRANDED_USERNAME="my-cobranded-user"
data/lib/yodlicious.rb CHANGED
@@ -8,7 +8,7 @@ require File.dirname(__FILE__) + "/yodlicious/version"
8
8
  require File.dirname(__FILE__) + "/yodlicious/config"
9
9
  require File.dirname(__FILE__) + "/yodlicious/parameter_translator"
10
10
  require File.dirname(__FILE__) + "/yodlicious/response"
11
- require File.dirname(__FILE__) + "/yodlicious/yodlicious"
11
+ require File.dirname(__FILE__) + "/yodlicious/yodlee_api"
12
12
 
13
13
  class Faraday::Adapter::NetHttp
14
14
  def net_http_connection(env)
@@ -17,7 +17,7 @@ module Yodlicious
17
17
  params["credentialFields[#{i}].valueIdentifier"] = field['valueIdentifier']
18
18
  params["credentialFields[#{i}].valueMask"] = field['valueMask']
19
19
  params["credentialFields[#{i}].isEditable"] = field['isEditable']
20
- params["credentialFields[#{i}].value"] = field['value']
20
+ params["credentialFields[#{i}].value"] = field['fieldValue']
21
21
 
22
22
  i += 1
23
23
  }
@@ -1,3 +1,3 @@
1
1
  module Yodlicious
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
@@ -121,7 +121,12 @@ module Yodlicious
121
121
  end
122
122
 
123
123
  def unregister_user
124
- user_session_execute_api '/jsonsdk/UserRegistration/unregister'
124
+ response = user_session_execute_api '/jsonsdk/UserRegistration/unregister'
125
+ if response.success?
126
+ @user_auth = nil
127
+ end
128
+
129
+ response
125
130
  end
126
131
 
127
132
  def site_search search_string
@@ -141,34 +146,101 @@ module Yodlicious
141
146
  end
142
147
 
143
148
 
144
- def add_site_account_and_wait site_id, site_login_form, refresh_interval = 0.5, refresh_trys = 5
145
- response = add_site_account(site_id, site_login_form)
149
+ def add_site_account_and_wait site_id, site_login_form, refresh_interval = 0.5, max_trys = 5
150
+ add_site_account_response = add_site_account(site_id, site_login_form)
146
151
 
147
- #TODO validate response with assert
148
- if response.success?
149
-
150
- if response.body['siteRefreshInfo']['siteRefreshStatus']['siteRefreshStatus'] == 'REFRESH_TRIGGERED' &&
151
- response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode'] == 'NORMAL'
152
-
153
- site_account_id = response.body['siteAccountId']
154
- trys = 1
152
+ if add_site_account_response.success?
153
+ if normal_site_refresh_in_progress?(add_site_account_response.body['siteRefreshInfo'])
154
+ site_account_id = add_site_account_response.body['siteAccountId']
155
+ try = 1
155
156
  begin
156
- info_log "try #{trys} to get refresh_info for #{site_id}"
157
- trys += 1
157
+ debug_log "try #{try} to get refresh_info for #{site_id}"
158
+ try += 1
158
159
  sleep(refresh_interval)
159
160
  refresh_info_response = get_site_refresh_info site_account_id
160
- response.body['siteRefreshInfo'] = refresh_info_response.body unless refresh_info_response.fail?
161
- end until (refresh_info_response.success? && refresh_info_response.body['siteRefreshStatus']['siteRefreshStatus'] != 'REFRESH_TRIGGERED') || trys > refresh_trys
161
+ add_site_account_response.body['siteRefreshInfo'] = refresh_info_response.body unless refresh_info_response.fail?
162
+ end while should_retry_get_site_refresh_info? refresh_info_response, try, max_trys
162
163
  end
163
164
 
164
- response
165
+ add_site_account_response
166
+ end
167
+ end
168
+
169
+ def should_retry_get_site_refresh_info? response, try, max_trys
170
+ return response.success? && try < max_trys && (response.body['code'] == 801 || (response.body['code'] == 0 &&
171
+ response.body['siteRefreshStatus']['siteRefreshStatus'] != 'REFRESH_COMPLETED' &&
172
+ response.body['siteRefreshStatus']['siteRefreshStatus'] != 'REFRESH_TIMED_OUT' &&
173
+ response.body['siteRefreshStatus']['siteRefreshStatus'] != 'LOGIN_SUCCESS' ))
174
+ end
175
+
176
+ def normal_site_refresh_in_progress? site_refresh_info
177
+ return site_refresh_info['siteRefreshMode']['refreshMode'] == 'NORMAL' &&
178
+ ['REFRESH_TRIGGERED','PARTIAL_COMPLETE'].include?(site_refresh_info['siteRefreshStatus']['siteRefreshStatus'])
179
+ end
180
+
181
+ def get_mfa_response_for_site site_account_id
182
+ user_session_execute_api '/jsonsdk/Refresh/getMFAResponseForSite', { memSiteAccId: site_account_id }
183
+ end
184
+
185
+ def get_mfa_response_for_site_and_wait site_account_id, refresh_interval=0.5, max_trys=5
186
+ response = get_mfa_response_for_site site_account_id
187
+
188
+ try = 1
189
+ while should_retry_get_mfa_response? response, try, max_trys
190
+ debug_log "try #{try} to get mfa message for #{site_account_id}"
191
+ try += 1
192
+ sleep(refresh_interval)
193
+ response = get_mfa_response_for_site site_account_id
165
194
  end
195
+
196
+ response
197
+ end
198
+
199
+ def should_retry_get_mfa_response? response, try, max_trys
200
+ return response.success? && response.body['errorCode'].nil? && response.body['isMessageAvailable'] != true && try < max_trys
201
+ end
202
+
203
+ def put_mfa_request_for_site site_account_id, mfa_type, field_info
204
+ params = {
205
+ 'memSiteAccId' => site_account_id,
206
+ 'userResponse.objectInstanceType' => "com.yodlee.core.mfarefresh.#{mfa_type}"
207
+ }
208
+
209
+ case mfa_type.to_sym
210
+ when :MFATokenResponse
211
+ params['userResponse.token']=field_info['fieldValue']
212
+ when :MFAImageResponse
213
+ params['userResponse.imageString']=field_info['fieldValue']
214
+ when :MFAQuesAnsResponse
215
+ questionsArray = field_info['questionAndAnswerValues']
216
+ i = 0
217
+ while i < questionsArray.length do
218
+ puts "questionsArray= #{questionsArray[i].class} #{i}"
219
+ params["userResponse.quesAnsDetailArray[#{i}].answer"]=questionsArray[i]['fieldValue']
220
+ params["userResponse.quesAnsDetailArray[#{i}].answerFieldType"]=questionsArray[i]['answerFieldType']
221
+ params["userResponse.quesAnsDetailArray[#{i}].metaData"]=questionsArray[i]['metaData']
222
+ params["userResponse.quesAnsDetailArray[#{i}].question"]=questionsArray[i]['question']
223
+ params["userResponse.quesAnsDetailArray[#{i}].questionFieldType"]=questionsArray[i]['questionFieldType']
224
+ i += 1
225
+ end
226
+ end
227
+
228
+ user_session_execute_api '/jsonsdk/Refresh/putMFARequestForSite', params
166
229
  end
167
230
 
168
231
  def get_site_refresh_info site_account_id
169
232
  user_session_execute_api '/jsonsdk/Refresh/getSiteRefreshInfo', { memSiteAccId: site_account_id }
170
233
  end
171
234
 
235
+ def get_content_service_info_by_routing_number routing_number, no_trim = true
236
+ params = {
237
+ routingNumber: routing_number,
238
+ notrim: no_trim
239
+ }
240
+
241
+ cobranded_session_execute_api '/jsonsdk/RoutingNumberService/getContentServiceInfoByRoutingNumber', params
242
+ end
243
+
172
244
  def get_item_summaries
173
245
  user_session_execute_api '/jsonsdk/DataService/getItemSummaries', { 'bridgetAppId' => '10003200' }
174
246
  end
@@ -233,7 +305,7 @@ module Yodlicious
233
305
  connection = Faraday.new(url: base_url, ssl: ssl_opts, request: { proxy: proxy_opts })
234
306
 
235
307
  response = connection.post("#{base_url}#{uri}", params)
236
- debug_log "response=#{response.status} body=#{response.body} headers=#{response.headers}"
308
+ debug_log "response=#{response.status} success?=#{response.success?} body=#{response.body} "
237
309
 
238
310
  case response.status
239
311
  when 200
@@ -265,7 +337,7 @@ module Yodlicious
265
337
  end
266
338
 
267
339
  def debug_log msg
268
- logger.info msg
340
+ logger.debug msg
269
341
  end
270
342
 
271
343
  def info_log msg
@@ -97,6 +97,31 @@ describe 'the yodlee api client integration tests', integration: true do
97
97
  end
98
98
  end
99
99
 
100
+ describe '#unregister_user' do
101
+ context 'Given a valid cobranded credentials and base_url' do
102
+ context 'Given a user who it logged into the api' do
103
+ context 'When #unregister_user is called the response' do
104
+ subject {
105
+ api.cobranded_login
106
+ api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
107
+ expect(api.user_session_token).not_to be_nil
108
+ api.unregister_user
109
+ }
110
+
111
+
112
+ it 'is expected to offer a valid response' do
113
+ is_expected.to be_kind_of(Yodlicious::Response)
114
+ is_expected.to be_success
115
+ expect(api.user_session_token).to be_nil
116
+ end
117
+
118
+ after { api.unregister_user }
119
+
120
+ end
121
+ end
122
+ end
123
+ end
124
+
100
125
  describe 'the yodlicious login_or_register_user method' do
101
126
  before { api.cobranded_login }
102
127
 
@@ -131,7 +156,7 @@ describe 'the yodlee api client integration tests', integration: true do
131
156
  end
132
157
  end
133
158
 
134
- describe 'the yodlee apis site info endpoint' do
159
+ describe '#get_site_info' do
135
160
  context 'Given a valid cobranded credentials and base_url' do
136
161
  before {
137
162
  api.cobranded_login
@@ -152,8 +177,40 @@ describe 'the yodlee api client integration tests', integration: true do
152
177
  end
153
178
  end
154
179
 
180
+
181
+ describe '#get_content_service_info_by_routing_number' do
182
+ context 'Given a valid cobranded credentials and base_url' do
183
+ before { api.cobranded_login }
184
+
185
+ context 'When #get_content_service_info_by_routing_number is called with a valid routing number the result' do
186
+ subject { api.get_content_service_info_by_routing_number 999988181 }
187
+
188
+ it 'is expected to contain valid content services info' do
189
+ is_expected.not_to be_nil
190
+ is_expected.to be_kind_of(Yodlicious::Response)
191
+ is_expected.to be_success
192
+ expect(subject.body['errorOccurred']).to be_nil
193
+ expect(subject.body['siteId']).to eq(16441)
194
+ expect(subject.body['contentServiceDisplayName']).to eq('Dag Site (US) - Bank')
195
+ end
196
+ end
197
+
198
+ context 'When #get_content_service_info_by_routing_number is called with an invalid routing number' do
199
+ subject { api.get_content_service_info_by_routing_number -23423 }
200
+
201
+ it 'is expected to contain valid error details' do
202
+ is_expected.not_to be_nil
203
+ is_expected.to be_kind_of(Yodlicious::Response)
204
+ is_expected.to be_fail
205
+ expect(subject.body['errorOccurred']).to be_truthy
206
+ expect(subject.body['exceptionType']).to eq('com.yodlee.core.routingnumberservice.InvalidRoutingNumberException')
207
+ end
208
+ end
209
+ end
210
+ end
211
+
155
212
  #todo reorganize this spec to use given-when-then
156
- describe 'Yodilicious add_site_account_and_wait method' do
213
+ describe '#add_site_account_and_wait' do
157
214
  context 'Given a user who has registered and does not have any accounts' do
158
215
  before {
159
216
  api.cobranded_login
@@ -164,7 +221,7 @@ describe 'the yodlee api client integration tests', integration: true do
164
221
  }
165
222
 
166
223
  let(:seconds_between_retry) { 3 }
167
- let(:retry_count) { 10 }
224
+ let(:max_retrys) { 10 }
168
225
 
169
226
  after {
170
227
  begin
@@ -175,10 +232,10 @@ describe 'the yodlee api client integration tests', integration: true do
175
232
 
176
233
  context 'When a invalid username and password for an account is added' do
177
234
  before {
178
- dag_login_form['componentList'][0]['value'] = 'invalid_username'
179
- dag_login_form['componentList'][1]['value'] = 'invalid_password'
235
+ dag_login_form['componentList'][0]['fieldValue'] = 'invalid_username'
236
+ dag_login_form['componentList'][1]['fieldValue'] = 'invalid_password'
180
237
  }
181
- subject { api.add_site_account_and_wait(16441, dag_login_form, seconds_between_retry, retry_count) }
238
+ subject { api.add_site_account_and_wait(16441, dag_login_form, seconds_between_retry, max_retrys) }
182
239
 
183
240
  it 'is expected to respond with siteRefreshStatus=LOGIN_FAILURE and refreshMode=NORMAL a siteAccountId' do
184
241
  # puts JSON.pretty_generate(subject.body)
@@ -191,10 +248,10 @@ describe 'the yodlee api client integration tests', integration: true do
191
248
 
192
249
  context 'When a valid username and password for an account is added' do
193
250
  before {
194
- dag_login_form['componentList'][0]['value'] = 'yodlicious.site16441.1'
195
- dag_login_form['componentList'][1]['value'] = 'site16441.1'
251
+ dag_login_form['componentList'][0]['fieldValue'] = 'yodlicious.site16441.1'
252
+ dag_login_form['componentList'][1]['fieldValue'] = 'site16441.1'
196
253
  }
197
- subject { api.add_site_account_and_wait(16441, dag_login_form, seconds_between_retry, retry_count) }
254
+ subject { api.add_site_account_and_wait(16441, dag_login_form, seconds_between_retry, max_retrys) }
198
255
 
199
256
  it 'is expected to respond with siteRefreshStatus=LOGIN_SUCCESS and refreshMode=NORMAL a siteAccountId' do
200
257
  is_expected.to be_success
@@ -207,6 +264,239 @@ describe 'the yodlee api client integration tests', integration: true do
207
264
  end
208
265
  end
209
266
 
267
+ describe '#get_mfa_response_for_site' do
268
+ context 'Given a valid cobranded credentials and base_url' do
269
+ context 'Given a user who it logged into the api' do
270
+ context 'When #get_mfa_response_for_site is called the response' do
271
+ subject {
272
+ api.cobranded_login
273
+ response = api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
274
+
275
+ dag_fmfa_login_form['componentList'][0]['fieldValue'] = 'yodlicious1.site16445.1'
276
+ dag_fmfa_login_form['componentList'][1]['fieldValue'] = 'site16445.1'
277
+
278
+ response = api.add_site_account_and_wait(16445, dag_fmfa_login_form)
279
+ expect(response).to be_success
280
+ # puts "refreshMode= #{response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']}"
281
+ expect(response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']).to eq('MFA')
282
+ api.get_mfa_response_for_site response.body['siteAccountId']
283
+ }
284
+
285
+ it 'is expected be a valid response' do
286
+ is_expected.to be_kind_of(Yodlicious::Response)
287
+ is_expected.to be_success
288
+ expect(subject.body['isMessageAvailable']).not_to be_nil
289
+ end
290
+
291
+ after { api.unregister_user }
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ describe '#get_mfa_response_for_site_and_wait' do
298
+ context 'Given a valid cobranded credentials and base_url' do
299
+ context 'Given a user who it logged into the api' do
300
+ context 'When #get_mfa_response_for_site_and_wait is called the response' do
301
+ subject {
302
+ api.cobranded_login
303
+ response = api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
304
+
305
+ dag_fmfa_login_form['componentList'][0]['fieldValue'] = 'yodlicious1.site16445.1'
306
+ dag_fmfa_login_form['componentList'][1]['fieldValue'] = 'site16445.1'
307
+
308
+ response = api.add_site_account_and_wait(16445, dag_fmfa_login_form)
309
+ expect(response).to be_success
310
+
311
+ expect(response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']).to eq('MFA')
312
+ api.get_mfa_response_for_site_and_wait response.body['siteAccountId'], 1
313
+ }
314
+
315
+ it 'is expected be a valid response' do
316
+ is_expected.to be_kind_of(Yodlicious::Response)
317
+ is_expected.to be_success
318
+ expect(subject.body['isMessageAvailable']).to be_truthy
319
+ expect(subject.body['fieldInfo']).not_to be_nil
320
+ end
321
+
322
+ after { api.unregister_user }
323
+ end
324
+ end
325
+ end
326
+ end
327
+
328
+ describe '#put_mfa_request_for_site' do
329
+ context 'Given a valid cobranded credentials and base_url' do
330
+ context 'Given a user who is logged into the api' do
331
+ context 'Given a user attempting to add a site with Token Based MFA' do
332
+ context 'When #put_mfa_request_for_site is called the response' do
333
+ subject {
334
+ api.cobranded_login
335
+ response = api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
336
+
337
+ dag_fmfa_login_form['componentList'][0]['fieldValue'] = 'yodlicious1.site16445.1'
338
+ dag_fmfa_login_form['componentList'][1]['fieldValue'] = 'site16445.1'
339
+
340
+ response = api.add_site_account_and_wait(16445, dag_fmfa_login_form)
341
+ expect(response).to be_success
342
+
343
+ expect(response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']).to eq('MFA')
344
+ site_account_id = response.body['siteAccountId']
345
+ response = api.get_mfa_response_for_site_and_wait site_account_id, 2
346
+ #{
347
+ # "isMessageAvailable":true,
348
+ # "fieldInfo":{
349
+ # "responseFieldType":"text",
350
+ # "minimumLength":-1,
351
+ # "maximumLength":6,
352
+ # "displayString":"Security Key"
353
+ # },
354
+ # "timeOutTime":116420,
355
+ # "itemId":0,
356
+ # "memSiteAccId":10992295,
357
+ # "retry":false
358
+ #}
359
+ expect(response.body['isMessageAvailable']).to be_truthy
360
+
361
+ field_info = response.body['fieldInfo']
362
+ field_info['fieldValue'] = "monkeys"
363
+ api.put_mfa_request_for_site site_account_id, :MFATokenResponse, field_info
364
+ }
365
+
366
+ it 'is expected be a valid response' do
367
+ is_expected.to be_kind_of(Yodlicious::Response)
368
+ is_expected.to be_success
369
+ expect(subject.body['primitiveObj']).to be_truthy
370
+ end
371
+
372
+ after { api.unregister_user }
373
+ end
374
+ end
375
+
376
+ context 'Given a user attempting to add a site with Security Question and Answer MFA' do
377
+ context 'When #put_mfa_request_for_site is called the response' do
378
+ subject {
379
+ api.cobranded_login
380
+ response = api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
381
+
382
+ dag_sqa_login_form['componentList'][0]['fieldValue'] = 'yodlicious1.site16486.1'
383
+ dag_sqa_login_form['componentList'][1]['fieldValue'] = 'site16486.1'
384
+
385
+ response = api.add_site_account(16486, dag_sqa_login_form)
386
+ expect(response).to be_success
387
+
388
+ expect(response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']).to eq('MFA')
389
+ site_account_id = response.body['siteAccountId']
390
+ response = api.get_mfa_response_for_site_and_wait site_account_id, 2
391
+ # {
392
+ # "isMessageAvailable":true,
393
+ # "fieldInfo":{
394
+ # "questionAndAnswerValues":[
395
+ # {
396
+ # "question":"What is the name of your state?",
397
+ # "questionFieldType":"label",
398
+ # "responseFieldType":"text",
399
+ # "isRequired":"true",
400
+ # "sequence":1,
401
+ # "metaData":"QUESTION_1"
402
+ # },
403
+ # {
404
+ # "question":"What is the name of your first school",
405
+ # "questionFieldType":"label",
406
+ # "responseFieldType":"text",
407
+ # "isRequired":"true",
408
+ # "sequence":2,
409
+ # "metaData":"QUESTION_2"
410
+ # }
411
+ # ],
412
+ # "numOfMandatoryQuestions":-1
413
+ # },
414
+ # "timeOutTime":96180,
415
+ # "itemId":0,
416
+ # "memSiteAccId":11025687,
417
+ # "retry":false
418
+ # }
419
+ expect(response.body['isMessageAvailable']).to be_truthy
420
+
421
+ field_info = response.body['fieldInfo']
422
+ field_info['questionAndAnswerValues'][0]['fieldValue'] = 'Texas'
423
+ field_info['questionAndAnswerValues'][1]['fieldValue'] = 'w3schools'
424
+ api.put_mfa_request_for_site site_account_id, :MFAQuesAnsResponse, field_info
425
+ }
426
+
427
+ it 'is expected be a valid response' do
428
+ is_expected.to be_kind_of(Yodlicious::Response)
429
+ is_expected.to be_success
430
+ expect(subject.body['primitiveObj']).to be_truthy
431
+ end
432
+
433
+ after { api.unregister_user }
434
+ end
435
+ end
436
+
437
+ context 'Given a user attempting to add a site with Captcha MFA' do
438
+ context 'When #put_mfa_request_for_site is called the response' do
439
+ subject {
440
+ api.cobranded_login
441
+ response = api.login_or_register_user "testuser#{rand(100...200)}", 'testpassword143', 'test@test.com'
442
+
443
+ dag_sqa_login_form['componentList'][0]['fieldValue'] = 'yodlicious1.site18769.1'
444
+ dag_sqa_login_form['componentList'][1]['fieldValue'] = 'site18769.1'
445
+
446
+ response = api.add_site_account(18769, dag_sqa_login_form)
447
+ expect(response).to be_success
448
+
449
+ expect(response.body['siteRefreshInfo']['siteRefreshMode']['refreshMode']).to eq('MFA')
450
+ site_account_id = response.body['siteAccountId']
451
+ response = api.get_mfa_response_for_site_and_wait site_account_id, 2
452
+ # {
453
+ # "isMessageAvailable":true,
454
+ # "fieldInfo":{
455
+ # "questionAndAnswerValues":[
456
+ # {
457
+ # "question":"What is the name of your state?",
458
+ # "questionFieldType":"label",
459
+ # "responseFieldType":"text",
460
+ # "isRequired":"true",
461
+ # "sequence":1,
462
+ # "metaData":"QUESTION_1"
463
+ # },
464
+ # {
465
+ # "question":"What is the name of your first school",
466
+ # "questionFieldType":"label",
467
+ # "responseFieldType":"text",
468
+ # "isRequired":"true",
469
+ # "sequence":2,
470
+ # "metaData":"QUESTION_2"
471
+ # }
472
+ # ],
473
+ # "numOfMandatoryQuestions":-1
474
+ # },
475
+ # "timeOutTime":96180,
476
+ # "itemId":0,
477
+ # "memSiteAccId":11025687,
478
+ # "retry":false
479
+ # }
480
+ expect(response.body['isMessageAvailable']).to be_truthy
481
+
482
+ field_info = response.body['fieldInfo']
483
+ field_info['fieldValue'] = "monkeys"
484
+ api.put_mfa_request_for_site site_account_id, :MFAImageResponse, field_info
485
+ }
486
+
487
+ it 'is expected be a valid response' do
488
+ is_expected.to be_kind_of(Yodlicious::Response)
489
+ is_expected.to be_success
490
+ expect(subject.body['primitiveObj']).to be_truthy
491
+ end
492
+
493
+ after { api.unregister_user }
494
+ end
495
+ end
496
+ end
497
+ end
498
+ end
499
+
210
500
  describe 'the yodlee apis fetching summary data about registered site accounts endpoints' do
211
501
  context 'Given a registered user with registered accounts' do
212
502
  before {
@@ -276,8 +566,8 @@ describe 'the yodlee api client integration tests', integration: true do
276
566
  before {
277
567
  api.cobranded_login
278
568
  api.login_or_register_user 'testuser_with_transactions@liftforward.com', 'testpassword143', 'testuser_with_transactions@liftforward.com'
279
- dag_login_form['componentList'][0]['value'] = 'yodlicious.site16441.1'
280
- dag_login_form['componentList'][1]['value'] = 'site16441.1'
569
+ dag_login_form['componentList'][0]['fieldValue'] = 'yodlicious.site16441.1'
570
+ dag_login_form['componentList'][1]['fieldValue'] = 'site16441.1'
281
571
  api.add_site_account_and_wait(16441, dag_login_form)
282
572
  }
283
573
 
@@ -466,4 +756,140 @@ describe 'the yodlee api client integration tests', integration: true do
466
756
  "defaultHelpText": "16103"
467
757
  }')
468
758
  }
759
+
760
+ let(:dag_fmfa_login_form) {
761
+ {
762
+ "conjunctionOp"=> {
763
+ "conjuctionOp"=>1
764
+ },
765
+ "componentList"=> [
766
+ {
767
+ "valueIdentifier"=>"LOGIN1",
768
+ "valueMask"=>"LOGIN_FIELD",
769
+ "fieldType"=>{"typeName"=>"IF_LOGIN"},
770
+ "size"=>20,
771
+ "maxlength"=>40,
772
+ "name"=>"LOGIN1",
773
+ "displayName"=>"Catalog",
774
+ "isEditable"=>true,
775
+ "isOptional"=>false,
776
+ "isEscaped"=>false,
777
+ "helpText"=>"150876",
778
+ "isOptionalMFA"=>false,
779
+ "isMFA"=>false
780
+ },
781
+ {
782
+ "valueIdentifier"=>"PASSWORD1",
783
+ "valueMask"=>"LOGIN_FIELD",
784
+ "fieldType"=>{"typeName"=>"IF_PASSWORD"},
785
+ "size"=>20,
786
+ "maxlength"=>40,
787
+ "name"=>"PASSWORD1",
788
+ "displayName"=>"Password",
789
+ "isEditable"=>true,
790
+ "isOptional"=>false,
791
+ "isEscaped"=>false,
792
+ "helpText"=>"150877",
793
+ "isOptionalMFA"=>false,
794
+ "isMFA"=>false
795
+ }
796
+ ],
797
+ "defaultHelpText"=>"16126"
798
+ }
799
+ }
800
+
801
+ let(:dag_sqa_login_form) {
802
+ JSON.parse('{
803
+ "conjunctionOp":{
804
+ "conjuctionOp":1
805
+ },
806
+ "componentList":[
807
+ {
808
+ "valueIdentifier":"LOGIN",
809
+ "valueMask":"LOGIN_FIELD",
810
+ "fieldType":{
811
+ "typeName":"IF_LOGIN"
812
+ },
813
+ "size":20,
814
+ "maxlength":40,
815
+ "name":"LOGIN",
816
+ "displayName":"Catalog",
817
+ "isEditable":true,
818
+ "isOptional":false,
819
+ "isEscaped":false,
820
+ "helpText":"150978",
821
+ "isOptionalMFA":false,
822
+ "isMFA":false
823
+ },
824
+ {
825
+ "valueIdentifier":"PASSWORD",
826
+ "valueMask":"LOGIN_FIELD",
827
+ "fieldType":{
828
+ "typeName":"IF_PASSWORD"
829
+ },
830
+ "size":20,
831
+ "maxlength":40,
832
+ "name":"PASSWORD",
833
+ "displayName":"Password",
834
+ "isEditable":true,
835
+ "isOptional":false,
836
+ "isEscaped":false,
837
+ "helpText":"150979",
838
+ "isOptionalMFA":false,
839
+ "isMFA":false
840
+ }
841
+ ],
842
+ "defaultHelpText":"16176"
843
+ }')
844
+ }
469
845
  end
846
+
847
+
848
+
849
+ # {"popularity"=>0,
850
+ # "siteId"=>16477,
851
+ # "orgId"=>1148,
852
+ # "defaultDisplayName"=>"DagSIteMFAAndNonMFA (US)",
853
+ # "defaultOrgDisplayName"=>"Demo Bank",
854
+ # "contentServiceInfos"=>
855
+ # [{"contentServiceId"=>20631, "siteId"=>16477, "containerInfo"=>{"containerName"=>"bank", "assetType"=>1}},
856
+ # {"contentServiceId"=>20632, "siteId"=>16477, "containerInfo"=>{"containerName"=>"miles", "assetType"=>0}}],
857
+ # "enabledContainers"=>[{"containerName"=>"bank", "assetType"=>1}, {"containerName"=>"miles", "assetType"=>0}],
858
+ # "baseUrl"=>"http://192.168.210.152:9090/dag/dhaction.do",
859
+ # "loginForms"=>
860
+ # [{"conjunctionOp"=>{"conjuctionOp"=>1},
861
+ # "componentList"=>
862
+ # [{"valueIdentifier"=>"LOGIN",
863
+ # "valueMask"=>"LOGIN_FIELD",
864
+ # "fieldType"=>{"typeName"=>"IF_LOGIN"},
865
+ # "size"=>20,
866
+ # "maxlength"=>40,
867
+ # "name"=>"LOGIN",
868
+ # "displayName"=>"Catalog",
869
+ # "isEditable"=>true,
870
+ # "isOptional"=>false,
871
+ # "isEscaped"=>false,
872
+ # "helpText"=>"150970",
873
+ # "isOptionalMFA"=>false,
874
+ # "isMFA"=>false},
875
+ # {"valueIdentifier"=>"PASSWORD",
876
+ # "valueMask"=>"LOGIN_FIELD",
877
+ # "fieldType"=>{"typeName"=>"IF_PASSWORD"},
878
+ # "size"=>20,
879
+ # "maxlength"=>40,
880
+ # "name"=>"PASSWORD",
881
+ # "displayName"=>"Password",
882
+ # "isEditable"=>true,
883
+ # "isOptional"=>false,
884
+ # "isEscaped"=>false,
885
+ # "helpText"=>"150971",
886
+ # "isOptionalMFA"=>false,
887
+ # "isMFA"=>false}],
888
+ # "defaultHelpText"=>"16167"}],
889
+ # "isHeld"=>false,
890
+ # "isCustom"=>false,
891
+ # "mfaType"=>{"typeId"=>4, "typeName"=>"SECURITY_QUESTION"},
892
+ # "mfaCoverage"=>"FMPA",
893
+ # "siteSearchVisibility"=>true,
894
+ # "isAlreadyAddedByUser"=>false,
895
+ # "isOauthEnabled"=>false}
data/spec/spec_helper.rb CHANGED
@@ -2,7 +2,8 @@ unless defined?(SPEC_HELPER_LOADED)
2
2
  SPEC_HELPER_LOADED = true
3
3
 
4
4
  require "yodlicious"
5
-
5
+ require 'dotenv'
6
+ Dotenv.load
6
7
 
7
8
  RSpec.configure do |config|
8
9
 
@@ -22,7 +22,7 @@ describe 'parameter translator' do
22
22
  'helpText' => 4710,
23
23
  'isOptionalMFA' => false,
24
24
  'isMFA' => false,
25
- 'value' => 'kanyewest'
25
+ 'fieldValue' => 'kanyewest'
26
26
  },
27
27
  {
28
28
  'valueIdentifier' => 'PASSWORD',
@@ -40,7 +40,7 @@ describe 'parameter translator' do
40
40
  'helpText' => 11976,
41
41
  'isOptionalMFA' => false,
42
42
  'isMFA' => false,
43
- 'value' => 'iLoveTheGrammys'
43
+ 'fieldValue' => 'iLoveTheGrammys'
44
44
  }
45
45
  ]
46
46
  }
@@ -3,7 +3,7 @@ require "yodlicious/config"
3
3
 
4
4
  describe Yodlicious::YodleeApi do
5
5
 
6
- context 'Given a new uninitialized YodleeApi objecvt' do
6
+ context 'Given a new uninitialized YodleeApi object' do
7
7
  before {
8
8
  Yodlicious::Config.base_url=nil
9
9
  Yodlicious::Config.cobranded_username=nil
@@ -234,4 +234,107 @@ describe Yodlicious::YodleeApi do
234
234
  end
235
235
  end
236
236
  end
237
+
238
+ describe '#should_retry_get_mfa_response?' do
239
+ let (:api) { Yodlicious::YodleeApi.new }
240
+ let (:response) { double("response") }
241
+
242
+ context 'Given get mfa response has failed' do
243
+ before { allow(response).to receive(:success?).and_return(false) }
244
+ subject { api.should_retry_get_mfa_response?(response,0,1) }
245
+ it { is_expected.to be_falsy }
246
+ end
247
+
248
+ context 'Given get mfa response is success' do
249
+ before { allow(response).to receive(:success?).and_return(true) }
250
+
251
+ context 'Given an error code is returned' do
252
+ before { allow(response).to receive(:body).and_return({ 'errorCode' => 100 }) }
253
+ subject { api.should_retry_get_mfa_response?(response,0,1) }
254
+ it { is_expected.to be_falsy }
255
+ end
256
+
257
+ context 'Given no error code is returned' do
258
+ context 'Given the MFA message is available' do
259
+ before { allow(response).to receive(:body).and_return({ 'isMessageAvailable' => true }) }
260
+ subject { api.should_retry_get_mfa_response?(response,0,1) }
261
+ it { is_expected.to be_falsy }
262
+ end
263
+
264
+ context 'Given the MFA message is not available' do
265
+ before { allow(response).to receive(:body).and_return({ 'isMessageAvailable' => false }) }
266
+ context 'Given all the trys have been used up' do
267
+ subject { api.should_retry_get_mfa_response?(response,1,1) }
268
+ it { is_expected.to be_falsy }
269
+ end
270
+
271
+ context 'Given the trys have not been used up' do
272
+ subject { api.should_retry_get_mfa_response?(response,0,2) }
273
+ it { is_expected.to be_truthy }
274
+ end
275
+ end
276
+ end
277
+ end
278
+ end
279
+
280
+ describe '#should_retry_get_site_refresh_info' do
281
+ let (:api) { Yodlicious::YodleeApi.new }
282
+ let (:response) { double("response") }
283
+
284
+ context 'Given get mfa response has failed' do
285
+ before { allow(response).to receive(:success?).and_return(false) }
286
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
287
+ it { is_expected.to be_falsy }
288
+ end
289
+
290
+ context 'Given get mfa response is success' do
291
+ before { allow(response).to receive(:success?).and_return(true) }
292
+
293
+ context 'Given an code 801 is returned' do
294
+ before { allow(response).to receive(:body).and_return({ 'code' => 801 }) }
295
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
296
+ it { is_expected.to be_truthy }
297
+ end
298
+
299
+ context 'Given not 801 and not 0 code is returned' do
300
+ before { allow(response).to receive(:body).and_return({ 'code' => 5 }) }
301
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
302
+ it { is_expected.to be_falsy }
303
+ end
304
+
305
+ context 'Given a code 0 is returned' do
306
+ context 'Given a siteRefreshStatus of REFRESH_COMPLETED' do
307
+ before { allow(response).to receive(:body).and_return({ 'code' => 0, "siteRefreshStatus" => { "siteRefreshStatus" => "REFRESH_COMPLETED" }}) }
308
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
309
+ it { is_expected.to be_falsy }
310
+ end
311
+
312
+ context 'Given a siteRefreshStatus of REFRESH_TIMED_OUT' do
313
+ before { allow(response).to receive(:body).and_return({ 'code' => 0, "siteRefreshStatus" => { "siteRefreshStatus" => "REFRESH_TIMED_OUT" }}) }
314
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
315
+ it { is_expected.to be_falsy }
316
+ end
317
+
318
+ context 'Given a siteRefreshStatus of LOGIN_SUCCESS' do
319
+ before { allow(response).to receive(:body).and_return({ 'code' => 0, "siteRefreshStatus" => { "siteRefreshStatus" => "LOGIN_SUCCESS" }}) }
320
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
321
+ it { is_expected.to be_falsy }
322
+ end
323
+
324
+ context 'Given a siteRefreshStatus of REFRESH_TRIGGERED' do
325
+ before { allow(response).to receive(:body).and_return({ 'code' => 0, "siteRefreshStatus" => { "siteRefreshStatus" => "REFRESH_TRIGGERED" }}) }
326
+ subject { api.should_retry_get_site_refresh_info?(response,0,1) }
327
+ it { is_expected.to be_truthy }
328
+ end
329
+
330
+ context 'Given a siteRefreshStatus of REFRESH_TRIGGERED' do
331
+ before { allow(response).to receive(:body).and_return({ 'code' => 0, "siteRefreshStatus" => { "siteRefreshStatus" => "REFRESH_TRIGGERED" }}) }
332
+ context 'Given trys have been used up' do
333
+ subject { api.should_retry_get_site_refresh_info?(response,1,1) }
334
+ it { is_expected.to be_falsy }
335
+ end
336
+ end
337
+ end
338
+ end
339
+ end
237
340
  end
data/yodlicious.png ADDED
Binary file
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yodlicious
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Drew Nichols
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-23 00:00:00.000000000 Z
11
+ date: 2015-03-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -110,14 +110,15 @@ files:
110
110
  - lib/yodlicious/parameter_translator.rb
111
111
  - lib/yodlicious/response.rb
112
112
  - lib/yodlicious/version.rb
113
- - lib/yodlicious/yodlicious.rb
113
+ - lib/yodlicious/yodlee_api.rb
114
114
  - spec/integration_spec.rb
115
115
  - spec/spec_helper.rb
116
116
  - spec/unit/config_spec.rb
117
117
  - spec/unit/parameter_translator_spec.rb
118
118
  - spec/unit/response_spec.rb
119
- - spec/unit/yodlicious_spec.rb
119
+ - spec/unit/yodlee_api_spec.rb
120
120
  - yodlicious.gemspec
121
+ - yodlicious.png
121
122
  homepage: https://github.com/liftforward/yodlicious
122
123
  licenses:
123
124
  - MIT
@@ -148,4 +149,4 @@ test_files:
148
149
  - spec/unit/config_spec.rb
149
150
  - spec/unit/parameter_translator_spec.rb
150
151
  - spec/unit/response_spec.rb
151
- - spec/unit/yodlicious_spec.rb
152
+ - spec/unit/yodlee_api_spec.rb