9.5 KB309 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import * as util from 'node:util';
9import {readExistingServerFile} from '../existingServerStateFiles';
10import * as startServer from '../server';
11import * as lifecycle from '../serverLifecycle';
12import {parseArgs, runProxyMain} from '../startServer';
13
14/* eslint-disable require-await */
15
16// to prevent permission issues and races, mock FS read/writes in memory.
17let mockFsData: {[key: string]: string} = {};
18jest.mock('node:fs', () => {
19 return {
20 promises: {
21 writeFile: jest.fn(async (path: string, data: string) => {
22 mockFsData[path] = data;
23 }),
24 readFile: jest.fn(async (path: string) => {
25 return mockFsData[path];
26 }),
27 stat: jest.fn(async (_path: string) => {
28 return {mode: 0o700, isSymbolicLink: () => false};
29 }),
30 rm: jest.fn(async (path: string) => {
31 delete mockFsData[path];
32 }),
33 mkdir: jest.fn(async (_path: string) => {
34 //
35 }),
36 mkdtemp: jest.fn(async (_path: string) => {
37 return '/tmp/';
38 }),
39 },
40 };
41});
42
43describe('run-proxy', () => {
44 let stdout: Array<string> = [];
45 let stderr: Array<string> = [];
46 function allConsoleStdout() {
47 return stdout.join('\n');
48 }
49 function resetStdout() {
50 stdout = [];
51 stderr = [];
52 }
53 beforeEach(() => {
54 resetStdout();
55 const appendStdout = jest.fn((...args) => stdout.push(util.format(...args)));
56 const appendStderr = jest.fn((...args) => stderr.push(util.format(...args)));
57
58 global.console = {
59 log: appendStdout,
60 info: appendStdout,
61 warn: appendStdout,
62 error: appendStderr,
63 } as unknown as Console;
64
65 // reset mock filesystem
66 mockFsData = {};
67
68 jest.clearAllMocks();
69 });
70
71 const killMock = jest.spyOn(process, 'kill').mockImplementation(() => true);
72 const exitMock = jest.spyOn(process, 'exit').mockImplementation((): never => {
73 throw new Error('exited');
74 });
75
76 const defaultArgs = {
77 help: false,
78 // subprocess spawning without --foreground doesn't work well in tests
79 // plus we don't want to manage closing servers after the tests
80 foreground: true,
81 // we don't want to actually open the url in the browser during a test
82 openUrl: false,
83 port: 3011,
84 isDevMode: false,
85 json: false,
86 stdout: false,
87 platform: undefined,
88 kill: false,
89 force: false,
90 slVersion: '1.0',
91 command: 'sl',
92 cwd: undefined,
93 sessionId: undefined,
94 };
95
96 it('spawns a server', async () => {
97 const startServerSpy = jest
98 .spyOn(startServer, 'startServer')
99 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
100
101 await runProxyMain(defaultArgs);
102
103 expect(startServerSpy).toHaveBeenCalledTimes(1);
104 });
105
106 it('can output json', async () => {
107 jest
108 .spyOn(startServer, 'startServer')
109 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
110
111 await runProxyMain({...defaultArgs, json: true});
112
113 expect(JSON.parse(allConsoleStdout())).toEqual(
114 expect.objectContaining({
115 command: 'sl',
116 cwd: expect.stringContaining('isl-server'),
117 logFileLocation: expect.stringContaining('isl-server.log'),
118 pid: 1000,
119 port: 3011,
120 token: expect.stringMatching(/[a-z0-9]{32}/),
121 url: expect.stringContaining('http://localhost:3011/'),
122 wasServerReused: false,
123 }),
124 );
125 });
126
127 it('can set current working directory manually', async () => {
128 jest
129 .spyOn(startServer, 'startServer')
130 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
131
132 await runProxyMain({...defaultArgs, json: true, cwd: 'foobar'});
133
134 expect(JSON.parse(allConsoleStdout())).toEqual(
135 expect.objectContaining({
136 cwd: 'foobar',
137 }),
138 );
139 });
140
141 it('writes existing server info', async () => {
142 jest
143 .spyOn(startServer, 'startServer')
144 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
145
146 await expect(readExistingServerFile(3011)).rejects.toEqual(expect.anything());
147
148 await runProxyMain(defaultArgs);
149
150 expect(await readExistingServerFile(3011)).toEqual(
151 expect.objectContaining({
152 sensitiveToken: expect.anything(),
153 challengeToken: expect.anything(),
154 command: 'sl',
155 slVersion: '1.0',
156 }),
157 );
158 });
159
160 it('can output json for a reused server', async () => {
161 jest
162 .spyOn(startServer, 'startServer')
163 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
164 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
165
166 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
167 return Promise.resolve(1000);
168 });
169
170 await runProxyMain(defaultArgs);
171 resetStdout();
172
173 await expect(() => runProxyMain({...defaultArgs, json: true})).rejects.toEqual(
174 new Error('exited'),
175 );
176
177 expect(JSON.parse(allConsoleStdout())).toEqual(
178 expect.objectContaining({
179 command: 'sl',
180 cwd: expect.stringContaining('isl-server'),
181 logFileLocation: expect.stringContaining('isl-server.log'),
182 pid: 1000,
183 port: 3011,
184 token: expect.stringMatching(/[a-z0-9]{32}/),
185 url: expect.stringContaining('http://localhost:3011/'),
186 wasServerReused: true,
187 }),
188 );
189 expect(exitMock).toHaveBeenCalledWith(0);
190 });
191
192 it('can kill a server', async () => {
193 const startServerSpy = jest
194 .spyOn(startServer, 'startServer')
195 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
196
197 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
198 return Promise.resolve(1000);
199 });
200
201 // successfully start normally
202 await runProxyMain(defaultArgs);
203
204 // now run with --kill
205 await expect(() => runProxyMain({...defaultArgs, kill: true})).rejects.toEqual(
206 new Error('exited'),
207 );
208
209 expect(killMock).toHaveBeenCalled();
210 expect(exitMock).toHaveBeenCalledWith(0); // exits after killing
211 expect(startServerSpy).toHaveBeenCalledTimes(1); // called for original server only
212 });
213
214 it('--force kills and starts a new server', async () => {
215 const startServerSpy = jest
216 .spyOn(startServer, 'startServer')
217 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
218
219 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
220 return Promise.resolve(1000);
221 });
222
223 // successfully start normally
224 await runProxyMain(defaultArgs);
225
226 // now run with --force
227 await runProxyMain({...defaultArgs, force: true});
228
229 expect(killMock).toHaveBeenCalled();
230 expect(exitMock).not.toHaveBeenCalled();
231 expect(startServerSpy).toHaveBeenCalledTimes(2); // original to be killed and new instance
232 });
233
234 it('forces a fresh server if sl version changed', async () => {
235 jest
236 .spyOn(startServer, 'startServer')
237 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
238 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
239
240 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
241 return Promise.resolve(1000);
242 });
243
244 await runProxyMain({...defaultArgs, slVersion: '0.1'});
245 resetStdout();
246
247 await runProxyMain({...defaultArgs, json: true, slVersion: '0.2'});
248
249 expect(JSON.parse(allConsoleStdout())).toEqual(
250 expect.objectContaining({
251 wasServerReused: false,
252 }),
253 );
254 });
255
256 it('forces a fresh server if sl command changed', async () => {
257 jest
258 .spyOn(startServer, 'startServer')
259 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
260 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
261
262 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
263 return Promise.resolve(1000);
264 });
265
266 await runProxyMain({...defaultArgs, command: 'sl'});
267 resetStdout();
268
269 await runProxyMain({...defaultArgs, json: true, command: '/bin/sl'});
270
271 expect(JSON.parse(allConsoleStdout())).toEqual(
272 expect.objectContaining({
273 wasServerReused: false,
274 }),
275 );
276 });
277});
278
279describe('argument parsing', () => {
280 it('can parse arguments', () => {
281 expect(parseArgs(['--port', '3001', '--force'])).toEqual(
282 expect.objectContaining({
283 port: 3001,
284 force: true,
285 }),
286 );
287 });
288});
289
290describe('validateServerChallengeResponse', () => {
291 it('only accepts real responses', () => {
292 expect(lifecycle.validateServerChallengeResponse(null)).toEqual(false);
293 expect(lifecycle.validateServerChallengeResponse(undefined)).toEqual(false);
294 expect(lifecycle.validateServerChallengeResponse(123)).toEqual(false);
295 expect(lifecycle.validateServerChallengeResponse('1')).toEqual(false);
296 expect(lifecycle.validateServerChallengeResponse({})).toEqual(false);
297 expect(lifecycle.validateServerChallengeResponse([])).toEqual(false);
298 expect(lifecycle.validateServerChallengeResponse({challengeToken: '123'})).toEqual(false);
299 expect(lifecycle.validateServerChallengeResponse({pid: 123})).toEqual(false);
300 expect(lifecycle.validateServerChallengeResponse({challengeToken: 123, pid: 123})).toEqual(
301 false,
302 );
303
304 expect(lifecycle.validateServerChallengeResponse({challengeToken: '123', pid: 123})).toEqual(
305 true,
306 );
307 });
308});
309