dinero 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +84 -0
- data/MIT-LICENSE +20 -0
- data/README.md +208 -0
- data/Rakefile +24 -0
- data/dinero.gemspec +33 -0
- data/examples/get_balances.rb +48 -0
- data/lib/dinero/account.rb +14 -0
- data/lib/dinero/banks/capital_one.rb +67 -0
- data/lib/dinero/banks/capital_one_360.rb +120 -0
- data/lib/dinero/banks.rb +74 -0
- data/lib/dinero/version.rb +3 -0
- data/lib/dinero.rb +10 -0
- data/log/.keep +0 -0
- data/spec/banks/capital_one_360_spec.rb +41 -0
- data/spec/banks/capital_one_spec.rb +51 -0
- data/spec/fixtures/vcr_cassettes/.keep +0 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/support/config.rb +18 -0
- metadata +212 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 5127bb025bcedb230c70da3c94fb2c79d51d2d2e
|
4
|
+
data.tar.gz: df996cbf0a393e271f07c7157b9532e7352b75ee
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c7c9e8640651381ce48fbfa84be999b9c60ae87b4e2bd533c93c6bd1abeac2ddd5f2ddf4ba1c430d485240f898d2b6fc255c2ccb96972a422440222799b35791
|
7
|
+
data.tar.gz: ce83fea40a4f76ac91c9fdea94092e7e28508604679be3bde318b5c3ea1c00989cc9f37d915e86795a23b7076089e94ad3e91df216f54ed4d81a836d9caa66b6
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
dinero (0.0.1)
|
5
|
+
nokogiri (~> 1.6.6)
|
6
|
+
selenium-webdriver (~> 2.45.0)
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
addressable (2.3.8)
|
12
|
+
byebug (4.0.5)
|
13
|
+
columnize (= 0.9.0)
|
14
|
+
childprocess (0.5.6)
|
15
|
+
ffi (~> 1.0, >= 1.0.11)
|
16
|
+
coderay (1.1.0)
|
17
|
+
columnize (0.9.0)
|
18
|
+
crack (0.4.2)
|
19
|
+
safe_yaml (~> 1.0.0)
|
20
|
+
diff-lcs (1.2.5)
|
21
|
+
docile (1.1.5)
|
22
|
+
ffi (1.9.8)
|
23
|
+
json (1.8.2)
|
24
|
+
method_source (0.8.2)
|
25
|
+
mini_portile (0.6.2)
|
26
|
+
multi_json (1.11.0)
|
27
|
+
nokogiri (1.6.6.2)
|
28
|
+
mini_portile (~> 0.6.0)
|
29
|
+
pry (0.10.1)
|
30
|
+
coderay (~> 1.1.0)
|
31
|
+
method_source (~> 0.8.1)
|
32
|
+
slop (~> 3.4)
|
33
|
+
pry-byebug (3.1.0)
|
34
|
+
byebug (~> 4.0)
|
35
|
+
pry (~> 0.10)
|
36
|
+
rake (10.4.2)
|
37
|
+
rspec (3.2.0)
|
38
|
+
rspec-core (~> 3.2.0)
|
39
|
+
rspec-expectations (~> 3.2.0)
|
40
|
+
rspec-mocks (~> 3.2.0)
|
41
|
+
rspec-core (3.2.3)
|
42
|
+
rspec-support (~> 3.2.0)
|
43
|
+
rspec-expectations (3.2.1)
|
44
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
45
|
+
rspec-support (~> 3.2.0)
|
46
|
+
rspec-its (1.2.0)
|
47
|
+
rspec-core (>= 3.0.0)
|
48
|
+
rspec-expectations (>= 3.0.0)
|
49
|
+
rspec-mocks (3.2.1)
|
50
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
51
|
+
rspec-support (~> 3.2.0)
|
52
|
+
rspec-support (3.2.2)
|
53
|
+
rubyzip (1.1.7)
|
54
|
+
safe_yaml (1.0.4)
|
55
|
+
selenium-webdriver (2.45.0)
|
56
|
+
childprocess (~> 0.5)
|
57
|
+
multi_json (~> 1.0)
|
58
|
+
rubyzip (~> 1.0)
|
59
|
+
websocket (~> 1.0)
|
60
|
+
simplecov (0.10.0)
|
61
|
+
docile (~> 1.1.0)
|
62
|
+
json (~> 1.8)
|
63
|
+
simplecov-html (~> 0.10.0)
|
64
|
+
simplecov-html (0.10.0)
|
65
|
+
slop (3.6.0)
|
66
|
+
vcr (2.9.3)
|
67
|
+
webmock (1.21.0)
|
68
|
+
addressable (>= 2.3.6)
|
69
|
+
crack (>= 0.3.2)
|
70
|
+
websocket (1.2.2)
|
71
|
+
|
72
|
+
PLATFORMS
|
73
|
+
ruby
|
74
|
+
|
75
|
+
DEPENDENCIES
|
76
|
+
bundler (~> 1.6)
|
77
|
+
dinero!
|
78
|
+
pry-byebug (~> 3.1.0)
|
79
|
+
rake
|
80
|
+
rspec (~> 3.2.0)
|
81
|
+
rspec-its (~> 1.2.0)
|
82
|
+
simplecov (~> 0.10.0)
|
83
|
+
vcr (~> 2.9.3)
|
84
|
+
webmock (~> 1.21.0)
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2015 Michael Lang
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,208 @@
|
|
1
|
+
# Dinero
|
2
|
+
Dinero makes logging into all your online banking websites trivial for retrieving accounts, balances, and transactions.
|
3
|
+
|
4
|
+
Banks are rightly concerned about security, anti-phishing, and general brute force attacks on their sites, so they have developed a number of creative ways of protecting access to their sites. Some techniques include:
|
5
|
+
|
6
|
+
* Asking for only username on first page, then password on the next page.
|
7
|
+
* Multiple redirects.
|
8
|
+
* Asking security questions when browser cookies aren't set.
|
9
|
+
* loading login forms with JavaScript or requiring JS is enabled.
|
10
|
+
* loading login forms inside of IFRAME
|
11
|
+
* Randomizing code behind the PIN pad that you must click.
|
12
|
+
|
13
|
+
So much trickery requires something more than just Mechanize or RestClient. Dinero uses Selenium and PhantomJS to drive the data collection. So, you'll need to install Selenium before you can begin using Dinero. If you're on a mac:
|
14
|
+
|
15
|
+
brew install selenium-server-standalone
|
16
|
+
|
17
|
+
And then:
|
18
|
+
|
19
|
+
gem install dinero
|
20
|
+
|
21
|
+
## The Vision
|
22
|
+
|
23
|
+
Much like ActiveRecord sought to standardize the API for accessing and modeling domain data in the DBMS without having to drop down to raw SQL and as ActiveMerchant seeks to standardize payment processing to a common set of API's, Dinero aims to standardize access to bank accounts. To that end, I have started implementing all the banks I have accounts with.
|
24
|
+
|
25
|
+
## Project Status
|
26
|
+
|
27
|
+
The following banks are implemented:
|
28
|
+
|
29
|
+
* Capital One - https://capitalone.com (only U.S. credit card logins -- there's also banking, loans, investing, business, and Canada/UK credit cards)
|
30
|
+
* Capital One 360 - https://capitalone360.com (formerly Ing Direct).
|
31
|
+
|
32
|
+
Currently, only Accounts balances are essentially implemented. The following properties are available on each Account:
|
33
|
+
|
34
|
+
* account_type -- one of :bank, :brokerage, :credit_card
|
35
|
+
* name -- the name of the account (e.g. "Checking 360 - primary", "Worldview MasterCard")
|
36
|
+
* number -- the account number in-as-much as it's displayed
|
37
|
+
* balance -- the balance on the account. For :credit_card, this is outstanding balance
|
38
|
+
* available -- the amount available to you. For :credit_card, difference between your credit limit and balance
|
39
|
+
|
40
|
+
## How to Use
|
41
|
+
|
42
|
+
You'll find at least one example in the examples folder called 'get_balances.' This example can take a bank_name, username, and password and print out balances to the console something like this:
|
43
|
+
|
44
|
+
~~~ bash
|
45
|
+
>> bundle exec ruby examples/get_balances.rb --bank capital_one_360 --user scrooge
|
46
|
+
enter password:
|
47
|
+
Retrieving your bank account information...
|
48
|
+
+--------------------------------------------------------------------------------+
|
49
|
+
| name | number | balance | available |
|
50
|
+
+--------------------------------------------------------------------------------+
|
51
|
+
| 360 Checking - primary | 735546410 | $ 111543.07 | $ 111210.61 |
|
52
|
+
| MONEY - SK's Money | 112335590 | $ 302.39 | $ 302.39 |
|
53
|
+
| 360 Savings - Rainy Day Fund | 12341232 | $ 12144.45 | $ 12144.45 |
|
54
|
+
| SB Individual | 0903959692 | $ 10191.23 | $ 10191.23 |
|
55
|
+
| Visa Platinum | ....1165 | $ 87.10 | $ 19912.90 |
|
56
|
+
| World MasterCard | ....2978 | $ 192.66 | $ 19807.34 |
|
57
|
+
+--------------------------------------------------------------------------------+
|
58
|
+
~~~
|
59
|
+
|
60
|
+
Don't worry, all those numbers are made up. ;-)
|
61
|
+
|
62
|
+
So, yeah, with more banks, we can collect more info. The above shows banking accounts, brokerage account, and credit card accounts.
|
63
|
+
|
64
|
+
To use the gem inside your app:
|
65
|
+
|
66
|
+
~~~
|
67
|
+
require 'dinero'
|
68
|
+
|
69
|
+
bank_info = CapitalOne360.new(username: @username, password: @password))
|
70
|
+
bank_info.accounts.each do |acct|
|
71
|
+
puts [acct.name, acct.number, acct.account_type, acct.balance, acct.available].join("\t")
|
72
|
+
~~~
|
73
|
+
|
74
|
+
If you have a really slow Bank site or Internet connection, try passing ```timeout: 15``` when initializing a Bank class.
|
75
|
+
|
76
|
+
## Contribute!
|
77
|
+
|
78
|
+
I'm planning to continue implementing more banks, but I can use your help since I don't have access to all the world's banks.
|
79
|
+
|
80
|
+
I plan to implement the following banks:
|
81
|
+
|
82
|
+
* Wells Fargo (Loans)
|
83
|
+
* Scottrade (brokerage, IRA, and Bank Account)
|
84
|
+
* San Diego County Credit Union
|
85
|
+
* Georgia's Own Credit Union
|
86
|
+
* South State Bank
|
87
|
+
|
88
|
+
I know I'll need to do those fun security questions for unregistered browsers on some of these. I haven't quite decided on how to structure this, but at least the Bank class has an open-ended options hash parameter to accommodate additional fields being passed in.
|
89
|
+
|
90
|
+
If you want to add a new bank, here's how:
|
91
|
+
|
92
|
+
# Pick one of the existing banks that most closely follows the login pattern of your chosen bank and model your effort after it.
|
93
|
+
# Set up a new class in the lib/banks folder.
|
94
|
+
# Set up rspec specs in the spec/banks folder.
|
95
|
+
# Set up a spec/config/banks.yml file with your credentials (don't commit to the repo! -- it's .gitignore'd)
|
96
|
+
|
97
|
+
Here's an example banks.yml file:
|
98
|
+
|
99
|
+
~~~ yaml
|
100
|
+
capital_one_360:
|
101
|
+
username: mickeymouse
|
102
|
+
password: moosamoosamickeymouse
|
103
|
+
account_types:
|
104
|
+
- :bank
|
105
|
+
- :brokerage
|
106
|
+
- :credit_card
|
107
|
+
accounts: 3
|
108
|
+
|
109
|
+
capital_one:
|
110
|
+
username: mickeymouse
|
111
|
+
password: moosamoosamickeymouse
|
112
|
+
account_types:
|
113
|
+
- :credit_card
|
114
|
+
accounts: 2
|
115
|
+
~~~
|
116
|
+
|
117
|
+
The bank rspecs are wrapped with ```if bank_configured? :capital_one_360``` if block that allows the spec to run or not, so if the first thing you did was 'rspec' and saw 0 examples, that means you don't have a banks.yml file, yet -- or it's incorrectly configured.
|
118
|
+
|
119
|
+
Once you have the basic structure in place, then implement the following methods for your Bank class:
|
120
|
+
|
121
|
+
### Inherit from the Base class
|
122
|
+
|
123
|
+
Be sure your Bank class is inherited from the Bank::Base class.
|
124
|
+
|
125
|
+
### #login!
|
126
|
+
|
127
|
+
The #login! method expects to navigate to the login URL and then key in account credentials and finish with the user fully authenticated on the site. I found it best to browse the website in Firefox and Inspect the elements I wanted to key data into (user and password fields). You may have to switch to a frame if the login form is inside an IFRAME. You may have to wait until the login form is presented if it's delay-loaded via JavaScript. These are the two principal challenges I encountered. Some banks split user account and password credentials into two screens. Use a Private/Incognito Window as the Selenium environment will be without cookies so your browser experience should be, too.
|
128
|
+
|
129
|
+
### #post_username!
|
130
|
+
The #login! calls post_username! after navigating to the login URL. If your bank only prompts for User account here, key it and post the form. If password and username are entered on this screen, just key the username and then implement the form submit in the #post_password! method. It should look something like this:
|
131
|
+
|
132
|
+
~~~ ruby
|
133
|
+
def post_username!
|
134
|
+
begin
|
135
|
+
wait.until { connection.find_element(id: "Signin").displayed? }
|
136
|
+
rescue
|
137
|
+
connection.save_screenshot('log/capital_one_360_signin_failed.png')
|
138
|
+
raise
|
139
|
+
end
|
140
|
+
|
141
|
+
signin_form = connection.find_element(id: "Signin")
|
142
|
+
username_field = connection.find_element(id: "ACNID")
|
143
|
+
raise "Sign in Form not reached!" unless username_field && signin_form
|
144
|
+
|
145
|
+
username_field.send_keys username
|
146
|
+
signin_form.submit
|
147
|
+
end
|
148
|
+
~~~
|
149
|
+
|
150
|
+
### #post_password!
|
151
|
+
Here, key the password and submit the form. You may have to get the handle on the button and call the button.click method instead of simply calling form.submit as some banks put JavaScript here to make sure the button's being clicked rather than automated submittals via scripts. The implementation should look something like this:
|
152
|
+
|
153
|
+
~~~ ruby
|
154
|
+
def post_password!
|
155
|
+
begin
|
156
|
+
wait.until { connection.find_element(id: "PasswordForm").displayed? }
|
157
|
+
rescue
|
158
|
+
connection.save_screenshot('log/capital_one_360_password_failed.png')
|
159
|
+
raise
|
160
|
+
end
|
161
|
+
|
162
|
+
password_form = connection.find_element(id: "PasswordForm")
|
163
|
+
password_field = connection.find_element(id: "currentPassword_TLNPI")
|
164
|
+
submit_button = connection.find_element :css, ".bluebutton > a:nth-child(1)"
|
165
|
+
raise "Password Form not reached!" unless password_field && password_form
|
166
|
+
|
167
|
+
password_field.send_keys password
|
168
|
+
submit_button.click
|
169
|
+
end
|
170
|
+
~~~
|
171
|
+
|
172
|
+
## Tips to getting logged in successfully.
|
173
|
+
|
174
|
+
```binding.pry``` is your friend. Set up the very basic rspec spec to trigger the login process and stick binding.pry at the point where you're having trouble. Then use connection.page_source and connection.find_element, etc. to work your way through successfully grabbing controls, keying data, and posting the form. Compare what connection.page_source prints out to what you get when viewing source in your browser. Take screenshots with connection.save_screenshot('somefilename') to get a visual cue on what's really going on.
|
175
|
+
|
176
|
+
### #accounts_summary_document
|
177
|
+
|
178
|
+
Once you're successfully logging in, the expectation is that we'll get the list of accounts from the page along with name, number, and balances. Almost all banks drop you in at an accounts summary page with enough information to gather the basic data we're interested in. So, the next task after logging in is to implement accounts_summary_document so that you parse this page's contents into a Nokogiri document. Once you can successfully construct the Nokogiri document, you can capture the above login chain of events through #accounts_summary_document via the VCR gem and you can rapidly evolve the #accounts solution through TDD. All the fun Bank tricks will be neatly captured for near instant playback, which is a boon since these banking sites can take upwards of 45 seconds to go from Login page to Accounts Summary page. Be aware that if you have to touch the connection object again after getting your accounts_summary_document, you'll see spurious errors from VCR, so it's best to be self-reliant on the Nokogiri document once you can retrieve the accounts summaries.
|
179
|
+
|
180
|
+
### #accounts
|
181
|
+
|
182
|
+
This method returns an Array of Account objects. How you get from HTML page to an Array of Accounts is largely dependent on what the website is feeding you and some of these pages can be quite *ugly*, but fortunately, go years without any significant changes. A typical extraction from HTML to Accounts might look like this:
|
183
|
+
|
184
|
+
~~~ ruby
|
185
|
+
# extract account data from the account summary page
|
186
|
+
def accounts
|
187
|
+
return @accounts if @accounts
|
188
|
+
|
189
|
+
# find the bricklet articles, which contains the balance data
|
190
|
+
articles = accounts_summary_document.xpath("//article").
|
191
|
+
select{|a| a.attributes["class"].value == "bricklet"}
|
192
|
+
|
193
|
+
@accounts = articles.map do |article|
|
194
|
+
prefix = article.attributes["id"].value.gsub("_bricklet", '')
|
195
|
+
name = article.xpath(".//a[@class='product_desc_link']").text
|
196
|
+
number = article.xpath(".//span[@id='#{prefix}_number']").text
|
197
|
+
balance = article.xpath(".//span[@id='#{prefix}_current_balance_amount']").text
|
198
|
+
credit = article.xpath(".//div[@id='#{prefix}_available_credit_amount']").text.split("\n").first
|
199
|
+
Account.new(:credit_card, name, number, balance, credit)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
~~~
|
203
|
+
|
204
|
+
Try to structure your specs so that likely scenarios of others who have accounts will also pass. In other words, if there's something you really want to test, like number of accounts retrieved, or types of accounts retrieved, place those as new options in the banks.yml file. Reference those in the let(:option { ... } blocks at the top of the specs. See capital_one_spec and capital_one_360_spec for examples.
|
205
|
+
|
206
|
+
Once you've implemented and tested, send me your PR. Don't check in your banks.yml nor your vcr_cassettes classes. Others who need to fix will just have to get their own credentials working -- at least for now. At some point, it would be nice to figure out how to version the cassettes without risking accidental inclusion of sensitive credentials. I'm thinking a before commit hook on git that checks the banks.yml file against the cassettes before making a commit or something along those lines.
|
207
|
+
|
208
|
+
Happy Banking!
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
begin
|
2
|
+
require 'bundler/setup'
|
3
|
+
rescue LoadError
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'rdoc/task'
|
8
|
+
require 'rspec/core'
|
9
|
+
require 'rspec/core/rake_task'
|
10
|
+
|
11
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
12
|
+
rdoc.rdoc_dir = 'rdoc'
|
13
|
+
rdoc.title = 'Dinero'
|
14
|
+
rdoc.options << '--line-numbers'
|
15
|
+
rdoc.rdoc_files.include('README.rdoc')
|
16
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
17
|
+
end
|
18
|
+
|
19
|
+
Bundler::GemHelper.install_tasks
|
20
|
+
|
21
|
+
desc "Run all specs in spec directory (excluding plugin specs)"
|
22
|
+
RSpec::Core::RakeTask.new(:spec)
|
23
|
+
|
24
|
+
task :default => :spec
|
data/dinero.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'dinero/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "dinero"
|
8
|
+
spec.version = Dinero::VERSION
|
9
|
+
spec.authors = ["Michael Lang"]
|
10
|
+
spec.email = ["mwlang@cybrains.net"]
|
11
|
+
spec.homepage = "https://github.com/mwlang/dinero"
|
12
|
+
spec.summary = "Dinero automates the process of logging into banking and financial websites to collect account balances."
|
13
|
+
spec.description = "Dinero automates the process of logging into banking and financial websites to collect account balances. For now, only account balances are collected. Planned to download transactions and download eStatements in the future"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
|
24
|
+
spec.add_development_dependency "rspec", "~> 3.2.0"
|
25
|
+
spec.add_development_dependency "rspec-its", "~> 1.2.0"
|
26
|
+
spec.add_development_dependency "vcr", "~> 2.9.3"
|
27
|
+
spec.add_development_dependency "webmock", "~> 1.21.0"
|
28
|
+
spec.add_development_dependency "simplecov", "~> 0.10.0"
|
29
|
+
spec.add_development_dependency "pry-byebug", "~> 3.1.0"
|
30
|
+
|
31
|
+
spec.add_dependency 'selenium-webdriver', "~> 2.45.0"
|
32
|
+
spec.add_dependency "nokogiri", "~> 1.6.6"
|
33
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require_relative '../lib/dinero'
|
2
|
+
|
3
|
+
args = ARGV.dup
|
4
|
+
while !args.empty? do
|
5
|
+
option = args.shift
|
6
|
+
if option =~ /\A\-\-/
|
7
|
+
value = args.shift
|
8
|
+
end
|
9
|
+
case option
|
10
|
+
when "--bank" then @bank = value
|
11
|
+
when "--user" then @username = value
|
12
|
+
when "--password" then @password = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def camelize term
|
17
|
+
string = term.to_s.sub(/^[a-z\d]*/) { $&.capitalize }
|
18
|
+
string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
|
19
|
+
string.gsub!(/\//, '::')
|
20
|
+
string
|
21
|
+
end
|
22
|
+
|
23
|
+
def show_balances bank
|
24
|
+
puts "Retrieving your bank account information..."
|
25
|
+
bank.accounts
|
26
|
+
|
27
|
+
puts "+" + "-" * 80 + "+"
|
28
|
+
puts "| %35s | %12s | %11s | %11s |" % %w(name number balance available)
|
29
|
+
puts "+" + "-" * 80 + "+"
|
30
|
+
bank.accounts.each do |acct|
|
31
|
+
puts "| %35s | %12s | $%10.2f | $%10.2f |" % [acct.name, acct.number, acct.balance, acct.available]
|
32
|
+
end
|
33
|
+
puts "+" + "-" * 80 + "+"
|
34
|
+
end
|
35
|
+
|
36
|
+
if @bank && @username
|
37
|
+
(print "enter password: "; @password = STDIN.noecho(&:gets).chomp; puts) unless @password
|
38
|
+
bank_class = eval("Dinero::Bank::#{camelize(@bank)}")
|
39
|
+
show_balances(bank_class.new(username: @username, password: @password))
|
40
|
+
else
|
41
|
+
puts <<-USAGE
|
42
|
+
usage: bundle exec ruby examples/get_balances.rb --bank <bank_name> --user <login_account_name> [--password <login_password>]
|
43
|
+
|
44
|
+
* bank_name needs to match one of the class_names supported in the lib/banks folder.
|
45
|
+
* if password omitted, you'll be prompted to supply one.
|
46
|
+
|
47
|
+
USAGE
|
48
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Dinero
|
2
|
+
class Account
|
3
|
+
attr_reader :account_type, :name, :name_other, :number, :balance, :available
|
4
|
+
def initialize account_type, name, number, balance, available
|
5
|
+
@account_type = account_type
|
6
|
+
name_parts = name.split("\n")
|
7
|
+
@name = name_parts.shift
|
8
|
+
@name_other = name_parts.join("\n")
|
9
|
+
@number = number
|
10
|
+
@balance = balance.scan(/[\d|\-|\.]+/).join.to_f
|
11
|
+
@available = available.scan(/[\d|\-|\.]+/).join.to_f
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Dinero
|
2
|
+
module Bank
|
3
|
+
class CapitalOne < Base
|
4
|
+
LOGIN_URL = "https://servicing.capitalone.com/c1/Login.aspx"
|
5
|
+
ACCOUNTS_SUMMARY_PATH = "/accounts"
|
6
|
+
CONNECTION_TIMEOUT = 10
|
7
|
+
|
8
|
+
def default_options
|
9
|
+
{ timeout: CONNECTION_TIMEOUT, login_url: LOGIN_URL }
|
10
|
+
end
|
11
|
+
|
12
|
+
def post_username!
|
13
|
+
connection.switch_to.frame "loginframe"
|
14
|
+
wait.until { connection.find_element(id: "uname") }
|
15
|
+
username_field = connection.find_element(id: "uname")
|
16
|
+
username_field.send_keys username
|
17
|
+
end
|
18
|
+
|
19
|
+
def post_password!
|
20
|
+
signin_form = connection.find_element(name: "login")
|
21
|
+
password_field = signin_form.find_element(id: "cofisso_ti_passw")
|
22
|
+
login_btn = signin_form.find_element(id: "cofisso_btn_login")
|
23
|
+
|
24
|
+
password_field.send_keys password
|
25
|
+
login_btn.click
|
26
|
+
end
|
27
|
+
|
28
|
+
def post_credentials!
|
29
|
+
post_username!
|
30
|
+
post_password!
|
31
|
+
end
|
32
|
+
|
33
|
+
def after_successful_login
|
34
|
+
# the subdomain frequently changes, so capture the actual URL
|
35
|
+
# so we can return to the page if necessary.
|
36
|
+
@accounts_summary_url = connection.current_url
|
37
|
+
end
|
38
|
+
|
39
|
+
def on_accounts_summary_page?
|
40
|
+
URI(connection.current_url).path == ACCOUNTS_SUMMARY_PATH
|
41
|
+
end
|
42
|
+
|
43
|
+
def goto_accounts_summary_page
|
44
|
+
return if authenticated? && on_accounts_summary_page?
|
45
|
+
authenticated? ? connection.navigate.to(@accounts_summary_url) : login!
|
46
|
+
end
|
47
|
+
|
48
|
+
# extract account data from the account summary page
|
49
|
+
def accounts
|
50
|
+
return @accounts if @accounts
|
51
|
+
|
52
|
+
# find the bricklet articles, which contains the balance data
|
53
|
+
articles = accounts_summary_document.xpath("//article").
|
54
|
+
select{|a| a.attributes["class"].value == "bricklet"}
|
55
|
+
|
56
|
+
@accounts = articles.map do |article|
|
57
|
+
prefix = article.attributes["id"].value.gsub("_bricklet", '')
|
58
|
+
name = article.xpath(".//a[@class='product_desc_link']").text
|
59
|
+
number = article.xpath(".//span[@id='#{prefix}_number']").text
|
60
|
+
balance = article.xpath(".//span[@id='#{prefix}_current_balance_amount']").text
|
61
|
+
credit = article.xpath(".//div[@id='#{prefix}_available_credit_amount']").text.split("\n").first
|
62
|
+
Account.new(:credit_card, name, number, balance, credit)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
module Dinero
|
2
|
+
module Bank
|
3
|
+
class CapitalOne360 < Base
|
4
|
+
LOGIN_URL = "https://secure.capitalone360.com/myaccount/banking/login.vm"
|
5
|
+
ACCOUNTS_SUMMARY_URL = "https://secure.capitalone360.com/myaccount/banking/account_summary.vm"
|
6
|
+
|
7
|
+
def default_options
|
8
|
+
{ login_url: LOGIN_URL }
|
9
|
+
end
|
10
|
+
|
11
|
+
def post_username!
|
12
|
+
begin
|
13
|
+
wait.until { connection.find_element(id: "Signin").displayed? }
|
14
|
+
rescue
|
15
|
+
connection.save_screenshot('log/capital_one_360_signin_failed.png')
|
16
|
+
raise
|
17
|
+
end
|
18
|
+
|
19
|
+
signin_form = connection.find_element(id: "Signin")
|
20
|
+
username_field = connection.find_element(id: "ACNID")
|
21
|
+
raise "Sign in Form not reached!" unless username_field && signin_form
|
22
|
+
|
23
|
+
username_field.send_keys username
|
24
|
+
signin_form.submit
|
25
|
+
end
|
26
|
+
|
27
|
+
def post_password!
|
28
|
+
begin
|
29
|
+
wait.until { connection.find_element(id: "PasswordForm").displayed? }
|
30
|
+
rescue
|
31
|
+
connection.save_screenshot('log/capital_one_360_password_failed.png')
|
32
|
+
raise
|
33
|
+
end
|
34
|
+
|
35
|
+
password_form = connection.find_element(id: "PasswordForm")
|
36
|
+
password_field = connection.find_element(id: "currentPassword_TLNPI")
|
37
|
+
submit_button = connection.find_element :css, ".bluebutton > a:nth-child(1)"
|
38
|
+
raise "Password Form not reached!" unless password_field && password_form
|
39
|
+
|
40
|
+
password_field.send_keys password
|
41
|
+
submit_button.click
|
42
|
+
end
|
43
|
+
|
44
|
+
def accounts_summary_page_fully_loaded?
|
45
|
+
tables = connection.find_elements css: 'table'
|
46
|
+
!(tables.empty? or tables.detect{|t| t.text =~ /\sLoading/})
|
47
|
+
end
|
48
|
+
|
49
|
+
def on_accounts_summary_page?
|
50
|
+
connection.current_url == ACCOUNTS_SUMMARY_URL
|
51
|
+
end
|
52
|
+
|
53
|
+
def goto_accounts_summary_page
|
54
|
+
return if authenticated? && on_accounts_summary_page?
|
55
|
+
authenticated? ? connection.navigate.to(ACCOUNTS_SUMMARY_URL) : login!
|
56
|
+
wait.until { accounts_summary_page_fully_loaded? }
|
57
|
+
end
|
58
|
+
|
59
|
+
def balance_row? row
|
60
|
+
row[1] =~ /Total/
|
61
|
+
end
|
62
|
+
|
63
|
+
def promo_table? table
|
64
|
+
table.empty? or table[0].empty? or table[0][0].empty?
|
65
|
+
end
|
66
|
+
|
67
|
+
def decipher_account_type title
|
68
|
+
return :credit_card if title =~ /Credit Cards/
|
69
|
+
return :brokerage if title =~ /ShareBuilder/
|
70
|
+
return :bank if title =~ /Checking/
|
71
|
+
return :unknown
|
72
|
+
end
|
73
|
+
|
74
|
+
def sanitize value
|
75
|
+
return unless value
|
76
|
+
value.split("\u00A0").first.strip
|
77
|
+
end
|
78
|
+
|
79
|
+
# extract account data from the account summary page
|
80
|
+
def accounts
|
81
|
+
return @accounts if @accounts
|
82
|
+
@accounts = []
|
83
|
+
|
84
|
+
# lots of spaces, tabs and the #00A0 characters, so extract
|
85
|
+
# text with this extraneous junk suppressed.
|
86
|
+
tables = accounts_summary_document.xpath("//table")
|
87
|
+
account_tables = tables.map do |table|
|
88
|
+
rows = table.xpath(".//tr").map{|row| row.xpath(".//td|.//th").
|
89
|
+
map{|cell| cell.text.strip.gsub(/\s+|\t/, " ")}}
|
90
|
+
end.reject{|table| promo_table? table }
|
91
|
+
|
92
|
+
# Turn tablular data into Account classes
|
93
|
+
account_tables.map do |table|
|
94
|
+
|
95
|
+
# the header row tells us what kind of account we're looking at
|
96
|
+
header = table.shift
|
97
|
+
account_type = decipher_account_type header[0]
|
98
|
+
has_account_number = header[1] =~ /Account/
|
99
|
+
|
100
|
+
# ignore balance rows at bottom of tables
|
101
|
+
rows = table.reject{|row| balance_row? row }
|
102
|
+
|
103
|
+
# turn those rows into accounts
|
104
|
+
rows.each do |row|
|
105
|
+
name = sanitize(row.shift)
|
106
|
+
number = (has_account_number ? sanitize(row.shift) : nil)
|
107
|
+
if number.nil? || name =~ /(\.{4})(\d+)\Z/
|
108
|
+
number = name.match(/(\.{4})(\d+)\Z/).captures.join
|
109
|
+
name = name.gsub(number,'')
|
110
|
+
end
|
111
|
+
balance = row.shift
|
112
|
+
available = row.shift || balance
|
113
|
+
@accounts << Account.new(account_type, name, number, balance, available)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
return @accounts
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
data/lib/dinero/banks.rb
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
module Dinero
|
2
|
+
module Bank
|
3
|
+
DEFAULT_TIMEOUT = 5
|
4
|
+
|
5
|
+
class Base
|
6
|
+
attr_reader :username, :password
|
7
|
+
attr_reader :timeout, :login_url
|
8
|
+
|
9
|
+
def initialize options
|
10
|
+
opts = default_options.merge options
|
11
|
+
|
12
|
+
@username = opts[:username]
|
13
|
+
@password = opts[:password]
|
14
|
+
@login_url = opts[:login_url]
|
15
|
+
@timeout = opts[:timeout] || DEFAULT_TIMEOUT
|
16
|
+
@authenticated = false
|
17
|
+
validate!
|
18
|
+
end
|
19
|
+
|
20
|
+
def validate!
|
21
|
+
raise "Must supply :username" if @username.to_s.empty?
|
22
|
+
raise "Must supply :password" if @password.to_s.empty?
|
23
|
+
raise "Must have a :login_url" if @login_url.to_s.empty?
|
24
|
+
end
|
25
|
+
|
26
|
+
def default_options
|
27
|
+
{}
|
28
|
+
end
|
29
|
+
|
30
|
+
def establish_connection
|
31
|
+
Selenium::WebDriver.for :phantomjs
|
32
|
+
end
|
33
|
+
|
34
|
+
def connection
|
35
|
+
@connection ||= establish_connection
|
36
|
+
end
|
37
|
+
|
38
|
+
def authenticated?
|
39
|
+
!!@authenticated
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait
|
43
|
+
@wait ||= Selenium::WebDriver::Wait.new(:timeout => timeout)
|
44
|
+
end
|
45
|
+
|
46
|
+
def accounts_summary_document
|
47
|
+
return @accounts_summary_document if @accounts_summary_document
|
48
|
+
|
49
|
+
goto_accounts_summary_page
|
50
|
+
@accounts_summary_document = Nokogiri::HTML connection.page_source
|
51
|
+
end
|
52
|
+
|
53
|
+
def after_successful_login
|
54
|
+
# NOP
|
55
|
+
end
|
56
|
+
|
57
|
+
def login!
|
58
|
+
return if authenticated?
|
59
|
+
begin
|
60
|
+
connection.navigate.to login_url
|
61
|
+
post_username!
|
62
|
+
post_password!
|
63
|
+
wait.until { on_accounts_summary_page? }
|
64
|
+
after_successful_login
|
65
|
+
@authenticated = true
|
66
|
+
rescue
|
67
|
+
connection.save_screenshot('log/#{self.to_s.downcase}_login_failure.png')
|
68
|
+
raise
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
data/lib/dinero.rb
ADDED
data/log/.keep
ADDED
File without changes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
if bank_configured? :capital_one_360
|
4
|
+
|
5
|
+
RSpec.describe Dinero::Bank::CapitalOne360 do
|
6
|
+
let(:bank_configuration) { bank_configurations[:capital_one_360] }
|
7
|
+
let(:account_types) { bank_configuration[:account_types].sort }
|
8
|
+
|
9
|
+
before(:all) do
|
10
|
+
VCR.use_cassette("accounts_capital_one_360") do
|
11
|
+
@bank = Dinero::Bank::CapitalOne360.new(bank_configurations[:capital_one_360])
|
12
|
+
@bank.accounts
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
it "has expected timeout" do
|
17
|
+
expect(@bank.timeout).to eq Dinero::Bank::DEFAULT_TIMEOUT
|
18
|
+
end
|
19
|
+
|
20
|
+
it "retrieves accounts" do
|
21
|
+
expect(@bank.accounts).to_not be_empty
|
22
|
+
end
|
23
|
+
|
24
|
+
it "extracts account names" do
|
25
|
+
expect(@bank.accounts.map(&:name)).to include /Checking|Savings/
|
26
|
+
end
|
27
|
+
|
28
|
+
it "does not include junk data in the names" do
|
29
|
+
@bank.accounts.each{|acct| expect(acct.name).to_not include "Opens a new window"}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "extracts account numbers" do
|
33
|
+
expect(@bank.accounts.map(&:number).select{|s| s.to_s.scan /\A\d+\Z/}).to_not be_empty
|
34
|
+
end
|
35
|
+
|
36
|
+
it "sets account types" do
|
37
|
+
expect(@bank.accounts.map(&:account_type).uniq.sort).to eq account_types
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
if bank_configured? :capital_one
|
4
|
+
|
5
|
+
RSpec.describe Dinero::Bank::CapitalOne do
|
6
|
+
let(:bank_configuration) { bank_configurations[:capital_one] }
|
7
|
+
let(:account_types) { bank_configuration[:account_types].sort }
|
8
|
+
let(:accounts) { bank_configuration[:accounts] }
|
9
|
+
|
10
|
+
before(:all) do
|
11
|
+
VCR.use_cassette("accounts_capital_one") do
|
12
|
+
@bank = Dinero::Bank::CapitalOne.new(bank_configurations[:capital_one])
|
13
|
+
@bank.accounts
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "has expected timeout" do
|
18
|
+
expect(@bank.timeout).to eq Dinero::Bank::CapitalOne::CONNECTION_TIMEOUT
|
19
|
+
end
|
20
|
+
|
21
|
+
it "authenticates" do
|
22
|
+
@bank.login!
|
23
|
+
expect(@bank.authenticated?).to eq true
|
24
|
+
end
|
25
|
+
|
26
|
+
it "retrieves accounts_summary_document" do
|
27
|
+
expect(@bank.accounts_summary_document).to be_kind_of Nokogiri::HTML::Document
|
28
|
+
end
|
29
|
+
|
30
|
+
it "has article sections" do
|
31
|
+
expect(@bank.accounts_summary_document.xpath("//article").size).to eq accounts + 1
|
32
|
+
end
|
33
|
+
|
34
|
+
it "gets expected accounts" do
|
35
|
+
expect(@bank.accounts.size).to eq accounts
|
36
|
+
end
|
37
|
+
|
38
|
+
it "extracts account names" do
|
39
|
+
expect(@bank.accounts.map(&:name)).to include /MasterCard|Visa/
|
40
|
+
end
|
41
|
+
|
42
|
+
it "extracts account numbers" do
|
43
|
+
expect(@bank.accounts.map(&:number).select{|s| s.to_s.scan /\A[\.|\d]+\Z/}).to_not be_empty
|
44
|
+
end
|
45
|
+
|
46
|
+
it "sets account types" do
|
47
|
+
expect(@bank.accounts.map(&:account_type).uniq).to eq account_types
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
File without changes
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
|
3
|
+
SimpleCov.profiles.define 'dinero' do
|
4
|
+
add_filter '/spec/'
|
5
|
+
end
|
6
|
+
|
7
|
+
SimpleCov.start 'dinero'
|
8
|
+
|
9
|
+
require 'rspec'
|
10
|
+
require 'rspec/its'
|
11
|
+
require 'vcr'
|
12
|
+
|
13
|
+
VCR.configure do |config|
|
14
|
+
config.cassette_library_dir = "spec/fixtures/vcr_cassettes"
|
15
|
+
config.hook_into :webmock
|
16
|
+
config.debug_logger = File.open('log/vcr.log', 'w')
|
17
|
+
config.ignore_request do |request|
|
18
|
+
URI(request.uri).path == "/shutdown"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
require_relative '../lib/dinero'
|
23
|
+
|
24
|
+
RSpec.configure do |config|
|
25
|
+
config.expect_with :rspec do |expectations|
|
26
|
+
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
|
27
|
+
end
|
28
|
+
|
29
|
+
config.mock_with :rspec do |mocks|
|
30
|
+
mocks.verify_partial_doubles = true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
Dir["./spec/support/**/*.rb"].sort.each { |f| require f }
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
|
3
|
+
def symbolized_keys hash
|
4
|
+
hash.keys.each do |key|
|
5
|
+
hash[(key.to_sym rescue key) || key] = hash.delete(key)
|
6
|
+
end
|
7
|
+
hash.each_pair{|k,v| hash[k] = symbolized_keys(v) if v.is_a?(Hash)}
|
8
|
+
return hash
|
9
|
+
end
|
10
|
+
|
11
|
+
def bank_configurations
|
12
|
+
config_path = File.expand_path(File.join(File.dirname(__FILE__), '..', 'config'))
|
13
|
+
@bank_configurations ||= symbolized_keys YAML::load_file(File.join(config_path, 'banks.yml'))
|
14
|
+
end
|
15
|
+
|
16
|
+
def bank_configured? bank
|
17
|
+
!!bank_configurations[bank.to_sym]
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dinero
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Michael Lang
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-05-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 3.2.0
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.2.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec-its
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.2.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 1.2.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: vcr
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.9.3
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.9.3
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: webmock
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.21.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.21.0
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.10.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.10.0
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: pry-byebug
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 3.1.0
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 3.1.0
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: selenium-webdriver
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 2.45.0
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 2.45.0
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: nokogiri
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 1.6.6
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: 1.6.6
|
153
|
+
description: Dinero automates the process of logging into banking and financial websites
|
154
|
+
to collect account balances. For now, only account balances are collected. Planned
|
155
|
+
to download transactions and download eStatements in the future
|
156
|
+
email:
|
157
|
+
- mwlang@cybrains.net
|
158
|
+
executables: []
|
159
|
+
extensions: []
|
160
|
+
extra_rdoc_files: []
|
161
|
+
files:
|
162
|
+
- ".gitignore"
|
163
|
+
- Gemfile
|
164
|
+
- Gemfile.lock
|
165
|
+
- MIT-LICENSE
|
166
|
+
- README.md
|
167
|
+
- Rakefile
|
168
|
+
- dinero.gemspec
|
169
|
+
- examples/get_balances.rb
|
170
|
+
- lib/dinero.rb
|
171
|
+
- lib/dinero/account.rb
|
172
|
+
- lib/dinero/banks.rb
|
173
|
+
- lib/dinero/banks/capital_one.rb
|
174
|
+
- lib/dinero/banks/capital_one_360.rb
|
175
|
+
- lib/dinero/version.rb
|
176
|
+
- log/.keep
|
177
|
+
- spec/banks/capital_one_360_spec.rb
|
178
|
+
- spec/banks/capital_one_spec.rb
|
179
|
+
- spec/fixtures/vcr_cassettes/.keep
|
180
|
+
- spec/spec_helper.rb
|
181
|
+
- spec/support/config.rb
|
182
|
+
homepage: https://github.com/mwlang/dinero
|
183
|
+
licenses:
|
184
|
+
- MIT
|
185
|
+
metadata: {}
|
186
|
+
post_install_message:
|
187
|
+
rdoc_options: []
|
188
|
+
require_paths:
|
189
|
+
- lib
|
190
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
191
|
+
requirements:
|
192
|
+
- - ">="
|
193
|
+
- !ruby/object:Gem::Version
|
194
|
+
version: '0'
|
195
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - ">="
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: '0'
|
200
|
+
requirements: []
|
201
|
+
rubyforge_project:
|
202
|
+
rubygems_version: 2.4.6
|
203
|
+
signing_key:
|
204
|
+
specification_version: 4
|
205
|
+
summary: Dinero automates the process of logging into banking and financial websites
|
206
|
+
to collect account balances.
|
207
|
+
test_files:
|
208
|
+
- spec/banks/capital_one_360_spec.rb
|
209
|
+
- spec/banks/capital_one_spec.rb
|
210
|
+
- spec/fixtures/vcr_cassettes/.keep
|
211
|
+
- spec/spec_helper.rb
|
212
|
+
- spec/support/config.rb
|