aasmith-yodlee 0.0.1.20090227002742

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,296 @@
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
+ @uris = {}
14
+ end
15
+
16
+ def accounts
17
+ return @accounts if @accounts
18
+
19
+ handle_connection!
20
+
21
+ page = @agent.get(@uris[:accounts])
22
+ doc = Nokogiri::HTML.parse(page.body)
23
+
24
+ @accounts = doc.search(".acctbean a").map{|e|
25
+ acct = Account.new(self)
26
+
27
+ e['href'].scan(/(\w+Id)=(\d+)/).each do |k,v|
28
+ case k
29
+ when /itemAccountId/ then acct.id = v
30
+ when /itemId/ then acct.institute_id = v
31
+ end
32
+ end
33
+
34
+ acct.institute_name = e.at('strong').text
35
+ acct.name = e.children.last.text.sub(/^\s*-\s*/,'')
36
+ acct
37
+ }
38
+ end
39
+
40
+ def account_info(acct)
41
+ page = @agent.get(@uris[:accounts])
42
+
43
+ link = page.links.detect{|lnk| lnk.href =~ /itemAccountId=#{acct.id}/ } or raise AccountNotFound, "Could not find account in list"
44
+ link.href << "&dateRangeId=-1"
45
+ page = link.click
46
+
47
+ doc = Nokogiri::HTML.parse(page.body)
48
+
49
+ last_upd, next_upd = doc.at(".accountlinks").text.scan(/Last updated (.*?)\s*\(next scheduled update (.*)\)/).flatten
50
+
51
+ # Regular accounts have a heading + div, investments have heading + table
52
+ regular_acct = doc.at("h2[contains('Account Overview')] + div")
53
+
54
+ account_info = regular_acct ? regular_account_info(doc) : investment_account_info(doc)
55
+ account_info[:next_update] = next_upd
56
+ account_info[:last_updated] = last_upd
57
+
58
+ csv_page = page.form_with(:name => 'rep').submit
59
+ account_info[:simple_transactions] = csv_page.response['content-type'] =~ /csv/ ? csv_page.body : []
60
+
61
+ account_info
62
+ end
63
+
64
+ def regular_account_info(doc)
65
+ info_block = doc.at("h2[contains('Account Overview')] + div")
66
+
67
+ Hash[*info_block.search("dt").
68
+ zip(info_block.search("dt+dd")).
69
+ map{|a,b|[a.text.gsub(/\W/,'').underscore.to_sym, b.text]}.
70
+ flatten
71
+ ]
72
+ end
73
+
74
+ def investment_account_info(doc)
75
+ account_info = {}
76
+ account_info[:holdings] = [] # TODO
77
+ account_info[:current_balance] =
78
+ doc.search("h2[contains('Account Overview')] + table tr[last()] td[last()]").text
79
+ account_info
80
+ end
81
+
82
+ # This method returns each transaction as an object, based on the underyling javascript
83
+ # structures used to build the transactions as displayed in the Yodlee UI. These objects
84
+ # are able to access more information than the CSV Yodlee provides, such as the finanical
85
+ # institute's transaction id, useful for tracking duplicates.
86
+ #
87
+ # Calling this method requires Johnson to be installed, otherwise an exception is raised.
88
+ def transactions(acct)
89
+ unless Object.const_defined? "Johnson"
90
+ raise "Johnson not found. Install the johnson gem, or use simple_transactions instead."
91
+ end
92
+
93
+ post_headers = {
94
+ "c0-scriptName"=>"TxnService",
95
+ "c0-methodName"=>"searchTransactions",
96
+ "c0-id"=>"#{rand(5000)}_#{Time.now.to_i}#{Time.now.usec / 1000}}",
97
+ "c0-e1"=>"number:10000004",
98
+ "c0-e2"=>"string:17CBE222A42161A3FF450E47CF4C1A00",
99
+ "c0-e3"=>"null:null",
100
+ "c0-e4"=>"number:1",
101
+ "c0-e5"=>"boolean:false",
102
+ "c0-e6"=>"string:#{acct.id}",
103
+ "c0-e7"=>"string:-1",
104
+ "c0-e8"=>"null:null",
105
+ "c0-e9"=>"string:-1",
106
+ "c0-e10"=>"null:null",
107
+ "c0-e11"=>"null:null",
108
+ "c0-e12"=>"null:null",
109
+ "c0-e13"=>"string:-1",
110
+ "c0-e14"=>"null:null",
111
+ "c0-e15"=>"number:-1",
112
+ "c0-e16"=>"number:-1",
113
+ "c0-e17"=>"boolean:false",
114
+ "c0-e18"=>"Boolean:false",
115
+ "c0-e19"=>"boolean:false",
116
+ "c0-e20"=>"Boolean:false",
117
+ "c0-e21"=>"string:",
118
+ "c0-e22"=>"string:",
119
+ "c0-e23"=>"Boolean:false",
120
+ "c0-e24"=>"Boolean:false",
121
+ "c0-e25"=>"boolean:false",
122
+ "c0-e26"=>"Number:0",
123
+ "c0-e27"=>"string:0",
124
+ "c0-e28"=>"null:null",
125
+ "c0-e29"=>"null:null",
126
+ "c0-e30"=>"string:allTransactions",
127
+ "c0-e31"=>"string:InProgressAndCleared",
128
+ "c0-e32"=>"number:999",
129
+ "c0-e33"=>"string:",
130
+ "c0-e34"=>"null:null",
131
+ "c0-e35"=>"null:null",
132
+ "c0-e36"=>"string:",
133
+ "c0-e37"=>"null:null",
134
+ "c0-e38"=>"string:ALL",
135
+ "c0-e39"=>"string:false",
136
+ "c0-e40"=>"string:0.0",
137
+ "c0-e41"=>"string:0.0",
138
+
139
+ "c0-param0"=>"Object:{
140
+ cobrandId:reference:c0-e1,
141
+ applicationId:reference:c0-e2,
142
+ csit:reference:c0-e3,
143
+ loggingLevel:reference:c0-e4,
144
+ loggingEnabled:reference:c0-e5}",
145
+
146
+ "c0-param1"=>"Object:{
147
+ itemAccountId:reference:c0-e6,
148
+ categoryId:reference:c0-e7,
149
+ categoryLevelId:reference:c0-e8,
150
+ dateRangeId:reference:c0-e9,
151
+ fromDate:reference:c0-e10,
152
+ toDate:reference:c0-e11,
153
+ groupBy:reference:c0-e12,
154
+ groupAccountId:reference:c0-e13,
155
+ filterTranasctions:reference:c0-e14,
156
+ transactionTypeId:reference:c0-e15,
157
+ transactionStatusId:reference:c0-e16,
158
+ ignorePendingTransactions:reference:c0-e17,
159
+ includeBusinessExpense:reference:c0-e18,
160
+ includeTransfer:reference:c0-e19,
161
+ includeReimbursableExpense:refrence:c0-e20,
162
+ fromDate1:reference:c0-e21,
163
+ toDate1:reference:c0-e22,
164
+ includeMedicalExpense:reference:c0-e23,
165
+ includeTaxDeductible:reference:c0-e24,
166
+ includePersonalExpense:reference:c0-e25,
167
+ transactionAmount:reference:c0-e26,
168
+ transactionAmountRange:reference:c0-e27,
169
+ billStatementRange:reference:c0-e28,
170
+ criteria:reference:c0-e29,
171
+ module:reference:c0-e30,
172
+ transactionType:reference:c0-e31,
173
+ pageSize:reference:c0-e32,
174
+ sharedMemId:reference:c0-e33,
175
+ overRideDateRangeId:reference:c0-e34,
176
+ overRideContainer:referencec0-e35,
177
+ searchString:reference:c0-e36,
178
+ pageId:reference:c0-e37,
179
+ splitTypeTransaction:reference:c0-e38,
180
+ isAvailableBalance:reference:c0-e39,
181
+ currentBalance:reference:c0-e40,
182
+ availableBalance:reference:c0-e41}",
183
+
184
+ "c0-param2"=>"boolean:false",
185
+
186
+ "callCount"=>"1",
187
+ "xml"=>"true",
188
+ }
189
+ page = @agent.post(
190
+ 'https://moneycenter.yodlee.com/moneycenter/dwr/exec/TxnService.searchTransactions.dwr',
191
+ post_headers
192
+ )
193
+
194
+ j = Johnson::Runtime.new
195
+
196
+ # Remove the last line (a call to DWREngine), and execute
197
+ j.evaluate page.body.strip.sub(/\n[^\n]+\Z/m, '')
198
+
199
+ if x = j['s0']
200
+ transactions = x.transactions.map do |e|
201
+ transaction = Yodlee::Transaction.new
202
+ transaction.account_name = e.accountName
203
+ transaction.currency = e.amount.cobCurrencyCode
204
+ transaction.amount = e.amount.cobPreciseAmount
205
+ transaction.description = e.description
206
+ transaction.account_id = e.itemAccountId
207
+ transaction.fit_id = e.transactionId
208
+ transaction.status = e['type']['type']
209
+
210
+ # Re-parse in order to get a real Time, not a Johnson::SpiderMonkey::RubyLandProxy.
211
+ transaction.date = Time.parse(e.date.to_s)
212
+ transaction
213
+ end
214
+
215
+ return transactions
216
+ end
217
+
218
+ return []
219
+ end
220
+
221
+ def handle_connection!
222
+ login unless connected?
223
+ end
224
+
225
+ def login
226
+ @connected = false
227
+ page = nil
228
+
229
+ %w(provide_username answer_question check_expectation provide_password).each do |m|
230
+ page = send(*[m, page].compact)
231
+ end
232
+
233
+ @connected = true
234
+ end
235
+
236
+ def connected?
237
+ @connected
238
+ end
239
+
240
+ def log(level, msg)
241
+ @logger.__send__(level, question) if @logger
242
+ end
243
+
244
+ # login scrapers
245
+
246
+ def provide_username
247
+ p = @agent.get 'https://moneycenter.yodlee.com/'
248
+ f = p.form_with(:name => 'loginForm')
249
+ f['loginName'] = @credentials.username
250
+ @agent.submit(f)
251
+ end
252
+
253
+ def answer_question(page)
254
+ question = Nokogiri::HTML.parse(page.body).at("label[@for=answer]").text
255
+ log(:debug, question)
256
+
257
+ begin
258
+ answer = @credentials.answers.detect{|q, a| question =~ /^#{Regexp.escape(q)}/}.last
259
+ rescue
260
+ raise NoAnswerForQuestion, "No answer found for #{question}"
261
+ end
262
+
263
+ f = page.form_with(:name => 'loginForm')
264
+ f['answer'] = answer
265
+
266
+ @agent.submit(f)
267
+ end
268
+
269
+ def check_expectation(page)
270
+ d = Nokogiri::HTML.parse(page.body)
271
+ node = d.at("dl > dt[contains('Secret Phrase')] + dd .caption")
272
+
273
+ if node
274
+ if @credentials.expectation == node.previous.text.strip
275
+ return page
276
+ else
277
+ raise ExpectationMismatch, "Expectation found, but was incorrect"
278
+ end
279
+ else
280
+ raise ExpectationNotFound, "Didn't find expectation"
281
+ end
282
+ end
283
+
284
+ def provide_password(page)
285
+ f = page.form_with(:name => 'loginForm')
286
+ f['password'] = @credentials.password
287
+ page = @agent.submit(f)
288
+
289
+ # ack javascript disabled
290
+ f = page.form_with(:name => 'updateForm')
291
+ page = @agent.submit(f)
292
+
293
+ @uris[:accounts] = page.uri.to_s << "&filter_id=-1"
294
+ end
295
+ end
296
+ 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,152 @@
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
+ page = flexmock("page")
99
+ page.should_receive(:form_with).with(:name => 'loginForm').once.and_return(Hash.new)
100
+ page.should_receive(:form_with).with(:name => 'updateForm').once.and_return(Hash.new)
101
+ page.should_receive(:uri).and_return(URI.parse("http://example.com/accounts.page?x=y"))
102
+
103
+ agent.should_receive(:submit).with('password' => "foo").and_return(page)
104
+ agent.should_receive(:submit).with(Hash.new).and_return(page)
105
+
106
+ @conn.provide_password(page)
107
+ end
108
+
109
+ def test_login
110
+ mock = flexmock(@conn)
111
+ mock.should_receive(:provide_username).ordered.with_no_args.once.and_return(page = flexmock("page"))
112
+ mock.should_receive(:answer_question, :check_expectation, :provide_password).ordered.once.and_return(page)
113
+
114
+ assert !mock.connected?
115
+ mock.login
116
+ assert mock.connected?
117
+ end
118
+
119
+ def test_accounts
120
+ mock = flexmock(@conn)
121
+ inject_agent(agent = flexmock("mechanize"))
122
+
123
+ mock.should_receive(:handle_connection!).once
124
+ agent.should_receive(:get).once.and_return(
125
+ flexmock(:body => "
126
+ <div class='acctbean'>
127
+ <a href='u?itemId=1&itemAccountId=2'><strong>x</strong> - y</a>
128
+ <a href='u?itemId=8&itemAccountId=9'><strong>a</strong> - b</a>
129
+ </div>")
130
+ )
131
+
132
+ assert_equal "x", @conn.accounts.first.institute_name
133
+ assert_equal "y", @conn.accounts.first.name
134
+ assert_equal "1", @conn.accounts.first.institute_id
135
+ assert_equal "2", @conn.accounts.first.id
136
+
137
+ assert_equal "a", @conn.accounts.last.institute_name
138
+ assert_equal "b", @conn.accounts.last.name
139
+ assert_equal "8", @conn.accounts.last.institute_id
140
+ assert_equal "9", @conn.accounts.last.id
141
+
142
+ assert_equal 2, @conn.accounts.size
143
+ end
144
+
145
+ def test_account_transactions
146
+ end
147
+
148
+ def inject_agent(mock)
149
+ @conn.instance_eval { @agent = mock }
150
+ end
151
+
152
+ 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.20090227002742"
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-02-27}
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,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: aasmith-yodlee
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.20090227002742
5
+ platform: ruby
6
+ authors:
7
+ - Andrew A. Smith
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-02-27 00:00:00 -08: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: Fetches accounts and their transaction details from the Yodlee MoneyCenter (https://moneycenter.yodlee.com).
56
+ email:
57
+ - andy@tinnedfruit.org
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ extra_rdoc_files:
63
+ - History.txt
64
+ - Manifest.txt
65
+ - README.rdoc
66
+ files:
67
+ - History.txt
68
+ - Manifest.txt
69
+ - README.rdoc
70
+ - Rakefile
71
+ - lib/yodlee.rb
72
+ - lib/yodlee/account.rb
73
+ - lib/yodlee/connection.rb
74
+ - lib/yodlee/credentials.rb
75
+ - lib/yodlee/exceptions.rb
76
+ - lib/yodlee/monkeypatches.rb
77
+ - lib/yodlee/version.rb
78
+ - test/test_yodlee.rb
79
+ - yodlee.gemspec
80
+ has_rdoc: true
81
+ homepage: http://github.com/aasmith/yodlee
82
+ post_install_message:
83
+ rdoc_options:
84
+ - --main
85
+ - README.rdoc
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: "0"
99
+ version:
100
+ requirements: []
101
+
102
+ rubyforge_project: yodlee
103
+ rubygems_version: 1.2.0
104
+ signing_key:
105
+ specification_version: 2
106
+ summary: Fetches financial data from Yodlee MoneyCenter.
107
+ test_files:
108
+ - test/test_yodlee.rb