yodlee 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.
@@ -0,0 +1,6 @@
1
+ === 0.0.1 / 2009-01-30
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,13 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.rdoc
4
+ Rakefile
5
+ lib/yodlee.rb
6
+ lib/yodlee/account.rb
7
+ lib/yodlee/connection.rb
8
+ lib/yodlee/credentials.rb
9
+ lib/yodlee/exceptions.rb
10
+ lib/yodlee/monkeypatches.rb
11
+ lib/yodlee/version.rb
12
+ test/test_yodlee.rb
13
+ yodlee.gemspec
@@ -0,0 +1,110 @@
1
+ = yodlee
2
+
3
+ http://github.com/aasmith/yodlee
4
+
5
+ == DESCRIPTION:
6
+
7
+ Fetches accounts and their transaction details from the Yodlee
8
+ MoneyCenter (https://moneycenter.yodlee.com).
9
+
10
+ == NOTES:
11
+
12
+ * The strings returned by Yodlee::Account#{last_updated,next_update}
13
+ can be parsed with Chronic (http://chronic.rubyforge.org), if a
14
+ timestamp is needed.
15
+
16
+ * Raises exceptions when exceptional things happen. These are scenarios
17
+ where the connection probably needs to be re-instantiated with the
18
+ correct details after prompting the user or some external source for
19
+ more complete login or account details.
20
+
21
+ == BUGS / TODO:
22
+
23
+ * Does not handle lists containing bill pay accounts
24
+
25
+ * Does not handle cases where the session has timed out. To avoid this,
26
+ use the instantiated objects in short durations. Don't leave them
27
+ hanging around.
28
+
29
+ * Add support for investment holdings
30
+
31
+ * Update account transactions / polling
32
+
33
+ == SYNOPSIS:
34
+
35
+ require 'rubygems'
36
+ require 'yodlee'
37
+
38
+ # Create some credentials to login with.
39
+ cred = Yodlee::Credentials.new
40
+ cred.username = 'bob'
41
+ cred.password = 'weak'
42
+
43
+ # The word the remote system stores and shows back to you to prove
44
+ # they really are Yodlee.
45
+ cred.expectation = 'roflcopter'
46
+
47
+ # An array of questions and answers. Yodlee expects you to answer
48
+ # three of thirty defined in Yodlee::Credentials::QUESTIONS.
49
+ cred.answers = [[Yodlee::Credentials::QUESTIONS[1], "The Queen"], [...]]
50
+
51
+ # That's enough credentials. Create a connection.
52
+ conn = Yodlee::Connection.new(cred)
53
+
54
+ # Connect, and get an account list.
55
+ conn.accounts.each do |account|
56
+ puts account.institute_name, account.name, account.current_balance
57
+
58
+ # grab a handy list of transactions. Parseable as CSV.
59
+ puts account.simple_transactions
60
+
61
+ # Next line needs johnson.
62
+ # p account.transactions
63
+
64
+ # take a look in account.account_info for a hash of useful stuff.
65
+ # available keys vary by account type and institute.
66
+ end
67
+
68
+ # Should look something like this:
69
+
70
+ First Bank of Excess
71
+ Checking
72
+ $123.45
73
+ [...some csv...]
74
+ First Bank of Mattress
75
+ Savings
76
+ $1,234.56
77
+ [...more csv!...]
78
+
79
+ == REQUIREMENTS:
80
+
81
+ mechanize, nokogiri.
82
+
83
+ Optional dependency on johnson (http://github.com/jbarnette/johnson)
84
+
85
+ == INSTALL:
86
+
87
+ sudo gem install aasmith-yodlee --source http://gems.github.com
88
+
89
+ == LICENSE:
90
+
91
+ Copyright (c) 2009 Andrew A. Smith <andy@tinnedfruit.org>
92
+
93
+ Permission is hereby granted, free of charge, to any person obtaining
94
+ a copy of this software and associated documentation files (the
95
+ 'Software'), to deal in the Software without restriction, including
96
+ without limitation the rights to use, copy, modify, merge, publish,
97
+ distribute, sublicense, and/or sell copies of the Software, and to
98
+ permit persons to whom the Software is furnished to do so, subject to
99
+ the following conditions:
100
+
101
+ The above copyright notice and this permission notice shall be
102
+ included in all copies or substantial portions of the Software.
103
+
104
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
105
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
106
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
107
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
108
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
109
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
110
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/yodlee/version.rb'
4
+
5
+ HOE = Hoe.new('yodlee', Yodlee::VERSION) do |p|
6
+ p.developer 'Andrew A. Smith', 'andy@tinnedfruit.org'
7
+ p.readme_file = "README.rdoc"
8
+ p.extra_rdoc_files = [p.readme_file]
9
+ p.extra_deps = %w(mechanize nokogiri)
10
+ p.extra_dev_deps = %w(flexmock)
11
+ p.summary = "Fetches financial data from Yodlee MoneyCenter."
12
+ end
13
+
14
+ missing = (HOE.extra_deps + HOE.extra_dev_deps).
15
+ reject { |d| Gem.available? *d }
16
+
17
+ unless missing.empty?
18
+ puts "You may be missing gems. Try:\ngem install #{missing.join(' ')}"
19
+ end
20
+
21
+ namespace :gem do
22
+ desc 'Generate a gem spec'
23
+ task :spec do
24
+ File.open("#{HOE.name}.gemspec", 'w') do |f|
25
+ HOE.spec.version = "#{HOE.version}.#{Time.now.strftime("%Y%m%d%H%M%S")}"
26
+ f.write(HOE.spec.to_ruby)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,26 @@
1
+ require 'rubygems'
2
+ require 'mechanize'
3
+ require 'nokogiri'
4
+ require 'enumerator'
5
+ require 'time'
6
+
7
+ begin
8
+ require 'johnson'
9
+ rescue LoadError
10
+ end
11
+
12
+ require 'yodlee/account'
13
+ require 'yodlee/connection'
14
+ require 'yodlee/credentials'
15
+ require 'yodlee/exceptions'
16
+ require 'yodlee/monkeypatches'
17
+ require 'yodlee/version'
18
+
19
+ module Yodlee
20
+ class Transaction < Struct.new(
21
+ :account_name, :account_id,
22
+ :currency, :amount, :date,
23
+ :fit_id, :status, :description
24
+ )
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ module Yodlee
2
+ class Account
3
+ attr_accessor :id, :name, :institute_id, :institute_name, :account_info
4
+
5
+ def initialize(connection)
6
+ @connection = connection
7
+ @account_info, @transactions = nil
8
+ end
9
+
10
+ [:simple_transactions, :current_balance, :account_info, :last_updated, :next_update].each do |m|
11
+ define_method m do
12
+ @account_info = @connection.account_info(self) unless @account_info
13
+ m == :account_info ? @account_info : @account_info[m]
14
+ end
15
+ end
16
+
17
+ def transactions
18
+ return @transactions if @transactions
19
+ @transactions = @connection.transactions(self)
20
+ end
21
+
22
+ def to_s
23
+ "#{@institute_name} - #{@name}"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,294 @@
1
+ module Yodlee
2
+ class Connection
3
+ def initialize(credentials, logger = nil)
4
+ @credentials = credentials
5
+ @logger = logger
6
+
7
+ @connected = false
8
+ @accounts = nil
9
+
10
+ @agent = WWW::Mechanize.new
11
+ @agent.user_agent = 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.0.5) Gecko/2008120122 Firefox/3.0.5'
12
+
13
+ @accounts_page = nil
14
+ end
15
+
16
+ def accounts
17
+ return @accounts if @accounts
18
+
19
+ handle_connection!
20
+
21
+ doc = Nokogiri::HTML.parse(@accounts_page.body)
22
+
23
+ @accounts = doc.search(".acctbean a").map{|e|
24
+ acct = Account.new(self)
25
+
26
+ e['href'].scan(/(\w+Id)=(\d+)/).each do |k,v|
27
+ case k
28
+ when /itemAccountId/ then acct.id = v
29
+ when /itemId/ then acct.institute_id = v
30
+ end
31
+ end
32
+
33
+ acct.institute_name = e.at('strong').text
34
+ acct.name = e.children.last.text.sub(/^\s*-\s*/,'')
35
+ acct
36
+ }
37
+ end
38
+
39
+ def account_info(acct)
40
+ page = @accounts_page
41
+
42
+ link = page.links.detect{|lnk| lnk.href =~ /itemAccountId=#{acct.id}/ } or raise AccountNotFound, "Could not find account in list"
43
+ link.href << "&dateRangeId=-1"
44
+ page = link.click
45
+
46
+ doc = Nokogiri::HTML.parse(page.body)
47
+
48
+ last_upd, next_upd = doc.at(".accountlinks").text.scan(/Last updated (.*?)\s*\(next scheduled update (.*)\)/).flatten
49
+
50
+ # Regular accounts have a heading + div, investments have heading + table
51
+ regular_acct = doc.at("h2[contains('Account Overview')] + div")
52
+
53
+ account_info = regular_acct ? regular_account_info(doc) : investment_account_info(doc)
54
+ account_info[:next_update] = next_upd
55
+ account_info[:last_updated] = last_upd
56
+
57
+ csv_page = page.form_with(:name => 'rep').submit
58
+ account_info[:simple_transactions] = csv_page.response['content-type'] =~ /csv/ ? csv_page.body : []
59
+
60
+ account_info
61
+ end
62
+
63
+ def regular_account_info(doc)
64
+ info_block = doc.at("h2[contains('Account Overview')] + div")
65
+
66
+ Hash[*info_block.search("dt").
67
+ zip(info_block.search("dt+dd")).
68
+ map{|a,b|[a.text.gsub(/\W/,'').underscore.to_sym, b.text]}.
69
+ flatten
70
+ ]
71
+ end
72
+
73
+ def investment_account_info(doc)
74
+ account_info = {}
75
+ account_info[:holdings] = [] # TODO
76
+ account_info[:current_balance] =
77
+ doc.search("h2[contains('Account Overview')] + table tr[last()] td[last()]").text
78
+ account_info
79
+ end
80
+
81
+ # This method returns each transaction as an object, based on the underyling javascript
82
+ # structures used to build the transactions as displayed in the Yodlee UI. These objects
83
+ # are able to access more information than the CSV Yodlee provides, such as the finanical
84
+ # institute's transaction id, useful for tracking duplicates.
85
+ #
86
+ # Calling this method requires Johnson to be installed, otherwise an exception is raised.
87
+ def transactions(acct)
88
+ unless Object.const_defined? "Johnson"
89
+ raise "Johnson not found. Install the johnson gem, or use simple_transactions instead."
90
+ end
91
+
92
+ post_headers = {
93
+ "c0-scriptName"=>"TxnService",
94
+ "c0-methodName"=>"searchTransactions",
95
+ "c0-id"=>"#{rand(5000)}_#{Time.now.to_i}#{Time.now.usec / 1000}}",
96
+ "c0-e1"=>"number:10000004",
97
+ "c0-e2"=>"string:17CBE222A42161A3FF450E47CF4C1A00",
98
+ "c0-e3"=>"null:null",
99
+ "c0-e4"=>"number:1",
100
+ "c0-e5"=>"boolean:false",
101
+ "c0-e6"=>"string:#{acct.id}",
102
+ "c0-e7"=>"string:-1",
103
+ "c0-e8"=>"null:null",
104
+ "c0-e9"=>"string:-1",
105
+ "c0-e10"=>"null:null",
106
+ "c0-e11"=>"null:null",
107
+ "c0-e12"=>"null:null",
108
+ "c0-e13"=>"string:-1",
109
+ "c0-e14"=>"null:null",
110
+ "c0-e15"=>"number:-1",
111
+ "c0-e16"=>"number:-1",
112
+ "c0-e17"=>"boolean:false",
113
+ "c0-e18"=>"Boolean:false",
114
+ "c0-e19"=>"boolean:false",
115
+ "c0-e20"=>"Boolean:false",
116
+ "c0-e21"=>"string:",
117
+ "c0-e22"=>"string:",
118
+ "c0-e23"=>"Boolean:false",
119
+ "c0-e24"=>"Boolean:false",
120
+ "c0-e25"=>"boolean:false",
121
+ "c0-e26"=>"Number:0",
122
+ "c0-e27"=>"string:0",
123
+ "c0-e28"=>"null:null",
124
+ "c0-e29"=>"null:null",
125
+ "c0-e30"=>"string:allTransactions",
126
+ "c0-e31"=>"string:InProgressAndCleared",
127
+ "c0-e32"=>"number:999",
128
+ "c0-e33"=>"string:",
129
+ "c0-e34"=>"null:null",
130
+ "c0-e35"=>"null:null",
131
+ "c0-e36"=>"string:",
132
+ "c0-e37"=>"null:null",
133
+ "c0-e38"=>"string:ALL",
134
+ "c0-e39"=>"string:false",
135
+ "c0-e40"=>"string:0.0",
136
+ "c0-e41"=>"string:0.0",
137
+
138
+ "c0-param0"=>"Object:{
139
+ cobrandId:reference:c0-e1,
140
+ applicationId:reference:c0-e2,
141
+ csit:reference:c0-e3,
142
+ loggingLevel:reference:c0-e4,
143
+ loggingEnabled:reference:c0-e5}",
144
+
145
+ "c0-param1"=>"Object:{
146
+ itemAccountId:reference:c0-e6,
147
+ categoryId:reference:c0-e7,
148
+ categoryLevelId:reference:c0-e8,
149
+ dateRangeId:reference:c0-e9,
150
+ fromDate:reference:c0-e10,
151
+ toDate:reference:c0-e11,
152
+ groupBy:reference:c0-e12,
153
+ groupAccountId:reference:c0-e13,
154
+ filterTranasctions:reference:c0-e14,
155
+ transactionTypeId:reference:c0-e15,
156
+ transactionStatusId:reference:c0-e16,
157
+ ignorePendingTransactions:reference:c0-e17,
158
+ includeBusinessExpense:reference:c0-e18,
159
+ includeTransfer:reference:c0-e19,
160
+ includeReimbursableExpense:refrence:c0-e20,
161
+ fromDate1:reference:c0-e21,
162
+ toDate1:reference:c0-e22,
163
+ includeMedicalExpense:reference:c0-e23,
164
+ includeTaxDeductible:reference:c0-e24,
165
+ includePersonalExpense:reference:c0-e25,
166
+ transactionAmount:reference:c0-e26,
167
+ transactionAmountRange:reference:c0-e27,
168
+ billStatementRange:reference:c0-e28,
169
+ criteria:reference:c0-e29,
170
+ module:reference:c0-e30,
171
+ transactionType:reference:c0-e31,
172
+ pageSize:reference:c0-e32,
173
+ sharedMemId:reference:c0-e33,
174
+ overRideDateRangeId:reference:c0-e34,
175
+ overRideContainer:referencec0-e35,
176
+ searchString:reference:c0-e36,
177
+ pageId:reference:c0-e37,
178
+ splitTypeTransaction:reference:c0-e38,
179
+ isAvailableBalance:reference:c0-e39,
180
+ currentBalance:reference:c0-e40,
181
+ availableBalance:reference:c0-e41}",
182
+
183
+ "c0-param2"=>"boolean:false",
184
+
185
+ "callCount"=>"1",
186
+ "xml"=>"true",
187
+ }
188
+ page = @agent.post(
189
+ 'https://moneycenter.yodlee.com/moneycenter/dwr/exec/TxnService.searchTransactions.dwr',
190
+ post_headers
191
+ )
192
+
193
+ j = Johnson::Runtime.new
194
+
195
+ # Remove the last line (a call to DWREngine), and execute
196
+ j.evaluate page.body.strip.sub(/\n[^\n]+\Z/m, '')
197
+
198
+ if x = j['s0']
199
+ transactions = x.transactions.map do |e|
200
+ transaction = Yodlee::Transaction.new
201
+ transaction.account_name = e.accountName
202
+ transaction.currency = e.amount.cobCurrencyCode
203
+ transaction.amount = e.amount.cobPreciseAmount
204
+ transaction.description = e.description
205
+ transaction.account_id = e.itemAccountId
206
+ transaction.fit_id = e.transactionId
207
+ transaction.status = e['type']['type']
208
+
209
+ # Re-parse in order to get a real Time, not a Johnson::SpiderMonkey::RubyLandProxy.
210
+ transaction.date = Time.parse(e.date.to_s)
211
+ transaction
212
+ end
213
+
214
+ return transactions
215
+ end
216
+
217
+ return []
218
+ end
219
+
220
+ def handle_connection!
221
+ login unless connected?
222
+ end
223
+
224
+ def login
225
+ @connected = false
226
+ page = nil
227
+
228
+ %w(provide_username answer_question check_expectation provide_password).each do |m|
229
+ page = send(*[m, page].compact)
230
+ end
231
+
232
+ @connected = true
233
+ end
234
+
235
+ def connected?
236
+ @connected
237
+ end
238
+
239
+ def log(level, msg)
240
+ @logger.__send__(level, question) if @logger
241
+ end
242
+
243
+ # login scrapers
244
+
245
+ def provide_username
246
+ p = @agent.get 'https://moneycenter.yodlee.com/'
247
+ f = p.form_with(:name => 'loginForm')
248
+ f['loginName'] = @credentials.username
249
+ @agent.submit(f)
250
+ end
251
+
252
+ def answer_question(page)
253
+ question = Nokogiri::HTML.parse(page.body).at("label[@for=answer]").text
254
+ log(:debug, question)
255
+
256
+ begin
257
+ answer = @credentials.answers.detect{|q, a| question =~ /^#{Regexp.escape(q)}/}.last
258
+ rescue
259
+ raise NoAnswerForQuestion, "No answer found for #{question}"
260
+ end
261
+
262
+ f = page.form_with(:name => 'loginForm')
263
+ f['answer'] = answer
264
+
265
+ @agent.submit(f)
266
+ end
267
+
268
+ def check_expectation(page)
269
+ d = Nokogiri::HTML.parse(page.body)
270
+ node = d.at("dl > dt[contains('Secret Phrase')] + dd .caption")
271
+
272
+ if node
273
+ if @credentials.expectation == node.previous.text.strip
274
+ return page
275
+ else
276
+ raise ExpectationMismatch, "Expectation found, but was incorrect"
277
+ end
278
+ else
279
+ raise ExpectationNotFound, "Didn't find expectation"
280
+ end
281
+ end
282
+
283
+ def provide_password(page)
284
+ f = page.form_with(:name => 'loginForm')
285
+ f['password'] = @credentials.password
286
+ page = @agent.submit(f)
287
+
288
+ # ack javascript disabled
289
+ f = page.form_with(:name => 'updateForm')
290
+
291
+ @accounts_page = @agent.submit(f)
292
+ end
293
+ end
294
+ end
@@ -0,0 +1,41 @@
1
+ module Yodlee
2
+ class Credentials
3
+ QUESTIONS = <<-EOS.split(/\n\s*\n?/).map{|e|e.strip}
4
+ In what city was your high school? (full name of city only)
5
+ What is your maternal grandmother's first name?
6
+ What is your father's middle name?
7
+ What was the name of your High School?
8
+ What is the name of the first company you worked for?
9
+ What is the first name of the maid of honor at your wedding?
10
+ What is the first name of your oldest nephew?
11
+ What is your maternal grandfather's first name?
12
+ What is your best friend's first name?
13
+ In what city were you married? (Enter full name of city)
14
+
15
+ What is the first name of the best man at your wedding?
16
+ What was your high school mascot?
17
+ What was the first name of your first manager?
18
+ In what city was your father born? (Enter full name of city only)
19
+ What was the name of your first girlfriend/boyfriend?
20
+ What was the name of your first pet?
21
+ What is the first name of your oldest niece?
22
+ What is your paternal grandmother's first name?
23
+ In what city is your vacation home? (Enter full name of city only)
24
+ What was the nickname of your grandfather?
25
+
26
+ In what city was your mother born? (Enter full name of city only)
27
+ What is your mother's middle name?
28
+ In what city were you born? (Enter full name of city only)
29
+ Where did you meet your spouse for the first time? (Enter full name of city only)
30
+ What was your favorite restaurant in college?
31
+ What is your paternal grandfather's first name?
32
+ What was the name of your junior high school? (Enter only \"Riverdale\" for Riverdale Junior High School)
33
+ What was the last name of your favorite teacher in final year of high school?
34
+ What was the name of the town your grandmother lived in? (Enter full name of town only)
35
+ What street did your best friend in high school live on? (Enter full name of street only)
36
+ EOS
37
+
38
+ # answers = [[QUESTIONS[n], "answer"], ... ]
39
+ attr_accessor :username, :password, :answers, :expectation
40
+ end
41
+ end
@@ -0,0 +1,6 @@
1
+ module Yodlee
2
+ class NoAnswerForQuestion < StandardError; end
3
+ class ExpectationMismatch < StandardError; end
4
+ class ExpectationNotFound < StandardError; end
5
+ class AccountNotFound < StandardError; end
6
+ end
@@ -0,0 +1,9 @@
1
+ class String
2
+ def underscore
3
+ gsub(/::/, '/').
4
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
5
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
6
+ tr("-", "_").
7
+ downcase
8
+ end unless respond_to?(:underscore)
9
+ end
@@ -0,0 +1,3 @@
1
+ module Yodlee
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,156 @@
1
+ %w(../lib).each do |path|
2
+ $LOAD_PATH.unshift(File.expand_path(File.join(File.dirname(__FILE__), path)))
3
+ end
4
+
5
+ require 'rubygems'
6
+ require 'test/unit'
7
+ require 'flexmock/test_unit'
8
+
9
+ require 'yodlee'
10
+
11
+ class TestYodlee < Test::Unit::TestCase
12
+ def setup
13
+ @cred = Yodlee::Credentials.new
14
+ @cred.username = "bob"
15
+ @cred.password = "foo"
16
+ @cred.expectation = "bar"
17
+ @cred.answers = [
18
+ [Yodlee::Credentials::QUESTIONS[0], "aa"],
19
+ [Yodlee::Credentials::QUESTIONS[10], "ab"],
20
+ [Yodlee::Credentials::QUESTIONS[20], "ac"]
21
+ ]
22
+
23
+ @conn = Yodlee::Connection.new(@cred)
24
+
25
+ # prevent accidental connections
26
+ inject_agent(nil)
27
+ end
28
+
29
+ def test_not_connected
30
+ assert !@conn.connected?
31
+ end
32
+
33
+ def test_valid_session
34
+ # flunk
35
+ end
36
+
37
+ def test_provide_username
38
+ inject_agent(mock = flexmock("mechanize"))
39
+
40
+ mock.should_receive(:get).once.and_return(flexmock(:form_with => Hash.new))
41
+ mock.should_receive(:submit).with('loginName' => "bob")
42
+
43
+ @conn.provide_username
44
+ end
45
+
46
+ def test_answer_question
47
+ inject_agent(agent = flexmock("mechanize"))
48
+
49
+ page = flexmock("page")
50
+ page.should_receive(:body).once.and_return("<label for='answer'>#{Yodlee::Credentials::QUESTIONS[0]}</label>")
51
+ page.should_receive(:form_with).with(:name => 'loginForm').once.and_return(Hash.new)
52
+
53
+ agent.should_receive(:submit).with('answer' => "aa")
54
+
55
+ @conn.answer_question(page)
56
+ end
57
+
58
+ def test_answer_question_fails
59
+ inject_agent(agent = flexmock("mechanize"))
60
+
61
+ page = flexmock("page")
62
+ page.should_receive(:body).once.and_return("<label for='answer'>#{Yodlee::Credentials::QUESTIONS[9]}</label>")
63
+
64
+ assert_raises Yodlee::NoAnswerForQuestion do
65
+ @conn.answer_question(page)
66
+ end
67
+ end
68
+
69
+ def test_check_expectation
70
+ page = flexmock("page")
71
+ page.should_receive(:body).once.and_return("
72
+ <dl>
73
+ <dt>Secret Phrase:</dt>
74
+ <dd> bar
75
+ <div class=\"caption\">Ensure etc etc blah blah</div>
76
+ </dd></dl>")
77
+
78
+ assert_equal page, @conn.check_expectation(page)
79
+ end
80
+
81
+ def test_check_expectation_fails
82
+ page = flexmock("page")
83
+ page.should_receive(:body).once.and_return("
84
+ <dl>
85
+ <dt>Secret Phrase:</dt>
86
+ <dd> wrong expectation!
87
+ <div class=\"caption\">Ensure etc etc trick it by putting it here bar blah blah</div>
88
+ </dd></dl>")
89
+
90
+ assert_raises Yodlee::ExpectationMismatch do
91
+ @conn.check_expectation(page)
92
+ end
93
+ end
94
+
95
+ def test_provide_password
96
+ inject_agent(agent = flexmock("mechanize"))
97
+
98
+ link = flexmock("link")
99
+ link.should_receive(:href).and_return("https://example.com/accounts.page?x=y")
100
+
101
+ page = flexmock("page")
102
+ page.should_receive(:form_with).with(:name => 'loginForm').once.and_return(Hash.new)
103
+ page.should_receive(:form_with).with(:name => 'updateForm').once.and_return(Hash.new)
104
+ page.should_receive(:"links.detect").and_return(link)
105
+
106
+ agent.should_receive(:submit).with('password' => "foo").and_return(page)
107
+ agent.should_receive(:submit).with(Hash.new).and_return(page)
108
+
109
+ @conn.provide_password(page)
110
+ end
111
+
112
+ def test_login
113
+ mock = flexmock(@conn)
114
+ mock.should_receive(:provide_username).ordered.with_no_args.once.and_return(page = flexmock("page"))
115
+ mock.should_receive(:answer_question, :check_expectation, :provide_password).ordered.once.and_return(page)
116
+
117
+ assert !mock.connected?
118
+ mock.login
119
+ assert mock.connected?
120
+ end
121
+
122
+ def test_accounts
123
+ mock = flexmock(@conn)
124
+ inject_agent(agent = flexmock("mechanize"))
125
+
126
+ page = flexmock
127
+ page.should_receive(:body).and_return(
128
+ "<div class='acctbean'>
129
+ <a href='u?itemId=1&itemAccountId=2'><strong>x</strong> - y</a>
130
+ <a href='u?itemId=8&itemAccountId=9'><strong>a</strong> - b</a>
131
+ </div>")
132
+
133
+ mock.should_receive(:handle_connection!).once
134
+ mock.instance_variable_set "@accounts_page", page
135
+
136
+ assert_equal "x", @conn.accounts.first.institute_name
137
+ assert_equal "y", @conn.accounts.first.name
138
+ assert_equal "1", @conn.accounts.first.institute_id
139
+ assert_equal "2", @conn.accounts.first.id
140
+
141
+ assert_equal "a", @conn.accounts.last.institute_name
142
+ assert_equal "b", @conn.accounts.last.name
143
+ assert_equal "8", @conn.accounts.last.institute_id
144
+ assert_equal "9", @conn.accounts.last.id
145
+
146
+ assert_equal 2, @conn.accounts.size
147
+ end
148
+
149
+ def test_account_transactions
150
+ end
151
+
152
+ def inject_agent(mock)
153
+ @conn.instance_eval { @agent = mock }
154
+ end
155
+
156
+ end
@@ -0,0 +1,44 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{yodlee}
5
+ s.version = "0.0.1.20090301132701"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Andrew A. Smith"]
9
+ s.date = %q{2009-03-01}
10
+ s.description = %q{Fetches accounts and their transaction details from the Yodlee MoneyCenter (https://moneycenter.yodlee.com).}
11
+ s.email = ["andy@tinnedfruit.org"]
12
+ s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.rdoc"]
13
+ s.files = ["History.txt", "Manifest.txt", "README.rdoc", "Rakefile", "lib/yodlee.rb", "lib/yodlee/account.rb", "lib/yodlee/connection.rb", "lib/yodlee/credentials.rb", "lib/yodlee/exceptions.rb", "lib/yodlee/monkeypatches.rb", "lib/yodlee/version.rb", "test/test_yodlee.rb", "yodlee.gemspec"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/aasmith/yodlee}
16
+ s.rdoc_options = ["--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{yodlee}
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = %q{Fetches financial data from Yodlee MoneyCenter.}
21
+ s.test_files = ["test/test_yodlee.rb"]
22
+
23
+ if s.respond_to? :specification_version then
24
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
25
+ s.specification_version = 2
26
+
27
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
28
+ s.add_runtime_dependency(%q<mechanize>, [">= 0"])
29
+ s.add_runtime_dependency(%q<nokogiri>, [">= 0"])
30
+ s.add_development_dependency(%q<flexmock>, [">= 0"])
31
+ s.add_development_dependency(%q<hoe>, [">= 1.9.0"])
32
+ else
33
+ s.add_dependency(%q<mechanize>, [">= 0"])
34
+ s.add_dependency(%q<nokogiri>, [">= 0"])
35
+ s.add_dependency(%q<flexmock>, [">= 0"])
36
+ s.add_dependency(%q<hoe>, [">= 1.9.0"])
37
+ end
38
+ else
39
+ s.add_dependency(%q<mechanize>, [">= 0"])
40
+ s.add_dependency(%q<nokogiri>, [">= 0"])
41
+ s.add_dependency(%q<flexmock>, [">= 0"])
42
+ s.add_dependency(%q<hoe>, [">= 1.9.0"])
43
+ end
44
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yodlee
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andrew A. Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-18 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: mechanize
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: flexmock
37
+ type: :development
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: hoe
47
+ type: :development
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.9.0
54
+ version:
55
+ description: |-
56
+ Fetches accounts and their transaction details from the Yodlee
57
+ MoneyCenter (https://moneycenter.yodlee.com).
58
+ email:
59
+ - andy@tinnedfruit.org
60
+ executables: []
61
+
62
+ extensions: []
63
+
64
+ extra_rdoc_files:
65
+ - History.txt
66
+ - Manifest.txt
67
+ - README.rdoc
68
+ files:
69
+ - History.txt
70
+ - Manifest.txt
71
+ - README.rdoc
72
+ - Rakefile
73
+ - lib/yodlee.rb
74
+ - lib/yodlee/account.rb
75
+ - lib/yodlee/connection.rb
76
+ - lib/yodlee/credentials.rb
77
+ - lib/yodlee/exceptions.rb
78
+ - lib/yodlee/monkeypatches.rb
79
+ - lib/yodlee/version.rb
80
+ - test/test_yodlee.rb
81
+ - yodlee.gemspec
82
+ has_rdoc: true
83
+ homepage: http://github.com/aasmith/yodlee
84
+ licenses: []
85
+
86
+ post_install_message:
87
+ rdoc_options:
88
+ - --main
89
+ - README.rdoc
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: "0"
97
+ version:
98
+ required_rubygems_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: "0"
103
+ version:
104
+ requirements: []
105
+
106
+ rubyforge_project: yodlee
107
+ rubygems_version: 1.3.5
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: Fetches financial data from Yodlee MoneyCenter.
111
+ test_files:
112
+ - test/test_yodlee.rb