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.
@@ -0,0 +1,412 @@
1
+ require 'spec_helper'
2
+
3
+ describe Gini::Api::OAuth do
4
+
5
+ let(:user) { 'user@gini.net' }
6
+ let(:pass) { 'secret' }
7
+ let(:auth_code) { '1234567890' }
8
+ let(:state) { '1234567890' }
9
+ let(:code) { 'abcdefghij'}
10
+ let(:redirect) { 'http://localhost' }
11
+ let(:status) { 303 }
12
+ let(:token_status) { 200 }
13
+ let(:token_body) { 'client_id=cid&client_secret=sec&code=1234567890&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost' }
14
+ let(:header) { { 'location' => "https://api.gini.net?code=#{code}&state=#{state}" } }
15
+ let(:oauth_site) { 'https://user.gini.net' }
16
+ let(:authorize_uri) { "#{oauth_site}/authorize?client_id=cid&redirect_uri=#{redirect}&response_type=code&state=#{state}" }
17
+ let(:api) do
18
+ double('API',
19
+ client_id: 'cid',
20
+ client_secret: 'sec',
21
+ oauth_site: oauth_site,
22
+ oauth_redirect: redirect
23
+ )
24
+ end
25
+
26
+ describe '#new' do
27
+
28
+ context 'login with username/password' do
29
+
30
+ before do
31
+ allow(SecureRandom).to \
32
+ receive(:hex) { state }
33
+
34
+ stub_request(:post,
35
+ authorize_uri
36
+ ).to_return(
37
+ status: status,
38
+ headers: header,
39
+ body: {}
40
+ )
41
+
42
+ stub_request(:post,
43
+ "#{oauth_site}/token"
44
+ ).to_return(
45
+ status: token_status,
46
+ headers: {
47
+ 'content-type' => 'application/json'
48
+ },
49
+ body: {
50
+ access_token: '123-456',
51
+ token_type: 'bearer',
52
+ expires_in: 300,
53
+ refresh_token: '987-654'
54
+ }.to_json
55
+ )
56
+ end
57
+
58
+ subject(:oauth) do
59
+ Gini::Api::OAuth.new(api,
60
+ username: user,
61
+ password: pass
62
+ )
63
+ end
64
+
65
+ it { should respond_to(:login_with_credentials) }
66
+ it { should respond_to(:exchange_code_for_token) }
67
+ it { should respond_to(:destroy) }
68
+
69
+ it 'does set token' do
70
+ expect(oauth.token.token).to eql('123-456')
71
+ end
72
+
73
+ context 'with invalid credentials' do
74
+
75
+ let(:status) { 500 }
76
+
77
+ it do
78
+ expect {
79
+ Gini::Api::OAuth.new(
80
+ api,
81
+ username: user,
82
+ password: pass
83
+ ) }.to raise_error(Gini::Api::OAuthError, /Failed to acquire auth_code/)
84
+ end
85
+
86
+ end
87
+
88
+ context 'with non-redirect status code' do
89
+
90
+ let(:status) { 200 }
91
+
92
+ it do
93
+ expect {
94
+ Gini::Api::OAuth.new(
95
+ api,
96
+ username: user,
97
+ password: pass
98
+ ) }.to raise_error(Gini::Api::OAuthError, /API login failed/)
99
+ end
100
+ end
101
+
102
+ context 'with invalid location header' do
103
+
104
+ let(:header) { { location: 'https://api.gini.net' } }
105
+
106
+ it do
107
+ expect {
108
+ Gini::Api::OAuth.new(
109
+ api,
110
+ username: user,
111
+ password: pass
112
+ ) }.to raise_error(Gini::Api::OAuthError, /Failed to parse location header/)
113
+ end
114
+
115
+ end
116
+
117
+ context 'with CSRF token mismatch' do
118
+
119
+ let(:header) { { location: "https://rspec.gini.net?code=#{code}&state=hacked"} }
120
+
121
+ it do
122
+ expect {
123
+ Gini::Api::OAuth.new(
124
+ api,
125
+ username: user,
126
+ password: pass
127
+ ) }.to raise_error(Gini::Api::OAuthError, /CSRF token mismatch detected/)
128
+ end
129
+
130
+ end
131
+
132
+ context 'without code' do
133
+
134
+ let(:header) { { location: "https://api.gini.net?state=#{state}"} }
135
+
136
+ it do
137
+ expect {
138
+ Gini::Api::OAuth.new(
139
+ api,
140
+ username: user,
141
+ password: pass
142
+ ) }.to raise_error(Gini::Api::OAuthError, /Failed to extract code from location/)
143
+ end
144
+
145
+ end
146
+
147
+ context 'with invalid client credentials' do
148
+
149
+ let(:token_status) { 401 }
150
+
151
+ it do
152
+ expect {
153
+ Gini::Api::OAuth.new(
154
+ api,
155
+ username: user,
156
+ password: pass
157
+ ) }.to raise_error(Gini::Api::OAuthError, /Failed to exchange auth_code/)
158
+ end
159
+
160
+ end
161
+
162
+ end
163
+
164
+ context 'login with auth_code' do
165
+
166
+ before do
167
+ stub_request(:post,
168
+ "#{oauth_site}/token"
169
+ ).with(
170
+ body: 'client_id=cid&client_secret=sec&code=1234567890&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost'
171
+ ).to_return(
172
+ status: token_status,
173
+ headers: {
174
+ 'content-type' => 'application/json'
175
+ },
176
+ body: {
177
+ access_token: '123-456',
178
+ token_type: 'bearer',
179
+ expires_in: 300,
180
+ refresh_token: '987-654'
181
+ }.to_json
182
+ )
183
+ end
184
+
185
+ subject(:oauth) { Gini::Api::OAuth.new(api, auth_code: auth_code) }
186
+
187
+ it 'does set token' do
188
+ expect(oauth.token.token).to eql('123-456')
189
+ end
190
+
191
+ context 'overrides #refresh!' do
192
+
193
+ it do
194
+ expect(oauth.token).to respond_to(:refresh!)
195
+ end
196
+
197
+ end
198
+
199
+ context 'overrides #request' do
200
+
201
+ it do
202
+ expect(oauth.token).to respond_to(:request)
203
+ end
204
+
205
+ end
206
+
207
+ end
208
+
209
+ end
210
+
211
+ describe '#destroy' do
212
+
213
+ let(:status) { 204 }
214
+ let(:refresh_token) { false }
215
+
216
+ before do
217
+ stub_request(:post,
218
+ "#{oauth_site}/token"
219
+ ).with(
220
+ body: 'client_id=cid&client_secret=sec&code=1234567890&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost'
221
+ ).to_return(
222
+ status: token_status,
223
+ headers: {
224
+ 'content-type' => 'application/json'
225
+ },
226
+ body: {
227
+ access_token: '123-456',
228
+ token_type: 'bearer',
229
+ expires_in: 300,
230
+ refresh_token: '987-654'
231
+ }.to_json
232
+ )
233
+
234
+ stub_request(
235
+ :delete,
236
+ %r{/accessToken/123-456}
237
+ ).to_return(status: status)
238
+
239
+ oauth.token.stub(:refresh_token).and_return(refresh_token)
240
+ end
241
+
242
+ subject(:oauth) { Gini::Api::OAuth.new(api, auth_code: auth_code) }
243
+
244
+ context 'with refresh token' do
245
+
246
+ let(:refresh_token) { true }
247
+
248
+ it 'does a refresh first' do
249
+ expect(oauth.token).to receive(:refresh_token)
250
+ expect(oauth.token).to receive(:refresh!)
251
+ expect(oauth.destroy).to be_nil
252
+ end
253
+
254
+ end
255
+
256
+ context 'without refresh token' do
257
+
258
+ it 'destroys token directly' do
259
+ expect(oauth.token).to receive(:refresh_token)
260
+ expect(oauth.token).not_to receive(:refresh!)
261
+ expect(oauth.destroy).to be_nil
262
+ end
263
+ end
264
+
265
+ context 'with invalid token' do
266
+
267
+ let(:status) { 404 }
268
+
269
+ it do
270
+ expect{oauth.destroy}.to raise_error Gini::Api::OAuthError, /Failed to destroy token/
271
+ end
272
+ end
273
+
274
+ context 'with unexpected http status code' do
275
+
276
+ let(:status) { 200 }
277
+
278
+ it do
279
+ expect{oauth.destroy}.to raise_error Gini::Api::OAuthError, /Failed to destroy token/
280
+ end
281
+ end
282
+ end
283
+
284
+ describe 'overridden AccessToken#refresh!' do
285
+
286
+ before do
287
+ stub_request(:post,
288
+ "#{oauth_site}/token"
289
+ ).with(
290
+ body: 'client_id=cid&client_secret=sec&code=1234567890&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost'
291
+ ).to_return(
292
+ status: token_status,
293
+ headers: {
294
+ 'content-type' => 'application/json'
295
+ },
296
+ body: {
297
+ access_token: '123-456',
298
+ token_type: 'bearer',
299
+ expires_in: 300,
300
+ refresh_token: '987-654'
301
+ }.to_json
302
+ )
303
+ end
304
+
305
+ subject(:oauth) { Gini::Api::OAuth.new(api, auth_code: auth_code) }
306
+
307
+
308
+ it do
309
+ expect(oauth.token.token).to eql('123-456')
310
+
311
+ stub_request(
312
+ :post,
313
+ "#{oauth_site}/token"
314
+ ).to_return(
315
+ status: 200,
316
+ headers: {
317
+ 'content-type' => 'application/json'
318
+ },
319
+ body: {
320
+ access_token: '42-42-42',
321
+ token_type: 'bearer',
322
+ expires_in: 300,
323
+ refresh_token: '987-654'
324
+ }.to_json
325
+ )
326
+
327
+ oauth.token.refresh!
328
+ expect(oauth.token.token).to eql('42-42-42')
329
+ end
330
+
331
+ end
332
+
333
+ describe 'overridden AccessToken#request' do
334
+
335
+ before do
336
+ stub_request(:post,
337
+ "#{oauth_site}/token"
338
+ ).with(
339
+ body: 'client_id=cid&client_secret=sec&code=1234567890&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost'
340
+ ).to_return(
341
+ status: token_status,
342
+ headers: {
343
+ 'content-type' => 'application/json'
344
+ },
345
+ body: {
346
+ access_token: '123-456',
347
+ token_type: 'bearer',
348
+ expires_in: 300,
349
+ refresh_token: '987-654'
350
+ }.to_json
351
+ )
352
+ end
353
+
354
+ subject(:oauth) { Gini::Api::OAuth.new(api, auth_code: auth_code) }
355
+
356
+
357
+ context 'with expired token' do
358
+
359
+ it 'does call refresh!' do
360
+ stub_request(:get, "https://user.gini.net/a")
361
+ stub_request(
362
+ :post,
363
+ "#{oauth_site}/token"
364
+ ).to_return(
365
+ status: 200,
366
+ headers: {
367
+ 'content-type' => 'application/json'
368
+ },
369
+ body: {
370
+ access_token: '42-42-42',
371
+ token_type: 'bearer',
372
+ expires_in: -10,
373
+ refresh_token: '987-654'
374
+ }.to_json
375
+ )
376
+
377
+ expect(oauth.token).to receive(:refresh!)
378
+ oauth.token.request(:get, '/a')
379
+ end
380
+
381
+ end
382
+
383
+ context 'with valid token' do
384
+
385
+ it 'does not call refresh!' do
386
+
387
+ stub_request(:get, "https://user.gini.net/a")
388
+ stub_request(
389
+ :post,
390
+ "#{oauth_site}/token"
391
+ ).to_return(
392
+ status: 200,
393
+ headers: {
394
+ 'content-type' => 'application/json'
395
+ },
396
+ body: {
397
+ access_token: '42-42-42',
398
+ token_type: 'bearer',
399
+ expires_in: 300,
400
+ refresh_token: '987-654'
401
+ }.to_json
402
+ )
403
+
404
+ expect(oauth.token).not_to receive(:refresh!)
405
+ oauth.token.request(:get, '/a')
406
+ end
407
+
408
+ end
409
+
410
+ end
411
+
412
+ end
@@ -0,0 +1,124 @@
1
+ require 'spec_helper'
2
+
3
+ # Disable coverage for integration tests
4
+ ENV['COVERAGE'] = nil
5
+
6
+ # Cancel integration test when mandatory env vars are missing
7
+ ['GINI_API_USER', 'GINI_API_PASS', 'GINI_CLIENT_SECRET'].each do |m|
8
+ fail "Unset ENV variable #{m}. Tests aborted" unless ENV.has_key?(m)
9
+ end
10
+
11
+ # Let's make some REAL requests
12
+ WebMock.allow_net_connect!
13
+
14
+ describe 'Gini::Api integration test' do
15
+
16
+ before :all do
17
+ @user = ENV['GINI_API_USER']
18
+ @pass = ENV['GINI_API_PASS']
19
+ @testdoc = "#{File.dirname(__FILE__)}/files/test.pdf"
20
+ @api = Gini::Api::Client.new(
21
+ client_id: ENV['GINI_CLIENT_ID'],
22
+ client_secret: ENV['GINI_CLIENT_SECRET'],
23
+ oauth_site: 'https://user.gini.net/',
24
+ api_uri: 'https://api.gini.net',
25
+ log: Logger.new('/dev/null')
26
+ )
27
+ end
28
+
29
+ context 'OAuth' do
30
+
31
+ it '#login sets token' do
32
+ @api.login(username: @user, password: @pass)
33
+ expect(@api.token.token).to match(/\w+-\w+/)
34
+ expect(@api.token.expired?).to be_false
35
+ end
36
+
37
+ it '#logout destroys token' do
38
+ expect(@api.token.get("/accessToken/#{@api.token.token}").status).to eql(200)
39
+ @api.logout
40
+ expect { @api.token.get("/accessToken/#{@api.token.token}") }.to raise_error(OAuth2::Error)
41
+ end
42
+
43
+ end
44
+
45
+ context 'document' do
46
+
47
+ before do
48
+ @api.login(username: @user, password: @pass)
49
+ @doc = @api.upload(@testdoc)
50
+ end
51
+
52
+ it '#upload returns Gini::Api::Doucment' do
53
+ @api.logout
54
+ expect(@doc.id).to match(/\w+-\w+/)
55
+ @api.delete(@doc.id)
56
+ end
57
+
58
+ it '#get returns Gini::Api::Doucment' do
59
+ doc_get = @api.get(@doc.id)
60
+ expect(@doc.id).to eql(doc_get.id)
61
+ @api.delete(@doc.id)
62
+ @api.logout
63
+ end
64
+
65
+ it '#list returns Gini::Api::DocumentSet' do
66
+ list = @api.list
67
+ expect(list).to be_a(Gini::Api::DocumentSet)
68
+ expect(list.total).to eql(1)
69
+ expect(list.documents[0]).to be_a(Gini::Api::Document)
70
+ @api.delete(@doc.id)
71
+ @api.logout
72
+ end
73
+
74
+ it '#delete returns true' do
75
+ expect(@api.delete(@doc.id)).to be_true
76
+ expect { @api.get(@doc.id) }.to raise_error(Gini::Api::RequestError)
77
+ @api.logout
78
+ end
79
+
80
+ context 'data' do
81
+
82
+ after do
83
+ @api.delete(@doc.id)
84
+ @api.logout
85
+ end
86
+
87
+ context 'extractions' do
88
+
89
+ subject(:extractions) { @doc.extractions }
90
+
91
+ it '#extractions populates instance vars' do
92
+ expect(extractions).to be_kind_of(Gini::Api::Document::Extractions)
93
+ expect(extractions.candidates).to be_kind_of(Hash)
94
+ expect(extractions).to respond_to(:amountToPay)
95
+ expect(extractions[:amountToPay]).to eql('1039.87:EUR')
96
+ end
97
+
98
+ it '#[] returns data' do
99
+ expect(extractions[:docType]).to eql('Invoice')
100
+ end
101
+
102
+ end
103
+
104
+ context 'layout' do
105
+
106
+ subject(:layout) { @doc.layout }
107
+
108
+ it '#to_xml returns XML string' do
109
+ expect(layout).to be_kind_of(Gini::Api::Document::Layout)
110
+ expect(layout.to_xml).to match(/xml/)
111
+ end
112
+
113
+ it '#to_json returns JSON string' do
114
+ expect(layout).to be_kind_of(Gini::Api::Document::Layout)
115
+ expect(layout.to_json).to satisfy { |json| JSON.parse(json).is_a?(Hash) }
116
+ end
117
+
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ end