sharetribe-flex-sdk 1.14.0

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.
Files changed (87) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +3 -0
  3. package/.eslintrc.js +19 -0
  4. package/CHANGELOG.md +231 -0
  5. package/LICENSE +201 -0
  6. package/README.md +76 -0
  7. package/build/sharetribe-flex-sdk-node.js +13592 -0
  8. package/build/sharetribe-flex-sdk-web.js +1 -0
  9. package/docs/README.md +20 -0
  10. package/docs/authentication.md +179 -0
  11. package/docs/calling-the-api.md +217 -0
  12. package/docs/configurations.md +95 -0
  13. package/docs/developing-sdk.md +131 -0
  14. package/docs/docpress.json +14 -0
  15. package/docs/features.md +14 -0
  16. package/docs/keep-alive.md +48 -0
  17. package/docs/object-query-parameters.md +36 -0
  18. package/docs/scripts.js +41 -0
  19. package/docs/serializing-types-to-json.md +40 -0
  20. package/docs/sharing-session-between-client-and-server.md +19 -0
  21. package/docs/styles.css +95 -0
  22. package/docs/token-store.md +114 -0
  23. package/docs/try-it-in-browser.md +32 -0
  24. package/docs/try-it-in-the-playground.md +153 -0
  25. package/docs/types.md +27 -0
  26. package/docs/writing-your-own-token-store.md +29 -0
  27. package/docs/your-own-types.md +61 -0
  28. package/examples/README.md +5 -0
  29. package/examples/getting-started-browser/README.md +22 -0
  30. package/examples/getting-started-browser/index.html +89 -0
  31. package/examples/getting-started-browser/index.js +156 -0
  32. package/examples/getting-started-browser/screenshots/screenshot1.png +0 -0
  33. package/examples/getting-started-browser/screenshots/screenshot2.png +0 -0
  34. package/examples/getting-started-node/README.md +23 -0
  35. package/examples/getting-started-node/index.js +139 -0
  36. package/examples/getting-started-node/screenshots/screenshot.png +0 -0
  37. package/package.json +83 -0
  38. package/playground.js +295 -0
  39. package/src/browser_cookie_store.js +26 -0
  40. package/src/context_runner.js +151 -0
  41. package/src/context_runner.test.js +185 -0
  42. package/src/detect.js +11 -0
  43. package/src/express_cookie_store.js +57 -0
  44. package/src/fake/adapter.js +130 -0
  45. package/src/fake/api.js +137 -0
  46. package/src/fake/auth.js +84 -0
  47. package/src/fake/token_store.js +231 -0
  48. package/src/index.js +25 -0
  49. package/src/interceptors/.eslintrc.js +5 -0
  50. package/src/interceptors/add_auth_header.js +32 -0
  51. package/src/interceptors/add_auth_header.test.js +50 -0
  52. package/src/interceptors/add_auth_token_response.js +16 -0
  53. package/src/interceptors/add_client_id_to_params.js +12 -0
  54. package/src/interceptors/add_client_secret_to_params.js +15 -0
  55. package/src/interceptors/add_grant_type_to_params.js +23 -0
  56. package/src/interceptors/add_idp_client_id_to_params.js +17 -0
  57. package/src/interceptors/add_idp_id_to_params.js +17 -0
  58. package/src/interceptors/add_idp_token_to_params.js +17 -0
  59. package/src/interceptors/add_scope_to_params.js +18 -0
  60. package/src/interceptors/add_subject_token_to_params.js +18 -0
  61. package/src/interceptors/add_token_exchange_grant_type_to_params.js +12 -0
  62. package/src/interceptors/auth_info.js +50 -0
  63. package/src/interceptors/clear_token_after_revoke.js +45 -0
  64. package/src/interceptors/default_params.js +12 -0
  65. package/src/interceptors/fetch_auth_token_from_api.js +33 -0
  66. package/src/interceptors/fetch_auth_token_from_store.js +27 -0
  67. package/src/interceptors/fetch_refresh_token_for_revoke.js +24 -0
  68. package/src/interceptors/multipart_request.js +35 -0
  69. package/src/interceptors/retry_with_anon_token.js +58 -0
  70. package/src/interceptors/retry_with_refresh_token.js +70 -0
  71. package/src/interceptors/save_token.js +20 -0
  72. package/src/interceptors/transit_request.js +27 -0
  73. package/src/interceptors/transit_request.test.js +58 -0
  74. package/src/interceptors/transit_response.js +27 -0
  75. package/src/memory_store.js +19 -0
  76. package/src/params_serializer.js +65 -0
  77. package/src/params_serializer.test.js +58 -0
  78. package/src/sdk.js +894 -0
  79. package/src/sdk.test.js +908 -0
  80. package/src/serializer.js +279 -0
  81. package/src/serializer.test.js +229 -0
  82. package/src/token_store.js +15 -0
  83. package/src/types.js +108 -0
  84. package/src/types.test.js +75 -0
  85. package/src/utils.js +68 -0
  86. package/src/utils.test.js +85 -0
  87. package/webpack.config.babel.js +47 -0
@@ -0,0 +1,908 @@
1
+ /* eslint camelcase: "off" */
2
+ import _ from 'lodash';
3
+ import { UUID, LatLng } from './types';
4
+ import createAdapter from './fake/adapter';
5
+ import SharetribeSdk from './sdk';
6
+ import memoryStore from './memory_store';
7
+
8
+ /**
9
+ Helper to improve error messages.
10
+
11
+ Includes the `response` in the error message if
12
+ `response` exists.
13
+ */
14
+ const report = responsePromise =>
15
+ responsePromise.catch(error => {
16
+ if (error.response) {
17
+ // eslint-disable-next-line no-param-reassign
18
+ error.message = `${error.message}. Response: ${JSON.stringify(error.response)}`;
19
+ }
20
+
21
+ throw error;
22
+ });
23
+
24
+ /**
25
+ Helper to create SDK instance for tests with default configurations.
26
+
27
+ Pass additional configurations in `config` param to override defaults.
28
+
29
+ Returns a map that contains all the instances that might be useful for
30
+ tests, i.e. sdk, sdkTokenStore and adapter.
31
+ */
32
+ const createSdk = (config = {}) => {
33
+ const defaults = {
34
+ clientId: '08ec69f6-d37e-414d-83eb-324e94afddf0',
35
+ };
36
+
37
+ // Extract adapter and token store here so that they can be passed to SDK
38
+ // constructor and included in the returned object
39
+ const { adapter: configAdapter, tokenStore: configTokenStore, ...restConfig } = config;
40
+ const adapter = configAdapter || createAdapter();
41
+ const sdkTokenStore = configTokenStore || memoryStore();
42
+
43
+ const sdk = new SharetribeSdk({
44
+ ...defaults,
45
+ ...restConfig,
46
+ tokenStore: sdkTokenStore,
47
+ adapter: adapter.adapterFn,
48
+ });
49
+
50
+ return {
51
+ sdkTokenStore,
52
+ adapter,
53
+ sdk,
54
+ adapterTokenStore: adapter.tokenStore,
55
+ };
56
+ };
57
+
58
+ describe('new SharetribeSdk', () => {
59
+ it('validates presence of clientId', () => {
60
+ expect(() => new SharetribeSdk()).toThrowError('clientId must be provided');
61
+ });
62
+
63
+ it('validates presence of baseUrl', () => {
64
+ expect(
65
+ () =>
66
+ new SharetribeSdk({
67
+ clientId: '08ec69f6-d37e-414d-83eb-324e94afddf0',
68
+ baseUrl: null,
69
+ })
70
+ ).toThrowError('baseUrl must be provided');
71
+ });
72
+
73
+ it('uses default baseUrl, if none is set', () => {
74
+ const adapter = createAdapter((config, resolve) => {
75
+ // Fake adapter that echoes the URL
76
+ resolve({ data: { baseURL: config.baseURL } });
77
+ });
78
+
79
+ const sdk = new SharetribeSdk({
80
+ clientId: '08ec69f6-d37e-414d-83eb-324e94afddf0',
81
+ adapter: adapter.adapterFn,
82
+ });
83
+
84
+ return sdk.login().then(res => {
85
+ expect(res.data.baseURL).toMatch(/^https:\/\/flex-api.sharetribe.com/);
86
+ });
87
+ });
88
+
89
+ it('calls users endpoint with query params', () => {
90
+ const { sdk } = createSdk();
91
+
92
+ return report(
93
+ sdk.users.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
94
+ const resource = res.data.data;
95
+ const attrs = resource.attributes;
96
+
97
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
98
+ expect(attrs).toEqual(
99
+ expect.objectContaining({
100
+ email: 'user@sharetribe.com',
101
+ description: 'A team member',
102
+ })
103
+ );
104
+ })
105
+ );
106
+ });
107
+
108
+ it('calls marketplace endpoint with query params', () => {
109
+ const { sdk } = createSdk();
110
+
111
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
112
+ const resource = res.data.data;
113
+ const attrs = resource.attributes;
114
+
115
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
116
+ expect(attrs).toEqual(
117
+ expect.objectContaining({
118
+ name: 'Awesome skies.',
119
+ description: 'Meet and greet with fanatical sky divers.',
120
+ })
121
+ );
122
+ });
123
+ });
124
+
125
+ it('calls listing search with query params', () => {
126
+ const { sdk } = createSdk();
127
+
128
+ return sdk.listings
129
+ .search({
130
+ id: new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'),
131
+ origin: new LatLng(40.0, -70.0),
132
+ })
133
+ .then(res => {
134
+ const { data } = res.data;
135
+
136
+ expect(data).toHaveLength(2);
137
+ expect(data[0].attributes.description).toEqual('27-speed Hybrid. Fully functional.');
138
+ expect(data[0].attributes.geolocation instanceof LatLng).toEqual(true);
139
+ expect(data[0].attributes.geolocation).toEqual(new LatLng(40.64542, -74.08508));
140
+ expect(data[1].attributes.description).toEqual(
141
+ 'Goes together perfectly with a latte and a bow tie.'
142
+ );
143
+ expect(data[1].attributes.geolocation instanceof LatLng).toEqual(true);
144
+ expect(data[1].attributes.geolocation).toEqual(new LatLng(40.64542, -74.08508));
145
+ });
146
+ });
147
+
148
+ it('allows user to pass custom read handlers', () => {
149
+ class MyUuid {
150
+ constructor(uuid) {
151
+ this.myUuid = uuid;
152
+ }
153
+ }
154
+
155
+ const handlers = [
156
+ {
157
+ sdkType: UUID,
158
+ appType: MyUuid,
159
+ reader: v => new MyUuid(v.uuid), // reader fn type: UUID -> MyUuid
160
+ },
161
+ ];
162
+
163
+ const { sdk } = createSdk({
164
+ typeHandlers: handlers,
165
+ });
166
+
167
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
168
+ const resource = res.data.data;
169
+ const attrs = resource.attributes;
170
+
171
+ expect(resource.id).toEqual(new MyUuid('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
172
+ expect(attrs).toEqual(
173
+ expect.objectContaining({
174
+ name: 'Awesome skies.',
175
+ description: 'Meet and greet with fanatical sky divers.',
176
+ })
177
+ );
178
+ });
179
+ });
180
+
181
+ it('[DEPRECATED, uses keys that are renamed] allows user to pass custom read handlers', () => {
182
+ class MyUuid {
183
+ constructor(uuid) {
184
+ this.myUuid = uuid;
185
+ }
186
+ }
187
+
188
+ const handlers = [
189
+ {
190
+ type: UUID,
191
+ customType: MyUuid,
192
+ reader: v => new MyUuid(v.uuid), // reader fn type: UUID -> MyUuid
193
+ },
194
+ ];
195
+
196
+ const { sdk } = createSdk({
197
+ typeHandlers: handlers,
198
+ });
199
+
200
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
201
+ const resource = res.data.data;
202
+ const attrs = resource.attributes;
203
+
204
+ expect(resource.id).toEqual(new MyUuid('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
205
+ expect(attrs).toEqual(
206
+ expect.objectContaining({
207
+ name: 'Awesome skies.',
208
+ description: 'Meet and greet with fanatical sky divers.',
209
+ })
210
+ );
211
+ });
212
+ });
213
+
214
+ it('reads auth token from store and includes it in request headers', () => {
215
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk({
216
+ // The Fake server doesn't know this clientId. However, the request passes because
217
+ // the access_token is in the store
218
+ clientId: 'daaf8871-4723-45b8-bc97-9e335f46966d',
219
+ });
220
+
221
+ const anonToken = adapterTokenStore.createAnonToken();
222
+
223
+ sdkTokenStore.setToken(anonToken);
224
+
225
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
226
+ const resource = res.data.data;
227
+ const attrs = resource.attributes;
228
+
229
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
230
+ expect(attrs).toEqual(
231
+ expect.objectContaining({
232
+ name: 'Awesome skies.',
233
+ description: 'Meet and greet with fanatical sky divers.',
234
+ })
235
+ );
236
+ });
237
+ });
238
+
239
+ it('stores the auth token to the store', () => {
240
+ const { sdk, sdkTokenStore } = createSdk();
241
+
242
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
243
+ const resource = res.data.data;
244
+ const attrs = resource.attributes;
245
+ const token = sdkTokenStore.getToken();
246
+
247
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
248
+ expect(attrs).toEqual(
249
+ expect.objectContaining({
250
+ name: 'Awesome skies.',
251
+ description: 'Meet and greet with fanatical sky divers.',
252
+ })
253
+ );
254
+
255
+ expect(token.access_token).toEqual('anonymous-access-1');
256
+ expect(token.token_type).toEqual('bearer');
257
+ expect(token.expires_in).toEqual(86400);
258
+ });
259
+ });
260
+
261
+ it('stores auth token after login', () => {
262
+ const { sdk, sdkTokenStore } = createSdk();
263
+
264
+ // First we get the anonymous token
265
+ return report(
266
+ sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
267
+ expect(sdkTokenStore.getToken().access_token).toEqual('anonymous-access-1');
268
+
269
+ // After login, the anonymous token will be overriden
270
+ return sdk
271
+ .login({
272
+ username: 'joe.dunphy@example.com',
273
+ password: 'secret-joe',
274
+ })
275
+ .then(() => {
276
+ expect(sdkTokenStore.getToken().access_token).toEqual(
277
+ 'joe.dunphy@example.com-access-1'
278
+ );
279
+ });
280
+ })
281
+ );
282
+ });
283
+
284
+ it('refreshes login token', () => {
285
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
286
+
287
+ // First, login
288
+ return report(
289
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
290
+ const { access_token } = sdkTokenStore.getToken();
291
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
292
+
293
+ adapterTokenStore.expireAccessToken(access_token);
294
+
295
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
296
+ expect(sdkTokenStore.getToken().access_token).toEqual('joe.dunphy@example.com-access-2');
297
+
298
+ const resource = res.data.data;
299
+ const attrs = resource.attributes;
300
+
301
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
302
+ expect(attrs).toEqual(
303
+ expect.objectContaining({
304
+ name: 'Awesome skies.',
305
+ description: 'Meet and greet with fanatical sky divers.',
306
+ })
307
+ );
308
+ });
309
+ })
310
+ );
311
+ });
312
+
313
+ it('refreshes anonymous token', () => {
314
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
315
+
316
+ // First we get the anonymous token
317
+ return report(
318
+ sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
319
+ const { access_token } = sdkTokenStore.getToken();
320
+ expect(access_token).toEqual('anonymous-access-1');
321
+
322
+ adapterTokenStore.expireAccessToken(access_token);
323
+
324
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(res => {
325
+ expect(sdkTokenStore.getToken().access_token).toEqual('anonymous-access-2');
326
+
327
+ const resource = res.data.data;
328
+ const attrs = resource.attributes;
329
+
330
+ expect(resource.id).toEqual(new UUID('0e0b60fe-d9a2-11e6-bf26-cec0c932ce01'));
331
+ expect(attrs).toEqual(
332
+ expect.objectContaining({
333
+ name: 'Awesome skies.',
334
+ description: 'Meet and greet with fanatical sky divers.',
335
+ })
336
+ );
337
+ });
338
+ })
339
+ );
340
+ });
341
+
342
+ it('logs in with an authorization code', () => {
343
+ const { sdk, sdkTokenStore } = createSdk();
344
+
345
+ return sdk.login({ code: 'flex-authorization-code' }).then(() => {
346
+ const { access_token, refresh_token } = sdkTokenStore.getToken();
347
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
348
+ expect(refresh_token).toEqual('joe.dunphy@example.com-refresh-1');
349
+ });
350
+ });
351
+
352
+ it('logs in with idp token', () => {
353
+ const { sdk, sdkTokenStore } = createSdk({
354
+ clientSecret: '8af2bf99c380b3a303ab90ae4012c8cd8f69d309',
355
+ });
356
+
357
+ return sdk
358
+ .loginWithIdp({
359
+ idpId: 'facebook',
360
+ idpClientId: 'idp-client-id',
361
+ idpToken: 'idp-token',
362
+ })
363
+ .then(() => {
364
+ const { access_token, refresh_token } = sdkTokenStore.getToken();
365
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
366
+ expect(refresh_token).toEqual('joe.dunphy@example.com-refresh-1');
367
+ });
368
+ });
369
+
370
+ it('revokes token (a.k.a logout)', () => {
371
+ const { sdk, sdkTokenStore } = createSdk();
372
+
373
+ // First, login
374
+ return report(
375
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
376
+ expect(sdkTokenStore.getToken().access_token).toEqual('joe.dunphy@example.com-access-1');
377
+
378
+ // Revoke token
379
+ return sdk.logout().then(res => {
380
+ expect(res.data.action).toEqual('revoked');
381
+
382
+ expect(sdkTokenStore.getToken()).toEqual(null);
383
+
384
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
385
+ expect(sdkTokenStore.getToken().access_token).toEqual('anonymous-access-1');
386
+ });
387
+ });
388
+ })
389
+ );
390
+ });
391
+
392
+ it('refreshes token before revoke', () => {
393
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
394
+
395
+ // First, login
396
+ return report(
397
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
398
+ const { access_token } = sdkTokenStore.getToken();
399
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
400
+
401
+ adapterTokenStore.expireAccessToken(access_token);
402
+
403
+ // Revoke token
404
+ return sdk.logout().then(res => {
405
+ expect(res.data.action).toEqual('revoked');
406
+
407
+ expect(sdkTokenStore.getToken()).toEqual(null);
408
+
409
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
410
+ expect(sdkTokenStore.getToken().access_token).toEqual('anonymous-access-1');
411
+ });
412
+ });
413
+ })
414
+ );
415
+ });
416
+
417
+ it('refreshes token after unsuccessful revoke, but if the refresh fails because of 401, return OK.', () => {
418
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
419
+
420
+ // First, login
421
+ return report(
422
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
423
+ const { access_token, refresh_token } = sdkTokenStore.getToken();
424
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
425
+
426
+ adapterTokenStore.expireAccessToken(access_token);
427
+ adapterTokenStore.revokeRefreshToken(refresh_token);
428
+
429
+ // Revoke token
430
+ return sdk.logout().then(() => {
431
+ expect(sdkTokenStore.getToken()).toEqual(null);
432
+
433
+ return sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
434
+ expect(sdkTokenStore.getToken().access_token).toEqual('anonymous-access-1');
435
+ });
436
+ });
437
+ })
438
+ );
439
+ });
440
+
441
+ it('refreshes token after unsuccessful revoke, but if the refresh fails because of network error, fail.', () => {
442
+ const { sdk, sdkTokenStore, adapterTokenStore, adapter } = createSdk();
443
+
444
+ // Two requests passes (login and first revoke try), but after that the server goes down
445
+ adapter.offlineAfter(2);
446
+
447
+ // First, login
448
+ return report(
449
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
450
+ const { access_token, refresh_token } = sdkTokenStore.getToken();
451
+ expect(access_token).toEqual('joe.dunphy@example.com-access-1');
452
+
453
+ adapterTokenStore.expireAccessToken(access_token);
454
+ adapterTokenStore.revokeRefreshToken(refresh_token);
455
+
456
+ // Revoke token
457
+ return sdk
458
+ .logout()
459
+ .then(() => {
460
+ // Should not pass
461
+ expect(true).toEqual(false);
462
+ })
463
+ .catch(() => {
464
+ expect(sdkTokenStore.getToken().access_token).toEqual(access_token);
465
+ expect(sdkTokenStore.getToken().refresh_token).toEqual(refresh_token);
466
+ });
467
+ })
468
+ );
469
+ });
470
+
471
+ it('encodes new listing post body to Transit', () => {
472
+ const { sdk, adapter } = createSdk();
473
+
474
+ const testData = {
475
+ title: 'A new hope',
476
+ description: 'Our Nth listing!',
477
+ address: 'Bulevardi 14, Helsinki, Finland',
478
+ geolocation: new LatLng(10.152, 15.375),
479
+ };
480
+
481
+ const transitEncoded =
482
+ '["^ ","~:title","A new hope","~:description","Our Nth listing!","~:address","Bulevardi 14, Helsinki, Finland","~:geolocation",["~#geo",[10.152,15.375]]]';
483
+
484
+ return report(
485
+ sdk
486
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
487
+ .then(() => sdk.ownListings.create(testData))
488
+ .then(() => {
489
+ const req = _.last(adapter.requests);
490
+ expect(req.data).toEqual(transitEncoded);
491
+ expect(req.headers).toEqual(
492
+ expect.objectContaining({
493
+ 'Content-Type': 'application/transit+json',
494
+ })
495
+ );
496
+ })
497
+ );
498
+ });
499
+
500
+ it('encodes new listing post body to Transit, using type appTypes', () => {
501
+ class MyLatLng {
502
+ constructor(lat, lng) {
503
+ this.val = [lat, lng];
504
+ }
505
+ }
506
+
507
+ const handlers = [
508
+ {
509
+ sdkType: LatLng,
510
+ appType: MyLatLng,
511
+ writer: v => new LatLng(v.val[0], v.val[1]),
512
+ },
513
+ ];
514
+
515
+ const { sdk, adapter } = createSdk({ typeHandlers: handlers });
516
+
517
+ const testData = {
518
+ title: 'A new hope',
519
+ description: 'Our Nth listing!',
520
+ address: 'Bulevardi 14, Helsinki, Finland',
521
+ geolocation: new MyLatLng(10.152, 15.375),
522
+ };
523
+
524
+ const transitEncoded =
525
+ '["^ ","~:title","A new hope","~:description","Our Nth listing!","~:address","Bulevardi 14, Helsinki, Finland","~:geolocation",["~#geo",[10.152,15.375]]]';
526
+
527
+ return report(
528
+ sdk
529
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
530
+ .then(() => sdk.ownListings.create(testData))
531
+ .then(() => {
532
+ const req = _.last(adapter.requests);
533
+ expect(req.data).toEqual(transitEncoded);
534
+ expect(req.headers).toEqual(
535
+ expect.objectContaining({
536
+ 'Content-Type': 'application/transit+json',
537
+ })
538
+ );
539
+ })
540
+ );
541
+ });
542
+
543
+ it('encodes new listing post body to Transit, using canHandle fn', () => {
544
+ const handlers = [
545
+ {
546
+ sdkType: LatLng,
547
+ canHandle: v => v[0] === '__my_lat_lng_type',
548
+ writer: v => new LatLng(v[1], v[2]),
549
+ },
550
+ ];
551
+
552
+ const { sdk, adapter } = createSdk({ typeHandlers: handlers });
553
+
554
+ const testData = {
555
+ title: 'A new hope',
556
+ description: 'Our Nth listing!',
557
+ address: 'Bulevardi 14, Helsinki, Finland',
558
+ geolocation: ['__my_lat_lng_type', 10.152, 15.375],
559
+ };
560
+
561
+ const transitEncoded =
562
+ '["^ ","~:title","A new hope","~:description","Our Nth listing!","~:address","Bulevardi 14, Helsinki, Finland","~:geolocation",["~#geo",[10.152,15.375]]]';
563
+
564
+ return report(
565
+ sdk
566
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
567
+ .then(() => sdk.ownListings.create(testData))
568
+ .then(() => {
569
+ const req = _.last(adapter.requests);
570
+ expect(req.data).toEqual(transitEncoded);
571
+ expect(req.headers).toEqual(
572
+ expect.objectContaining({
573
+ 'Content-Type': 'application/transit+json',
574
+ })
575
+ );
576
+ })
577
+ );
578
+ });
579
+
580
+ it('encodes new listing post body to Transit JSON Verbose', () => {
581
+ const { sdk, adapter } = createSdk({ transitVerbose: true });
582
+
583
+ const testData = {
584
+ title: 'A new hope',
585
+ description: 'Our Nth listing!',
586
+ address: 'Bulevardi 14, Helsinki, Finland',
587
+ geolocation: new LatLng(10.152, 15.375),
588
+ };
589
+
590
+ const transitEncoded =
591
+ '{"~:title":"A new hope","~:description":"Our Nth listing!","~:address":"Bulevardi 14, Helsinki, Finland","~:geolocation":{"~#geo":[10.152,15.375]}}';
592
+
593
+ return report(
594
+ sdk
595
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
596
+ .then(() => sdk.ownListings.create(testData))
597
+ .then(() => {
598
+ const req = _.last(adapter.requests);
599
+ expect(req.data).toEqual(transitEncoded);
600
+ expect(req.headers).toEqual(
601
+ expect.objectContaining({
602
+ 'Content-Type': 'application/transit+json',
603
+ })
604
+ );
605
+ })
606
+ );
607
+ });
608
+
609
+ it('requests the server to send back Transit JSON Verbose', () => {
610
+ const { sdk, adapter } = createSdk({ transitVerbose: true });
611
+
612
+ return report(
613
+ sdk.marketplace.show({ id: '0e0b60fe-d9a2-11e6-bf26-cec0c932ce01' }).then(() => {
614
+ const req = _.last(adapter.requests);
615
+ expect(req.headers).toEqual(
616
+ expect.objectContaining({
617
+ 'X-Transit-Verbose': 'true',
618
+ Accept: 'application/transit+json',
619
+ })
620
+ );
621
+ })
622
+ );
623
+ });
624
+
625
+ it('does not double encode in case we need to retry with fresh token', () => {
626
+ const { sdk, sdkTokenStore, adapterTokenStore, adapter } = createSdk();
627
+
628
+ const testData = {
629
+ title: 'A new hope',
630
+ description: 'Our Nth listing!',
631
+ address: 'Bulevardi 14, Helsinki, Finland',
632
+ geolocation: new LatLng(10.152, 15.375),
633
+ };
634
+
635
+ const transitEncoded =
636
+ '["^ ","~:title","A new hope","~:description","Our Nth listing!","~:address","Bulevardi 14, Helsinki, Finland","~:geolocation",["~#geo",[10.152,15.375]]]';
637
+
638
+ return report(
639
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
640
+ const { access_token } = sdkTokenStore.getToken();
641
+ adapterTokenStore.expireAccessToken(access_token);
642
+
643
+ return sdk.ownListings.create(testData).then(() => {
644
+ const req = _.last(adapter.requests);
645
+ expect(req.data).toEqual(transitEncoded);
646
+ expect(req.headers).toEqual(
647
+ expect.objectContaining({
648
+ 'Content-Type': 'application/transit+json',
649
+ })
650
+ );
651
+ });
652
+ })
653
+ );
654
+ });
655
+
656
+ describe('authInfo', () => {
657
+ it('returns authentication information', () => {
658
+ const { sdk } = createSdk();
659
+
660
+ return report(
661
+ sdk
662
+ .authInfo()
663
+ .then(authInfo => {
664
+ // No auth info yet.
665
+ expect(authInfo.grantType).toBeUndefined();
666
+ expect(authInfo.isAnonymous).toBeUndefined();
667
+ expect(authInfo.scopes).toBeUndefined();
668
+ })
669
+ .then(() =>
670
+ sdk.marketplace
671
+ .show()
672
+ .then(sdk.authInfo)
673
+ .then(authInfo => {
674
+ // Anonymous token
675
+ expect(authInfo.grantType).toEqual('client_credentials');
676
+ expect(authInfo.isAnonymous).toEqual(true);
677
+ expect(authInfo.scopes).toEqual(['public-read']);
678
+ })
679
+ )
680
+ .then(() =>
681
+ sdk
682
+ .login({
683
+ username: 'joe.dunphy@example.com',
684
+ password: 'secret-joe',
685
+ })
686
+ .then(sdk.authInfo)
687
+ .then(authInfo => {
688
+ // Login token
689
+ expect(authInfo.grantType).toEqual('refresh_token');
690
+ expect(authInfo.isAnonymous).toEqual(false);
691
+ expect(authInfo.scopes).toEqual(['user']);
692
+ })
693
+ )
694
+ .then(() =>
695
+ sdk
696
+ .logout()
697
+ .then(sdk.authInfo)
698
+ .then(authInfo => {
699
+ // Logout
700
+ expect(authInfo.grantType).toBeUndefined();
701
+ expect(authInfo.isAnonymous).toBeUndefined();
702
+ expect(authInfo.scopes).toBeUndefined();
703
+ })
704
+ )
705
+ .then(() =>
706
+ sdk
707
+ .logout()
708
+ .then(sdk.authInfo)
709
+ .then(authInfo => {
710
+ // Logging out already logged out user does nothing
711
+ expect(authInfo.grantType).toBeUndefined();
712
+ expect(authInfo.isAnonymous).toBeUndefined();
713
+ expect(authInfo.scopes).toBeUndefined();
714
+ })
715
+ )
716
+ );
717
+ });
718
+
719
+ it('supports anonymous tokens without scope attribute', () => {
720
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
721
+ const anonToken = adapterTokenStore.createAnonToken();
722
+ const { scope, ...rest } = anonToken;
723
+ sdkTokenStore.setToken({ ...rest });
724
+
725
+ return report(
726
+ sdk.authInfo().then(authInfo => {
727
+ expect(authInfo.grantType).toEqual('client_credentials');
728
+ expect(authInfo.isAnonymous).toEqual(true);
729
+ expect(authInfo.scopes).toBeUndefined();
730
+ })
731
+ );
732
+ });
733
+
734
+ it('supports access tokens without scope attribute', () => {
735
+ const { sdk, sdkTokenStore, adapterTokenStore } = createSdk();
736
+ const accessToken = adapterTokenStore.createTokenWithCredentials(
737
+ 'joe.dunphy@example.com',
738
+ 'secret-joe'
739
+ );
740
+ const { scope, ...rest } = accessToken;
741
+ sdkTokenStore.setToken({ ...rest });
742
+
743
+ return report(
744
+ sdk.authInfo().then(authInfo => {
745
+ expect(authInfo.grantType).toEqual('refresh_token');
746
+ expect(authInfo.isAnonymous).toEqual(false);
747
+ expect(authInfo.scopes).toBeUndefined();
748
+ })
749
+ );
750
+ });
751
+ });
752
+
753
+ it('allows sending query params in POST request (such as `expand=true`)', () => {
754
+ const { sdk } = createSdk();
755
+
756
+ const params = {
757
+ title: 'Pelago bike',
758
+ description: 'City bike for city hipster!',
759
+ address: 'Bulevardi 14, 00200 Helsinki, Finland',
760
+ geolocation: new LatLng(40.0, 73.0),
761
+ };
762
+
763
+ return report(
764
+ sdk
765
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
766
+ .then(() => sdk.ownListings.create(params))
767
+ .then(res => {
768
+ const { data } = res.data;
769
+ const attrs = data.attributes;
770
+
771
+ expect(data).toEqual(
772
+ expect.objectContaining({
773
+ id: expect.any(UUID),
774
+ type: 'ownListing',
775
+ })
776
+ );
777
+ expect(attrs).toBeUndefined();
778
+ })
779
+ .then(() => sdk.ownListings.create(params, { expand: true }))
780
+ .then(res => {
781
+ const { data } = res.data;
782
+ const attrs = data.attributes;
783
+
784
+ expect(data).toEqual(
785
+ expect.objectContaining({
786
+ id: expect.any(UUID),
787
+ type: 'ownListing',
788
+ })
789
+ );
790
+ expect(attrs).toBeDefined();
791
+ })
792
+ );
793
+ });
794
+
795
+ it('returns error in expected error format, data as plain text', () => {
796
+ const { sdk } = createSdk();
797
+
798
+ return report(
799
+ sdk
800
+ .login({ username: 'wrong username', password: 'wrong password' })
801
+ .then(() => {
802
+ // Fail
803
+ expect(true).toEqual(false);
804
+ })
805
+ .catch(e => {
806
+ expect(e).toBeInstanceOf(Error);
807
+ expect(e).toEqual(
808
+ expect.objectContaining({
809
+ status: 401,
810
+ statusText: 'Unauthorized',
811
+ data: 'Unauthorized',
812
+ })
813
+ );
814
+ return Promise.resolve();
815
+ })
816
+ );
817
+ });
818
+
819
+ it('returns error in expected error format, data as an object', () => {
820
+ const { sdk } = createSdk();
821
+
822
+ return report(
823
+ sdk
824
+ .login({ username: 'joe.dunphy@example.com', password: 'secret-joe' })
825
+ .then(() => sdk.ownListings.create())
826
+ .then(() => {
827
+ // Fail
828
+ expect(true).toEqual(false);
829
+ })
830
+ .catch(e => {
831
+ expect(e).toBeInstanceOf(Error);
832
+ expect(e).toEqual(
833
+ expect.objectContaining({
834
+ status: 400,
835
+ statusText: 'Bad Request',
836
+ data: expect.objectContaining({
837
+ errors: [
838
+ expect.objectContaining({
839
+ id: expect.any(UUID),
840
+ status: 400,
841
+ code: 'bad-request',
842
+ title: 'Bad request',
843
+ details: {
844
+ error: {
845
+ 'body-params': {
846
+ title: 'missing-required-key',
847
+ description: 'missing-required-key',
848
+ address: 'missing-required-key',
849
+ geolocation: 'missing-required-key',
850
+ },
851
+ },
852
+ },
853
+ }),
854
+ ],
855
+ }),
856
+ })
857
+ );
858
+ return Promise.resolve();
859
+ })
860
+ );
861
+ });
862
+ });
863
+
864
+ describe('exchangeToken', () => {
865
+ it('returns a trusted token on exchange', () => {
866
+ const { sdk, sdkTokenStore, adapter } = createSdk();
867
+
868
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
869
+ const stdToken = sdkTokenStore.getToken();
870
+ expect(stdToken.access_token).toEqual('joe.dunphy@example.com-access-1');
871
+
872
+ const subjectTokenStore = memoryStore();
873
+ subjectTokenStore.setToken(stdToken);
874
+
875
+ const { sdk: subjectSdk } = createSdk({
876
+ clientSecret: '8af2bf99c380b3a303ab90ae4012c8cd8f69d309',
877
+ tokenStore: subjectTokenStore,
878
+ adapter,
879
+ });
880
+
881
+ subjectSdk.exchangeToken().then(res => {
882
+ expect(res.data.scope).toEqual('trusted:user');
883
+ });
884
+ });
885
+ });
886
+
887
+ it('does not store a trusted token on exchange', () => {
888
+ const { sdk, sdkTokenStore, adapter } = createSdk();
889
+
890
+ sdk.login({ username: 'joe.dunphy@example.com', password: 'secret-joe' }).then(() => {
891
+ const stdToken = sdkTokenStore.getToken();
892
+ expect(stdToken.access_token).toEqual('joe.dunphy@example.com-access-1');
893
+
894
+ const subjectTokenStore = memoryStore();
895
+ subjectTokenStore.setToken(stdToken);
896
+
897
+ const { sdk: subjectSdk, sdkTokenStore: subjectSdkTokenStore } = createSdk({
898
+ clientSecret: '8af2bf99c380b3a303ab90ae4012c8cd8f69d309',
899
+ tokenStore: subjectTokenStore,
900
+ adapter,
901
+ });
902
+
903
+ subjectSdk.exchangeToken().then(() => {
904
+ expect(subjectSdkTokenStore.getToken().scope).toEqual('user');
905
+ });
906
+ });
907
+ });
908
+ });