ti2 1.0.7 → 1.0.8
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.
- package/.github/workflows/deploy.yml +1 -2
- package/.github/workflows/tests.yml +32 -8
- package/README.md +1 -1
- package/api.yml +24 -0
- package/controllers/__tests__/admin.js +27 -30
- package/controllers/__tests__/app.js +33 -37
- package/controllers/__tests__/bookings.js +56 -34
- package/controllers/__tests__/ping.js +5 -10
- package/controllers/__tests__/user.js +57 -62
- package/controllers/admin.js +28 -5
- package/controllers/user.js +2 -2
- package/index.js +3 -1
- package/migrations/20200114173707-addTCApp.js +0 -3
- package/package.json +2 -2
- package/seeders/.gitkeep +0 -0
- package/test/plugin.js +17 -0
- package/test/slugify.js +21 -0
- package/test/utils.js +95 -96
|
@@ -8,14 +8,13 @@ on:
|
|
|
8
8
|
|
|
9
9
|
jobs:
|
|
10
10
|
tag:
|
|
11
|
-
if:
|
|
11
|
+
if: github.event.workflow_run.conclusion == 'success' && github.repository == 'ti2travel/ti2'
|
|
12
12
|
runs-on: ubuntu-latest
|
|
13
13
|
steps:
|
|
14
14
|
- uses: actions/checkout@master
|
|
15
15
|
- uses: tool3/bump@master
|
|
16
16
|
with:
|
|
17
17
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
18
|
-
unrelated: true
|
|
19
18
|
user: 'Github Action Job'
|
|
20
19
|
email: 'engineering@tourconnect.com'
|
|
21
20
|
branch: main
|
|
@@ -5,13 +5,37 @@ on:
|
|
|
5
5
|
push:
|
|
6
6
|
|
|
7
7
|
jobs:
|
|
8
|
-
|
|
8
|
+
test:
|
|
9
|
+
timeout-minutes: 10
|
|
9
10
|
runs-on: ubuntu-latest
|
|
11
|
+
container:
|
|
12
|
+
image: node:erbium
|
|
13
|
+
services:
|
|
14
|
+
mysql:
|
|
15
|
+
image: mysql
|
|
16
|
+
env:
|
|
17
|
+
MYSQL_DATABASE: ti2development
|
|
18
|
+
MYSQL_ALLOW_EMPTY_PASSWORD: true
|
|
19
|
+
MYSQL_ROOT_PASSWORD: root
|
|
20
|
+
ports:
|
|
21
|
+
- 3306:3306
|
|
22
|
+
env:
|
|
23
|
+
CI: true
|
|
24
|
+
DB_URL: 'mysql://root:root@mysql/ti2development'
|
|
25
|
+
dbCryptoKey: 'YmYzYzE5OWMyNDcwY2I0NzdkOTA3YjFlMDkxN2MxN2I='
|
|
26
|
+
jwtSecret: '9rifawckwztuKehocJOtGM6deTqICGWzIyc5CqgNyi'
|
|
27
|
+
frontendKey: 'lYjEJHzb7lbQ1hk3ypjX'
|
|
28
|
+
adminKey: 'IxEAYjHbbUYhsPjQXwX1'
|
|
10
29
|
steps:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
30
|
+
- uses: actions/checkout@v2
|
|
31
|
+
- uses: actions/setup-node@v1
|
|
32
|
+
with:
|
|
33
|
+
node-version: 12
|
|
34
|
+
- name: install dependencies
|
|
35
|
+
run: npm i
|
|
36
|
+
- name: create/migrate the database
|
|
37
|
+
run: npx sequelize db:create && npx sequelize db:migrate
|
|
38
|
+
- name: seed the database
|
|
39
|
+
run: npx sequelize db:seed:all
|
|
40
|
+
- name: run the tests
|
|
41
|
+
run: npx jest -i --coverage --forceExit
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Tourism Industry Interchange
|
|
|
8
8
|
Some environment variables are required for ti2 to start
|
|
9
9
|
|
|
10
10
|
- DB_URL (mysql connection in url format)
|
|
11
|
-
- dbCryptoKey (integration details encription key)
|
|
11
|
+
- dbCryptoKey (integration details encription key, should be a 32 chars random string base64 encoded)
|
|
12
12
|
- adminKey (a key for admin related requests)
|
|
13
13
|
- jwtKey (a key to encrypt user sessions request)
|
|
14
14
|
- frontendKey (an additional optional key to validate the origin of the requests)
|
package/api.yml
CHANGED
|
@@ -227,6 +227,30 @@ paths:
|
|
|
227
227
|
application/json:
|
|
228
228
|
schema:
|
|
229
229
|
$ref: '#/components/schemas/Message'
|
|
230
|
+
|
|
231
|
+
/app/{appKey}:
|
|
232
|
+
delete:
|
|
233
|
+
security:
|
|
234
|
+
- bearerAuth: ['admin']
|
|
235
|
+
tags:
|
|
236
|
+
- admin
|
|
237
|
+
summary: Should delete an App, the name of the app should be url safe (no spaces)
|
|
238
|
+
operationId: deleteApp
|
|
239
|
+
parameters:
|
|
240
|
+
- name: appKey
|
|
241
|
+
in: path
|
|
242
|
+
description: App name to delete, should be urlsafe and have a correspondant npm package already in the required npm modules as ti2-appName
|
|
243
|
+
required: true
|
|
244
|
+
example: 'expedia'
|
|
245
|
+
schema:
|
|
246
|
+
type: string
|
|
247
|
+
responses:
|
|
248
|
+
200:
|
|
249
|
+
description: success
|
|
250
|
+
content:
|
|
251
|
+
application/json:
|
|
252
|
+
schema:
|
|
253
|
+
$ref: '#/components/schemas/Message'
|
|
230
254
|
/apps:
|
|
231
255
|
get:
|
|
232
256
|
security:
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
/* globals describe it expect
|
|
1
|
+
/* globals describe it expect */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const sqldb = require('../../models/db');
|
|
7
|
-
const { slugify } = require('../../test/utils');
|
|
4
|
+
const testUtils = require('../../test/utils');
|
|
5
|
+
const slugify = require('../../test/slugify');
|
|
8
6
|
|
|
9
7
|
const { env: { adminKey } } = process;
|
|
10
8
|
|
|
@@ -17,26 +15,24 @@ describe('admin', () => {
|
|
|
17
15
|
packageName: `ti2-${appName}`,
|
|
18
16
|
adminEmail: chance.email(),
|
|
19
17
|
};
|
|
18
|
+
const { doApiPost, doApiGet } = testUtils({
|
|
19
|
+
plugins: [appName],
|
|
20
|
+
});
|
|
20
21
|
let appKey;
|
|
21
22
|
const userId = '536830b6ed19afa44a000002';
|
|
22
|
-
afterAll(async () => {
|
|
23
|
-
await sqldb.connectionManager.close();
|
|
24
|
-
});
|
|
25
23
|
it('should be able to create an app', async () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expect(
|
|
32
|
-
appKey = resp.body.value;
|
|
24
|
+
({ value: appKey } = await doApiPost({
|
|
25
|
+
url: '/app',
|
|
26
|
+
token: adminKey,
|
|
27
|
+
payload: newApp,
|
|
28
|
+
}));
|
|
29
|
+
expect(appKey).toBeTruthy();
|
|
33
30
|
});
|
|
34
31
|
it('the created app should be on the list of apps', async () => {
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const { body: { integrations } } = resp;
|
|
32
|
+
const { integrations } = await doApiGet({
|
|
33
|
+
url: '/apps',
|
|
34
|
+
token: adminKey,
|
|
35
|
+
});
|
|
40
36
|
expect(integrations).toEqual(
|
|
41
37
|
expect.arrayContaining([
|
|
42
38
|
expect.objectContaining(newApp),
|
|
@@ -45,18 +41,19 @@ describe('admin', () => {
|
|
|
45
41
|
});
|
|
46
42
|
it('should be able to reset an app\'s key', async () => {
|
|
47
43
|
const url = `/app/resetAppKey/${newApp.name}`;
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const { body: { value } } = resp;
|
|
44
|
+
const { value } = await doApiGet({
|
|
45
|
+
url,
|
|
46
|
+
token: adminKey,
|
|
47
|
+
});
|
|
53
48
|
expect(value).toBeTruthy();
|
|
54
49
|
expect(value).not.toBe(appKey);
|
|
55
50
|
});
|
|
56
|
-
it
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
it('should be able to create a new user authentication token', async () => {
|
|
52
|
+
const { value } = await doApiPost({
|
|
53
|
+
url: '/user',
|
|
54
|
+
token: adminKey,
|
|
55
|
+
payload: { userId },
|
|
56
|
+
});
|
|
57
|
+
expect(value).toBeTruthy();
|
|
61
58
|
});
|
|
62
59
|
});
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
/* globals describe it expect
|
|
1
|
+
/* globals describe it expect */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
|
-
const request = require('supertest');
|
|
5
4
|
const jwt = require('jwt-promise');
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const sqldb = require('../../models/db');
|
|
9
|
-
const { slugify } = require('../../test/utils');
|
|
5
|
+
const testUtils = require('../../test/utils');
|
|
6
|
+
const slugify = require('../../test/slugify');
|
|
10
7
|
|
|
11
8
|
const { env: { adminKey, jwtSecret } } = process;
|
|
12
9
|
|
|
13
10
|
describe('app', () => {
|
|
14
11
|
const appName = slugify(
|
|
15
12
|
chance.company(),
|
|
16
|
-
)
|
|
13
|
+
);
|
|
17
14
|
const newApp = {
|
|
18
15
|
name: appName,
|
|
19
16
|
packageName: `ti2-${appName}`,
|
|
20
17
|
adminEmail: chance.email(),
|
|
21
18
|
};
|
|
19
|
+
const { doApiPost, doApiGet, doApiPut } = testUtils({
|
|
20
|
+
plugins: [appName],
|
|
21
|
+
});
|
|
22
22
|
let appKey;
|
|
23
23
|
const userId = chance.guid();
|
|
24
24
|
const apiKey = chance.guid();
|
|
@@ -31,47 +31,43 @@ describe('app', () => {
|
|
|
31
31
|
payload: { lorem: chance.paragraph() },
|
|
32
32
|
};
|
|
33
33
|
let encodedKey;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
expect(
|
|
41
|
-
appKey = resp.body.value;
|
|
42
|
-
});
|
|
43
|
-
afterAll(async () => {
|
|
44
|
-
await sqldb.connectionManager.close();
|
|
34
|
+
it('should create a new app', async () => {
|
|
35
|
+
({ value: appKey } = await doApiPost({
|
|
36
|
+
url: '/app',
|
|
37
|
+
token: adminKey,
|
|
38
|
+
payload: newApp,
|
|
39
|
+
}));
|
|
40
|
+
expect(appKey).toBeTruthy();
|
|
45
41
|
});
|
|
46
42
|
it('should encode an airbitrary object', async () => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
expect(
|
|
53
|
-
encodedKey = resp.body.value;
|
|
43
|
+
({ value: encodedKey } = await doApiPut({
|
|
44
|
+
url: `/app/encode/${appName}`,
|
|
45
|
+
token: appKey,
|
|
46
|
+
payload: encodePayload,
|
|
47
|
+
}));
|
|
48
|
+
expect(encodedKey).toBeTruthy();
|
|
54
49
|
});
|
|
55
50
|
it('the encoded key should be decodable', async () => {
|
|
56
51
|
const decoded = await jwt.verify(encodedKey, `${appName}.${jwtSecret}`);
|
|
57
|
-
expect(decoded
|
|
52
|
+
expect(decoded).toEqual(expect.objectContaining(encodePayload));
|
|
58
53
|
});
|
|
59
54
|
it('should be able to create a user token for the app', async () => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
const { value } = await doApiPost({
|
|
56
|
+
url: `/${appName}/${userId}`,
|
|
57
|
+
token: appKey,
|
|
58
|
+
payload: {
|
|
64
59
|
tokenHint: apiKey.split('-')[0],
|
|
65
60
|
token,
|
|
66
|
-
}
|
|
67
|
-
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
expect(parseInt(value, 10)).toBeGreaterThan(0);
|
|
68
64
|
});
|
|
69
65
|
it('should be able to get a list of all tokens related to the app', async () => {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
expect(
|
|
66
|
+
const { userAppKeys } = await doApiGet({
|
|
67
|
+
url: `/app/tokens/${appName}`,
|
|
68
|
+
token: appKey,
|
|
69
|
+
});
|
|
70
|
+
expect(userAppKeys).toEqual(
|
|
75
71
|
expect.arrayContaining([
|
|
76
72
|
expect.objectContaining({
|
|
77
73
|
hint: apiKey.split('-')[0],
|
|
@@ -1,43 +1,59 @@
|
|
|
1
1
|
/* globals describe it expect */
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const chance = require('chance').Chance();
|
|
4
|
+
const testUtils = require('../../test/utils');
|
|
4
5
|
|
|
5
6
|
const { env: { adminKey } } = process;
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
token: {
|
|
8
|
+
describe('user: booking search', () => {
|
|
9
|
+
const newApp = {
|
|
10
|
+
name: 'travelgate',
|
|
11
|
+
packageName: 'ti2-travelgate',
|
|
12
|
+
adminEmail: 'engineering+travelgate@tourconnect.com',
|
|
13
|
+
};
|
|
14
|
+
let appKey = newApp.name;
|
|
15
|
+
const userId = '551394be5ac58e5c76000019';
|
|
16
|
+
const token = {
|
|
17
17
|
endpoint: 'https://api.travelgatex.com',
|
|
18
|
-
apiKey:
|
|
18
|
+
apiKey: chance.guid(),
|
|
19
19
|
client: 'tourconnect',
|
|
20
|
-
}
|
|
21
|
-
|
|
20
|
+
};
|
|
21
|
+
const newIntegration = {
|
|
22
|
+
tokenHint: 'testingToken',
|
|
23
|
+
token,
|
|
24
|
+
};
|
|
25
|
+
const {
|
|
26
|
+
doApiDelete,
|
|
27
|
+
doApiGet,
|
|
28
|
+
doApiPost,
|
|
29
|
+
plugins,
|
|
30
|
+
} = testUtils({
|
|
31
|
+
plugins: [newApp.name],
|
|
32
|
+
});
|
|
22
33
|
|
|
23
|
-
describe('user: booking search', () => {
|
|
24
34
|
let userToken;
|
|
35
|
+
it('drop any existing travelgateapp integration', async () => {
|
|
36
|
+
try {
|
|
37
|
+
await doApiDelete({
|
|
38
|
+
url: `/travelgate/${userId}`,
|
|
39
|
+
token: adminKey,
|
|
40
|
+
payload: { tokenHint: 'testingToken' },
|
|
41
|
+
});
|
|
42
|
+
} catch {}
|
|
43
|
+
try {
|
|
44
|
+
await doApiDelete({
|
|
45
|
+
url: '/app/travelgate',
|
|
46
|
+
token: adminKey,
|
|
47
|
+
});
|
|
48
|
+
} catch {}
|
|
49
|
+
});
|
|
25
50
|
it('create the travelgate app', async () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const { integrations } = await doApiGet({
|
|
29
|
-
url: '/apps',
|
|
51
|
+
const { value: apiKey } = await doApiPost({
|
|
52
|
+
url: '/app',
|
|
30
53
|
token: adminKey,
|
|
54
|
+
payload: newApp,
|
|
31
55
|
});
|
|
32
|
-
|
|
33
|
-
// creating the app
|
|
34
|
-
const { value: appKey } = await doApiPost({
|
|
35
|
-
url: '/app',
|
|
36
|
-
token: adminKey,
|
|
37
|
-
payload: newApp,
|
|
38
|
-
});
|
|
39
|
-
expect(appKey).toBeTruthy();
|
|
40
|
-
}
|
|
56
|
+
expect(apiKey).toBeTruthy();
|
|
41
57
|
// create the user token
|
|
42
58
|
({ value: userToken } = await doApiPost({
|
|
43
59
|
url: '/user',
|
|
@@ -62,14 +78,20 @@ describe('user: booking search', () => {
|
|
|
62
78
|
}
|
|
63
79
|
});
|
|
64
80
|
it('should be able to search a travel gate for a booking', async () => {
|
|
65
|
-
const
|
|
81
|
+
const payload = {
|
|
82
|
+
bookingId: '', supplierId: '', name: '',
|
|
83
|
+
};
|
|
84
|
+
const { bookings } = await doApiPost({
|
|
66
85
|
url: `/bookings/${appKey}/${userId}/search`,
|
|
67
86
|
token: userToken,
|
|
68
|
-
payload
|
|
69
|
-
bookingId: '', supplierId: '', name: '',
|
|
70
|
-
},
|
|
87
|
+
payload,
|
|
71
88
|
});
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
89
|
+
expect(plugins[0].searchHotelBooking).toHaveBeenCalled();
|
|
90
|
+
expect(Array.isArray(bookings)).toBeTruthy();
|
|
91
|
+
expect(plugins[0].searchHotelBooking.mock.calls[0][0].payload).toEqual(payload);
|
|
92
|
+
expect(plugins[0].searchHotelBooking.mock.calls[0][0].token).toEqual(token);
|
|
93
|
+
|
|
94
|
+
// expect(Array.isArray(retVal.bookings)).toBeTruthy();
|
|
95
|
+
// expect(retVal.bookings.length > 0).toBeTruthy();
|
|
74
96
|
});
|
|
75
97
|
});
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
/* global describe, it, expect */
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
const request = require('supertest');
|
|
5
|
-
const server = require('../../index');
|
|
3
|
+
const { doApiGet } = require('../../test/utils')();
|
|
6
4
|
|
|
7
5
|
describe('ping', () => {
|
|
8
6
|
it('should get a server pong', async () => {
|
|
9
|
-
const response = await
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
expect(
|
|
13
|
-
expect(response.statusCode).toBe(200);
|
|
14
|
-
expect(response.body.error).toBeUndefined();
|
|
15
|
-
expect(Object.keys(response.body))
|
|
7
|
+
const response = await doApiGet({
|
|
8
|
+
url: '/ping',
|
|
9
|
+
});
|
|
10
|
+
expect(Object.keys(response))
|
|
16
11
|
.toEqual(expect.arrayContaining([
|
|
17
12
|
'name',
|
|
18
13
|
'description',
|
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
/* globals describe it expect beforeAll,
|
|
1
|
+
/* globals describe it expect beforeAll, jest, afterEach */
|
|
2
2
|
|
|
3
3
|
const chance = require('chance').Chance();
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const app = require('../../index');
|
|
7
|
-
const sqldb = require('../../models/db');
|
|
4
|
+
const testUtils = require('../../test/utils');
|
|
5
|
+
const slugify = require('../../test/slugify');
|
|
8
6
|
|
|
9
7
|
const { env: { adminKey } } = process;
|
|
10
8
|
|
|
@@ -17,6 +15,9 @@ describe('user', () => {
|
|
|
17
15
|
packageName: `ti2-${appName}`,
|
|
18
16
|
adminEmail: chance.email(),
|
|
19
17
|
};
|
|
18
|
+
const { doApiPost, doApiGet, doApiDelete } = testUtils({
|
|
19
|
+
plugins: [appName],
|
|
20
|
+
});
|
|
20
21
|
let appKey;
|
|
21
22
|
const userId = chance.guid();
|
|
22
23
|
let apiKey = chance.guid();
|
|
@@ -27,43 +28,39 @@ describe('user', () => {
|
|
|
27
28
|
};
|
|
28
29
|
beforeAll(async () => {
|
|
29
30
|
// create an App
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.set('Authorization', `Bearer ${appKey}`)
|
|
41
|
-
.send({
|
|
31
|
+
({ value: appKey } = await doApiPost({
|
|
32
|
+
url: '/app',
|
|
33
|
+
token: adminKey,
|
|
34
|
+
payload: newApp,
|
|
35
|
+
}));
|
|
36
|
+
const url = `/${appName}/${userId}`;
|
|
37
|
+
await doApiPost({
|
|
38
|
+
url,
|
|
39
|
+
token: appKey,
|
|
40
|
+
payload: {
|
|
42
41
|
tokenHint: apiKey.split('-')[0],
|
|
43
42
|
token,
|
|
44
|
-
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
45
|
});
|
|
46
46
|
let userKey;
|
|
47
|
-
afterAll(async () => {
|
|
48
|
-
await sqldb.connectionManager.close();
|
|
49
|
-
});
|
|
50
47
|
afterEach(() => jest.clearAllMocks());
|
|
51
48
|
it('a user should be able to get a user token via an admin key', async () => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
userKey
|
|
49
|
+
({ value: userKey } = await doApiPost({
|
|
50
|
+
url: '/user',
|
|
51
|
+
token: adminKey,
|
|
52
|
+
payload: {
|
|
53
|
+
userId,
|
|
54
|
+
},
|
|
55
|
+
}));
|
|
56
|
+
expect(userKey).toBeTruthy();
|
|
60
57
|
});
|
|
61
58
|
it('a user should be able to get a list of it\'s mapped apps', async () => {
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
59
|
+
const { userAppKeys } = await doApiGet({
|
|
60
|
+
token: userKey,
|
|
61
|
+
url: `/user/${userId}/apps`,
|
|
62
|
+
});
|
|
63
|
+
expect(userAppKeys).toEqual(
|
|
67
64
|
expect.arrayContaining([
|
|
68
65
|
expect.objectContaining({
|
|
69
66
|
hint: apiKey.split('-')[0],
|
|
@@ -74,17 +71,16 @@ describe('user', () => {
|
|
|
74
71
|
);
|
|
75
72
|
});
|
|
76
73
|
it('should be able to delete a user/app key', async () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
expect(
|
|
87
|
-
expect(respList.body.userAppKeys).toEqual(
|
|
74
|
+
await doApiDelete({
|
|
75
|
+
url: `/${appName}/${userId}`,
|
|
76
|
+
token: userKey,
|
|
77
|
+
payload: { tokenHint: apiKey.split('-')[0] },
|
|
78
|
+
});
|
|
79
|
+
const { userAppKeys } = await doApiGet({
|
|
80
|
+
url: `/user/${userId}/apps`,
|
|
81
|
+
token: userKey,
|
|
82
|
+
});
|
|
83
|
+
expect(userAppKeys).toEqual(
|
|
88
84
|
expect.not.arrayContaining([
|
|
89
85
|
expect.objectContaining({
|
|
90
86
|
hint: apiKey.split('-')[0],
|
|
@@ -98,20 +94,20 @@ describe('user', () => {
|
|
|
98
94
|
// set up new token
|
|
99
95
|
apiKey = chance.guid();
|
|
100
96
|
token.apiKey = apiKey;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
await doApiPost({
|
|
98
|
+
url: `/${appName}/${userId}`,
|
|
99
|
+
token: userKey,
|
|
100
|
+
payload: {
|
|
105
101
|
tokenHint: apiKey.split('-')[0],
|
|
106
102
|
token,
|
|
107
|
-
}
|
|
108
|
-
|
|
103
|
+
},
|
|
104
|
+
});
|
|
109
105
|
// make sure the app token is there
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
expect(
|
|
106
|
+
const { userAppKeys } = await doApiGet({
|
|
107
|
+
url: `/user/${userId}/apps`,
|
|
108
|
+
token: userKey,
|
|
109
|
+
});
|
|
110
|
+
expect(userAppKeys).toEqual(
|
|
115
111
|
expect.arrayContaining([
|
|
116
112
|
expect.objectContaining({
|
|
117
113
|
hint: apiKey.split('-')[0],
|
|
@@ -122,12 +118,11 @@ describe('user', () => {
|
|
|
122
118
|
);
|
|
123
119
|
});
|
|
124
120
|
it('should be able to get all the methods for an app', async () => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
expect(methods
|
|
130
|
-
expect(methods.body.methods).toEqual(
|
|
121
|
+
const { methods } = await doApiGet({
|
|
122
|
+
url: `/app/${appName}/methods`,
|
|
123
|
+
token: userKey,
|
|
124
|
+
});
|
|
125
|
+
expect(methods).toEqual(
|
|
131
126
|
expect.arrayContaining([
|
|
132
127
|
'validateToken', 'getProduct',
|
|
133
128
|
]),
|
package/controllers/admin.js
CHANGED
|
@@ -12,21 +12,43 @@ const createApp = async (req, res, next) => {
|
|
|
12
12
|
adminEmail,
|
|
13
13
|
} = req.body;
|
|
14
14
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
const existing = await sqldb.Integration.findByPk(name, { paranoid: false });
|
|
16
|
+
const apiKey = await (async () => {
|
|
17
|
+
if (existing) {
|
|
18
|
+
await existing.restore();
|
|
19
|
+
return existing.apiKey;
|
|
20
|
+
}
|
|
21
|
+
const newApp = await sqldb.Integration.create({
|
|
22
|
+
name,
|
|
23
|
+
packageName,
|
|
24
|
+
adminEmail,
|
|
25
|
+
});
|
|
26
|
+
return newApp.apiKey;
|
|
27
|
+
})();
|
|
28
|
+
return res.json({ value: apiKey });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return next(err);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const deleteApp = async (req, res, next) => {
|
|
35
|
+
const { pathParams: { appKey: name } } = req;
|
|
36
|
+
try {
|
|
37
|
+
const newApp = await sqldb.Integration.destroy({
|
|
38
|
+
where: { name },
|
|
19
39
|
});
|
|
20
40
|
const { apiKey } = newApp;
|
|
21
41
|
return res.json({ value: apiKey });
|
|
22
42
|
} catch (err) {
|
|
43
|
+
console.log({ err });
|
|
23
44
|
return next(err);
|
|
24
45
|
}
|
|
25
46
|
};
|
|
26
47
|
|
|
27
48
|
const listApps = async (req, res, next) => {
|
|
28
49
|
try {
|
|
29
|
-
const integrations = await sqldb.Integration.findAll()
|
|
50
|
+
const integrations = (await sqldb.Integration.findAll())
|
|
51
|
+
.map(int => omit(['apiKey'], int.dataValues));
|
|
30
52
|
return res.json({ integrations });
|
|
31
53
|
} catch (err) {
|
|
32
54
|
return next(err);
|
|
@@ -71,6 +93,7 @@ const resetIntegrationToken = async (req, res, next) => {
|
|
|
71
93
|
|
|
72
94
|
module.exports = {
|
|
73
95
|
createApp,
|
|
96
|
+
deleteApp,
|
|
74
97
|
createUserToken,
|
|
75
98
|
listApps,
|
|
76
99
|
resetIntegrationToken,
|
package/controllers/user.js
CHANGED
|
@@ -22,9 +22,9 @@ const getAppMethods = plugins => async (req, res, next) => {
|
|
|
22
22
|
const { params: { appKey } } = req;
|
|
23
23
|
try {
|
|
24
24
|
const app = plugins.filter(({ name }) => name === appKey)[0];
|
|
25
|
-
|
|
25
|
+
const methods = Object.getOwnPropertyNames(app);
|
|
26
26
|
return res.json({
|
|
27
|
-
methods
|
|
27
|
+
methods,
|
|
28
28
|
});
|
|
29
29
|
} catch (err) {
|
|
30
30
|
return next(err);
|
package/index.js
CHANGED
|
@@ -39,6 +39,7 @@ module.exports = ({
|
|
|
39
39
|
});
|
|
40
40
|
return new Plugin({ name: pluginName, ...params });
|
|
41
41
|
});
|
|
42
|
+
// console.log({ plugins });
|
|
42
43
|
const api = {
|
|
43
44
|
...pingController,
|
|
44
45
|
...adminController,
|
|
@@ -46,6 +47,7 @@ module.exports = ({
|
|
|
46
47
|
...userController(plugins),
|
|
47
48
|
...bookingsController(plugins),
|
|
48
49
|
}; // mehthods that should map to the yaml api spec
|
|
50
|
+
app.plugins = plugins;
|
|
49
51
|
createMiddleware(schema, app, (_err, middleware) => {
|
|
50
52
|
if (apiDocs) {
|
|
51
53
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(schema));
|
|
@@ -170,4 +172,4 @@ module.exports = ({
|
|
|
170
172
|
});
|
|
171
173
|
|
|
172
174
|
return app;
|
|
173
|
-
}
|
|
175
|
+
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ti2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Tourist Industry Exchange (TI2)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"test": "jest
|
|
7
|
+
"test": "jest",
|
|
8
8
|
"sequelize": "sequelize-cli"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
package/seeders/.gitkeep
ADDED
|
File without changes
|
package/test/plugin.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/* global jest */
|
|
2
|
+
|
|
3
|
+
class Plugin {
|
|
4
|
+
constructor(params = {}) {
|
|
5
|
+
Object.entries(params).forEach(([attr, value]) => {
|
|
6
|
+
this[attr] = value;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
validateToken = jest.fn();
|
|
11
|
+
|
|
12
|
+
getProduct = jest.fn();
|
|
13
|
+
|
|
14
|
+
searchHotelBooking = jest.fn(() => ({ bookings: [] }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = Plugin;
|
package/test/slugify.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// source : https://gist.github.com/codeguy/6684588
|
|
2
|
+
|
|
3
|
+
const slugify = strParam => {
|
|
4
|
+
let str = strParam.replace(/^\s+|\s+$/g, ''); // trim
|
|
5
|
+
str = str.toLowerCase();
|
|
6
|
+
|
|
7
|
+
// remove accents, swap ñ for n, etc
|
|
8
|
+
const from = 'àáäâèéëêìíïîòóöôùúüûñç·/_,:;';
|
|
9
|
+
const to = 'aaaaeeeeiiiioooouuuunc------';
|
|
10
|
+
for (let i = 0, l = from.length; i < l; i += 1) {
|
|
11
|
+
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
str = str.replace(/[^a-z0-9 -]/g, '') // remove invalid chars
|
|
15
|
+
.replace(/\s+/g, '-') // collapse whitespace and replace by -
|
|
16
|
+
.replace(/-+/g, '-'); // collapse dashes
|
|
17
|
+
|
|
18
|
+
return str;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
module.exports = slugify;
|
package/test/utils.js
CHANGED
|
@@ -1,111 +1,110 @@
|
|
|
1
|
+
/* global afterAll */
|
|
2
|
+
|
|
1
3
|
const chance = require('chance').Chance();
|
|
2
4
|
const request = require('supertest');
|
|
3
5
|
const assert = require('assert');
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
6
|
+
const appReq = require('../index');
|
|
7
|
+
const Plugin = require('./plugin');
|
|
8
|
+
const slugify = require('./slugify');
|
|
9
|
+
const sqldb = require('../models/db');
|
|
7
10
|
|
|
8
11
|
const { env: { adminKey } } = process;
|
|
9
12
|
|
|
10
13
|
const timeout = ms => (new Promise(resolve => setTimeout(resolve, ms)));
|
|
11
14
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
15
|
+
afterAll(async () => {
|
|
16
|
+
await sqldb.connectionManager.close();
|
|
17
|
+
});
|
|
18
|
+
module.exports = (appParams = {}) => {
|
|
19
|
+
const plugins = (() => {
|
|
20
|
+
if (Array.isArray(appParams.plugins)) {
|
|
21
|
+
const retVal = {};
|
|
22
|
+
appParams.plugins.forEach(pluginName => {
|
|
23
|
+
retVal[pluginName] = Plugin;
|
|
24
|
+
});
|
|
25
|
+
return retVal;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
mockName: Plugin,
|
|
29
|
+
};
|
|
30
|
+
})();
|
|
31
|
+
const app = appReq({
|
|
32
|
+
startServer: false,
|
|
33
|
+
...appParams,
|
|
34
|
+
plugins,
|
|
35
|
+
});
|
|
31
36
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const appSetup = async ({ userId: userIdParam } = {}) => {
|
|
40
|
-
const userId = userIdParam || chance.guid();
|
|
41
|
-
const appName1 = slugify(
|
|
42
|
-
chance.company(),
|
|
43
|
-
).toLowerCase();
|
|
44
|
-
const newApp = {
|
|
45
|
-
name: appName1,
|
|
46
|
-
packageName: `ti2-${appName1}`,
|
|
47
|
-
adminEmail: chance.email(),
|
|
48
|
-
};
|
|
49
|
-
const apiKey1 = chance.guid();
|
|
50
|
-
const token = {
|
|
51
|
-
endpoint: chance.url(),
|
|
52
|
-
apiKey: apiKey1,
|
|
53
|
-
};
|
|
54
|
-
const { body: { value: appKey } } = await request(app)
|
|
55
|
-
.post('/app')
|
|
56
|
-
.set('Authorization', `Bearer ${adminKey}`)
|
|
57
|
-
.send(newApp);
|
|
58
|
-
await request(app)
|
|
59
|
-
.post(`/${newApp.name}/${userId}`)
|
|
60
|
-
.set('Authorization', `Bearer ${appKey}`)
|
|
61
|
-
.send({
|
|
62
|
-
tokenHint: token.apiKey.split('-')[0],
|
|
63
|
-
token,
|
|
64
|
-
});
|
|
65
|
-
return {
|
|
66
|
-
newApp,
|
|
37
|
+
const doApi = async ({
|
|
38
|
+
query,
|
|
39
|
+
verb,
|
|
40
|
+
payload,
|
|
41
|
+
url,
|
|
67
42
|
token,
|
|
68
|
-
|
|
69
|
-
|
|
43
|
+
expectStatusCode = 200,
|
|
44
|
+
rawResponse = false,
|
|
45
|
+
}) => {
|
|
46
|
+
let resp;
|
|
47
|
+
if (token) {
|
|
48
|
+
resp = await request(app)[verb](url)
|
|
49
|
+
.set({ Authorization: `Bearer ${token}` })
|
|
50
|
+
.query(query)
|
|
51
|
+
.send(payload);
|
|
52
|
+
} else {
|
|
53
|
+
resp = await request(app)[verb](url).query(query).send(payload);
|
|
54
|
+
}
|
|
55
|
+
// if (resp.statusCode !== 200) assert.strictEqual(resp, `${url}-${expectStatusCode}`);
|
|
56
|
+
assert.strictEqual(`${url}-${resp.statusCode}`, `${url}-${expectStatusCode}`);
|
|
57
|
+
if (rawResponse) return resp;
|
|
58
|
+
return resp.body;
|
|
70
59
|
};
|
|
71
|
-
};
|
|
72
60
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
verb
|
|
76
|
-
|
|
77
|
-
url,
|
|
78
|
-
token,
|
|
79
|
-
expectStatusCode = 200,
|
|
80
|
-
rawResponse = false,
|
|
81
|
-
}) => {
|
|
82
|
-
let resp;
|
|
83
|
-
if (token) {
|
|
84
|
-
resp = await request(app)[verb](url)
|
|
85
|
-
.set({ Authorization: `Bearer ${token}` })
|
|
86
|
-
.query(query)
|
|
87
|
-
.send(payload);
|
|
88
|
-
} else {
|
|
89
|
-
resp = await request(app)[verb](url).query(query).send(payload);
|
|
90
|
-
}
|
|
91
|
-
// if (resp.statusCode !== 200) assert.strictEqual(resp, `${url}-${expectStatusCode}`);
|
|
92
|
-
assert.strictEqual(`${url}-${resp.statusCode}`, `${url}-${expectStatusCode}`);
|
|
93
|
-
if (rawResponse) return resp;
|
|
94
|
-
return resp.body;
|
|
95
|
-
};
|
|
61
|
+
const doApiGet = params => doApi({ ...params, verb: 'get' });
|
|
62
|
+
const doApiPost = params => doApi({ ...params, verb: 'post' });
|
|
63
|
+
const doApiPut = params => doApi({ ...params, verb: 'put' });
|
|
64
|
+
const doApiDelete = params => doApi({ ...params, verb: 'delete' });
|
|
96
65
|
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
66
|
+
const appSetup = async ({ userId: userIdParam } = {}) => {
|
|
67
|
+
const userId = userIdParam || chance.guid();
|
|
68
|
+
const appName1 = slugify(
|
|
69
|
+
chance.company(),
|
|
70
|
+
).toLowerCase();
|
|
71
|
+
const newApp = {
|
|
72
|
+
name: appName1,
|
|
73
|
+
packageName: `ti2-${appName1}`,
|
|
74
|
+
adminEmail: chance.email(),
|
|
75
|
+
};
|
|
76
|
+
const apiKey1 = chance.guid();
|
|
77
|
+
const token = {
|
|
78
|
+
endpoint: chance.url(),
|
|
79
|
+
apiKey: apiKey1,
|
|
80
|
+
};
|
|
81
|
+
const { body: { value: appKey } } = await request(app)
|
|
82
|
+
.post('/app')
|
|
83
|
+
.set('Authorization', `Bearer ${adminKey}`)
|
|
84
|
+
.send(newApp);
|
|
85
|
+
await request(app)
|
|
86
|
+
.post(`/${newApp.name}/${userId}`)
|
|
87
|
+
.set('Authorization', `Bearer ${appKey}`)
|
|
88
|
+
.send({
|
|
89
|
+
tokenHint: token.apiKey.split('-')[0],
|
|
90
|
+
token,
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
newApp,
|
|
94
|
+
token,
|
|
95
|
+
appKey,
|
|
96
|
+
userId,
|
|
97
|
+
};
|
|
98
|
+
};
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
return {
|
|
101
|
+
appSetup,
|
|
102
|
+
doApiDelete,
|
|
103
|
+
doApiGet,
|
|
104
|
+
doApiPost,
|
|
105
|
+
doApiPut,
|
|
106
|
+
slugify,
|
|
107
|
+
timeout,
|
|
108
|
+
plugins: app.plugins,
|
|
109
|
+
};
|
|
111
110
|
};
|