ti2 1.0.6 → 1.0.10
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 +23 -2
- 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 +8 -8
- 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
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Ti2
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
Ti2 (Tourism Information Interchange) is an open source integration framework, built by and for the tourism community. Ti2's goal is to make integrations in our industry faster and easier to build, while providing a framework for innovation on top of these integrations.
|
|
4
|
+
|
|
5
|
+
Core functions of Ti2 are currently focused on content, rates, and bookings.
|
|
6
|
+
|
|
7
|
+
## About this Project
|
|
8
|
+
|
|
9
|
+
### How it Works
|
|
10
|
+
|
|
11
|
+
Ti2 has a core codebase of standardized industry functions (new, update, cancel, etc.) for bookings, content, and rates - with more to come. Plugins are used to connect to the Ti2 to perform a task (like create a booking). There are two types of plugins: Integration and App.
|
|
12
|
+
|
|
13
|
+
Integration Plugins are used to connect core systems, like booking/reservation or content management systems, to Ti2. For example, you would use a Integration Plugin to connect Ventrata to Ti2. You would do this to be able to accept bookings from another system (like an OTA) connected to Ti2. *Ti2 allows any Integration Plugin to communicate with each other; therefore, once a Integration Plugin is live, no one ever needs to write a custom integration for that system again.*
|
|
14
|
+
|
|
15
|
+
App Plugins are value added tools that only speak to Ti2 and other plugins. For example, you might create an App Plugin that sends email notifications every morning with a list of all active bookings for the day. You would retrieve the list of bookings for the day by using a System Plugin.
|
|
16
|
+
|
|
17
|
+
Ti2 can be deployed on your own servers, or you can connect to a Ti2 instance that another company is hosting.
|
|
18
|
+
|
|
19
|
+
### Why use Ti2 ?
|
|
20
|
+
|
|
21
|
+
Ti2 gives a free, open source solution to building integrations across our industry. In addition, tools can be built on top of Ti2, that add additional value to these integrations - whether for the good of our industry or for profit.
|
|
22
|
+
|
|
23
|
+
**Become part of the community, start building on Ti2 today!**
|
|
3
24
|
|
|
4
25
|
## Getting Started
|
|
5
26
|
|
|
@@ -8,7 +29,7 @@ Tourism Industry Interchange
|
|
|
8
29
|
Some environment variables are required for ti2 to start
|
|
9
30
|
|
|
10
31
|
- DB_URL (mysql connection in url format)
|
|
11
|
-
- dbCryptoKey (integration details encription key)
|
|
32
|
+
- dbCryptoKey (integration details encription key, should be a 32 chars random string base64 encoded)
|
|
12
33
|
- adminKey (a key for admin related requests)
|
|
13
34
|
- jwtKey (a key to encrypt user sessions request)
|
|
14
35
|
- 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,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ti2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.10",
|
|
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": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "git+https://github.com/
|
|
12
|
+
"url": "git+https://github.com/ti2travel/ti2.git"
|
|
13
13
|
},
|
|
14
14
|
"author": "",
|
|
15
15
|
"license": "GPL-3",
|
|
16
16
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/
|
|
17
|
+
"url": "https://github.com/ti2travel/ti2/issues"
|
|
18
18
|
},
|
|
19
|
-
"homepage": "https://github.com/
|
|
19
|
+
"homepage": "https://github.com/ti2travel/ti2#readme",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"bluebird": "^3.7.2",
|
|
22
22
|
"express": "^4.17.2",
|
|
@@ -28,9 +28,9 @@
|
|
|
28
28
|
"seque": "^1.2.1",
|
|
29
29
|
"sequelize": "^6.13.0",
|
|
30
30
|
"sequelize-cli": "^6.4.1",
|
|
31
|
-
"swagger-express-middleware": "
|
|
32
|
-
"swagger-routes-express": "
|
|
33
|
-
"swagger-ui-express": "
|
|
31
|
+
"swagger-express-middleware": "3.0.0-alpha.5",
|
|
32
|
+
"swagger-routes-express": "3.3.1",
|
|
33
|
+
"swagger-ui-express": "4.3.0",
|
|
34
34
|
"uninstall": "^0.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
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
|
};
|