yodlee 0.0.1

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