sage_one 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|