forked from ether/etherpad-lite
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcommon.js
255 lines (234 loc) · 8.33 KB
/
common.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
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
'use strict';
const AttributePool = require('../../static/js/AttributePool');
const apiHandler = require('../../node/handler/APIHandler');
const assert = require('assert').strict;
const io = require('socket.io-client');
const log4js = require('log4js');
const process = require('process');
const server = require('../../node/server');
const setCookieParser = require('set-cookie-parser');
const settings = require('../../node/utils/Settings');
const supertest = require('supertest');
const webaccess = require('../../node/hooks/express/webaccess');
const backups = {};
let agentPromise = null;
exports.apiKey = apiHandler.exportedForTestingOnly.apiKey;
exports.agent = null;
exports.baseUrl = null;
exports.httpServer = null;
exports.logger = log4js.getLogger('test');
const logger = exports.logger;
const logLevel = logger.level;
// Mocha doesn't monitor unhandled Promise rejections, so convert them to uncaught exceptions.
// https://github.com/mochajs/mocha/issues/2640
process.on('unhandledRejection', (reason, promise) => { throw reason; });
before(async function () {
this.timeout(60000);
await exports.init();
});
exports.init = async function () {
if (agentPromise != null) return await agentPromise;
let agentResolve;
agentPromise = new Promise((resolve) => { agentResolve = resolve; });
if (!logLevel.isLessThanOrEqualTo(log4js.levels.DEBUG)) {
logger.warn('Disabling non-test logging for the duration of the test. ' +
'To enable non-test logging, change the loglevel setting to DEBUG.');
log4js.setGlobalLogLevel(log4js.levels.OFF);
logger.setLevel(logLevel);
}
// Note: This is only a shallow backup.
backups.settings = Object.assign({}, settings);
// Start the Etherpad server on a random unused port.
settings.port = 0;
settings.ip = 'localhost';
settings.importExportRateLimiting = {max: 0};
settings.commitRateLimiting = {duration: 0.001, points: 1e6};
exports.httpServer = await server.start();
exports.baseUrl = `http://localhost:${exports.httpServer.address().port}`;
logger.debug(`HTTP server at ${exports.baseUrl}`);
// Create a supertest user agent for the HTTP server.
exports.agent = supertest(exports.baseUrl);
// Speed up authn tests.
backups.authnFailureDelayMs = webaccess.authnFailureDelayMs;
webaccess.authnFailureDelayMs = 0;
after(async function () {
webaccess.authnFailureDelayMs = backups.authnFailureDelayMs;
// Note: This does not unset settings that were added.
Object.assign(settings, backups.settings);
log4js.setGlobalLogLevel(logLevel);
await server.exit();
});
agentResolve(exports.agent);
return exports.agent;
};
/**
* Waits for the next named socket.io event. Rejects if there is an error event while waiting
* (unless waiting for that error event).
*
* @param {io.Socket} socket - The socket.io Socket object to listen on.
* @param {string} event - The socket.io Socket event to listen for.
* @returns The argument(s) passed to the event handler.
*/
exports.waitForSocketEvent = async (socket, event) => {
const errorEvents = [
'error',
'connect_error',
'connect_timeout',
'reconnect_error',
'reconnect_failed',
];
const handlers = new Map();
let cancelTimeout;
try {
const timeoutP = new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`timed out waiting for ${event} event`));
cancelTimeout = () => {};
}, 1000);
cancelTimeout = () => {
clearTimeout(timeout);
resolve();
cancelTimeout = () => {};
};
});
const errorEventP = Promise.race(errorEvents.map((event) => new Promise((resolve, reject) => {
handlers.set(event, (errorString) => {
logger.debug(`socket.io ${event} event: ${errorString}`);
reject(new Error(errorString));
});
})));
const eventP = new Promise((resolve) => {
// This will overwrite one of the above handlers if the user is waiting for an error event.
handlers.set(event, (...args) => {
logger.debug(`socket.io ${event} event`);
if (args.length > 1) return resolve(args);
resolve(args[0]);
});
});
for (const [event, handler] of handlers) socket.on(event, handler);
// timeoutP and errorEventP are guaranteed to never resolve here (they can only reject), so the
// Promise returned by Promise.race() is guaranteed to resolve to the eventP value (if
// the event arrives).
return await Promise.race([timeoutP, errorEventP, eventP]);
} finally {
cancelTimeout();
for (const [event, handler] of handlers) socket.off(event, handler);
}
};
/**
* Establishes a new socket.io connection.
*
* @param {object} [res] - Optional HTTP response object. The cookies from this response's
* `set-cookie` header(s) are passed to the server when opening the socket.io connection. If
* nullish, no cookies are passed to the server.
* @returns {io.Socket} A socket.io client Socket object.
*/
exports.connect = async (res = null) => {
// Convert the `set-cookie` header(s) into a `cookie` header.
const resCookies = (res == null) ? {} : setCookieParser.parse(res, {map: true});
const reqCookieHdr = Object.entries(resCookies).map(
([name, cookie]) => `${name}=${encodeURIComponent(cookie.value)}`).join('; ');
logger.debug('socket.io connecting...');
let padId = null;
if (res) {
padId = res.req.path.split('/p/')[1];
}
const socket = io(`${exports.baseUrl}/`, {
forceNew: true, // Different tests will have different query parameters.
path: '/socket.io',
// socketio.js-client on node.js doesn't support cookies (see https://git.io/JU8u9), so the
// express_sid cookie must be passed as a query parameter.
query: {cookie: reqCookieHdr, padId},
});
try {
await exports.waitForSocketEvent(socket, 'connect');
} catch (e) {
socket.close();
throw e;
}
logger.debug('socket.io connected');
return socket;
};
/**
* Helper function to exchange CLIENT_READY+CLIENT_VARS messages for the named pad.
*
* @param {io.Socket} socket - Connected socket.io Socket object.
* @param {string} padId - Which pad to join.
* @returns The CLIENT_VARS message from the server.
*/
exports.handshake = async (socket, padId, token = 't.12345') => {
logger.debug('sending CLIENT_READY...');
socket.send({
component: 'pad',
type: 'CLIENT_READY',
padId,
sessionID: null,
token,
});
logger.debug('waiting for CLIENT_VARS response...');
const msg = await exports.waitForSocketEvent(socket, 'message');
logger.debug('received CLIENT_VARS message');
return msg;
};
/**
* Convenience wrapper around `socket.send()` that waits for acknowledgement.
*/
exports.sendMessage = async (socket, message) => await new Promise((resolve, reject) => {
socket.send(message, (errInfo) => {
if (errInfo != null) {
const {name, message} = errInfo;
const err = new Error(message);
err.name = name;
reject(err);
return;
}
resolve();
});
});
/**
* Convenience function to send a USER_CHANGES message. Waits for acknowledgement.
*/
exports.sendUserChanges = async (socket, data) => await exports.sendMessage(socket, {
type: 'COLLABROOM',
component: 'pad',
data: {
type: 'USER_CHANGES',
apool: new AttributePool(),
...data,
},
});
/**
* Convenience function that waits for an ACCEPT_COMMIT message. Asserts that the new revision
* matches the expected revision.
*
* Note: To avoid a race condition, this should be called before the USER_CHANGES message is sent.
* For example:
*
* await Promise.all([
* common.waitForAcceptCommit(socket, rev + 1),
* common.sendUserChanges(socket, {baseRev: rev, changeset}),
* ]);
*/
exports.waitForAcceptCommit = async (socket, wantRev) => {
const msg = await exports.waitForSocketEvent(socket, 'message');
assert.deepEqual(msg, {
type: 'COLLABROOM',
data: {
type: 'ACCEPT_COMMIT',
newRev: wantRev,
},
});
};
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
/**
* Generates a random string.
*
* @param {number} [len] - The desired length of the generated string.
* @param {string} [charset] - Characters to pick from.
* @returns {string}
*/
exports.randomString = (len = 10, charset = `${alphabet}${alphabet.toUpperCase()}0123456789`) => {
let ret = '';
while (ret.length < len) ret += charset[Math.floor(Math.random() * charset.length)];
return ret;
};