aggcat 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,15 +1,13 @@
1
1
  source 'https://rubygems.org'
2
2
 
3
- gem 'oauth', '~> 0.4'
4
- gem 'nori', '~> 2.0'
5
- gem 'nokogiri', '~> 1.5'
6
- gem 'active_support', '~> 3.0'
7
- gem 'builder', '~> 3.2'
8
3
  gem 'rake'
9
4
 
10
5
  group :test do
11
6
  gem 'minitest'
12
7
  gem 'test-unit'
13
8
  gem 'simplecov', :require => false
9
+ gem 'coveralls', :require => false
14
10
  gem 'webmock'
15
11
  end
12
+
13
+ gemspec
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Aggcat
2
2
  [![Build Status](https://travis-ci.org/cloocher/aggcat.png)](https://travis-ci.org/cloocher/aggcat)
3
+ [![Coverage Status](https://coveralls.io/repos/cloocher/aggcat/badge.png?branch=master)](https://coveralls.io/r/cloocher/aggcat)
4
+ [![Gem Version](https://badge.fury.io/rb/aggcat.png)](http://badge.fury.io/rb/aggcat)
3
5
 
4
6
  Intuit Customer Account Data API client
5
7
 
@@ -78,6 +80,10 @@ Aggcat.delete_customer
78
80
 
79
81
  Please make sure to read Intuit's [Account Data API](http://docs.developer.intuit.com/0020_Aggregation_Categorization_Apps/AggCat_API/0020_API_Documentation) docs.
80
82
 
83
+ ## Requirements
84
+
85
+ * Ruby 1.9.2 or higher
86
+
81
87
  ## Copyright
82
88
  Copyright (c) 2013 Gene Drabkin.
83
89
  See [LICENSE][] for details.
data/lib/aggcat/base.rb CHANGED
@@ -23,8 +23,6 @@ module Aggcat
23
23
 
24
24
  TIMEOUT = 120
25
25
 
26
- IGNORE_KEYS = Set.new([:'@xmlns', :'@xmlns:ns2', :'@xmlns:ns3', :'@xmlns:ns4', :'@xmlns:ns5', :'@xmlns:ns6', :'@xmlns:ns7', :'@xmlns:ns8', :'@xmlns:ns9'])
27
-
28
26
  protected
29
27
 
30
28
  def oauth_client
@@ -61,7 +59,7 @@ module Aggcat
61
59
  def saml_message(user_id)
62
60
  now = Time.now.utc
63
61
  reference_id = SecureRandom.uuid.gsub('-', '')
64
- assertion = %[<?xml version="1.0" encoding="UTF-8"?><saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{reference_id}" IssueInstant="#{iso8601(now)}" Version="2.0"><saml2:Issuer>#{@issuer_id}</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ds:Reference URI="#_#{reference_id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>%%DIGEST%%</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>%%SIGNATURE%%</ds:SignatureValue></ds:Signature><saml2:Subject><saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">#{user_id}</saml2:NameID><saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/></saml2:Subject><saml2:Conditions NotBefore="#{iso8601(now-5*60)}" NotOnOrAfter="#{iso8601(now+10*60)}"><saml2:AudienceRestriction><saml2:Audience>#{@issuer_id}</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions><saml2:AuthnStatement AuthnInstant="#{iso8601(now)}" SessionIndex="_#{reference_id}"><saml2:AuthnContext><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef></saml2:AuthnContext></saml2:AuthnStatement></saml2:Assertion>]
62
+ assertion = %[<?xml version="1.0" encoding="UTF-8"?><saml2:Assertion xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_#{reference_id}" IssueInstant="#{iso8601(now)}" Version="2.0"><saml2:Issuer>#{@issuer_id}</saml2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><ds:Reference URI="#_#{reference_id}"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><ds:DigestValue>%s</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>%s</ds:SignatureValue></ds:Signature><saml2:Subject><saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">#{user_id}</saml2:NameID><saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer"/></saml2:Subject><saml2:Conditions NotBefore="#{iso8601(now-5*60)}" NotOnOrAfter="#{iso8601(now+10*60)}"><saml2:AudienceRestriction><saml2:Audience>#{@issuer_id}</saml2:Audience></saml2:AudienceRestriction></saml2:Conditions><saml2:AuthnStatement AuthnInstant="#{iso8601(now)}" SessionIndex="_#{reference_id}"><saml2:AuthnContext><saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef></saml2:AuthnContext></saml2:AuthnStatement></saml2:Assertion>]
65
63
  doc = Nokogiri::XML(assertion)
66
64
  doc.xpath('//ds:Signature', 'ds' => 'http://www.w3.org/2000/09/xmldsig#').remove
67
65
  doc.xpath('//text()[not(normalize-space())]').remove
@@ -71,7 +69,7 @@ module Aggcat
71
69
  signature_value = Nokogiri::XML(signed_info).canonicalize
72
70
  key = OpenSSL::PKey::RSA.new(File.read(@certificate_path))
73
71
  encoded_signature_value = Base64.encode64(key.sign(OpenSSL::Digest::SHA1.new, signature_value)).gsub!(/\n/, '')
74
- Base64.encode64(assertion.gsub(/%%DIGEST%%/, encoded_digest).gsub(/%%SIGNATURE%%/, encoded_signature_value))
72
+ Base64.encode64(assertion % [encoded_digest, encoded_signature_value])
75
73
  end
76
74
 
77
75
  def iso8601(time)
@@ -88,7 +86,7 @@ module Aggcat
88
86
 
89
87
  def cleanup(hash)
90
88
  hash.each do |k, v|
91
- if IGNORE_KEYS.include?(k)
89
+ if k.to_s[/^@xmlns/]
92
90
  hash.delete(k)
93
91
  elsif v.respond_to?(:keys)
94
92
  cleanup(v)
@@ -1,3 +1,3 @@
1
1
  module Aggcat
2
- VERSION = '0.1.0'
2
+ VERSION = '0.1.1'
3
3
  end
@@ -0,0 +1,54 @@
1
+ require 'test_helper'
2
+
3
+ class AggcatTest < Test::Unit::TestCase
4
+ def setup
5
+ Aggcat.configure do |config|
6
+ config.issuer_id = 'issuer_id'
7
+ config.consumer_key = 'consumer_key'
8
+ config.consumer_secret = 'consumer_secret'
9
+ config.certificate_path = "#{fixture_path}/cert.key"
10
+ end
11
+ end
12
+
13
+ def test_configure
14
+ configurable = Aggcat.configure do |config|
15
+ config.issuer_id = 'issuer_id'
16
+ config.consumer_key = 'consumer_key'
17
+ config.consumer_secret = 'consumer_secret'
18
+ config.certificate_path = "#{fixture_path}/cert.key"
19
+ end
20
+ assert_equal 'issuer_id', configurable.instance_variable_get(:'@issuer_id')
21
+ assert_equal 'consumer_key', configurable.instance_variable_get(:'@consumer_key')
22
+ assert_equal 'consumer_secret', configurable.instance_variable_get(:'@consumer_secret')
23
+ assert_equal "#{fixture_path}/cert.key", configurable.instance_variable_get(:'@certificate_path')
24
+ end
25
+
26
+ def test_scope
27
+ client1 = Aggcat.scope('1')
28
+ assert_true client1.is_a?(Aggcat::Client)
29
+ assert_equal 'issuer_id', client1.instance_variable_get(:'@issuer_id')
30
+ assert_equal 'consumer_key', client1.instance_variable_get(:'@consumer_key')
31
+ assert_equal 'consumer_secret', client1.instance_variable_get(:'@consumer_secret')
32
+ assert_equal "#{fixture_path}/cert.key", client1.instance_variable_get(:'@certificate_path')
33
+ assert_equal '1', client1.instance_variable_get(:'@customer_id')
34
+ client2 = Aggcat.client
35
+ assert_equal client1, client2
36
+ client3 = Aggcat.scope('1')
37
+ assert_equal client1, client3
38
+ client4 = Aggcat.scope('2')
39
+ assert_not_equal client1, client4
40
+ end
41
+
42
+ def test_no_scope
43
+ exception = assert_raise(ArgumentError) { Aggcat.scope(nil) }
44
+ assert_equal('customer_id is required for scoping all requests', exception.message)
45
+ end
46
+
47
+ def test_client_api
48
+ stub_request(:post, Aggcat::Base::SAML_URL).to_return(:status => 200, :body => fixture('oauth_token.txt'))
49
+ Aggcat.scope('1')
50
+ stub_get('/institutions').to_return(:body => fixture('institutions.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
51
+ response = Aggcat.institutions
52
+ assert_equal response[:response][:institutions][:institution][0][:institution_id].to_i, 100000
53
+ end
54
+ end
@@ -27,9 +27,10 @@ class ClientTest < Test::Unit::TestCase
27
27
  assert_equal institution_id, response[:response][:institution_detail][:institution_id]
28
28
  end
29
29
 
30
- def test_institution_with_bad_id
31
- institution_id = '100000'
32
- stub_get("/institutions/#{institution_id}").to_return(:body => fixture('institution.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
30
+ def test_institution_with_bad_args
31
+ exception = assert_raise(ArgumentError) { @client.institution(nil) }
32
+ assert_equal('institution_id is required', exception.message)
33
+
33
34
  exception = assert_raise(ArgumentError) { @client.institution('') }
34
35
  assert_equal('institution_id is required', exception.message)
35
36
  end
@@ -43,6 +44,35 @@ class ClientTest < Test::Unit::TestCase
43
44
  assert_equal '000000000001', response[:response][:account_list][:banking_account][:account_id]
44
45
  end
45
46
 
47
+ def test_discover_and_add_accounts_with_challenge
48
+ institution_id = '100000'
49
+ stub_get("/institutions/#{institution_id}").to_return(:body => fixture('institution.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
50
+ stub_post("/institutions/#{institution_id}/logins").to_return(:code => 401, :body => fixture('account.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
51
+ response = @client.discover_and_add_accounts(institution_id, 'username', 'password')
52
+ assert_equal institution_id, response[:response][:account_list][:banking_account][:institution_id]
53
+ assert_equal '000000000001', response[:response][:account_list][:banking_account][:account_id]
54
+ end
55
+
56
+ def test_discover_and_add_accounts_bad_args
57
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts(nil, 'username', 'password') }
58
+ assert_equal('institution_id is required', exception.message)
59
+
60
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts('', 'username', 'password') }
61
+ assert_equal('institution_id is required', exception.message)
62
+
63
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts(1, nil, 'password') }
64
+ assert_equal('username is required', exception.message)
65
+
66
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts(1, '', 'password') }
67
+ assert_equal('username is required', exception.message)
68
+
69
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts(1, 'username', nil) }
70
+ assert_equal('password is required', exception.message)
71
+
72
+ exception = assert_raise(ArgumentError) { @client.discover_and_add_accounts(1, 'username', '') }
73
+ assert_equal('password is required', exception.message)
74
+ end
75
+
46
76
  def test_account
47
77
  account_id = '000000000001'
48
78
  stub_get("/accounts/#{account_id}").to_return(:body => fixture('account.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
@@ -50,6 +80,14 @@ class ClientTest < Test::Unit::TestCase
50
80
  assert_equal account_id, response[:response][:account_list][:banking_account][:account_id]
51
81
  end
52
82
 
83
+ def test_account_bad_args
84
+ exception = assert_raise(ArgumentError) { @client.account(nil) }
85
+ assert_equal('account_id is required', exception.message)
86
+
87
+ exception = assert_raise(ArgumentError) { @client.account('') }
88
+ assert_equal('account_id is required', exception.message)
89
+ end
90
+
53
91
  def test_accounts
54
92
  stub_get('/accounts').to_return(:body => fixture('accounts.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
55
93
  response = @client.accounts
@@ -62,8 +100,36 @@ class ClientTest < Test::Unit::TestCase
62
100
  start_date = Date.today - 30
63
101
  uri = "/accounts/#{account_id}/transactions?txnStartDate=#{start_date.strftime(Aggcat::Base::DATE_FORMAT)}"
64
102
  stub_get(uri).to_return(:body => fixture('transactions.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
65
- response = @client.account_transactions(account_id, Date.today - 30)
103
+ response = @client.account_transactions(account_id, start_date)
104
+ assert_equal '75000088503', response[:response][:transaction_list][:credit_card_transaction][:id]
105
+ end
106
+
107
+ def test_account_transactions_with_dates
108
+ account_id = '000000000001'
109
+ end_date = Date.today
110
+ start_date = end_date - 30
111
+ challenge_session_id = '1234'
112
+ challenge_node_id = '4321'
113
+ uri = "/accounts/#{account_id}/transactions?txnStartDate=#{start_date.strftime(Aggcat::Base::DATE_FORMAT)}&txnEndDate=#{end_date.strftime(Aggcat::Base::DATE_FORMAT)}"
114
+ stub_get(uri).to_return(:body => fixture('transactions.xml'), :headers => {:content_type => 'application/xml; charset=utf-8', :challengeSessionId => challenge_session_id, :challengeNodeId => challenge_node_id})
115
+ response = @client.account_transactions(account_id, start_date, end_date)
66
116
  assert_equal '75000088503', response[:response][:transaction_list][:credit_card_transaction][:id]
117
+ assert_equal response[:challenge_session_id], challenge_session_id
118
+ assert_equal response[:challenge_node_id], challenge_node_id
119
+ end
120
+
121
+ def test_account_transactions_bad_args
122
+ exception = assert_raise(ArgumentError) { @client.account_transactions(nil, Date.today) }
123
+ assert_equal('account_id is required', exception.message)
124
+
125
+ exception = assert_raise(ArgumentError) { @client.account_transactions('', Date.today) }
126
+ assert_equal('account_id is required', exception.message)
127
+
128
+ exception = assert_raise(ArgumentError) { @client.account_transactions(1, nil) }
129
+ assert_equal('start_date is required', exception.message)
130
+
131
+ exception = assert_raise(ArgumentError) { @client.account_transactions(1, '') }
132
+ assert_equal('start_date is required', exception.message)
67
133
  end
68
134
 
69
135
  def test_delete_account
@@ -73,6 +139,14 @@ class ClientTest < Test::Unit::TestCase
73
139
  assert_equal '200', response[:response_code]
74
140
  end
75
141
 
142
+ def test_delete_account_bad_args
143
+ exception = assert_raise(ArgumentError) { @client.delete_account(nil) }
144
+ assert_equal('account_id is required', exception.message)
145
+
146
+ exception = assert_raise(ArgumentError) { @client.delete_account('') }
147
+ assert_equal('account_id is required', exception.message)
148
+ end
149
+
76
150
  def test_delete_customer
77
151
  stub_delete('/customers').to_return(:status => 200)
78
152
  response = @client.delete_customer
@@ -88,4 +162,57 @@ class ClientTest < Test::Unit::TestCase
88
162
  assert_equal '200', response[:response_code]
89
163
  end
90
164
 
165
+ def test_update_login_bad_args
166
+ exception = assert_raise(ArgumentError) { @client.update_login(nil, 1, 'username', 'password') }
167
+ assert_equal('institution_id is required', exception.message)
168
+
169
+ exception = assert_raise(ArgumentError) { @client.update_login('', 1, 'username', 'password') }
170
+ assert_equal('institution_id is required', exception.message)
171
+
172
+ exception = assert_raise(ArgumentError) { @client.update_login(1, nil, 'username', 'password') }
173
+ assert_equal('login_id is required', exception.message)
174
+
175
+ exception = assert_raise(ArgumentError) { @client.update_login(1, '', 'username', 'password') }
176
+ assert_equal('login_id is required', exception.message)
177
+
178
+ exception = assert_raise(ArgumentError) { @client.update_login(1, 1, nil, 'password') }
179
+ assert_equal('username is required', exception.message)
180
+
181
+ exception = assert_raise(ArgumentError) { @client.update_login(1, 1, '', 'password') }
182
+ assert_equal('username is required', exception.message)
183
+
184
+ exception = assert_raise(ArgumentError) { @client.update_login(1, 1, 'username', nil) }
185
+ assert_equal('password is required', exception.message)
186
+
187
+ exception = assert_raise(ArgumentError) { @client.update_login(1, 1, 'username', '') }
188
+ assert_equal('password is required', exception.message)
189
+ end
190
+
191
+
192
+ def test_account_confirmation
193
+ institution_id = '100000'
194
+ challenge_session_id = '1234'
195
+ challenge_node_id = '4321'
196
+ answer = 'answer'
197
+ parser = Nori.new(:parser => :nokogiri, :strip_namespaces => true, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym })
198
+ stub_get("/institutions/#{institution_id}").to_return(:body => fixture('institution.xml'), :headers => {:content_type => 'application/xml; charset=utf-8'})
199
+ stub_post("/institutions/#{institution_id}/logins").to_return(:body => lambda { |request| assert_equal(parser.parse(fixture('challenge.xml').read), parser.parse(request.body)) })
200
+ @client.account_confirmation(institution_id, challenge_session_id, challenge_node_id, answer)
201
+ end
202
+
203
+ def test_update_login_confirmation
204
+ login_id = '1234567890'
205
+ challenge_session_id = '1234'
206
+ challenge_node_id = '4321'
207
+ answer = 'answer'
208
+ validator = lambda do |request|
209
+ parser = Nori.new(:parser => :nokogiri, :strip_namespaces => true, :convert_tags_to => lambda { |tag| tag.snakecase.to_sym })
210
+ assert_equal(parser.parse(fixture('challenge.xml').read), parser.parse(request.body))
211
+ assert_equal(challenge_session_id, request.headers['Challengesessionid'])
212
+ assert_equal(challenge_node_id, request.headers['Challengenodeid'])
213
+ end
214
+ stub_put("/logins/#{login_id}?refresh=true").to_return(:body => validator)
215
+ @client.update_login_confirmation(login_id, challenge_session_id, challenge_node_id, answer)
216
+ end
217
+
91
218
  end
@@ -0,0 +1,5 @@
1
+ <InstitutionLogin xmlns:v1="http://schema.intuit.com/platform/fdatafeed/institutionlogin/v1">
2
+ <challengeResponses>
3
+ <response xmlns:v11="http://schema.intuit.com/platform/fdatafeed/challenge/v1">answer</response>
4
+ </challengeResponses>
5
+ </InstitutionLogin>
data/test/test_helper.rb CHANGED
@@ -2,12 +2,20 @@ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  $LOAD_PATH.unshift(File.dirname(__FILE__))
3
3
 
4
4
  require 'simplecov'
5
+ require 'coveralls'
6
+
7
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
8
+ SimpleCov::Formatter::HTMLFormatter,
9
+ Coveralls::SimpleCov::Formatter
10
+ ]
5
11
  SimpleCov.start
6
12
 
7
13
  require 'webmock/test_unit'
8
14
  require 'test/unit'
9
15
  require 'aggcat'
10
16
 
17
+ WebMock.disable_net_connect!(:allow => 'coveralls.io')
18
+
11
19
  def stub_delete(path)
12
20
  stub_request(:delete, Aggcat::Client::BASE_URL + path)
13
21
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aggcat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-04-01 00:00:00.000000000 Z
12
+ date: 2013-04-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: oauth
@@ -142,10 +142,12 @@ files:
142
142
  - lib/aggcat/client.rb
143
143
  - lib/aggcat/configurable.rb
144
144
  - lib/aggcat/version.rb
145
+ - test/aggcat/aggcat_test.rb
145
146
  - test/aggcat/client_test.rb
146
147
  - test/fixtures/account.xml
147
148
  - test/fixtures/accounts.xml
148
149
  - test/fixtures/cert.key
150
+ - test/fixtures/challenge.xml
149
151
  - test/fixtures/institution.xml
150
152
  - test/fixtures/institutions.xml
151
153
  - test/fixtures/login.xml
@@ -178,10 +180,12 @@ signing_key:
178
180
  specification_version: 3
179
181
  summary: Intuit Customer Account Data API client
180
182
  test_files:
183
+ - test/aggcat/aggcat_test.rb
181
184
  - test/aggcat/client_test.rb
182
185
  - test/fixtures/account.xml
183
186
  - test/fixtures/accounts.xml
184
187
  - test/fixtures/cert.key
188
+ - test/fixtures/challenge.xml
185
189
  - test/fixtures/institution.xml
186
190
  - test/fixtures/institutions.xml
187
191
  - test/fixtures/login.xml