paid 1.0.1 → 1.0.2

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/History.txt +6 -0
  3. data/Rakefile +1 -1
  4. data/VERSION +1 -1
  5. data/lib/paid.rb +32 -207
  6. data/lib/paid/account.rb +22 -10
  7. data/lib/paid/api_list.rb +56 -17
  8. data/lib/paid/api_method.rb +93 -0
  9. data/lib/paid/api_resource.rb +130 -8
  10. data/lib/paid/customer.rb +104 -40
  11. data/lib/paid/errors/api_connection_error.rb +1 -1
  12. data/lib/paid/errors/api_error.rb +25 -3
  13. data/lib/paid/errors/authentication_error.rb +1 -1
  14. data/lib/paid/errors/paid_error.rb +2 -9
  15. data/lib/paid/event.rb +28 -17
  16. data/lib/paid/event_data.rb +10 -0
  17. data/lib/paid/headers_builder.rb +75 -0
  18. data/lib/paid/invoice.rb +55 -16
  19. data/lib/paid/params_builder.rb +26 -0
  20. data/lib/paid/path_builder.rb +38 -0
  21. data/lib/paid/plan.rb +38 -13
  22. data/lib/paid/refund_list.rb +26 -0
  23. data/lib/paid/requester.rb +97 -0
  24. data/lib/paid/subscription.rb +47 -16
  25. data/lib/paid/transaction.rb +64 -16
  26. data/lib/paid/util.rb +14 -40
  27. data/paid.gemspec +1 -1
  28. data/test/paid/{api_class_test.rb → _api_resource_test.rb} +31 -17
  29. data/test/paid/account_test.rb +3 -3
  30. data/test/paid/api_list_test.rb +14 -8
  31. data/test/paid/api_method_test.rb +89 -0
  32. data/test/paid/customer_test.rb +20 -10
  33. data/test/paid/event_test.rb +3 -4
  34. data/test/paid/headers_builder_test.rb +39 -0
  35. data/test/paid/invoice_test.rb +3 -3
  36. data/test/paid/params_builder_test.rb +57 -0
  37. data/test/paid/path_builder_test.rb +67 -0
  38. data/test/paid/plan_test.rb +3 -3
  39. data/test/paid/requester_test.rb +86 -0
  40. data/test/paid/subscription_test.rb +3 -3
  41. data/test/paid/transaction_test.rb +4 -4
  42. data/test/paid/util_test.rb +36 -35
  43. data/test/test_data.rb +9 -2
  44. data/test/test_helper.rb +14 -14
  45. metadata +23 -19
  46. data/lib/paid/api_class.rb +0 -338
  47. data/lib/paid/api_singleton.rb +0 -5
  48. data/lib/paid/errors/invalid_request_error.rb +0 -10
  49. data/test/mock_resource.rb +0 -69
  50. data/test/paid/api_resource_test.rb +0 -28
  51. data/test/paid/api_singleton_test.rb +0 -12
  52. data/test/paid/authentication_test.rb +0 -50
  53. data/test/paid/status_codes_test.rb +0 -63
data/lib/paid/util.rb CHANGED
@@ -1,46 +1,6 @@
1
1
  module Paid
2
2
  module Util
3
3
 
4
- def self.query_string(params)
5
- if params && params.any?
6
- return query_array(params).join('&')
7
- else
8
- return ""
9
- end
10
- end
11
-
12
- # Three major use cases (and nesting of them needs to be supported):
13
- # { :a => { :b => "bvalue" } } => ["a[b]=bvalue"]
14
- # { :a => [1, 2] } => ["a[]=1", "a[]=2"]
15
- # { :a => "value" } => ["a=value"]
16
- def self.query_array(params, key_prefix=nil)
17
- ret = []
18
- params.each do |key, value|
19
- if params.is_a?(Array)
20
- value = key
21
- key = ''
22
- end
23
- key_suffix = escape(key)
24
- full_key = key_prefix ? "#{key_prefix}[#{key_suffix}]" : key_suffix
25
-
26
- if value.is_a?(Hash) || value.is_a?(Array)
27
- # Handles the following cases:
28
- # { :a => { :b => "bvalue" } } => ["a[b]=bvalue"]
29
- # { :a => [1, 2] } => ["a[]=1", "a[]=2"]
30
- ret += query_array(value, full_key)
31
- else
32
- # Handles the base case with just key and value:
33
- # { :a => "value" } => ["a=value"]
34
- ret << "#{full_key}=#{escape(value)}"
35
- end
36
- end
37
- ret
38
- end
39
-
40
- def self.escape(val)
41
- URI.escape(val.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
42
- end
43
-
44
4
  def self.symbolize_keys(obj)
45
5
  if obj.is_a?(Hash)
46
6
  ret = {}
@@ -73,5 +33,19 @@ module Paid
73
33
  end
74
34
  end
75
35
 
36
+ def self.constantize(str, prefix=false)
37
+ str = str.to_s
38
+ begin
39
+ str.split('::').reduce(Module, :const_get)
40
+ rescue NameError => e
41
+ if prefix
42
+ raise e
43
+ else
44
+ p = "#{self.name}".split("::").first
45
+ constantize("#{p}::#{str}", true)
46
+ end
47
+ end
48
+ end
49
+
76
50
  end
77
51
  end
data/paid.gemspec CHANGED
@@ -8,7 +8,7 @@ spec = Gem::Specification.new do |s|
8
8
  s.description = 'Paid is the programmatic way to manage payments. See https://paidapi.com for details.'
9
9
  s.homepage = 'http://docs.paidapi.com'
10
10
  s.authors = ['Jon Calhoun', 'Ryan Jackson']
11
- s.email = ['joncalhoun@gmail.com', 'ryan@paidapi.com']
11
+ s.email = ['jon@apibits.com', 'ryan@paidapi.com']
12
12
  s.version = Paid::VERSION
13
13
  s.license = 'MIT'
14
14
 
@@ -2,7 +2,7 @@
2
2
  require File.expand_path('../../test_helper', __FILE__)
3
3
 
4
4
  module Paid
5
- class ApiClassTest < Test::Unit::TestCase
5
+ class APIResourceTest < ::Test::Unit::TestCase
6
6
 
7
7
  context 'Non-network actions' do
8
8
  setup do
@@ -12,11 +12,11 @@ module Paid
12
12
  @mock.expects(:delete).never
13
13
  end
14
14
 
15
- should 'not fetch over the network when creating a new APIClass' do
15
+ should 'not fetch over the network when creating a new APIResource' do
16
16
  MockResource.new('fake_id')
17
17
  end
18
18
 
19
- should 'not fetch over the network when creating a new APIClass from a hash' do
19
+ should 'not fetch over the network when creating a new APIResource from a hash' do
20
20
  MockResource.construct(test_mock_resource)
21
21
  end
22
22
 
@@ -84,7 +84,7 @@ module Paid
84
84
  end
85
85
  end
86
86
 
87
- context 'APIClass :default_params' do
87
+ context 'APIResource :default_params' do
88
88
  context 'api_instance_method' do
89
89
  setup do
90
90
  @response = test_response(test_mock_resource_list)
@@ -199,7 +199,7 @@ module Paid
199
199
  end
200
200
 
201
201
 
202
- context 'APIClass#attribute' do
202
+ context 'APIResource#attribute' do
203
203
  should 'create a getter method' do
204
204
  assert(MockResource.method_defined?(:name))
205
205
  end
@@ -210,52 +210,66 @@ module Paid
210
210
 
211
211
  should 'have no changed attributes after init' do
212
212
  mr = MockResource.new(test_mock_resource)
213
- assert(mr.changed_attributes.empty?)
213
+ assert(mr.changed_api_attributes.empty?)
214
214
  end
215
215
 
216
216
  should 'keep track of changed attributes' do
217
217
  mr = MockResource.new(test_mock_resource)
218
- assert(mr.changed_attributes.empty?)
218
+ assert(mr.changed_api_attributes.empty?)
219
219
  mr.name = "new name"
220
- assert_equal({:name => "new name"}, mr.changed_attributes)
220
+ assert_equal({:name => "new name"}, mr.changed_api_attributes)
221
221
  end
222
222
 
223
223
  should 'keep track of changed arrays' do
224
224
  mr = MockResource.new(test_mock_resource)
225
- assert(mr.changed_attributes.empty?)
225
+ assert(mr.changed_api_attributes.empty?)
226
226
  mr.tarray << "new"
227
- assert_equal({:tarray => test_mock_resource[:tarray] + ["new"]}, mr.changed_attributes)
227
+ assert_equal({:tarray => test_mock_resource[:tarray] + ["new"]}, mr.changed_api_attributes)
228
228
  end
229
229
 
230
230
  should 'keep track of changed hashes' do
231
231
  mr = MockResource.new(test_mock_resource)
232
- assert(mr.changed_attributes.empty?)
232
+ assert(mr.changed_api_attributes.empty?)
233
233
  mr.thash[:some_key] = "new value"
234
- assert_equal({:thash => { :some_key => "new value" }}, mr.changed_attributes)
234
+ assert_equal({:thash => { :some_key => "new value" }}, mr.changed_api_attributes)
235
235
  end
236
236
 
237
237
  context 'constructors' do
238
238
  should 'instantiate on #new' do
239
239
  mr = MockResource.new(test_mock_resource)
240
240
  assert(mr.nested.is_a?(NestedResource))
241
+ assert(mr.nested_alt.is_a?(NestedResource))
242
+ assert(mr.nested_with.is_a?(NestedWithParent))
243
+ assert_equal(mr.path + "/nested_path", mr.nested_with.path)
241
244
  end
242
245
 
243
246
  should 'instantiate on #construct' do
244
247
  mr = MockResource.construct(test_mock_resource)
245
248
  assert(mr.nested.is_a?(NestedResource))
249
+ assert(mr.nested_alt.is_a?(NestedResource))
250
+ assert(mr.nested_with.is_a?(NestedWithParent))
251
+ assert_equal(mr.path + "/nested_path", mr.nested_with.path)
246
252
  end
247
253
 
248
254
  should 'instantiate on #refresh_from' do
249
255
  mr = MockResource.new('fake_id')
250
256
  assert(mr.nested.nil?)
251
-
252
257
  mr.refresh_from(test_mock_resource)
253
258
  assert(mr.nested.is_a?(NestedResource))
259
+ assert(mr.nested_alt.is_a?(NestedResource))
260
+ assert(mr.nested_with.is_a?(NestedWithParent))
261
+ assert_equal(mr.path + "/nested_path", mr.nested_with.path)
262
+ end
263
+
264
+ should 'with default values' do
265
+ mr = MockResource.new('fake_id')
266
+ assert(mr.nested_with.is_a?(NestedWithParent))
267
+ assert_equal(mr.path + "/nested_path", mr.nested_with.path)
254
268
  end
255
269
  end
256
270
  end
257
271
 
258
- context 'APIClass :constructor' do
272
+ context 'APIResource :constructor' do
259
273
  context 'for api_class_method' do
260
274
  setup do
261
275
  @mock.expects(:get).once.returns(test_response(test_mock_resource))
@@ -313,7 +327,7 @@ module Paid
313
327
  end
314
328
  end
315
329
 
316
- context 'APIClass api_*_method arguments' do
330
+ context 'APIResource api_*_method arguments' do
317
331
  should 'throw an ArgumentError if too few arguments are provided' do
318
332
  assert_raises(ArgumentError) { MockResource.retrieve }
319
333
  end
@@ -364,7 +378,7 @@ module Paid
364
378
  end
365
379
  end
366
380
 
367
- context 'APIClass api_instance_method paths' do
381
+ context 'APIResource api_instance_method paths' do
368
382
  should 'use the provided argument if it is present' do
369
383
  response = test_response(test_mock_resource)
370
384
  @mock.expects(:get).with("#{Paid.api_base}/custom_path", anything, anything).returns(response)
@@ -392,7 +406,7 @@ module Paid
392
406
  end
393
407
  end
394
408
 
395
- context 'APIClass api_class_method paths' do
409
+ context 'APIResource api_class_method paths' do
396
410
  should 'use the provided argument if it is present' do
397
411
  response = test_response(test_mock_resource)
398
412
  @mock.expects(:get).with("#{Paid.api_base}#{MockResource.path}/fake_id", anything, anything).returns(response)
@@ -3,7 +3,7 @@ require File.expand_path('../../test_helper', __FILE__)
3
3
  module Paid
4
4
  class AccountTest < Test::Unit::TestCase
5
5
  setup do
6
- @account_url = "#{Paid.api_base}/v0/account"
6
+ @account_url = "#{Paid.api_base}/account"
7
7
  end
8
8
 
9
9
  should 'be retrievable' do
@@ -41,8 +41,8 @@ module Paid
41
41
  end
42
42
 
43
43
  should 'be registered' do
44
- assert(APIClass.subclasses.include?(Paid::Account))
45
- assert_equal(Paid::Account, APIClass.subclass_fetch("account"))
44
+ assert(APIResource.api_subclasses.include?(Paid::Account))
45
+ assert_equal(Paid::Account, APIResource.api_subclass_fetch("account"))
46
46
  end
47
47
 
48
48
  end
@@ -1,16 +1,22 @@
1
1
  require File.expand_path('../../test_helper', __FILE__)
2
2
 
3
3
  module Paid
4
- class ApiListTest < Test::Unit::TestCase
4
+ class APIListTest < ::Test::Unit::TestCase
5
5
 
6
- should 'have an object attribute' do
7
- assert(Paid::APIList.method_defined?(:object))
8
- assert(Paid::APIList.method_defined?(:object=))
9
- end
6
+ context '#new / #initialize' do
7
+ setup do
8
+ @fake_resource = {:data => "fake-data"}
9
+ @list = APIList.new(APIResource, [@fake_resource])
10
+ end
11
+
12
+ should 'set the klass' do
13
+ assert_equal(APIResource, @list.klass)
14
+ end
10
15
 
11
- should 'have an data attribute' do
12
- assert(Paid::APIList.method_defined?(:data))
13
- assert(Paid::APIList.method_defined?(:data=))
16
+ should 'convert the data to klass instances' do
17
+ assert(@list.first.is_a?(APIResource))
18
+ assert_equal(@fake_resource, @list.first.json)
19
+ end
14
20
  end
15
21
 
16
22
  end
@@ -0,0 +1,89 @@
1
+ require File.expand_path('../../test_helper', __FILE__)
2
+
3
+ module Paid
4
+ class APIMethodTest < ::Test::Unit::TestCase
5
+ setup do
6
+ @method = :get
7
+ @path = "/testing"
8
+ @params = { :param_a => "1" }
9
+ @headers = { :header_a => "a" }
10
+ @object = mock
11
+ @api_method = APIMethod.new(@method, @path, @params, @headers, @object)
12
+ end
13
+
14
+ context '#new / #initialize' do
15
+ should 'set the api_key' do
16
+ assert_equal(Paid.api_key, @api_method.api_key)
17
+ end
18
+
19
+ should 'set the api_base' do
20
+ assert_equal(Paid.api_base, @api_method.api_base)
21
+ end
22
+
23
+ should 'use PathBuilder with path, object, and params' do
24
+ PathBuilder.expects(:build).with(@path, @object, @params).returns(@path)
25
+ APIMethod.new(@method, @path, @params, @headers, @object)
26
+ end
27
+
28
+ should 'use ParamsBuilder with params' do
29
+ ParamsBuilder.expects(:build).with(@params).returns(@params)
30
+ APIMethod.new(@method, @path, @params, @headers, @object)
31
+ end
32
+
33
+ should 'use HeadersBuilder with headers, api_key, and nil auth_header' do
34
+ HeadersBuilder.expects(:build).with(@headers, Paid.api_key, nil).returns(@headers)
35
+ APIMethod.new(@method, @path, @params, @headers, @object)
36
+ end
37
+
38
+ should 'verify the api key exists' do
39
+ Paid.api_key = nil
40
+ assert_raises(AuthenticationError) do
41
+ APIMethod.new(@method, @path, @params, @headers, @object)
42
+ end
43
+ end
44
+ end
45
+
46
+ context '#execute' do
47
+ setup do
48
+ @mock_response = mock
49
+ @mock_response.stubs(:body).returns('{"status": "success"}')
50
+ @mock_response.stubs(:code).returns(200)
51
+ end
52
+
53
+ should 'call Requester.request with the set attrs' do
54
+ Requester.expects(:request).with(@method, @api_method.url, @api_method.params, @api_method.headers).returns(@mock_response)
55
+ @api_method.execute
56
+ end
57
+
58
+ should 'create an APIError if the request fails' do
59
+ Requester.expects(:request).raises(RestClient::RequestTimeout.new)
60
+
61
+ assert_raises(APIError) { @api_method.execute }
62
+ end
63
+
64
+ should 'return the response parsed as json' do
65
+ Requester.expects(:request).returns(@mock_response)
66
+ assert_equal({:status => "success"}, @api_method.execute)
67
+ end
68
+
69
+ should 'return an AuthenticationError if the status is 401' do
70
+ error = RestClient::ExceptionWithResponse.new
71
+ error.expects(:http_code).returns(401)
72
+
73
+ Requester.expects(:request).raises(error)
74
+ assert_raises(AuthenticationError) { @api_method.execute }
75
+ end
76
+ end
77
+
78
+ context '#response_json' do
79
+ setup do
80
+ @api_method.response_body = 'not-valid-json'
81
+ end
82
+
83
+ should 'throw an error if the response_body isnt valid json' do
84
+ assert_raises(APIError) { @api_method.response_json }
85
+ end
86
+ end
87
+
88
+ end
89
+ end
@@ -3,7 +3,7 @@ require File.expand_path('../../test_helper', __FILE__)
3
3
  module Paid
4
4
  class CustomerTest < Test::Unit::TestCase
5
5
  setup do
6
- @customer_url = "#{Paid.api_base}/v0/customers"
6
+ @customer_url = "#{Paid.api_base}/customers"
7
7
  end
8
8
 
9
9
  context 'Customer class' do
@@ -16,7 +16,10 @@ module Paid
16
16
 
17
17
  should 'be retrieveable by external_id' do
18
18
  external_id = "external_id_for_cust"
19
- @mock.expects(:get).once.with("#{@customer_url}/by_external_id/#{external_id}", anything, anything).returns(test_response(test_customer))
19
+ @mock.expects(:get).once.with do |url, headers, params|
20
+ Regexp.new("/customers/by_external_id").match(url) &&
21
+ params[:external_id] == external_id
22
+ end.returns(test_response(test_customer))
20
23
  customer = Paid::Customer.by_external_id(external_id)
21
24
  assert(customer.is_a?(Paid::Customer))
22
25
  end
@@ -54,7 +57,7 @@ module Paid
54
57
  customer.email = "new_email@domain.com"
55
58
 
56
59
  @mock.expects(:put).once.with do |url, headers, params|
57
- params == customer.changed_attributes && url == "#{@customer_url}/#{customer.id}"
60
+ !params.nil? && url == "#{@customer_url}/#{customer.id}"
58
61
  end.returns(test_response(test_customer))
59
62
 
60
63
  # This should update this instance with test_customer since it was returned
@@ -65,7 +68,9 @@ module Paid
65
68
 
66
69
  should 'be able to generate an invoice' do
67
70
  customer = Paid::Customer.new(test_customer)
68
- @mock.expects(:post).once.with("#{@customer_url}/#{customer.id}/generate_invoice", anything, anything).returns(test_response(test_invoice))
71
+ @mock.expects(:post).once.with do |url, headers, params|
72
+ Regexp.new("/customers/#{customer.id}/generate_invoice").match(url)
73
+ end.returns(test_response(test_invoice))
69
74
 
70
75
  invoice = customer.generate_invoice
71
76
  assert(invoice.is_a?(Paid::Invoice))
@@ -73,7 +78,10 @@ module Paid
73
78
 
74
79
  should 'be able to list invoices' do
75
80
  customer = Paid::Customer.new(test_customer)
76
- @mock.expects(:get).once.with("#{Paid.api_base}#{Paid::Invoice.path}?customer=#{customer.id}", anything, anything).returns(test_response(test_invoice_list))
81
+ @mock.expects(:get).once.with do |url, headers, params|
82
+ Regexp.new("/invoices").match(url) &&
83
+ params[:customer] == customer.id
84
+ end.returns(test_response(test_invoice_list))
77
85
 
78
86
  invoices = customer.invoices
79
87
  assert(invoices.is_a?(Paid::APIList))
@@ -84,7 +92,10 @@ module Paid
84
92
 
85
93
  should 'be able to list transactions' do
86
94
  customer = Paid::Customer.new(test_customer)
87
- @mock.expects(:get).once.with("#{Paid.api_base}#{Paid::Transaction.path}?customer=#{customer.id}", anything, anything).returns(test_response(test_transaction_list))
95
+ @mock.expects(:get).once.with do |url, headers, params|
96
+ Regexp.new("/transactions").match(url) &&
97
+ params[:customer] == customer.id
98
+ end.returns(test_response(test_transaction_list))
88
99
 
89
100
  transactions = customer.transactions
90
101
  assert(transactions.is_a?(Paid::APIList))
@@ -97,8 +108,7 @@ module Paid
97
108
 
98
109
  context 'Retrieved Paid::Customer instance' do
99
110
  setup do
100
- @mock.expects(:get).once.returns(test_response(test_customer))
101
- @customer = Paid::Customer.retrieve('customer_id')
111
+ @customer = Paid::Customer.new(test_customer)
102
112
  end
103
113
 
104
114
  should 'have the id attribute' do
@@ -184,8 +194,8 @@ module Paid
184
194
  end
185
195
 
186
196
  should 'be registered' do
187
- assert(APIClass.subclasses.include?(Paid::Customer))
188
- assert_equal(Paid::Customer, APIClass.subclass_fetch("customer"))
197
+ assert(APIResource.api_subclasses.include?(Paid::Customer))
198
+ assert_equal(Paid::Customer, APIResource.api_subclass_fetch("customer"))
189
199
  end
190
200
 
191
201
  end
@@ -3,7 +3,7 @@ require File.expand_path('../../test_helper', __FILE__)
3
3
  module Paid
4
4
  class EventTest < Test::Unit::TestCase
5
5
  setup do
6
- @event_url = "#{Paid.api_base}/v0/events"
6
+ @event_url = "#{Paid.api_base}/events"
7
7
  end
8
8
 
9
9
  context 'Event class' do
@@ -58,7 +58,6 @@ module Paid
58
58
  end
59
59
 
60
60
  should 'have & convert the data attribute' do
61
- assert(@event.data.is_a?(Paid::APIClass))
62
61
  event2 = Paid::Event.new(test_event(test_invoice))
63
62
  assert(event2.data.is_a?(Paid::Invoice))
64
63
  end
@@ -66,8 +65,8 @@ module Paid
66
65
  end
67
66
 
68
67
  should 'be registered' do
69
- assert(APIClass.subclasses.include?(Paid::Event))
70
- assert_equal(Paid::Event, APIClass.subclass_fetch("event"))
68
+ assert(APIResource.api_subclasses.include?(Paid::Event))
69
+ assert_equal(Paid::Event, APIResource.api_subclass_fetch("event"))
71
70
  end
72
71
 
73
72
  end