sage_one 0.0.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.
Files changed (50) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +11 -0
  3. data/.yardopts +4 -0
  4. data/Gemfile +8 -0
  5. data/LICENSE +19 -0
  6. data/README.md +141 -0
  7. data/Rakefile +4 -0
  8. data/bin/autospec +16 -0
  9. data/bin/htmldiff +16 -0
  10. data/bin/ldiff +16 -0
  11. data/bin/rake +16 -0
  12. data/bin/rspec +16 -0
  13. data/bin/yard +16 -0
  14. data/bin/yardoc +16 -0
  15. data/bin/yri +16 -0
  16. data/lib/faraday/request/oauth2.rb +25 -0
  17. data/lib/faraday/response/convert_sdata_to_headers.rb +70 -0
  18. data/lib/faraday/response/raise_sage_one_exception.rb +42 -0
  19. data/lib/sage_one/client/contacts.rb +14 -0
  20. data/lib/sage_one/client/sales_invoices.rb +77 -0
  21. data/lib/sage_one/client.rb +33 -0
  22. data/lib/sage_one/configuration.rb +81 -0
  23. data/lib/sage_one/connection.rb +44 -0
  24. data/lib/sage_one/error.rb +39 -0
  25. data/lib/sage_one/oauth.rb +49 -0
  26. data/lib/sage_one/request.rb +72 -0
  27. data/lib/sage_one/version.rb +3 -0
  28. data/lib/sage_one.rb +31 -0
  29. data/sage_one.gemspec +32 -0
  30. data/spec/faraday/request/oauth2_spec.rb +44 -0
  31. data/spec/faraday/response/convert_sdata_to_headers_spec.rb +113 -0
  32. data/spec/faraday/response/raise_sage_one_exception_spec.rb +45 -0
  33. data/spec/fixtures/contact.json +28 -0
  34. data/spec/fixtures/invalid_sales_invoice.json +28 -0
  35. data/spec/fixtures/oauth/invalid_client.json +1 -0
  36. data/spec/fixtures/oauth/invalid_grant.json +1 -0
  37. data/spec/fixtures/oauth/oauth_token.json +4 -0
  38. data/spec/fixtures/sales_invoice.json +43 -0
  39. data/spec/fixtures/sales_invoices.json +90 -0
  40. data/spec/sage_one/client/contacts_spec.rb +19 -0
  41. data/spec/sage_one/client/sales_invoices_spec.rb +53 -0
  42. data/spec/sage_one/client_spec.rb +41 -0
  43. data/spec/sage_one/configuration_spec.rb +88 -0
  44. data/spec/sage_one/connection_spec.rb +36 -0
  45. data/spec/sage_one/oauth_spec.rb +44 -0
  46. data/spec/sage_one/request_spec.rb +113 -0
  47. data/spec/sage_one/version_spec.rb +7 -0
  48. data/spec/sage_one_spec.rb +38 -0
  49. data/spec/spec_helper.rb +76 -0
  50. metadata +301 -0
@@ -0,0 +1,81 @@
1
+ require 'faraday'
2
+ require 'sage_one/version'
3
+
4
+ module SageOne
5
+ # Provide numerous configuration options that control core behaviour.
6
+ module Configuration
7
+ VALID_OPTIONS_KEYS = [
8
+ :adapter,
9
+ :faraday_config_block,
10
+ :api_endpoint,
11
+ :proxy,
12
+ :access_token,
13
+ :client_id,
14
+ :client_secret,
15
+ :user_agent,
16
+ :request_host,
17
+ :auto_traversal,
18
+ :raw_response].freeze
19
+
20
+ DEFAULT_ADAPTER = Faraday.default_adapter
21
+ DEFAULT_API_ENDPOINT = 'https://app.sageone.com/api/v1/'.freeze
22
+ DEFAULT_USER_AGENT = "SageOne Ruby Gem #{SageOne::VERSION}".freeze
23
+
24
+ # Only get the first page when making paginated data requests
25
+ DEFAULT_AUTO_TRAVERSAL = false
26
+
27
+ # Parse Json, Mashify & convert SData when making requests
28
+ DEFAULT_RAW_RESPONSE = false
29
+
30
+ attr_accessor(*VALID_OPTIONS_KEYS)
31
+
32
+ # Override the default API endpoint, this ensures a trailing forward slash is added.
33
+ # @param [String] value for a different API endpoint
34
+ # @example
35
+ # SageOne.api_end_point = 'https://app.sageone.com/api/v2/'
36
+ def api_endpoint=(value)
37
+ @api_endpoint = File.join(value, "")
38
+ end
39
+
40
+ # Stores the given block which is called when the new Faraday::Connection
41
+ # is set up for all requests. This allow you to configure the connection,
42
+ # for example with your own middleware.
43
+ def faraday_config(&block)
44
+ @faraday_config_block = block
45
+ end
46
+
47
+ # Yields the given block passing in selfing allow you to set config options
48
+ # on the 'extend'-ing module.
49
+ # @example
50
+ # SageOne.configure do |config|
51
+ # config.access_token = 'my-access-token'
52
+ # end
53
+ def configure
54
+ yield self
55
+ end
56
+
57
+ # @return [Hash] All options with their current values
58
+ def options
59
+ VALID_OPTIONS_KEYS.inject({}){|o,k| o.merge!(k => send(k)) }
60
+ end
61
+
62
+ # When extended call reset
63
+ def self.extended(base)
64
+ base.reset
65
+ end
66
+
67
+ # Sets the options to the default values
68
+ def reset
69
+ self.adapter = DEFAULT_ADAPTER
70
+ self.api_endpoint = DEFAULT_API_ENDPOINT
71
+ self.proxy = nil
72
+ self.access_token = nil
73
+ self.client_id = nil
74
+ self.client_secret = nil
75
+ self.request_host = nil
76
+ self.user_agent = DEFAULT_USER_AGENT
77
+ self.auto_traversal = DEFAULT_AUTO_TRAVERSAL
78
+ self.raw_response = DEFAULT_RAW_RESPONSE
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,44 @@
1
+ require 'faraday_middleware'
2
+ require 'faraday/request/oauth2'
3
+ require 'faraday/response/raise_sage_one_exception'
4
+ require 'faraday/response/convert_sdata_to_headers'
5
+
6
+ module SageOne
7
+ # @api private
8
+ # @note Used by request.rb to make Faraday requests to the SageOne API.
9
+ module Connection
10
+ private
11
+
12
+ # @return [Faraday::Connection] configured with the headers SageOne expects
13
+ # and our required middleware stack. raw_response can be set to true to
14
+ # help with debugging.
15
+ def connection
16
+ options = {
17
+ headers: { 'Accept' => "application/json; charset=utf-8",
18
+ 'User-Agent' => user_agent,
19
+ 'Content-Type' => 'application/json' },
20
+ proxy: proxy,
21
+ ssl: { verify: false },
22
+ url: api_endpoint
23
+ }
24
+
25
+ Faraday.new(options) do |conn|
26
+ conn.request :json
27
+
28
+ conn.use FaradayMiddleware::OAuth2, access_token
29
+ conn.use FaradayMiddleware::RaiseSageOneException
30
+
31
+ unless raw_response
32
+ conn.use FaradayMiddleware::Mashify
33
+ conn.use FaradayMiddleware::ConvertSdataToHeaders
34
+ conn.use FaradayMiddleware::ParseJson
35
+ end
36
+
37
+ faraday_config_block.call(conn) if faraday_config_block
38
+
39
+ conn.adapter(adapter)
40
+ end
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,39 @@
1
+ module SageOne
2
+
3
+ # Custom error class for rescuing from all SageOne errors
4
+ class Error < StandardError; end
5
+
6
+ # Raised when SageOne returns a 400 HTTP status code
7
+ class BadRequest < Error; end
8
+
9
+ # Raised when SageOne returns a 401 HTTP status code
10
+ class Unauthorized < Error; end
11
+
12
+ # Raised when SageOne returns a 403 HTTP status code
13
+ class Forbidden < Error; end
14
+
15
+ # Raised when SageOne returns a 404 HTTP status code
16
+ class NotFound < Error; end
17
+
18
+ # Raised when SageOne returns a 406 HTTP status code
19
+ class NotAcceptable < Error; end
20
+
21
+ # Raised when SageOne returns a 409 HTTP status code
22
+ class Conflict < Error; end
23
+
24
+ # Raised when SageOne returns a 422 HTTP status code
25
+ class UnprocessableEntity < Error; end
26
+
27
+ # Raised when SageOne returns a 500 HTTP status code
28
+ class InternalServerError < Error; end
29
+
30
+ # Raised when SageOne returns a 501 HTTP status code
31
+ class NotImplemented < Error; end
32
+
33
+ # Raised when SageOne returns a 502 HTTP status code
34
+ class BadGateway < Error; end
35
+
36
+ # Raised when SageOne returns a 503 HTTP status code
37
+ class ServiceUnavailable < Error; end
38
+
39
+ end
@@ -0,0 +1,49 @@
1
+ require 'sage_one/connection'
2
+ require 'sage_one/request'
3
+
4
+ module SageOne
5
+
6
+ # This module helps with setting up the OAuth connection to SageOne. After the two
7
+ # step process you will have an access_token that you can store and use
8
+ # for making future API calls.
9
+ #
10
+ # @see OAuth#authorize_url Step 1 - Authorisation request
11
+ # @see #get_access_token Step 2 - Access Token Request
12
+ module OAuth
13
+
14
+ # Generates the OAuth URL for redirecting users to SageOne. You should ensure
15
+ # the your SageOne.client_id is configured before calling this method.
16
+ #
17
+ # @param [String] callback_url SageOne OAuth will pass an authorization code back to this URL.
18
+ # @return [String] The URL for you to redirect the user to SageOne.
19
+ # @example
20
+ # SageOne.authorize_url('https://example.com/auth/sageone/callback')
21
+ def authorize_url(callback_url)
22
+ params = {
23
+ client_id: client_id,
24
+ redirect_uri: callback_url,
25
+ response_type: 'code'
26
+ }
27
+ connection.build_url("/oauth/authorize/", params).to_s
28
+ end
29
+
30
+ # Returns an access token for future authentication.
31
+ #
32
+ # @param [String] code The authorisation code SageOne sent to your callback_url.
33
+ # @param [String] callback_url The callback URL you used to get the authorization code.
34
+ # @return [Hashie::Mash] Containing the access_token for you to store for making future API calls.
35
+ # @example
36
+ # # Assuming (Rails) your code is stored in params hash.
37
+ # SageOne.get_access_token(params[:code], 'https://example.com/auth/sageone/callback')
38
+ def get_access_token(code, callback_url)
39
+ params = {
40
+ client_id: client_id,
41
+ client_secret: client_secret,
42
+ grant_type: 'authorization_code',
43
+ code: code,
44
+ redirect_uri: callback_url
45
+ }
46
+ post("/oauth/token/", params)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,72 @@
1
+ require 'multi_json'
2
+
3
+ module SageOne
4
+ # This helper methods in this module are used by the public api methods.
5
+ # They use the Faraday::Connection defined in connection.rb for making
6
+ # requests. Setting 'SageOne.raw_response' to true can help with debugging.
7
+ # @api private
8
+ module Request
9
+ def delete(path, options={})
10
+ request(:delete, path, options)
11
+ end
12
+
13
+ def get(path, options={})
14
+ request(:get, path, options)
15
+ end
16
+
17
+ def post(path, options={})
18
+ request(:post, path, options)
19
+ end
20
+
21
+ def put(path, options={})
22
+ request(:put, path, options)
23
+ end
24
+
25
+ private
26
+
27
+ def request(method, path, options)
28
+ options = format_datelike_objects!(options) unless options.empty?
29
+ response = connection.send(method) do |request|
30
+ case method
31
+ when :delete, :get
32
+ options.merge!('$startIndex' => options.delete(:start_index)) if options[:start_index]
33
+ request.url(path, options)
34
+ when :post, :put
35
+ request.path = path
36
+ request.body = MultiJson.dump(options) unless options.empty?
37
+ end
38
+ request.headers['Host'] = request_host if request_host
39
+ end
40
+
41
+ if raw_response
42
+ response
43
+ elsif auto_traversal && ( next_url = links(response)["next"] )
44
+ response.body + request(method, next_url, options)
45
+ else
46
+ response.body
47
+ end
48
+ end
49
+
50
+ def links(response)
51
+ links = ( response.headers["X-SData-Pagination-Links"] || "" ).split(', ').map do |link|
52
+ url, type = link.match(/<(.*?)>; rel="(\w+)"/).captures
53
+ [ type, url ]
54
+ end
55
+
56
+ Hash[ *links.flatten ]
57
+ end
58
+
59
+ def format_datelike_objects!(options)
60
+ new_opts = {}
61
+ options.map do |k,v|
62
+ if v.respond_to?(:map)
63
+ new_opts[k] = format_datelike_objects!(v)
64
+ else
65
+ new_opts[k] = v.respond_to?(:strftime) ? v.strftime("%d/%m/%Y") : v
66
+ end
67
+ end
68
+ new_opts
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,3 @@
1
+ module SageOne
2
+ VERSION = "0.0.1"
3
+ end
data/lib/sage_one.rb ADDED
@@ -0,0 +1,31 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'sage_one/configuration'
4
+ require 'sage_one/client'
5
+ require 'sage_one/error'
6
+
7
+
8
+ module SageOne
9
+ extend Configuration
10
+ class << self
11
+ # Alias for SageOne::Client.new
12
+ #
13
+ # @return [SageOne::Client]
14
+ def new(options={})
15
+ SageOne::Client.new(options)
16
+ end
17
+
18
+ # True if the method can be delegated to the SageOne::Client
19
+ #
20
+ # @return [Boolean]
21
+ def respond_to?(method, include_private=false)
22
+ new.respond_to?(method, include_private) || super(method, include_private)
23
+ end
24
+
25
+ # Delegate to SageOne::Client.new
26
+ def method_missing(method, *args, &block)
27
+ return super unless new.respond_to?(method)
28
+ new.send(method, *args, &block)
29
+ end
30
+ end
31
+ end
data/sage_one.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+ require File.expand_path('../lib/sage_one/version', __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.add_dependency 'addressable', '~>2.3'
6
+ s.add_dependency 'faraday', '~>0.8'
7
+ s.add_dependency 'faraday_middleware', '~> 0.9'
8
+ s.add_dependency 'hashie', '~>1.2'
9
+ s.add_dependency 'multi_json', '~> 1.4'
10
+
11
+ s.add_development_dependency 'json'
12
+ s.add_development_dependency 'rake'
13
+ s.add_development_dependency 'rspec'
14
+ s.add_development_dependency 'simplecov'
15
+ s.add_development_dependency 'webmock'
16
+ s.add_development_dependency 'yard'
17
+
18
+ s.authors = ['Luke Brown', 'Chris Stainthorpe']
19
+ s.email = ['tsdbrown@gmail.com', 'chris@randomcat.co.uk']
20
+ s.name = 'sage_one'
21
+ s.platform = Gem::Platform::RUBY
22
+ s.homepage = 'https://github.com/customersure/sage_one'
23
+ s.summary = %q{Ruby wrapper for the Sage One V1 API.}
24
+ s.description = s.summary
25
+ s.rubyforge_project = s.name
26
+ s.files = `git ls-files`.split("\n")
27
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
28
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
29
+ s.require_paths = ['lib']
30
+ s.required_rubygems_version = Gem::Requirement.new('>= 1.3.6')
31
+ s.version = SageOne::VERSION
32
+ end
@@ -0,0 +1,44 @@
1
+ require 'spec_helper'
2
+
3
+ describe FaradayMiddleware::OAuth2 do
4
+
5
+ let(:client) { SageOne.new }
6
+
7
+ before { stub_get('sales_invoices') }
8
+
9
+ context 'when the access_token is nil' do
10
+ it 'does not add an Authorization header' do
11
+ client.send(:connection).get do |request|
12
+ request.url 'sales_invoices'
13
+ expect(request.headers.keys).not_to include("Authorization")
14
+ end
15
+ a_get("sales_invoices").should have_been_made.once
16
+ a_get("sales_invoices").with(:headers => { "Authorization" => "Bearer #{nil}"}).should_not have_been_made
17
+ end
18
+ end
19
+
20
+ context 'when the access_token is set' do
21
+ let(:access_token) { 'wHeHaMwscCIEOLGQ9uIi' }
22
+ before { client.access_token = access_token }
23
+
24
+ it 'adds an Authorization header with the Bearer token' do
25
+ client.send(:connection).get('sales_invoices')
26
+ a_get("sales_invoices").with(:headers => { 'Authorization' => "Bearer #{access_token}" }).should have_been_made.once
27
+ end
28
+ end
29
+
30
+ context 'when the Authorization header has already been set' do
31
+ let(:access_token) { 'wHeHaMwscCIEOLGQ9uIi' }
32
+ let(:pre_set_access_token) { 'SuperBearer my_pre_set_token' }
33
+ before { client.access_token = access_token }
34
+
35
+ it 'leaves the set Authorization header alone' do
36
+ client.send(:connection).get do |request|
37
+ request.url 'sales_invoices'
38
+ request.headers['Authorization'] = pre_set_access_token
39
+ end
40
+ a_get("sales_invoices").with(:headers => { 'Authorization' => pre_set_access_token }).should have_been_made.once
41
+ end
42
+ end
43
+
44
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ describe FaradayMiddleware::ConvertSdataToHeaders do
4
+
5
+ def connection(include_mashify=true)
6
+ Faraday.new(url: sage_url(nil)) do |conn|
7
+ conn.request :json
8
+ conn.use FaradayMiddleware::Mashify if include_mashify
9
+ conn.use FaradayMiddleware::ConvertSdataToHeaders
10
+ conn.use FaradayMiddleware::ParseJson
11
+ conn.adapter Faraday.default_adapter
12
+ end
13
+ end
14
+
15
+ context "when on the first page"do
16
+ it "does not add a prev link" do
17
+ stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 20, 0, 20))
18
+ response = connection.get('sales_invoices')
19
+ expect(response.headers).to be_empty
20
+ end
21
+ context "and there are more results than items_per_page" do
22
+ it "adds a next link" do
23
+ stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 40, 0, 20))
24
+ response = connection.get('sales_invoices')
25
+ expect(response.headers['X-Sdata-Pagination-Links']).to_not be_nil
26
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=20')}>; rel="next"})
27
+ end
28
+ end
29
+ end
30
+
31
+ context "when on the last page" do
32
+ before { stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 40, 20, 20)) }
33
+ it "does not add a next link" do
34
+ response = connection.get('sales_invoices')
35
+ expect(response.headers).to_not include('rel="next"')
36
+ end
37
+ it "adds a prev link" do
38
+ response = connection.get('sales_invoices')
39
+ expect(response.headers['X-Sdata-Pagination-Links']).to_not be_nil
40
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=0')}>; rel="prev"})
41
+ end
42
+ context "when requesting a value in the middle of the last page" do
43
+ before { stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 40, 30, 20)) }
44
+ it "does not add a next link" do
45
+ response = connection.get('sales_invoices')
46
+ expect(response.headers).to_not include('rel="next"')
47
+ end
48
+ it "adds a prev link" do
49
+ response = connection.get('sales_invoices')
50
+ expect(response.headers['X-Sdata-Pagination-Links']).to_not be_nil
51
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=10')}>; rel="prev"})
52
+ end
53
+ end
54
+ end
55
+
56
+ context "when there are fewer results than items_per_page" do
57
+ before { stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 5, 0, 20)) }
58
+ it "adds no links" do
59
+ response = connection.get('sales_invoices')
60
+ expect(response.headers['X-Sdata-Pagination-Links']).to be_nil
61
+ end
62
+ end
63
+
64
+ context "when on a middle page" do
65
+ before { stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 20, 10, 5)) }
66
+ it "adds a prev link" do
67
+ response = connection.get('sales_invoices')
68
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=5')}>; rel="prev"})
69
+ end
70
+ it "adds a next link" do
71
+ response = connection.get('sales_invoices')
72
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=15')}>; rel="next"})
73
+ end
74
+ end
75
+
76
+ it "places the resources in the body of the response" do
77
+ stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 20, 10, 5))
78
+ response = connection(false).get('sales_invoices')
79
+ response.body.to_s.should eq(JSON.parse(sdata_fixture('sales_invoices.json', 20,10,5))['$resources'].to_s)
80
+ end
81
+
82
+ context "when there are existing query parameters" do
83
+ before { stub_get('sales_invoices?search=salad&from_date=2012-11-11').to_return(body: sdata_fixture('sales_invoices.json', 20, 10, 5)) }
84
+ it "it passes them through without modification" do
85
+ response = connection.get('sales_invoices?from_date=2012-11-11&search=salad')
86
+ expect(response.headers['X-Sdata-Pagination-Links']).to eq('<https://app.sageone.com/api/v1/sales_invoices?%24startIndex=15&from_date=2012-11-11&search=salad>; rel="next", <https://app.sageone.com/api/v1/sales_invoices?%24startIndex=5&from_date=2012-11-11&search=salad>; rel="prev"')
87
+ end
88
+ end
89
+
90
+ context 'when there is no SData' do
91
+ before { stub_get('sales_invoices/954380').to_return(body: fixture('sales_invoice.json')) }
92
+ it "does not create link headers" do
93
+ response = connection.get('sales_invoices/954380')
94
+ expect(response.headers['X-Sdata-Pagination-Links']).to be_nil
95
+ end
96
+ it "leaves the resources in the body of the response" do
97
+ response = connection(false).get('sales_invoices/954380')
98
+ response.body.to_s.should eq(JSON.parse(fixture('sales_invoice.json').read).to_s)
99
+ end
100
+ end
101
+
102
+ context 'within first page of results, but not from start_index of 0' do
103
+ before { stub_get('sales_invoices').to_return(body: sdata_fixture('sales_invoices.json', 20, 3, 5)) }
104
+ it "Adds a previous link to start_index=0" do
105
+ response = connection.get('sales_invoices')
106
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=0')}>; rel="prev"})
107
+ end
108
+ it "Adds a next link to start_index=8" do
109
+ response = connection.get('sales_invoices')
110
+ expect(response.headers['X-Sdata-Pagination-Links']).to include(%Q{<#{sage_url('sales_invoices?%24startIndex=8')}>; rel="next"})
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,45 @@
1
+ require 'spec_helper'
2
+
3
+ describe FaradayMiddleware::RaiseSageOneException do
4
+ let(:client) { SageOne::Client.new }
5
+
6
+ {
7
+ 400 => SageOne::BadRequest,
8
+ 401 => SageOne::Unauthorized,
9
+ 403 => SageOne::Forbidden,
10
+ 404 => SageOne::NotFound,
11
+ 409 => SageOne::Conflict,
12
+ 422 => SageOne::UnprocessableEntity,
13
+ }.each do |status, exception|
14
+ context "when HTTP status is #{status}" do
15
+
16
+ before { stub_get('sales_invoices').to_return(:status => status) }
17
+
18
+ it "raises #{exception.name} error" do
19
+ expect { client.sales_invoices }.to raise_error(exception)
20
+ end
21
+ end
22
+ end
23
+
24
+ context "when the response body contains an error: key (i.e. from OAuth)" do
25
+ before { stub_get('sales_invoices').to_return(:status => 400, body: { error: "Unauthorised Access" }) }
26
+ it "raises an error with the error message" do
27
+ expect { client.sales_invoices }.to raise_error(SageOne::BadRequest, '{"method":"get","url":"https://app.sageone.com/api/v1/sales_invoices","status":400,"body":{"error":"Unauthorised Access"}}')
28
+ end
29
+ end
30
+
31
+ context "when the response body contains validation errors" do
32
+ before { stub_post('sales_invoices').to_return(:status => 422, body: fixture('invalid_sales_invoice.json')) }
33
+ it "includes the validation errors" do
34
+ expect { client.post('sales_invoices', {}) }.to raise_error(SageOne::UnprocessableEntity, %q{{"method":"post","url":"https://app.sageone.com/api/v1/sales_invoices","status":422,"body":{"$diagnoses":[{"$severity":"error","$dataCode":"ValidationError","$message":"blank","$source":"due_date"},{"$severity":"error","$dataCode":"ValidationError","$message":"invalid date","$source":"due_date"},{"$severity":"error","$dataCode":"ValidationError","$message":"blank","$source":"carriage_tax_code_id"},{"$severity":"error","$dataCode":"ValidationError","$message":"The type of sale associated with a product/service no longer exists. Check product/service and try again.","$source":"line_items"}]}}})
35
+ end
36
+ end
37
+
38
+ context "when the response body is nil" do
39
+ before { stub_post('sales_invoices').to_return(:status => 422, body: nil) }
40
+ it "just converts this to a blank string" do
41
+ expect { client.post('sales_invoices', {}) }.to raise_error(SageOne::UnprocessableEntity, %q{{"method":"post","url":"https://app.sageone.com/api/v1/sales_invoices","status":422,"body":""}})
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,28 @@
1
+ {
2
+ "id":568085,
3
+ "name":"Luke Brown",
4
+ "company_name":"Luke corp",
5
+ "name_and_company_name":"Luke corp (Luke Brown)",
6
+ "contact_type":{"id":1,"$key":1},
7
+ "email":"luke.brown@example.com",
8
+ "telephone":"",
9
+ "mobile":"",
10
+ "notes":"",
11
+ "tax_reference":"",
12
+ "lock_version":0,
13
+ "main_address":{"street_one":"",
14
+ "street_two":"",
15
+ "town":"",
16
+ "county":"",
17
+ "postcode":"",
18
+ "country":{"$key":null},
19
+ "$key":1167938},
20
+ "delivery_address":{"street_one":"",
21
+ "street_two":"",
22
+ "town":"",
23
+ "county":"",
24
+ "postcode":"",
25
+ "country":{"$key":null},
26
+ "$key":1167939},
27
+ "$key":568085
28
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$diagnoses": [
3
+ {
4
+ "$severity":"error",
5
+ "$dataCode":"ValidationError",
6
+ "$message":"blank",
7
+ "$source":"due_date"
8
+ },
9
+ {
10
+ "$severity":"error",
11
+ "$dataCode":"ValidationError",
12
+ "$message":"invalid date",
13
+ "$source":"due_date"
14
+ },
15
+ {
16
+ "$severity":"error",
17
+ "$dataCode":"ValidationError",
18
+ "$message":"blank",
19
+ "$source":"carriage_tax_code_id"
20
+ },
21
+ {
22
+ "$severity":"error",
23
+ "$dataCode":"ValidationError",
24
+ "$message":"The type of sale associated with a product/service no longer exists. Check product/service and try again.",
25
+ "$source":"line_items"
26
+ }
27
+ ]
28
+ }
@@ -0,0 +1 @@
1
+ {"error":"invalid_client"}
@@ -0,0 +1 @@
1
+ {"error":"invalid_grant"}
@@ -0,0 +1,4 @@
1
+ {
2
+ "access_token":"IguLPAK5VPvDSw3z4SjrkhHTyRXgnnqAPzt1mLUk",
3
+ "token_type":"bearer"
4
+ }
@@ -0,0 +1,43 @@
1
+ {
2
+ "id":954380,
3
+ "invoice_number":"SI-4",
4
+ "status":{"id":1, "$key":1},
5
+ "due_date":"19/04/2013",
6
+ "date":"20/03/2013",
7
+ "void_reason":null,
8
+ "outstanding_amount":"17.0",
9
+ "total_net_amount":"14.17",
10
+ "total_tax_amount":"2.83",
11
+ "tax_scheme_period_id":30394,
12
+ "carriage":"0.0",
13
+ "carriage_tax_code":{"id":1, "$key":1},
14
+ "carriage_tax_rate_percentage":"20.0",
15
+ "contact":{"id":568085, "$key":568085},
16
+ "contact_name":"Luke corp (Luke Brown)",
17
+ "main_address":"Killer Bees",
18
+ "delivery_address":"",
19
+ "delivery_address_same_as_main":false,
20
+ "reference":"",
21
+ "notes":"future date. did email it.",
22
+ "terms_and_conditions":"",
23
+ "lock_version":0,
24
+ "line_items":[
25
+ { "id":1570294,
26
+ "description":"Some fish - herring",
27
+ "quantity":"1.0",
28
+ "unit_price":"17.0",
29
+ "net_amount":"14.17",
30
+ "tax_amount":"2.83",
31
+ "tax_code":{"id":1, "$key":1},
32
+ "tax_rate_percentage":"20.0",
33
+ "unit_price_includes_tax":true,
34
+ "ledger_account":{"id":1898509, "$key":1898509},
35
+ "product_code":"HERRING",
36
+ "product":{"id":68656, "$key":68656},
37
+ "service":{"$key":null},
38
+ "lock_version":0,
39
+ "$key":1570294
40
+ }
41
+ ],
42
+ "$key":954380
43
+ }