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.
- data/.gitignore +14 -0
- data/.travis.yml +11 -0
- data/.yardopts +4 -0
- data/Gemfile +8 -0
- data/LICENSE +19 -0
- data/README.md +141 -0
- data/Rakefile +4 -0
- data/bin/autospec +16 -0
- data/bin/htmldiff +16 -0
- data/bin/ldiff +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bin/yard +16 -0
- data/bin/yardoc +16 -0
- data/bin/yri +16 -0
- data/lib/faraday/request/oauth2.rb +25 -0
- data/lib/faraday/response/convert_sdata_to_headers.rb +70 -0
- data/lib/faraday/response/raise_sage_one_exception.rb +42 -0
- data/lib/sage_one/client/contacts.rb +14 -0
- data/lib/sage_one/client/sales_invoices.rb +77 -0
- data/lib/sage_one/client.rb +33 -0
- data/lib/sage_one/configuration.rb +81 -0
- data/lib/sage_one/connection.rb +44 -0
- data/lib/sage_one/error.rb +39 -0
- data/lib/sage_one/oauth.rb +49 -0
- data/lib/sage_one/request.rb +72 -0
- data/lib/sage_one/version.rb +3 -0
- data/lib/sage_one.rb +31 -0
- data/sage_one.gemspec +32 -0
- data/spec/faraday/request/oauth2_spec.rb +44 -0
- data/spec/faraday/response/convert_sdata_to_headers_spec.rb +113 -0
- data/spec/faraday/response/raise_sage_one_exception_spec.rb +45 -0
- data/spec/fixtures/contact.json +28 -0
- data/spec/fixtures/invalid_sales_invoice.json +28 -0
- data/spec/fixtures/oauth/invalid_client.json +1 -0
- data/spec/fixtures/oauth/invalid_grant.json +1 -0
- data/spec/fixtures/oauth/oauth_token.json +4 -0
- data/spec/fixtures/sales_invoice.json +43 -0
- data/spec/fixtures/sales_invoices.json +90 -0
- data/spec/sage_one/client/contacts_spec.rb +19 -0
- data/spec/sage_one/client/sales_invoices_spec.rb +53 -0
- data/spec/sage_one/client_spec.rb +41 -0
- data/spec/sage_one/configuration_spec.rb +88 -0
- data/spec/sage_one/connection_spec.rb +36 -0
- data/spec/sage_one/oauth_spec.rb +44 -0
- data/spec/sage_one/request_spec.rb +113 -0
- data/spec/sage_one/version_spec.rb +7 -0
- data/spec/sage_one_spec.rb +38 -0
- data/spec/spec_helper.rb +76 -0
- 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
|
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,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
|
+
}
|