maestrano 0.1.0

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.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +34 -0
  3. data/Gemfile +2 -0
  4. data/Gemfile.lock +43 -0
  5. data/LICENSE +21 -0
  6. data/README.md +4 -0
  7. data/Rakefile +32 -0
  8. data/bin/maestrano-console +9 -0
  9. data/lib/maestrano.rb +114 -0
  10. data/lib/maestrano/account/bill.rb +14 -0
  11. data/lib/maestrano/api/error/authentication_error.rb +8 -0
  12. data/lib/maestrano/api/error/base_error.rb +24 -0
  13. data/lib/maestrano/api/error/connection_error.rb +8 -0
  14. data/lib/maestrano/api/error/invalid_request_error.rb +14 -0
  15. data/lib/maestrano/api/list_object.rb +37 -0
  16. data/lib/maestrano/api/object.rb +187 -0
  17. data/lib/maestrano/api/operation/base.rb +216 -0
  18. data/lib/maestrano/api/operation/create.rb +18 -0
  19. data/lib/maestrano/api/operation/delete.rb +13 -0
  20. data/lib/maestrano/api/operation/list.rb +18 -0
  21. data/lib/maestrano/api/operation/update.rb +59 -0
  22. data/lib/maestrano/api/resource.rb +39 -0
  23. data/lib/maestrano/api/util.rb +121 -0
  24. data/lib/maestrano/saml/attribute_value.rb +15 -0
  25. data/lib/maestrano/saml/metadata.rb +64 -0
  26. data/lib/maestrano/saml/request.rb +93 -0
  27. data/lib/maestrano/saml/response.rb +201 -0
  28. data/lib/maestrano/saml/schemas/saml20assertion_schema.xsd +283 -0
  29. data/lib/maestrano/saml/schemas/saml20protocol_schema.xsd +302 -0
  30. data/lib/maestrano/saml/schemas/xenc_schema.xsd +146 -0
  31. data/lib/maestrano/saml/schemas/xmldsig_schema.xsd +318 -0
  32. data/lib/maestrano/saml/settings.rb +37 -0
  33. data/lib/maestrano/saml/validation_error.rb +7 -0
  34. data/lib/maestrano/sso.rb +81 -0
  35. data/lib/maestrano/sso/base_group.rb +31 -0
  36. data/lib/maestrano/sso/base_user.rb +75 -0
  37. data/lib/maestrano/sso/group.rb +24 -0
  38. data/lib/maestrano/sso/session.rb +63 -0
  39. data/lib/maestrano/sso/user.rb +34 -0
  40. data/lib/maestrano/version.rb +3 -0
  41. data/lib/maestrano/xml_security/signed_document.rb +170 -0
  42. data/maestrano.gemspec +32 -0
  43. data/test/helpers/api_helpers.rb +82 -0
  44. data/test/helpers/saml_helpers.rb +62 -0
  45. data/test/maestrano/account/bill_test.rb +48 -0
  46. data/test/maestrano/api/list_object_test.rb +20 -0
  47. data/test/maestrano/api/object_test.rb +28 -0
  48. data/test/maestrano/api/resource_test.rb +343 -0
  49. data/test/maestrano/api/util_test.rb +31 -0
  50. data/test/maestrano/maestrano_test.rb +49 -0
  51. data/test/maestrano/saml/request_test.rb +168 -0
  52. data/test/maestrano/saml/response_test.rb +290 -0
  53. data/test/maestrano/saml/settings_test.rb +51 -0
  54. data/test/maestrano/sso/base_group_test.rb +54 -0
  55. data/test/maestrano/sso/base_user_test.rb +114 -0
  56. data/test/maestrano/sso/group_test.rb +47 -0
  57. data/test/maestrano/sso/session_test.rb +108 -0
  58. data/test/maestrano/sso/user_test.rb +65 -0
  59. data/test/maestrano/sso_test.rb +81 -0
  60. data/test/maestrano/xml_security/signed_document.rb +163 -0
  61. data/test/support/saml/certificates/certificate1 +12 -0
  62. data/test/support/saml/certificates/r1_certificate2_base64 +1 -0
  63. data/test/support/saml/responses/adfs_response_sha1.xml +46 -0
  64. data/test/support/saml/responses/adfs_response_sha256.xml +46 -0
  65. data/test/support/saml/responses/adfs_response_sha384.xml +46 -0
  66. data/test/support/saml/responses/adfs_response_sha512.xml +46 -0
  67. data/test/support/saml/responses/no_signature_ns.xml +48 -0
  68. data/test/support/saml/responses/open_saml_response.xml +56 -0
  69. data/test/support/saml/responses/r1_response6.xml.base64 +1 -0
  70. data/test/support/saml/responses/response1.xml.base64 +1 -0
  71. data/test/support/saml/responses/response2.xml.base64 +79 -0
  72. data/test/support/saml/responses/response3.xml.base64 +66 -0
  73. data/test/support/saml/responses/response4.xml.base64 +93 -0
  74. data/test/support/saml/responses/response5.xml.base64 +102 -0
  75. data/test/support/saml/responses/response_with_ampersands.xml +139 -0
  76. data/test/support/saml/responses/response_with_ampersands.xml.base64 +93 -0
  77. data/test/support/saml/responses/response_with_multiple_attribute_values.xml +57 -0
  78. data/test/support/saml/responses/simple_saml_php.xml +71 -0
  79. data/test/support/saml/responses/starfield_response.xml.base64 +1 -0
  80. data/test/support/saml/responses/wrapped_response_2.xml.base64 +150 -0
  81. data/test/test_helper.rb +46 -0
  82. metadata +305 -0
@@ -0,0 +1,32 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
3
+ require 'maestrano/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'maestrano'
7
+ s.version = Maestrano::VERSION
8
+ s.summary = 'Ruby bindings for the Maestrano API'
9
+ s.description = 'Maestrano is the next generation marketplace for SME applications. See https://maestrano.com for details.'
10
+ s.authors = ['Arnaud Lachaume']
11
+ s.email = ['arnaud.lachaume@maestrano.com']
12
+ s.homepage = 'https://maestrano.com'
13
+ s.license = 'MIT'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- test/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ['lib']
19
+
20
+ s.add_dependency('rest-client', '~> 1.4')
21
+ s.add_dependency('mime-types', '~> 1.25')
22
+ s.add_dependency('json', '~> 1.8')
23
+
24
+ s.add_development_dependency('test-unit', '~> 2')
25
+ s.add_development_dependency('mocha', '~> 0.13')
26
+ s.add_development_dependency('shoulda', '~> 2.11')
27
+ s.add_development_dependency('timecop', '<= 0.6.0')
28
+ s.add_development_dependency('rake', '~> 10')
29
+
30
+ s.add_runtime_dependency("uuid", ["~> 2.3"])
31
+ s.add_runtime_dependency("nokogiri", [">= 1.5.0"])
32
+ end
@@ -0,0 +1,82 @@
1
+ module APITestHelper
2
+ def test_response(body={}, code=200)
3
+ # When an exception is raised, restclient clobbers method_missing. Hence we
4
+ # can't just use the stubs interface.
5
+ body = JSON.generate(body) if !(body.kind_of? String)
6
+ m = mock
7
+ m.instance_variable_set('@resp_values', { :body => body, :code => code })
8
+ def m.body; @resp_values[:body]; end
9
+ def m.code; @resp_values[:code]; end
10
+ m
11
+ end
12
+
13
+ def test_account_bill_content(params={})
14
+ {
15
+ object: 'account_bill',
16
+ id: 'bill-1',
17
+ group_id: 'cld-1',
18
+ created_at: Time.now.utc.iso8601,
19
+ price_cents: 2300,
20
+ status: 'submitted',
21
+ currency: 'AUD',
22
+ units: 1,
23
+ description: 'Bill for something',
24
+ period_start: Time.now.utc.iso8601,
25
+ period_end: (Time.now + 3600000).utc.iso8601,
26
+ }.merge(params)
27
+ end
28
+
29
+ def test_account_bill(params={})
30
+ {
31
+ success: true,
32
+ errors: {},
33
+ data: test_account_bill_content(params)
34
+ }
35
+ end
36
+
37
+ def test_account_bill_array
38
+ {
39
+ success: true,
40
+ errors: {},
41
+ data: [test_account_bill_content, test_account_bill_content, test_account_bill_content],
42
+ }
43
+ end
44
+
45
+ def test_account_bill_array_one
46
+ {
47
+ success: true,
48
+ errors: {},
49
+ data: [test_account_bill_content],
50
+ }
51
+ end
52
+
53
+ def test_invalid_api_key_error
54
+ {
55
+ 'success' => false,
56
+ 'data' => {},
57
+ "errors" => {
58
+ "authentication" => ["Invalid API token"],
59
+ }
60
+ }
61
+ end
62
+
63
+ def test_missing_id_error
64
+ {
65
+ 'success' => false,
66
+ 'data' => {},
67
+ 'errors' => {
68
+ 'id' => ["does not exist"]
69
+ }
70
+ }
71
+ end
72
+
73
+ def test_api_error
74
+ {
75
+ 'success' => false,
76
+ 'data' => {},
77
+ 'errors' => {
78
+ 'system' => ["A system error occured. Please retry later or contact support@maestrano.com if the issue persists."]
79
+ }
80
+ }
81
+ end
82
+ end
@@ -0,0 +1,62 @@
1
+ module SamlTestHelper
2
+ def fixture(document, base64 = true)
3
+ response = Dir.glob(File.join(File.dirname(__FILE__), '..', 'support', 'saml', "responses", "#{document}*")).first
4
+ if base64 && response =~ /\.xml$/
5
+ Base64.encode64(File.read(response))
6
+ else
7
+ File.read(response)
8
+ end
9
+ end
10
+
11
+ def response_document
12
+ @response_document ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response1.xml.base64'))
13
+ end
14
+
15
+ def response_document_2
16
+ @response_document2 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response2.xml.base64'))
17
+ end
18
+
19
+ def response_document_3
20
+ @response_document3 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response3.xml.base64'))
21
+ end
22
+
23
+ def response_document_4
24
+ @response_document4 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response4.xml.base64'))
25
+ end
26
+
27
+ def response_document_5
28
+ @response_document5 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response5.xml.base64'))
29
+ end
30
+
31
+ def r1_response_document_6
32
+ @response_document6 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'r1_response6.xml.base64'))
33
+ end
34
+
35
+ def ampersands_response
36
+ @ampersands_resposne ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'response_with_ampersands.xml.base64'))
37
+ end
38
+
39
+ def response_document_6
40
+ doc = Base64.decode64(response_document)
41
+ doc.gsub!(/NotBefore=\"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\"/, "NotBefore=\"#{(Time.now-300).getutc.strftime("%Y-%m-%dT%XZ")}\"")
42
+ doc.gsub!(/NotOnOrAfter=\"(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})Z\"/, "NotOnOrAfter=\"#{(Time.now+300).getutc.strftime("%Y-%m-%dT%XZ")}\"")
43
+ Base64.encode64(doc)
44
+ end
45
+
46
+ def wrapped_response_2
47
+ @wrapped_response_2 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'responses', 'wrapped_response_2.xml.base64'))
48
+ end
49
+
50
+ def signature_fingerprint_1
51
+ @signature_fingerprint1 ||= "C5:19:85:D9:47:F1:BE:57:08:20:25:05:08:46:EB:27:F6:CA:B7:83"
52
+ end
53
+
54
+ def signature_1
55
+ @signature1 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'certificates', 'certificate1'))
56
+ end
57
+
58
+ def r1_signature_2
59
+ @signature2 ||= File.read(File.join(File.dirname(__FILE__), '..', 'support', 'saml', 'certificates', 'r1_certificate2_base64'))
60
+ end
61
+
62
+ end
@@ -0,0 +1,48 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+
3
+ module Maestrano
4
+ module Account
5
+ class BillTest < Test::Unit::TestCase
6
+ include APITestHelper
7
+
8
+ should "bills should be listable" do
9
+ @api_mock.expects(:get).once.returns(test_response(test_account_bill_array))
10
+ c = Maestrano::Account::Bill.all
11
+ assert c.data.kind_of? Array
12
+ c.each do |bill|
13
+ assert bill.kind_of?(Maestrano::Account::Bill)
14
+ end
15
+ end
16
+
17
+ should "bills should be cancellable" do
18
+ @api_mock.expects(:delete).once.returns(test_response(test_account_bill))
19
+ c = Maestrano::Account::Bill.construct_from(test_account_bill[:data])
20
+ c.cancel
21
+ end
22
+
23
+ should "bills should not be updateable" do
24
+ assert_raises NoMethodError do
25
+ @api_mock.stubs(:put).returns(test_response(test_account_bill))
26
+ c = Maestrano::Account::Bill.construct_from(test_account_bill[:data])
27
+ c.save
28
+ end
29
+ end
30
+
31
+
32
+ should "create should successfully create a remote bill when passed correct parameters" do
33
+ @api_mock.expects(:post).with do |url, api_key, params|
34
+ url == "#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills" && api_key.nil? &&
35
+ CGI.parse(params) == {"group_id"=>["cld-1"], "price_cents"=>["23000"], "currency"=>["AUD"], "description"=>["Some bill"]}
36
+ end.once.returns(test_response(test_account_bill))
37
+
38
+ bill = Maestrano::Account::Bill.create({
39
+ group_id: 'cld-1',
40
+ price_cents: 23000,
41
+ currency: 'AUD',
42
+ description: 'Some bill'
43
+ })
44
+ assert bill.id
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+
3
+ module Maestrano
4
+ module API
5
+ class ListObjectTest < Test::Unit::TestCase
6
+ include APITestHelper
7
+
8
+ should "be able to retrieve full lists given a listobject" do
9
+ @api_mock.expects(:get).twice.returns(test_response(test_account_bill_array))
10
+ c = Maestrano::Account::Bill.all
11
+ assert c.kind_of?(Maestrano::API::ListObject)
12
+ assert_equal('account/bills', c.url)
13
+ all = c.all
14
+ assert all.kind_of?(Maestrano::API::ListObject)
15
+ assert_equal('account/bills', all.url)
16
+ assert all.data.kind_of?(Array)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ require File.expand_path('../../../test_helper', __FILE__)
2
+
3
+ module Maestrano
4
+ module API
5
+ class ObjectTest < Test::Unit::TestCase
6
+ should "implement #respond_to correctly" do
7
+ obj = Maestrano::API::Object.construct_from({ :id => 1, :foo => 'bar' })
8
+ assert obj.respond_to?(:id)
9
+ assert obj.respond_to?(:foo)
10
+ assert !obj.respond_to?(:baz)
11
+ end
12
+
13
+ should "marshal a maestrano object correctly" do
14
+ date = Time.now.utc
15
+ obj = Maestrano::API::Object.construct_from({
16
+ id: 1,
17
+ name: 'Maestrano',
18
+ some_date: date.iso8601
19
+ }, 'apikey')
20
+ m = Marshal.load(Marshal.dump(obj))
21
+ assert_equal 1, m.id
22
+ assert_equal 'Maestrano', m.name
23
+ assert_equal 'apikey', m.api_key
24
+ assert_equal date.iso8601, m.some_date.iso8601
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,343 @@
1
+ # -*- coding: utf-8 -*-
2
+ require File.expand_path('../../../test_helper', __FILE__)
3
+
4
+ module Maestrano
5
+ module API
6
+ class ResourceTest < Test::Unit::TestCase
7
+ include APITestHelper
8
+
9
+ should "creating a new Resource should not fetch over the network" do
10
+ @api_mock.expects(:get).never
11
+ Maestrano::Account::Bill.new("someid")
12
+ end
13
+
14
+ should "creating a new Resource from a hash should not fetch over the network" do
15
+ @api_mock.expects(:get).never
16
+ Maestrano::Account::Bill.construct_from({
17
+ id: "somebill",
18
+ object: "account_bill",
19
+ price_cents: 2300,
20
+ currency: 'AUD'
21
+ })
22
+ end
23
+
24
+ should "setting an attribute should not cause a network request" do
25
+ @api_mock.expects(:get).never
26
+ @api_mock.expects(:post).never
27
+ c = Maestrano::Account::Bill.new("test_account_bill");
28
+ c.price_cents= 50000
29
+ end
30
+
31
+ should "accessing id should not issue a fetch" do
32
+ @api_mock.expects(:get).never
33
+ c = Maestrano::Account::Bill.new("test_account_bill");
34
+ c.id
35
+ end
36
+
37
+ should "specifying invalid api credentials should raise an exception" do
38
+ response = test_response(test_invalid_api_key_error, 401)
39
+ assert_raises Maestrano::API::Error::AuthenticationError do
40
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 401))
41
+ Maestrano::Account::Bill.retrieve("failing_bill")
42
+ end
43
+ end
44
+
45
+ should "AuthenticationErrors should have an http status, http body, and JSON body" do
46
+ response = test_response(test_invalid_api_key_error, 401)
47
+ begin
48
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 401))
49
+ Maestrano::Account::Bill.retrieve("failing_bill")
50
+ rescue Maestrano::API::Error::AuthenticationError => e
51
+ assert_equal(401, e.http_status)
52
+ assert_equal(true, !!e.http_body)
53
+ assert_equal(true, !!e.json_body[:errors])
54
+ assert_equal(test_invalid_api_key_error['errors'].first.join(" "), e.json_body[:errors].first.join(" "))
55
+ end
56
+ end
57
+
58
+ context "when specifying per-object credentials" do
59
+ context "with no global API key set" do
60
+ setup do
61
+ @original_api_key = Maestrano.param('api_key')
62
+ Maestrano.configure { |c| c.api_key = nil }
63
+ end
64
+
65
+ teardown do
66
+ Maestrano.configure { |c| c.api_key = @original_api_key }
67
+ end
68
+
69
+ should "use the per-object credential when creating" do
70
+ Maestrano::API::Operation::Base.expects(:execute_request).with do |opts|
71
+ opts[:headers][:authorization] == "Basic #{Base64.encode64('sk_test_local:')}"
72
+ end.returns(test_response(test_account_bill))
73
+
74
+ Maestrano::Account::Bill.create({
75
+ group_id: 'cld-1',
76
+ price_cents: 23000,
77
+ currency: 'AUD',
78
+ description: 'Some bill'
79
+ },
80
+ 'sk_test_local'
81
+ )
82
+ end
83
+ end
84
+
85
+ context "with a global API key set" do
86
+ should "use the per-object credential when creating" do
87
+ Maestrano::API::Operation::Base.expects(:execute_request).with do |opts|
88
+ opts[:headers][:authorization] == "Basic #{Base64.encode64('local:')}"
89
+ end.returns(test_response(test_account_bill))
90
+
91
+ Maestrano::Account::Bill.create({
92
+ group_id: 'cld-1',
93
+ price_cents: 23000,
94
+ currency: 'AUD',
95
+ description: 'Some bill'
96
+ },
97
+ 'local'
98
+ )
99
+ end
100
+ end
101
+ end
102
+
103
+ context "with valid credentials" do
104
+ should "urlencode values in GET params" do
105
+ response = test_response(test_account_bill_array)
106
+ @api_mock.expects(:get).with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills?bill=test%20bill", nil, nil).returns(response)
107
+ bills = Maestrano::Account::Bill.all(:bill => 'test bill').data
108
+ assert bills.kind_of? Array
109
+ end
110
+
111
+ should "construct URL properly with base query parameters" do
112
+ response = test_response(test_account_bill_array)
113
+ @api_mock.expects(:get).with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills?bill=test_account_bill", nil, nil).returns(response)
114
+ bills = Maestrano::Account::Bill.all(:bill => 'test_account_bill')
115
+
116
+
117
+ end
118
+
119
+ should "construct URL properly with multiple query parameters" do
120
+ response = test_response(test_account_bill_array)
121
+ @api_mock.expects(:get).with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills?bill=test_account_bill&paid=true", nil, nil).returns(response)
122
+ bills = Maestrano::Account::Bill.all(bill:'test_account_bill', paid: true)
123
+ end
124
+
125
+ should "a 400 should give an InvalidRequestError with http status, body, and JSON body" do
126
+ response = test_response(test_missing_id_error, 400)
127
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
128
+ begin
129
+ Maestrano::Account::Bill.retrieve("foo")
130
+ rescue Maestrano::API::Error::InvalidRequestError => e
131
+ assert_equal(400, e.http_status)
132
+ assert_equal(true, !!e.http_body)
133
+ assert_equal(true, e.json_body.kind_of?(Hash))
134
+ end
135
+ end
136
+
137
+ should "a 401 should give an AuthenticationError with http status, body, and JSON body" do
138
+ response = test_response(test_missing_id_error, 401)
139
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
140
+ begin
141
+ Maestrano::Account::Bill.retrieve("foo")
142
+ rescue Maestrano::API::Error::AuthenticationError => e
143
+ assert_equal(401, e.http_status)
144
+ assert_equal(true, !!e.http_body)
145
+ assert_equal(true, e.json_body.kind_of?(Hash))
146
+ end
147
+ end
148
+
149
+ should "a 404 should give an InvalidRequestError with http status, body, and JSON body" do
150
+ response = test_response(test_missing_id_error, 404)
151
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
152
+ begin
153
+ Maestrano::Account::Bill.retrieve("foo")
154
+ rescue Maestrano::API::Error::InvalidRequestError => e
155
+ assert_equal(404, e.http_status)
156
+ assert_equal(true, !!e.http_body)
157
+ assert_equal(true, e.json_body.kind_of?(Hash))
158
+ end
159
+ end
160
+
161
+ should "setting a nil value for a param should exclude that param from the GET request" do
162
+ @api_mock.expects(:get).with do |url, api_key, params|
163
+ uri = URI(url)
164
+ query = CGI.parse(uri.query)
165
+ (url =~ %r{^#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills?} &&
166
+ query.keys.sort == ['offset', 'sad'])
167
+ end.returns(test_response(test_account_bill_array_one))
168
+ Maestrano::Account::Bill.all(:count => nil, :offset => 5, :sad => false)
169
+ end
170
+
171
+ should "setting a nil value for a param should exclude that param from the POST request" do
172
+ @api_mock.expects(:post).with do |url, api_key, params|
173
+ url == "#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills" &&
174
+ api_key.nil? &&
175
+ CGI.parse(params) == { 'group_id' => ['cld-1'], 'price_cents' => ['23000'], 'currency' => ['AUD'] }
176
+ end.returns(test_response(test_account_bill))
177
+ Maestrano::Account::Bill.create({
178
+ group_id: 'cld-1',
179
+ price_cents: 23000,
180
+ currency: 'AUD',
181
+ description: nil
182
+ })
183
+ end
184
+
185
+ should "requesting with a unicode ID should result in a request" do
186
+ response = test_response(test_missing_id_error, 404)
187
+ @api_mock.expects(:get).once.with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills/%E2%98%83", nil, nil).raises(RestClient::ExceptionWithResponse.new(response, 404))
188
+ c = Maestrano::Account::Bill.new("☃")
189
+ assert_raises(Maestrano::API::Error::InvalidRequestError) { c.refresh }
190
+ end
191
+
192
+ should "requesting with no ID should result in an InvalidRequestError with no request" do
193
+ c = Maestrano::Account::Bill.new
194
+ assert_raises(Maestrano::API::Error::InvalidRequestError) { c.refresh }
195
+ end
196
+
197
+ should "making a GET request with parameters should have a query string and no body" do
198
+ params = { :limit => 1 }
199
+ @api_mock.expects(:get).once.with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills?limit=1", nil, nil).returns(test_response(test_account_bill_array_one))
200
+ Maestrano::Account::Bill.all(params)
201
+ end
202
+
203
+ should "making a POST request with parameters should have a body and no query string" do
204
+ date = Time.now.utc
205
+ params = {
206
+ group_id: 'cld-1',
207
+ price_cents: 23000,
208
+ currency: 'AUD',
209
+ description: 'Some bill',
210
+ period_started_at: date
211
+ }
212
+ @api_mock.expects(:post).once.with do |url, get, post|
213
+ get.nil? &&
214
+ CGI.parse(post) == {"group_id"=>["cld-1"], "price_cents"=>["23000"], "currency"=>["AUD"], "description"=>["Some bill"], "period_started_at" => ["#{date.iso8601}"]}
215
+ end.returns(test_response(test_account_bill))
216
+ Maestrano::Account::Bill.create(params)
217
+ end
218
+
219
+ should "loading an object should issue a GET request" do
220
+ @api_mock.expects(:get).once.returns(test_response(test_account_bill))
221
+ c = Maestrano::Account::Bill.new("test_account_bill")
222
+ c.refresh
223
+ end
224
+
225
+ should "using array accessors should be the same as the method interface" do
226
+ @api_mock.expects(:get).once.returns(test_response(test_account_bill))
227
+ c = Maestrano::Account::Bill.new("test_account_bill")
228
+ c.refresh
229
+ assert_equal c.created_at, c[:created_at]
230
+ assert_equal c.created_at, c['created_at']
231
+ date = Time.now.utc.iso8601
232
+ c['created'] = date
233
+ assert_equal c.created, date
234
+ end
235
+
236
+ # Not related object defined on any model yet
237
+ # should "accessing a property other than id or parent on an unfetched object should fetch it" do
238
+ # @api_mock.expects(:get).once.returns(test_response(test_account_bill))
239
+ # c = Maestrano::Account::Bill.new("test_account_bill")
240
+ # c.price_cents
241
+ # end
242
+
243
+ should "updating an object should issue a PUT request with only the changed properties" do
244
+ @api_mock.expects(:put).with do |url, api_key, params|
245
+ url == "#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills/bill-1" &&
246
+ api_key.nil? && CGI.parse(params) == {'description' => ['another_mn']}
247
+ end.once.returns(test_response(test_account_bill))
248
+
249
+ c = Maestrano::Account::Bill.construct_from(test_account_bill[:data])
250
+ class << c
251
+ include Maestrano::API::Operation::Update
252
+ end
253
+
254
+ c.description = "another_mn"
255
+ c.save
256
+ end
257
+
258
+ should "updating should merge in returned properties" do
259
+ @api_mock.expects(:put).once.returns(test_response(test_account_bill))
260
+ c = Maestrano::Account::Bill.new(test_account_bill[:data])
261
+ class << c
262
+ include Maestrano::API::Operation::Update
263
+ end
264
+
265
+ c.description = "another_mn"
266
+ c.save
267
+ end
268
+
269
+ should "deleting should send no props and result in an object that has no props other deleted" do
270
+ @api_mock.expects(:get).never
271
+ @api_mock.expects(:post).never
272
+ @api_mock.expects(:delete).with("#{Maestrano.param('api_host')}#{Maestrano.param('api_base')}account/bills/bill-1", nil, nil).once.returns(test_response(test_account_bill))
273
+
274
+ c = Maestrano::Account::Bill.construct_from(test_account_bill[:data])
275
+ class << c
276
+ include Maestrano::API::Operation::Delete
277
+ end
278
+
279
+ c.delete
280
+
281
+
282
+ assert_raises NoMethodError do
283
+ c.livemode
284
+ end
285
+ end
286
+
287
+ # should "loading an object with properties that have specific types should instantiate those classes" do
288
+ # @api_mock.expects(:get).once.returns(test_response(test_account_bill))
289
+ # c = Maestrano::Account::Bill.retrieve("test_account_bill")
290
+ # assert c.card.kind_of?(Maestrano::API::Object) && c.card.object == 'card'
291
+ # end
292
+
293
+ should "loading all of a Resource should return an array of recursively instantiated objects" do
294
+ @api_mock.expects(:get).once.returns(test_response(test_account_bill_array))
295
+ c = Maestrano::Account::Bill.all.data
296
+ assert c.kind_of? Array
297
+ assert c[0].kind_of? Maestrano::Account::Bill
298
+
299
+ # No object to test for the moment
300
+ #assert c[0].card.kind_of?(Maestrano::API::Object) && c[0].card.object == 'card'
301
+ end
302
+
303
+ context "error checking" do
304
+
305
+ should "404s should raise an InvalidRequestError" do
306
+ response = test_response(test_missing_id_error, 404)
307
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 404))
308
+
309
+ rescued = false
310
+ begin
311
+ Maestrano::Account::Bill.new("test_account_bill").refresh
312
+ assert false #shouldn't get here either
313
+ rescue Maestrano::API::Error::InvalidRequestError => e # we don't use assert_raises because we want to examine e
314
+ rescued = true
315
+ assert e.kind_of? Maestrano::API::Error::InvalidRequestError
316
+ assert_equal "id", e.param
317
+ assert_equal 'id does not exist', e.message
318
+ end
319
+
320
+ assert_equal true, rescued
321
+ end
322
+
323
+ should "5XXs should raise an API::Error" do
324
+ response = test_response(test_api_error, 500)
325
+ @api_mock.expects(:get).once.raises(RestClient::ExceptionWithResponse.new(response, 500))
326
+
327
+ rescued = false
328
+ begin
329
+ Maestrano::Account::Bill.new("test_account_bill").refresh
330
+ assert false #shouldn't get here either
331
+ rescue Maestrano::API::Error::BaseError => e # we don't use assert_raises because we want to examine e
332
+ rescued = true
333
+ assert e.kind_of? Maestrano::API::Error::BaseError
334
+ end
335
+
336
+ assert_equal true, rescued
337
+ end
338
+
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end