-
Notifications
You must be signed in to change notification settings - Fork 2.7k
/
api.js
240 lines (202 loc) · 8.08 KB
/
api.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
'use strict';
const assert = require('assert');
const path = require('path');
const SwaggerParser = require('@apidevtools/swagger-parser');
const request = require('request-promise-native');
const nconf = require('nconf');
const util = require('util');
const wait = util.promisify(setTimeout);
const db = require('./mocks/databasemock');
const helpers = require('./helpers');
const user = require('../src/user');
const groups = require('../src/groups');
const categories = require('../src/categories');
const topics = require('../src/topics');
const plugins = require('../src/plugins');
const flags = require('../src/flags');
const messaging = require('../src/messaging');
const socketUser = require('../src/socket.io/user');
describe('Read API', async () => {
let readApi = false;
const apiPath = path.resolve(__dirname, '../public/openapi/read.yaml');
let jar;
let setup = false;
const unauthenticatedRoutes = ['/api/login', '/api/register']; // Everything else will be called with the admin user
async function dummySearchHook(data) {
return [1];
}
after(async function () {
plugins.unregisterHook('core', 'filter:search.query', dummySearchHook);
});
async function setupData() {
if (setup) {
return;
}
// Create sample users
const adminUid = await user.create({ username: 'admin', password: '123456', email: 'test@example.org' });
const unprivUid = await user.create({ username: 'unpriv', password: '123456', email: 'unpriv@example.org' });
await groups.join('administrators', adminUid);
// Create a category
const testCategory = await categories.create({ name: 'test' });
// Post a new topic
const testTopic = await topics.post({
uid: adminUid,
cid: testCategory.cid,
title: 'Test Topic',
content: 'Test topic content',
});
// Create a sample flag
await flags.create('post', 1, unprivUid, 'sample reasons', Date.now());
// Create a new chat room
await messaging.newRoom(1, [2]);
// export data for admin user
await socketUser.exportProfile({ uid: adminUid }, { uid: adminUid });
await socketUser.exportPosts({ uid: adminUid }, { uid: adminUid });
await socketUser.exportUploads({ uid: adminUid }, { uid: adminUid });
// wait for export child process to complete
await wait(20000);
// Attach a search hook so /api/search is enabled
plugins.registerHook('core', {
hook: 'filter:search.query',
method: dummySearchHook,
});
jar = await helpers.loginUser('admin', '123456');
setup = true;
}
it('should pass OpenAPI v3 validation', async () => {
try {
await SwaggerParser.validate(apiPath);
} catch (e) {
assert.ifError(e);
}
});
readApi = await SwaggerParser.dereference(apiPath);
// Iterate through all documented paths, make a call to it, and compare the result body with what is defined in the spec
const paths = Object.keys(readApi.paths);
paths.forEach((path) => {
let schema;
let response;
let url;
const headers = {};
const qs = {};
function compare(schema, response, context) {
let required = [];
const additionalProperties = schema.hasOwnProperty('additionalProperties');
if (schema.allOf) {
schema = schema.allOf.reduce((memo, obj) => {
required = required.concat(obj.required ? obj.required : Object.keys(obj.properties));
memo = { ...memo, ...obj.properties };
return memo;
}, {});
} else if (schema.properties) {
required = schema.required || Object.keys(schema.properties);
schema = schema.properties;
} else {
// If schema contains no properties, check passes
return;
}
// Compare the schema to the response
required.forEach((prop) => {
if (schema.hasOwnProperty(prop)) {
assert(response.hasOwnProperty(prop), '"' + prop + '" is a required property (path: ' + path + ', context: ' + context + ')');
// Don't proceed with type-check if the value could possibly be unset (nullable: true, in spec)
if (response[prop] === null && schema[prop].nullable === true) {
return;
}
// Therefore, if the value is actually null, that's a problem (nullable is probably missing)
assert(response[prop] !== null, '"' + prop + '" was null, but schema does not specify it to be a nullable property (path: ' + path + ', context: ' + context + ')');
switch (schema[prop].type) {
case 'string':
assert.strictEqual(typeof response[prop], 'string', '"' + prop + '" was expected to be a string, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
break;
case 'boolean':
assert.strictEqual(typeof response[prop], 'boolean', '"' + prop + '" was expected to be a boolean, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
break;
case 'object':
assert.strictEqual(typeof response[prop], 'object', '"' + prop + '" was expected to be an object, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
compare(schema[prop], response[prop], context ? [context, prop].join('.') : prop);
break;
case 'array':
assert.strictEqual(Array.isArray(response[prop]), true, '"' + prop + '" was expected to be an array, but was ' + typeof response[prop] + ' instead (path: ' + path + ', context: ' + context + ')');
if (schema[prop].items) {
// Ensure the array items have a schema defined
assert(schema[prop].items.type || schema[prop].items.allOf, '"' + prop + '" is defined to be an array, but its items have no schema defined (path: ' + path + ', context: ' + context + ')');
// Compare types
if (schema[prop].items.type === 'object' || Array.isArray(schema[prop].items.allOf)) {
response[prop].forEach((res) => {
compare(schema[prop].items, res, context ? [context, prop].join('.') : prop);
});
} else if (response[prop].length) { // for now
response[prop].forEach((item) => {
assert.strictEqual(typeof item, schema[prop].items.type, '"' + prop + '" should have ' + schema[prop].items.type + ' items, but found ' + typeof items + ' instead (path: ' + path + ', context: ' + context + ')');
});
}
}
break;
}
}
});
// Compare the response to the schema
Object.keys(response).forEach((prop) => {
if (additionalProperties) { // All bets are off
return;
}
assert(schema[prop], '"' + prop + '" was found in response, but is not defined in schema (path: ' + path + ', context: ' + context + ')');
});
}
// TOXO: fix -- premature exit for POST-only routes
if (!readApi.paths[path].get) {
return;
}
it('should have examples when parameters are present', () => {
const parameters = readApi.paths[path].get.parameters;
let testPath = path;
if (parameters) {
parameters.forEach((param) => {
assert(param.example !== null && param.example !== undefined, path + ' has parameters without examples');
switch (param.in) {
case 'path':
testPath = testPath.replace('{' + param.name + '}', param.example);
break;
case 'header':
headers[param.name] = param.example;
break;
case 'query':
qs[param.name] = param.example;
break;
}
});
}
url = nconf.get('url') + testPath;
});
it('should resolve with a 200 when called', async () => {
await setupData();
try {
response = await request(url, {
jar: !unauthenticatedRoutes.includes(path) ? jar : undefined,
json: true,
headers: headers,
qs: qs,
});
} catch (e) {
assert(!e, path + ' resolved with ' + e.message);
}
});
// Recursively iterate through schema properties, comparing type
it('response should match schema definition', () => {
const has200 = readApi.paths[path].get.responses['200'];
if (!has200) {
return;
}
const hasJSON = has200.content && has200.content['application/json'];
if (hasJSON) {
schema = readApi.paths[path].get.responses['200'].content['application/json'].schema;
compare(schema, response, 'root');
}
// TODO someday: text/csv, binary file type checking?
});
});
});
describe('Write API', () => {
let writeApi;
});