Repository navigation
Expand file tree
/
Copy pathserver.test.js
More file actions
221 lines (209 loc) · 10.6 KB
/
Copy pathserver.test.js
File metadata and controls
221 lines (209 loc) · 10.6 KB
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
// Routing unit-checks for the relay. Run: node server.test.js
'use strict';
const assert = require('assert');
const { EventEmitter } = require('events');
const { relayTargets, regDecision, MAX_PLAYERS, lanIPv4s, parsePort,
browserOpenCommand, openGameBrowser, serverFailureMessage, attachServerErrorHandlers } = require('./server.js');
let n = 0;
const ok = (name, fn) => { fn(); n++; console.log(' ok ' + name); };
// --- relayTargets ---
const HOST = 1, ids = [1, 2, 3, 4]; // host + 3 clients
ok('host broadcast -> all clients, not self', () => {
assert.deepStrictEqual(relayTargets(HOST, { t: 's' }, HOST, ids), [2, 3, 4]);
});
ok('host targeted (m.to) -> only that client', () => {
assert.deepStrictEqual(relayTargets(HOST, { t: 'ev', to: 3 }, HOST, ids), [3]);
});
ok('client message -> host only', () => {
assert.deepStrictEqual(relayTargets(2, { t: 'in' }, HOST, ids), [HOST]);
});
ok('client message with no host -> nobody', () => {
assert.deepStrictEqual(relayTargets(2, { t: 'in' }, null, [2, 3]), []);
});
ok('host broadcast with to=0 is treated as targeted', () => {
// to is compared != null, so id 0 is a valid explicit target
assert.deepStrictEqual(relayTargets(HOST, { t: 'ev', to: 0 }, HOST, ids), [0]);
});
// --- regDecision ---
ok('first host registers ok', () => {
assert.deepStrictEqual(regDecision('h', false, 0, MAX_PLAYERS), { ok: true, role: 'host' });
});
ok('second host rejected', () => {
assert.strictEqual(regDecision('h', true, 1, MAX_PLAYERS).ok, false);
assert.strictEqual(regDecision('h', true, 1, MAX_PLAYERS).why, 'host-exists');
});
ok('joiner before any host -> nohost', () => {
assert.deepStrictEqual(regDecision('j', false, 0, MAX_PLAYERS), { ok: false, why: 'nohost' });
});
ok('joiner with host and room -> client', () => {
assert.deepStrictEqual(regDecision('j', true, 3, MAX_PLAYERS), { ok: true, role: 'client' });
});
ok('joiner when full -> match-full', () => {
assert.deepStrictEqual(regDecision('j', true, MAX_PLAYERS, MAX_PLAYERS), { ok: false, why: 'match-full' });
});
// --- player cap (raised 8 -> 16 for LAN) ---
ok('cap is 16', () => { assert.strictEqual(MAX_PLAYERS, 16); });
ok('16th joiner allowed, 17th rejected', () => {
assert.strictEqual(regDecision('j', true, 15, MAX_PLAYERS).ok, true); // seat 16 (host=1 + 15)
assert.strictEqual(regDecision('j', true, 16, MAX_PLAYERS).why, 'match-full');
});
// --- lanIPv4s (startup banner helper) ---
ok('lanIPv4s returns only external IPv4s', () => {
const stub = {
lo: [{ family: 'IPv4', address: '127.0.0.1', internal: true }],
eth0: [{ family: 'IPv4', address: '192.168.1.50', internal: false },
{ family: 'IPv6', address: 'fe80::1', internal: false }],
wifi: [{ family: 'IPv4', address: '169.254.83.107', internal: false }], // APIPA — must be skipped
};
assert.deepStrictEqual(lanIPv4s(stub), ['192.168.1.50']);
});
// --- startup: explicit TCP ports, PATH-independent browser opener, guarded errors ---
ok('ports default to 8666 and normalize valid TCP port numbers', () => {
for (const value of [undefined, null, '']) assert.strictEqual(parsePort(value), 8666);
for (const [value, expected] of [['1', 1], ['65535', 65535], ['08666', 8666], [8667, 8667]]) {
assert.strictEqual(parsePort(value), expected);
}
});
ok('invalid ports cannot become named pipes or shell arguments', () => {
for (const value of ['cod2-pipe', '0', '-1', '65536', '1.5', 'NaN', 'Infinity', '0x21da', '8666 & echo bad', ' ']) {
assert.throws(() => parsePort(value), /whole number from 1 to 65535/);
}
});
ok('Windows opener normalizes an explicit ComSpec even with no system PATH', () => {
const command = browserOpenCommand(8666, 'win32', { ComSpec: 'C:/Windows/System32/cmd.exe', PATH: 'D:/OnlyNode' });
assert.strictEqual(command.command, 'C:\\Windows\\System32\\cmd.exe');
assert.deepStrictEqual(command.args, ['/d', '/s', '/c', 'start', '""', 'http://localhost:8666/']);
assert.strictEqual(command.windowsVerbatimArguments, true);
});
ok('Windows opener handles quoted ComSpec and missing or relative ComSpec', () => {
assert.strictEqual(browserOpenCommand(8666, 'win32', { COMSPEC: '"D:/Windows/System32/cmd.exe"' }).command,
'D:\\Windows\\System32\\cmd.exe');
assert.strictEqual(browserOpenCommand(8666, 'win32', { ComSpec: 'cmd.exe', SystemRoot: 'D:/Windows' }).command,
'D:\\Windows\\System32\\cmd.exe');
assert.strictEqual(browserOpenCommand(8666, 'win32', {}).command, 'C:\\Windows\\System32\\cmd.exe');
});
ok('non-Windows browser openers receive the URL as a separate argument', () => {
assert.strictEqual(browserOpenCommand(8666, 'darwin', {}).command, 'open');
const linux = browserOpenCommand(8667, 'linux', {});
assert.strictEqual(linux.command, 'xdg-open');
assert.deepStrictEqual(linux.args, ['http://localhost:8667/']);
});
ok('browser spawn failures give the manual URL once without throwing', () => {
const child = new EventEmitter();
let detached = false;
child.unref = () => { detached = true; };
const warnings = [];
const result = openGameBrowser(8666, {
platform: 'win32', env: {}, warn: message => warnings.push(message),
spawn(command, args, options) {
assert.strictEqual(command, 'C:\\Windows\\System32\\cmd.exe');
assert.strictEqual(options.windowsHide, true);
assert.strictEqual(options.stdio, 'ignore');
assert.strictEqual(options.shell, undefined, 'never launch through a default bare shell');
return child;
}
});
assert.strictEqual(result, child);
assert.strictEqual(detached, true);
child.emit('error', new Error('ENOENT'));
child.emit('exit', 1, null);
assert.strictEqual(warnings.length, 1);
assert.match(warnings[0], /Open http:\/\/localhost:8666\/ manually/);
assert.match(warnings[0], /server is still running/);
});
ok('nonzero browser exit and synchronous spawn errors remain nonfatal', () => {
const warnings = [];
const child = new EventEmitter(); child.unref = () => {};
openGameBrowser(8666, { platform: 'win32', env: {}, warn: message => warnings.push(message), spawn: () => child });
child.emit('exit', 1, null);
assert.strictEqual(warnings.length, 1);
assert.strictEqual(openGameBrowser(8666, { platform: 'win32', env: {},
warn: message => warnings.push(message), spawn() { throw new Error('blocked opener'); } }), null);
assert.strictEqual(warnings.length, 2);
});
ok('successful browser opener does not warn', () => {
const child = new EventEmitter(); child.unref = () => {};
const warnings = [];
openGameBrowser(8666, { platform: 'win32', env: {}, warn: message => warnings.push(message), spawn: () => child });
child.emit('exit', 0, null);
assert.strictEqual(warnings.length, 0);
});
ok('HTTP errors forwarded by ws are handled exactly once without an uncaught error', () => {
const httpServer = new EventEmitter(), wss = new EventEmitter();
httpServer.on('error', error => wss.emit('error', error)); // Same forwarding as the installed ws package.
const failures = [];
attachServerErrorHandlers(httpServer, wss, error => failures.push(error));
const conflict = Object.assign(new Error('address in use'), { code: 'EADDRINUSE' });
assert.doesNotThrow(() => httpServer.emit('error', conflict));
assert.doesNotThrow(() => wss.emit('error', conflict));
assert.deepStrictEqual(failures, [conflict]);
});
ok('occupied port diagnostics give manual options and do not promise to kill a listener', () => {
const message = serverFailureMessage({ code: 'EADDRINUSE' }, 8666);
assert.match(message, /Port 8666 is already in use/);
assert.match(message, /http:\/\/localhost:8666\//);
assert.match(message, /No existing process was stopped/);
assert.match(serverFailureMessage(new Error('access denied'), 8667), /access denied/);
});
async function startupIntegrationChecks() {
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'cod2-startup-test-'));
const logFile = path.join(temporary, 'server.log');
const listener = http.createServer((request, response) => response.end('existing listener alive'));
function attempt(port) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, ['server.js'], {
cwd: __dirname, env: { ...process.env, PORT: String(port), NO_OPEN: '1', LOGFILE: logFile },
windowsHide: true, stdio: ['ignore', 'pipe', 'pipe']
});
let output = '', timedOut = false;
child.stdout.on('data', chunk => { output += chunk; });
child.stderr.on('data', chunk => { output += chunk; });
const timer = setTimeout(() => { timedOut = true; child.kill(); }, 5000);
child.once('error', error => { clearTimeout(timer); reject(error); });
child.once('close', code => {
clearTimeout(timer);
if (timedOut) reject(new Error('Startup failure did not exit within five seconds: ' + output));
else resolve({ code, output });
});
});
}
try {
// Match server.js's wildcard bind: Windows may allow a separate IPv6 bind
// beside an IPv4-loopback-only listener, which would not be a real conflict.
await new Promise((resolve, reject) => { listener.once('error', reject); listener.listen(0, resolve); });
const port = listener.address().port;
const occupied = await attempt(port);
assert.strictEqual(occupied.code, 1);
assert.match(occupied.output, /already in use/);
assert.doesNotMatch(occupied.output, /Unhandled 'error'|throw er;|node:events/);
assert.strictEqual(listener.listening, true, 'the existing listener must remain untouched');
const response = await new Promise((resolve, reject) => {
http.get(`http://127.0.0.1:${port}/`, res => {
let body = '';
res.on('data', chunk => { body += chunk; });
res.on('end', () => resolve(body));
res.on('error', reject);
}).on('error', reject);
});
assert.strictEqual(response, 'existing listener alive');
n++; console.log(' ok occupied port exits cleanly and leaves the existing listener working');
const invalid = await attempt('not-a-network-port');
assert.strictEqual(invalid.code, 1);
assert.match(invalid.output, /PORT must be a whole number/);
assert.doesNotMatch(invalid.output, /server up|Unhandled|\n\s+at /);
n++; console.log(' ok invalid PORT exits clearly without creating a named-pipe server');
} finally {
if (listener.listening) await new Promise(resolve => listener.close(resolve));
if (fs.existsSync(logFile)) fs.unlinkSync(logFile);
fs.rmdirSync(temporary);
}
}
startupIntegrationChecks().then(() => console.log(`\nALL PASS: ${n}`), error => {
console.error(error);
process.exitCode = 1;
});