addons/isl-server/proxy/__tests__/run-proxy.test.tsblame
View source
b69ab311/**
b69ab312 * Copyright (c) Meta Platforms, Inc. and affiliates.
b69ab313 *
b69ab314 * This source code is licensed under the MIT license found in the
b69ab315 * LICENSE file in the root directory of this source tree.
b69ab316 */
b69ab317
b69ab318import * as util from 'node:util';
b69ab319import {readExistingServerFile} from '../existingServerStateFiles';
b69ab3110import * as startServer from '../server';
b69ab3111import * as lifecycle from '../serverLifecycle';
b69ab3112import {parseArgs, runProxyMain} from '../startServer';
b69ab3113
b69ab3114/* eslint-disable require-await */
b69ab3115
b69ab3116// to prevent permission issues and races, mock FS read/writes in memory.
b69ab3117let mockFsData: {[key: string]: string} = {};
b69ab3118jest.mock('node:fs', () => {
b69ab3119 return {
b69ab3120 promises: {
b69ab3121 writeFile: jest.fn(async (path: string, data: string) => {
b69ab3122 mockFsData[path] = data;
b69ab3123 }),
b69ab3124 readFile: jest.fn(async (path: string) => {
b69ab3125 return mockFsData[path];
b69ab3126 }),
b69ab3127 stat: jest.fn(async (_path: string) => {
b69ab3128 return {mode: 0o700, isSymbolicLink: () => false};
b69ab3129 }),
b69ab3130 rm: jest.fn(async (path: string) => {
b69ab3131 delete mockFsData[path];
b69ab3132 }),
b69ab3133 mkdir: jest.fn(async (_path: string) => {
b69ab3134 //
b69ab3135 }),
b69ab3136 mkdtemp: jest.fn(async (_path: string) => {
b69ab3137 return '/tmp/';
b69ab3138 }),
b69ab3139 },
b69ab3140 };
b69ab3141});
b69ab3142
b69ab3143describe('run-proxy', () => {
b69ab3144 let stdout: Array<string> = [];
b69ab3145 let stderr: Array<string> = [];
b69ab3146 function allConsoleStdout() {
b69ab3147 return stdout.join('\n');
b69ab3148 }
b69ab3149 function resetStdout() {
b69ab3150 stdout = [];
b69ab3151 stderr = [];
b69ab3152 }
b69ab3153 beforeEach(() => {
b69ab3154 resetStdout();
b69ab3155 const appendStdout = jest.fn((...args) => stdout.push(util.format(...args)));
b69ab3156 const appendStderr = jest.fn((...args) => stderr.push(util.format(...args)));
b69ab3157
b69ab3158 global.console = {
b69ab3159 log: appendStdout,
b69ab3160 info: appendStdout,
b69ab3161 warn: appendStdout,
b69ab3162 error: appendStderr,
b69ab3163 } as unknown as Console;
b69ab3164
b69ab3165 // reset mock filesystem
b69ab3166 mockFsData = {};
b69ab3167
b69ab3168 jest.clearAllMocks();
b69ab3169 });
b69ab3170
b69ab3171 const killMock = jest.spyOn(process, 'kill').mockImplementation(() => true);
b69ab3172 const exitMock = jest.spyOn(process, 'exit').mockImplementation((): never => {
b69ab3173 throw new Error('exited');
b69ab3174 });
b69ab3175
b69ab3176 const defaultArgs = {
b69ab3177 help: false,
b69ab3178 // subprocess spawning without --foreground doesn't work well in tests
b69ab3179 // plus we don't want to manage closing servers after the tests
b69ab3180 foreground: true,
b69ab3181 // we don't want to actually open the url in the browser during a test
b69ab3182 openUrl: false,
b69ab3183 port: 3011,
b69ab3184 isDevMode: false,
b69ab3185 json: false,
b69ab3186 stdout: false,
b69ab3187 platform: undefined,
b69ab3188 kill: false,
b69ab3189 force: false,
b69ab3190 slVersion: '1.0',
b69ab3191 command: 'sl',
b69ab3192 cwd: undefined,
b69ab3193 sessionId: undefined,
b69ab3194 };
b69ab3195
b69ab3196 it('spawns a server', async () => {
b69ab3197 const startServerSpy = jest
b69ab3198 .spyOn(startServer, 'startServer')
b69ab3199 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31100
b69ab31101 await runProxyMain(defaultArgs);
b69ab31102
b69ab31103 expect(startServerSpy).toHaveBeenCalledTimes(1);
b69ab31104 });
b69ab31105
b69ab31106 it('can output json', async () => {
b69ab31107 jest
b69ab31108 .spyOn(startServer, 'startServer')
b69ab31109 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31110
b69ab31111 await runProxyMain({...defaultArgs, json: true});
b69ab31112
b69ab31113 expect(JSON.parse(allConsoleStdout())).toEqual(
b69ab31114 expect.objectContaining({
b69ab31115 command: 'sl',
b69ab31116 cwd: expect.stringContaining('isl-server'),
b69ab31117 logFileLocation: expect.stringContaining('isl-server.log'),
b69ab31118 pid: 1000,
b69ab31119 port: 3011,
b69ab31120 token: expect.stringMatching(/[a-z0-9]{32}/),
b69ab31121 url: expect.stringContaining('http://localhost:3011/'),
b69ab31122 wasServerReused: false,
b69ab31123 }),
b69ab31124 );
b69ab31125 });
b69ab31126
b69ab31127 it('can set current working directory manually', async () => {
b69ab31128 jest
b69ab31129 .spyOn(startServer, 'startServer')
b69ab31130 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31131
b69ab31132 await runProxyMain({...defaultArgs, json: true, cwd: 'foobar'});
b69ab31133
b69ab31134 expect(JSON.parse(allConsoleStdout())).toEqual(
b69ab31135 expect.objectContaining({
b69ab31136 cwd: 'foobar',
b69ab31137 }),
b69ab31138 );
b69ab31139 });
b69ab31140
b69ab31141 it('writes existing server info', async () => {
b69ab31142 jest
b69ab31143 .spyOn(startServer, 'startServer')
b69ab31144 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31145
b69ab31146 await expect(readExistingServerFile(3011)).rejects.toEqual(expect.anything());
b69ab31147
b69ab31148 await runProxyMain(defaultArgs);
b69ab31149
b69ab31150 expect(await readExistingServerFile(3011)).toEqual(
b69ab31151 expect.objectContaining({
b69ab31152 sensitiveToken: expect.anything(),
b69ab31153 challengeToken: expect.anything(),
b69ab31154 command: 'sl',
b69ab31155 slVersion: '1.0',
b69ab31156 }),
b69ab31157 );
b69ab31158 });
b69ab31159
b69ab31160 it('can output json for a reused server', async () => {
b69ab31161 jest
b69ab31162 .spyOn(startServer, 'startServer')
b69ab31163 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
b69ab31164 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
b69ab31165
b69ab31166 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
b69ab31167 return Promise.resolve(1000);
b69ab31168 });
b69ab31169
b69ab31170 await runProxyMain(defaultArgs);
b69ab31171 resetStdout();
b69ab31172
b69ab31173 await expect(() => runProxyMain({...defaultArgs, json: true})).rejects.toEqual(
b69ab31174 new Error('exited'),
b69ab31175 );
b69ab31176
b69ab31177 expect(JSON.parse(allConsoleStdout())).toEqual(
b69ab31178 expect.objectContaining({
b69ab31179 command: 'sl',
b69ab31180 cwd: expect.stringContaining('isl-server'),
b69ab31181 logFileLocation: expect.stringContaining('isl-server.log'),
b69ab31182 pid: 1000,
b69ab31183 port: 3011,
b69ab31184 token: expect.stringMatching(/[a-z0-9]{32}/),
b69ab31185 url: expect.stringContaining('http://localhost:3011/'),
b69ab31186 wasServerReused: true,
b69ab31187 }),
b69ab31188 );
b69ab31189 expect(exitMock).toHaveBeenCalledWith(0);
b69ab31190 });
b69ab31191
b69ab31192 it('can kill a server', async () => {
b69ab31193 const startServerSpy = jest
b69ab31194 .spyOn(startServer, 'startServer')
b69ab31195 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31196
b69ab31197 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
b69ab31198 return Promise.resolve(1000);
b69ab31199 });
b69ab31200
b69ab31201 // successfully start normally
b69ab31202 await runProxyMain(defaultArgs);
b69ab31203
b69ab31204 // now run with --kill
b69ab31205 await expect(() => runProxyMain({...defaultArgs, kill: true})).rejects.toEqual(
b69ab31206 new Error('exited'),
b69ab31207 );
b69ab31208
b69ab31209 expect(killMock).toHaveBeenCalled();
b69ab31210 expect(exitMock).toHaveBeenCalledWith(0); // exits after killing
b69ab31211 expect(startServerSpy).toHaveBeenCalledTimes(1); // called for original server only
b69ab31212 });
b69ab31213
b69ab31214 it('--force kills and starts a new server', async () => {
b69ab31215 const startServerSpy = jest
b69ab31216 .spyOn(startServer, 'startServer')
b69ab31217 .mockImplementation(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}));
b69ab31218
b69ab31219 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
b69ab31220 return Promise.resolve(1000);
b69ab31221 });
b69ab31222
b69ab31223 // successfully start normally
b69ab31224 await runProxyMain(defaultArgs);
b69ab31225
b69ab31226 // now run with --force
b69ab31227 await runProxyMain({...defaultArgs, force: true});
b69ab31228
b69ab31229 expect(killMock).toHaveBeenCalled();
b69ab31230 expect(exitMock).not.toHaveBeenCalled();
b69ab31231 expect(startServerSpy).toHaveBeenCalledTimes(2); // original to be killed and new instance
b69ab31232 });
b69ab31233
b69ab31234 it('forces a fresh server if sl version changed', async () => {
b69ab31235 jest
b69ab31236 .spyOn(startServer, 'startServer')
b69ab31237 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
b69ab31238 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
b69ab31239
b69ab31240 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
b69ab31241 return Promise.resolve(1000);
b69ab31242 });
b69ab31243
b69ab31244 await runProxyMain({...defaultArgs, slVersion: '0.1'});
b69ab31245 resetStdout();
b69ab31246
b69ab31247 await runProxyMain({...defaultArgs, json: true, slVersion: '0.2'});
b69ab31248
b69ab31249 expect(JSON.parse(allConsoleStdout())).toEqual(
b69ab31250 expect.objectContaining({
b69ab31251 wasServerReused: false,
b69ab31252 }),
b69ab31253 );
b69ab31254 });
b69ab31255
b69ab31256 it('forces a fresh server if sl command changed', async () => {
b69ab31257 jest
b69ab31258 .spyOn(startServer, 'startServer')
b69ab31259 .mockImplementationOnce(() => Promise.resolve({type: 'success', port: 3011, pid: 1000}))
b69ab31260 .mockImplementationOnce(() => Promise.resolve({type: 'addressInUse'}));
b69ab31261
b69ab31262 jest.spyOn(lifecycle, 'checkIfServerIsAliveAndIsISL').mockImplementation(() => {
b69ab31263 return Promise.resolve(1000);
b69ab31264 });
b69ab31265
b69ab31266 await runProxyMain({...defaultArgs, command: 'sl'});
b69ab31267 resetStdout();
b69ab31268
b69ab31269 await runProxyMain({...defaultArgs, json: true, command: '/bin/sl'});
b69ab31270
b69ab31271 expect(JSON.parse(allConsoleStdout())).toEqual(
b69ab31272 expect.objectContaining({
b69ab31273 wasServerReused: false,
b69ab31274 }),
b69ab31275 );
b69ab31276 });
b69ab31277});
b69ab31278
b69ab31279describe('argument parsing', () => {
b69ab31280 it('can parse arguments', () => {
b69ab31281 expect(parseArgs(['--port', '3001', '--force'])).toEqual(
b69ab31282 expect.objectContaining({
b69ab31283 port: 3001,
b69ab31284 force: true,
b69ab31285 }),
b69ab31286 );
b69ab31287 });
b69ab31288});
b69ab31289
b69ab31290describe('validateServerChallengeResponse', () => {
b69ab31291 it('only accepts real responses', () => {
b69ab31292 expect(lifecycle.validateServerChallengeResponse(null)).toEqual(false);
b69ab31293 expect(lifecycle.validateServerChallengeResponse(undefined)).toEqual(false);
b69ab31294 expect(lifecycle.validateServerChallengeResponse(123)).toEqual(false);
b69ab31295 expect(lifecycle.validateServerChallengeResponse('1')).toEqual(false);
b69ab31296 expect(lifecycle.validateServerChallengeResponse({})).toEqual(false);
b69ab31297 expect(lifecycle.validateServerChallengeResponse([])).toEqual(false);
b69ab31298 expect(lifecycle.validateServerChallengeResponse({challengeToken: '123'})).toEqual(false);
b69ab31299 expect(lifecycle.validateServerChallengeResponse({pid: 123})).toEqual(false);
b69ab31300 expect(lifecycle.validateServerChallengeResponse({challengeToken: 123, pid: 123})).toEqual(
b69ab31301 false,
b69ab31302 );
b69ab31303
b69ab31304 expect(lifecycle.validateServerChallengeResponse({challengeToken: '123', pid: 123})).toEqual(
b69ab31305 true,
b69ab31306 );
b69ab31307 });
b69ab31308});