gini-api 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|