aasmith-yodlee 0.0.1.20090227002742

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