aggcat 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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