gini-api 0.9.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.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Guardfile +4 -0
- data/README.md +298 -0
- data/Rakefile +48 -0
- data/gini-api.gemspec +45 -0
- data/lib/gini-api/client.rb +284 -0
- data/lib/gini-api/document/extractions.rb +59 -0
- data/lib/gini-api/document/layout.rb +50 -0
- data/lib/gini-api/document.rb +136 -0
- data/lib/gini-api/documentset.rb +35 -0
- data/lib/gini-api/error.rb +100 -0
- data/lib/gini-api/oauth.rb +175 -0
- data/lib/gini-api/version.rb +9 -0
- data/lib/gini-api.rb +15 -0
- data/spec/gini-api/client_spec.rb +505 -0
- data/spec/gini-api/document/extraction_spec.rb +121 -0
- data/spec/gini-api/document/layout_spec.rb +78 -0
- data/spec/gini-api/document_spec.rb +289 -0
- data/spec/gini-api/documentset_spec.rb +43 -0
- data/spec/gini-api/error_spec.rb +155 -0
- data/spec/gini-api/oauth_spec.rb +412 -0
- data/spec/integration/api_spec.rb +124 -0
- data/spec/integration/files/test.pdf +1860 -0
- data/spec/spec_helper.rb +35 -0
- data/yard/default/fulldoc/html/css/common.css +7 -0
- metadata +293 -0
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'oauth2'
|
2
|
+
|
3
|
+
module Gini
|
4
|
+
module Api
|
5
|
+
|
6
|
+
# OAuth2 related methods to access API resources
|
7
|
+
#
|
8
|
+
class OAuth
|
9
|
+
|
10
|
+
attr_reader :token
|
11
|
+
|
12
|
+
# Instantiate a new Gini::Api::OAuth object and acquire token(s)
|
13
|
+
#
|
14
|
+
# @param [Gini::Api::Client] api Instance of Gini::Api::Client that contains all required params
|
15
|
+
# @param [Hash] opts Your authorization credentials
|
16
|
+
# @option opts [String] auth_code OAuth authorization code. Will be exchanged for a token
|
17
|
+
# @option opts [String] username API username
|
18
|
+
# @option opts [String] password API password
|
19
|
+
#
|
20
|
+
def initialize(api, opts)
|
21
|
+
# Initialize client. max_redirect is required as oauth2 will otherwise redirect to location from header (localhost)
|
22
|
+
# https://github.com/intridea/oauth2/blob/master/lib/oauth2/client.rb#L100
|
23
|
+
# Our code is encoded in the URL and has to be parsed from there.
|
24
|
+
client = OAuth2::Client.new(
|
25
|
+
api.client_id,
|
26
|
+
api.client_secret,
|
27
|
+
site: api.oauth_site,
|
28
|
+
authorize_url: '/authorize',
|
29
|
+
token_url: '/token',
|
30
|
+
max_redirects: 0,
|
31
|
+
raise_errors: true,
|
32
|
+
)
|
33
|
+
|
34
|
+
# Verify opts. Prefered authorization methis is auth_code. If no auth_code is present a login from username/password
|
35
|
+
# is done.
|
36
|
+
auth_code =
|
37
|
+
if opts.key?(:auth_code) && !opts[:auth_code].empty?
|
38
|
+
opts[:auth_code]
|
39
|
+
else
|
40
|
+
login_with_credentials(api, client, opts[:username], opts[:password])
|
41
|
+
end
|
42
|
+
|
43
|
+
# Exchange code for a real token.
|
44
|
+
# @token is a Oauth2::AccessToken object. Accesstoken is @token.token
|
45
|
+
@token = exchange_code_for_token(api, client, auth_code)
|
46
|
+
|
47
|
+
# Override OAuth2::AccessToken#refresh! to update self instead of returnign a new object
|
48
|
+
# Inspired by https://github.com/intridea/oauth2/issues/116#issuecomment-8097675
|
49
|
+
#
|
50
|
+
# @param [Hash] opts Refresh opts passed to original refresh! method
|
51
|
+
#
|
52
|
+
# @return [OAuth2::AccessToken] Updated access token object
|
53
|
+
#
|
54
|
+
def @token.refresh!(opts = {})
|
55
|
+
new_token = super
|
56
|
+
(new_token.instance_variables - %w[@refresh_token]).each do |name|
|
57
|
+
instance_variable_set(name, new_token.instance_variable_get(name))
|
58
|
+
end
|
59
|
+
self
|
60
|
+
end
|
61
|
+
|
62
|
+
# Override OAuth2::AccessToken#request to refresh token when less then 60 seconds left
|
63
|
+
#
|
64
|
+
# @param [Symbol] verb the HTTP request method
|
65
|
+
# @param [String] path the HTTP URL path of the request
|
66
|
+
# @param [Hash] opts the options to make the request with
|
67
|
+
#
|
68
|
+
def @token.request(verb, path, opts = {}, &block)
|
69
|
+
refresh! if refresh_token && (expires_at < Time.now.to_i + 60)
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Login with username/password
|
75
|
+
#
|
76
|
+
# @param [Gini::Api::Client] api API object
|
77
|
+
# @param [OAuth2::Client] client OAuth2 client object
|
78
|
+
# @param [String] username API username
|
79
|
+
# @param [String] password API password
|
80
|
+
#
|
81
|
+
# @return [String] Collected authorization code
|
82
|
+
#
|
83
|
+
def login_with_credentials(api, client, username, password)
|
84
|
+
# Generate CSRF token to verify the response
|
85
|
+
csrf_token = SecureRandom.hex
|
86
|
+
|
87
|
+
# Build authentication URI
|
88
|
+
auth_uri = client.auth_code.authorize_url(
|
89
|
+
redirect_uri: api.oauth_redirect,
|
90
|
+
state: csrf_token
|
91
|
+
)
|
92
|
+
|
93
|
+
begin
|
94
|
+
# Accquire auth code
|
95
|
+
response = client.request(
|
96
|
+
:post,
|
97
|
+
auth_uri,
|
98
|
+
body: { username: username, password: password }
|
99
|
+
)
|
100
|
+
unless response.status == 303
|
101
|
+
raise Gini::Api::OAuthError.new(
|
102
|
+
"API login failed (code=#{response.status})",
|
103
|
+
response
|
104
|
+
)
|
105
|
+
end
|
106
|
+
rescue OAuth2::Error => e
|
107
|
+
raise Gini::Api::OAuthError.new(
|
108
|
+
"Failed to acquire auth_code (code=#{e.response.status})",
|
109
|
+
e.response
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Parse the location header from the response and fill hash
|
114
|
+
# query_params ({'code' => '123abc', 'state' => 'supersecret'})
|
115
|
+
begin
|
116
|
+
q = URI.parse(response.headers['location']).query
|
117
|
+
query_params = Hash[*q.split(/\=|\&/)]
|
118
|
+
rescue => e
|
119
|
+
raise Gini::Api::OAuthError.new("Failed to parse location header: #{e.message}")
|
120
|
+
end
|
121
|
+
|
122
|
+
unless query_params['state'] == csrf_token
|
123
|
+
raise Gini::Api::OAuthError.new(
|
124
|
+
"CSRF token mismatch detected (should=#{csrf_token}, "\
|
125
|
+
"is=#{query_params['state']})"
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
unless query_params.key?('code') && !query_params['code'].empty?
|
130
|
+
raise Gini::Api::OAuthError.new(
|
131
|
+
"Failed to extract code from location #{response.headers['location']}"
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
query_params['code']
|
136
|
+
end
|
137
|
+
|
138
|
+
# Exchange auth_code for a real token
|
139
|
+
#
|
140
|
+
# @param [Gini::Api::Client] api API object
|
141
|
+
# @param [OAuth2::Client] client OAuth2 client object
|
142
|
+
# @param [String] auth_code authorization code
|
143
|
+
#
|
144
|
+
# @return [OAuth2::AccessToken] AccessToken object
|
145
|
+
#
|
146
|
+
def exchange_code_for_token(api, client, auth_code)
|
147
|
+
client.auth_code.get_token(auth_code, redirect_uri: api.oauth_redirect)
|
148
|
+
rescue OAuth2::Error => e
|
149
|
+
raise Gini::Api::OAuthError.new(
|
150
|
+
"Failed to exchange auth_code for token (code=#{e.response.status})",
|
151
|
+
e.response
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Destroy token
|
156
|
+
#
|
157
|
+
def destroy
|
158
|
+
@token.refresh_token && @token.refresh!
|
159
|
+
response = token.delete("/accessToken/#{@token.token}")
|
160
|
+
unless response.status == 204
|
161
|
+
raise Gini::Api::OAuthError.new(
|
162
|
+
"Failed to destroy token /accessToken/#{@token.token} "\
|
163
|
+
"(code=#{response.status})",
|
164
|
+
response
|
165
|
+
)
|
166
|
+
end
|
167
|
+
rescue OAuth2::Error => e
|
168
|
+
raise Gini::Api::OAuthError.new(
|
169
|
+
"Failed to destroy token (code=#{e.response.status})",
|
170
|
+
e.response
|
171
|
+
)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
data/lib/gini-api.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'gini-api/error'
|
2
|
+
require 'gini-api/oauth'
|
3
|
+
require 'gini-api/client'
|
4
|
+
require 'gini-api/version'
|
5
|
+
require 'gini-api/document'
|
6
|
+
require 'gini-api/document/layout'
|
7
|
+
require 'gini-api/document/extractions'
|
8
|
+
require 'gini-api/documentset'
|
9
|
+
|
10
|
+
# Gini module namespace
|
11
|
+
module Gini
|
12
|
+
# Gini::Api module namespace
|
13
|
+
module Api
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,505 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Gini::Api::Client do
|
4
|
+
|
5
|
+
let(:user) { 'user@gini.net' }
|
6
|
+
let(:pass) { 'secret' }
|
7
|
+
let(:auth_code) { '1234567890' }
|
8
|
+
let(:header) { 'application/vnd.gini.v1+json' }
|
9
|
+
let(:oauth) do
|
10
|
+
double(
|
11
|
+
'Gini::Api::OAuth',
|
12
|
+
:token => 'TOKEN',
|
13
|
+
:destroy => nil
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
subject(:api) do
|
18
|
+
Gini::Api::Client.new(
|
19
|
+
client_id: 'gini-rspec',
|
20
|
+
client_secret: 'secret',
|
21
|
+
log: (Logger.new '/dev/null'),
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
it { should respond_to(:register_parser) }
|
26
|
+
it { should respond_to(:login) }
|
27
|
+
it { should respond_to(:logout) }
|
28
|
+
it { should respond_to(:version_header) }
|
29
|
+
it { should respond_to(:request) }
|
30
|
+
it { should respond_to(:upload) }
|
31
|
+
it { should respond_to(:delete) }
|
32
|
+
it { should respond_to(:get) }
|
33
|
+
it { should respond_to(:list) }
|
34
|
+
it { should respond_to(:search) }
|
35
|
+
|
36
|
+
describe '#new' do
|
37
|
+
|
38
|
+
it 'fails with missing options' do
|
39
|
+
expect { Gini::Api::Client.new }.to \
|
40
|
+
raise_error(Gini::Api::Error, /Mandatory option key is missing/)
|
41
|
+
end
|
42
|
+
|
43
|
+
it do
|
44
|
+
expect(api.log.class).to eq(Logger)
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
describe '#register_parser' do
|
50
|
+
|
51
|
+
it do
|
52
|
+
expect(OAuth2::Response::PARSERS.keys).to \
|
53
|
+
include(:gini_json)
|
54
|
+
end
|
55
|
+
|
56
|
+
it do
|
57
|
+
expect(OAuth2::Response::PARSERS.keys).to \
|
58
|
+
include(:gini_xml)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#login' do
|
64
|
+
|
65
|
+
context 'with auth_code' do
|
66
|
+
|
67
|
+
it 'sets @token' do
|
68
|
+
expect(Gini::Api::OAuth).to \
|
69
|
+
receive(:new).with(
|
70
|
+
api, auth_code: auth_code
|
71
|
+
) { oauth }
|
72
|
+
expect(oauth).to receive(:token)
|
73
|
+
|
74
|
+
api.login(auth_code: auth_code)
|
75
|
+
expect(api.token).to eql('TOKEN')
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'with username/password' do
|
81
|
+
|
82
|
+
it 'sets @token' do
|
83
|
+
expect(Gini::Api::OAuth).to \
|
84
|
+
receive(:new).with(
|
85
|
+
api, username:
|
86
|
+
user, password: pass
|
87
|
+
) { oauth }
|
88
|
+
expect(oauth).to receive(:token)
|
89
|
+
|
90
|
+
api.login(username: user, password: pass)
|
91
|
+
expect(api.token).to eql('TOKEN')
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
describe '#logout' do
|
99
|
+
|
100
|
+
it 'destroys token' do
|
101
|
+
expect(Gini::Api::OAuth).to \
|
102
|
+
receive(:new).with(
|
103
|
+
api,
|
104
|
+
auth_code: auth_code
|
105
|
+
) { oauth }
|
106
|
+
expect(oauth).to receive(:token)
|
107
|
+
api.login(auth_code: auth_code)
|
108
|
+
|
109
|
+
expect(oauth).to receive(:destroy)
|
110
|
+
api.logout
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#version_header' do
|
116
|
+
|
117
|
+
let(:api) do
|
118
|
+
Gini::Api::Client.new(
|
119
|
+
client_id: 1,
|
120
|
+
client_secret: 2,
|
121
|
+
log: (Logger.new '/dev/null')
|
122
|
+
)
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'with json' do
|
126
|
+
|
127
|
+
it 'returns accept header with json type' do
|
128
|
+
expect(api.version_header(:json)).to \
|
129
|
+
eql({ accept: header })
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'with xml' do
|
135
|
+
|
136
|
+
it 'returns accept header with xml type' do
|
137
|
+
expect(api.version_header(:xml)).to \
|
138
|
+
eql({ accept: 'application/vnd.gini.v1+xml' })
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'being logged in' do
|
146
|
+
|
147
|
+
before do
|
148
|
+
expect(Gini::Api::OAuth).to \
|
149
|
+
receive(:new).with(
|
150
|
+
api,
|
151
|
+
auth_code: auth_code
|
152
|
+
) { oauth }
|
153
|
+
api.login(auth_code: auth_code)
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '#request' do
|
157
|
+
|
158
|
+
let(:response) do
|
159
|
+
double('Response',
|
160
|
+
status: 200,
|
161
|
+
headers: {
|
162
|
+
'content-type' => header
|
163
|
+
},
|
164
|
+
body: body
|
165
|
+
)
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'token receives call' do
|
169
|
+
expect(api.token).to receive(:get)
|
170
|
+
api.request(:get, '/dummy')
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'raises RequestError from OAuth2::Error' do
|
174
|
+
expect(api.token).to \
|
175
|
+
receive(:get).and_raise(
|
176
|
+
OAuth2::Error.new(double.as_null_object)
|
177
|
+
)
|
178
|
+
expect { api.request(:get, '/invalid') }.to \
|
179
|
+
raise_error(Gini::Api::RequestError)
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'raises ProcessingError on timeout' do
|
183
|
+
expect(api.token).to \
|
184
|
+
receive(:get).and_raise(Timeout::Error)
|
185
|
+
expect { api.request(:get, '/timeout') }.to \
|
186
|
+
raise_error(Gini::Api::ProcessingError)
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'return JSON as default' do
|
190
|
+
|
191
|
+
let(:body) do
|
192
|
+
{
|
193
|
+
a: 1,
|
194
|
+
b: 2
|
195
|
+
}.to_json
|
196
|
+
end
|
197
|
+
|
198
|
+
it do
|
199
|
+
expect(api.token).to \
|
200
|
+
receive(:get).and_return(OAuth2::Response.new(response))
|
201
|
+
expect(api.request(:get, '/dummy').parsed).to be_a Hash
|
202
|
+
end
|
203
|
+
|
204
|
+
end
|
205
|
+
|
206
|
+
context 'return XML on request' do
|
207
|
+
|
208
|
+
let(:header) { 'application/vnd.gini.v1+xml' }
|
209
|
+
let(:body) { '<data><a>1</a><b>2</b></data>' }
|
210
|
+
|
211
|
+
it do
|
212
|
+
expect(api.token).to \
|
213
|
+
receive(:get).and_return(
|
214
|
+
OAuth2::Response.new(response)
|
215
|
+
)
|
216
|
+
|
217
|
+
expect(api.request(
|
218
|
+
:get,
|
219
|
+
'/dummy',
|
220
|
+
type: 'xml'
|
221
|
+
).parsed).to be_a Hash
|
222
|
+
end
|
223
|
+
|
224
|
+
end
|
225
|
+
|
226
|
+
context 'set custom accept header' do
|
227
|
+
|
228
|
+
let(:header) { 'application/octet-stream' }
|
229
|
+
let(:body) { 'Just a string' }
|
230
|
+
|
231
|
+
it do
|
232
|
+
expect(api.token).to \
|
233
|
+
receive(:get).with(
|
234
|
+
%r{/dummy},
|
235
|
+
headers: {
|
236
|
+
accept: 'application/octet-stream'
|
237
|
+
}
|
238
|
+
).and_return(OAuth2::Response.new(response))
|
239
|
+
expect(api.request(
|
240
|
+
:get,
|
241
|
+
'/dummy',
|
242
|
+
headers: {
|
243
|
+
accept: 'application/octet-stream'
|
244
|
+
}
|
245
|
+
).body).to be_a String
|
246
|
+
end
|
247
|
+
|
248
|
+
end
|
249
|
+
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#upload' do
|
253
|
+
|
254
|
+
let(:doc) { double(Gini::Api::Document, poll: true, id: 'abc-123') }
|
255
|
+
|
256
|
+
before do
|
257
|
+
allow(doc).to receive(:duration=)
|
258
|
+
allow(Gini::Api::Document).to \
|
259
|
+
receive(:new).with(api, 'LOC'
|
260
|
+
) { doc }
|
261
|
+
api.token.stub(:token).and_return('abc-123')
|
262
|
+
stub_request(
|
263
|
+
:post,
|
264
|
+
%r{/documents}
|
265
|
+
).to_return(
|
266
|
+
status: status,
|
267
|
+
headers: {
|
268
|
+
location: 'LOC'
|
269
|
+
},
|
270
|
+
body: {}
|
271
|
+
)
|
272
|
+
end
|
273
|
+
|
274
|
+
context 'when failed' do
|
275
|
+
|
276
|
+
let(:status) { 500 }
|
277
|
+
|
278
|
+
it do
|
279
|
+
expect { api.upload('spec/integration/files/test.pdf') }.to \
|
280
|
+
raise_error(Gini::Api::UploadError)
|
281
|
+
end
|
282
|
+
|
283
|
+
end
|
284
|
+
|
285
|
+
context 'when successful' do
|
286
|
+
|
287
|
+
let(:status) { 201 }
|
288
|
+
|
289
|
+
it 'Gini::Api::Document is created' do
|
290
|
+
api.upload('spec/integration/files/test.pdf')
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
context 'on timeout' do
|
295
|
+
|
296
|
+
let(:status) { 201 }
|
297
|
+
|
298
|
+
it 'raises ProcessingError on timeout' do
|
299
|
+
expect(doc).to receive(:poll).and_raise(Timeout::Error)
|
300
|
+
expect { api.upload('spec/integration/files/test.pdf') }.to \
|
301
|
+
raise_error(Gini::Api::ProcessingError)
|
302
|
+
end
|
303
|
+
|
304
|
+
end
|
305
|
+
|
306
|
+
end
|
307
|
+
|
308
|
+
describe '#delete' do
|
309
|
+
|
310
|
+
let(:response) do
|
311
|
+
double('Response',
|
312
|
+
status: status,
|
313
|
+
env: {},
|
314
|
+
body: {}
|
315
|
+
)
|
316
|
+
end
|
317
|
+
|
318
|
+
context 'with invalid docId' do
|
319
|
+
|
320
|
+
let(:status) { 203 }
|
321
|
+
|
322
|
+
it do
|
323
|
+
api.token.stub(:delete).and_return(response)
|
324
|
+
expect { api.delete('abc-123') }.to \
|
325
|
+
raise_error(Gini::Api::DocumentError, /Deletion of docId abc-123 failed/)
|
326
|
+
end
|
327
|
+
|
328
|
+
end
|
329
|
+
|
330
|
+
context 'with valid docId' do
|
331
|
+
|
332
|
+
let(:status) { 204 }
|
333
|
+
|
334
|
+
it do
|
335
|
+
api.token.stub(:delete).and_return(response)
|
336
|
+
expect(api.delete('abc-123')).to be_true
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
342
|
+
|
343
|
+
describe '#get' do
|
344
|
+
|
345
|
+
it do
|
346
|
+
expect(Gini::Api::Document).to \
|
347
|
+
receive(:new) { double('Gini::Api::Document') }
|
348
|
+
api.get('abc-123')
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
352
|
+
|
353
|
+
describe '#list' do
|
354
|
+
|
355
|
+
let(:response) do
|
356
|
+
double('Response',
|
357
|
+
status: 200,
|
358
|
+
headers: {
|
359
|
+
'content-type' => header
|
360
|
+
},
|
361
|
+
body: {
|
362
|
+
totalCount: doc_count,
|
363
|
+
next: nil,
|
364
|
+
documents: documents
|
365
|
+
}.to_json)
|
366
|
+
end
|
367
|
+
|
368
|
+
before do
|
369
|
+
api.token.stub(:get).and_return(OAuth2::Response.new(response))
|
370
|
+
end
|
371
|
+
|
372
|
+
context 'with documents' do
|
373
|
+
|
374
|
+
let(:doc_count) { 1 }
|
375
|
+
let(:documents) do
|
376
|
+
[
|
377
|
+
{
|
378
|
+
id: 42,
|
379
|
+
:_links => {
|
380
|
+
:document => 'https://rspec/123-abc'
|
381
|
+
}
|
382
|
+
}
|
383
|
+
]
|
384
|
+
end
|
385
|
+
|
386
|
+
it do
|
387
|
+
expect(api.list.total).to eql(1)
|
388
|
+
expect(api.list.offset).to be_nil
|
389
|
+
expect(api.list.documents[0]).to be_a(Gini::Api::Document)
|
390
|
+
expect(api.list).to be_a(Gini::Api::DocumentSet)
|
391
|
+
end
|
392
|
+
|
393
|
+
end
|
394
|
+
|
395
|
+
context 'without documents' do
|
396
|
+
|
397
|
+
let(:doc_count) { 0 }
|
398
|
+
let(:documents) { [] }
|
399
|
+
|
400
|
+
it do
|
401
|
+
expect(api.list.total).to eql(0)
|
402
|
+
expect(api.list.offset).to be_nil
|
403
|
+
expect(api.list.documents).to eql([])
|
404
|
+
expect(api.list).to be_a(Gini::Api::DocumentSet)
|
405
|
+
end
|
406
|
+
|
407
|
+
end
|
408
|
+
|
409
|
+
context 'with failed http request' do
|
410
|
+
|
411
|
+
let(:response) do
|
412
|
+
double('Response',
|
413
|
+
status: 500,
|
414
|
+
env: {},
|
415
|
+
body: {}
|
416
|
+
)
|
417
|
+
end
|
418
|
+
|
419
|
+
it do
|
420
|
+
expect { api.list }.to \
|
421
|
+
raise_error(Gini::Api::DocumentError, /Failed to get list of documents/)
|
422
|
+
end
|
423
|
+
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
|
428
|
+
describe '#search' do
|
429
|
+
|
430
|
+
before do
|
431
|
+
api.token.stub(:get).and_return(OAuth2::Response.new(response))
|
432
|
+
end
|
433
|
+
|
434
|
+
let(:status) { 200 }
|
435
|
+
let(:response) do
|
436
|
+
double('Response',
|
437
|
+
status: status,
|
438
|
+
headers: {
|
439
|
+
'content-type' => header
|
440
|
+
},
|
441
|
+
body: {
|
442
|
+
totalCount: doc_count,
|
443
|
+
next: nil,
|
444
|
+
documents: documents
|
445
|
+
}.to_json
|
446
|
+
)
|
447
|
+
end
|
448
|
+
|
449
|
+
context 'with found documents' do
|
450
|
+
|
451
|
+
let(:doc_count) { 1 }
|
452
|
+
let(:documents) do
|
453
|
+
[
|
454
|
+
{
|
455
|
+
id: '0f122e10-8dba-11e3-8a85-02015140775',
|
456
|
+
_links: {
|
457
|
+
document: 'https://rspec/123-abc'
|
458
|
+
}
|
459
|
+
}
|
460
|
+
]
|
461
|
+
end
|
462
|
+
|
463
|
+
it do
|
464
|
+
result = api.search('invoice')
|
465
|
+
|
466
|
+
expect(result.total).to eql(1)
|
467
|
+
expect(result.offset).to be_nil
|
468
|
+
expect(result.documents[0]).to be_a(Gini::Api::Document)
|
469
|
+
expect(result).to be_a(Gini::Api::DocumentSet)
|
470
|
+
end
|
471
|
+
|
472
|
+
end
|
473
|
+
|
474
|
+
context 'with no found documents' do
|
475
|
+
|
476
|
+
let(:doc_count) { 0 }
|
477
|
+
let(:documents) { [] }
|
478
|
+
|
479
|
+
it do
|
480
|
+
result = api.search('invoice')
|
481
|
+
|
482
|
+
expect(result.total).to eql(0)
|
483
|
+
expect(result.offset).to be_nil
|
484
|
+
expect(result.documents).to eql([])
|
485
|
+
expect(result).to be_a(Gini::Api::DocumentSet)
|
486
|
+
end
|
487
|
+
|
488
|
+
end
|
489
|
+
|
490
|
+
context 'with failed query' do
|
491
|
+
|
492
|
+
let(:response) { double('Response', status: 500, env: {}, body: {}) }
|
493
|
+
|
494
|
+
it do
|
495
|
+
expect{api.search('invoice')}.to \
|
496
|
+
raise_error(Gini::Api::SearchError, /Search query failed with code 500/)
|
497
|
+
end
|
498
|
+
|
499
|
+
end
|
500
|
+
|
501
|
+
end
|
502
|
+
|
503
|
+
end
|
504
|
+
|
505
|
+
end
|