Spaces:
Runtime error
Runtime error
| import CloudProvider from '../../../src/lib/cloud-provider'; | |
| let websocketConstructorCount = 0; | |
| // Stub the global websocket so we can call open/close/error/send on it | |
| global.WebSocket = function (url) { | |
| this._url = url; | |
| this._sentMessages = []; | |
| // These are not real websocket methods, but used to trigger callbacks | |
| this._open = () => this.onopen(); | |
| this._error = e => this.onerror(e); | |
| this._receive = msg => this.onmessage(msg); | |
| // Stub the real websocket.send to store sent messages | |
| this.send = msg => this._sentMessages.push(msg); | |
| this.close = () => this.onclose(); | |
| websocketConstructorCount++; | |
| }; | |
| global.WebSocket.CLOSING = 'CLOSING'; | |
| global.WebSocket.CLOSED = 'CLOSED'; | |
| describe('CloudProvider', () => { | |
| let cloudProvider = null; | |
| let vmIOData = []; | |
| let timeout = 0; | |
| beforeEach(() => { | |
| vmIOData = []; | |
| cloudProvider = new CloudProvider(); | |
| // Stub vm | |
| cloudProvider.vm = { | |
| postIOData: (_namespace, data) => { | |
| vmIOData.push(data); | |
| } | |
| }; | |
| // Stub setTimeout so this can run instantly. | |
| cloudProvider.setTimeout = (fn, after) => { | |
| timeout = after; | |
| fn(); | |
| }; | |
| // Stub randomize to make it consistent for testing. | |
| cloudProvider.randomizeDuration = t => t; | |
| }); | |
| test('createVariable', () => { | |
| cloudProvider.createVariable('hello', 1); | |
| const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); | |
| expect(obj.method).toEqual('create'); | |
| expect(obj.name).toEqual('hello'); | |
| expect(obj.value).toEqual(1); | |
| }); | |
| test('updateVariable', () => { | |
| cloudProvider.updateVariable('hello', 1); | |
| const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); | |
| expect(obj.method).toEqual('set'); | |
| expect(obj.name).toEqual('hello'); | |
| expect(obj.value).toEqual(1); | |
| }); | |
| test('updateVariable with falsey value', () => { | |
| cloudProvider.updateVariable('hello', 0); | |
| const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); | |
| expect(obj.method).toEqual('set'); | |
| expect(obj.name).toEqual('hello'); | |
| expect(obj.value).toEqual(0); | |
| }); | |
| test('renameVariable', () => { | |
| cloudProvider.renameVariable('oldName', 'newName'); | |
| const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); | |
| expect(obj.method).toEqual('rename'); | |
| expect(obj.name).toEqual('oldName'); | |
| expect(typeof obj.value).toEqual('undefined'); | |
| expect(obj.new_name).toEqual('newName'); | |
| }); | |
| test('deleteVariable', () => { | |
| cloudProvider.deleteVariable('hello'); | |
| const obj = JSON.parse(cloudProvider.connection._sentMessages[0]); | |
| expect(obj.method).toEqual('delete'); | |
| expect(obj.name).toEqual('hello'); | |
| expect(typeof obj.value).toEqual('undefined'); | |
| }); | |
| test('onMessage set', () => { | |
| const msg = JSON.stringify({ | |
| method: 'set', | |
| name: 'name', | |
| value: 'value' | |
| }); | |
| cloudProvider.connection._receive({data: msg}); | |
| expect(vmIOData[0].varUpdate.name).toEqual('name'); | |
| expect(vmIOData[0].varUpdate.value).toEqual('value'); | |
| }); | |
| test('onMessage with newline at the end', () => { | |
| const msg1 = JSON.stringify({ | |
| method: 'set', | |
| name: 'name1', | |
| value: 'value' | |
| }); | |
| cloudProvider.onMessage({data: `${msg1}\n`}); | |
| expect(vmIOData[0].varUpdate.name).toEqual('name1'); | |
| }); | |
| test('onMessage with multiple commands', () => { | |
| const msg1 = JSON.stringify({ | |
| method: 'set', | |
| name: 'name1', | |
| value: 'value' | |
| }); | |
| const msg2 = JSON.stringify({ | |
| method: 'set', | |
| name: 'name2', | |
| value: 'value2' | |
| }); | |
| cloudProvider.connection._receive({data: `${msg1}\n${msg2}`}); | |
| expect(vmIOData[0].varUpdate.name).toEqual('name1'); | |
| expect(vmIOData[1].varUpdate.name).toEqual('name2'); | |
| }); | |
| test('connnection attempts set back to 1 when socket is opened', () => { | |
| cloudProvider.connectionAttempts = 100; | |
| cloudProvider.connection._open(); | |
| expect(cloudProvider.connectionAttempts).toBe(1); | |
| }); | |
| test('disconnect waits for a period equal to 2^k-1 before trying again', () => { | |
| websocketConstructorCount = 1; // This is global, so set it back to 1 to start | |
| // Constructor attempts to open connection, so attempts is initially 1 | |
| expect(cloudProvider.connectionAttempts).toBe(1); | |
| // Make sure a close without a previous OPEN still waits 1s before reconnecting | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(1 * 1000); // 2^1 - 1 | |
| expect(websocketConstructorCount).toBe(2); | |
| expect(cloudProvider.connectionAttempts).toBe(2); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(3 * 1000); // 2^2 - 1 | |
| expect(websocketConstructorCount).toBe(3); | |
| expect(cloudProvider.connectionAttempts).toBe(3); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(7 * 1000); // 2^3 - 1 | |
| expect(websocketConstructorCount).toBe(4); | |
| expect(cloudProvider.connectionAttempts).toBe(4); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(15 * 1000); // 2^4 - 1 | |
| expect(websocketConstructorCount).toBe(5); | |
| expect(cloudProvider.connectionAttempts).toBe(5); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(31 * 1000); // 2^5 - 1 | |
| expect(websocketConstructorCount).toBe(6); | |
| expect(cloudProvider.connectionAttempts).toBe(6); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(31 * 1000); // maxed out at 2^5 - 1 | |
| expect(websocketConstructorCount).toBe(7); | |
| expect(cloudProvider.connectionAttempts).toBe(7); | |
| }); | |
| test('close after connection is opened waits 1s before reconnecting', () => { | |
| // This test is basically to check that opening the connection does not impact | |
| // the time until reconnection for the first reconnect. | |
| // It is easy to introduce a bug that causes reconnection time to be different | |
| // based on whether an initial connection was made. | |
| websocketConstructorCount = 1; | |
| cloudProvider.connection._open(); | |
| cloudProvider.connection.close(); | |
| expect(timeout).toEqual(1 * 1000); // 2^1 - 1 | |
| expect(websocketConstructorCount).toBe(2); | |
| expect(cloudProvider.connectionAttempts).toBe(2); | |
| }); | |
| test('exponentialTimeout caps connection attempt number', () => { | |
| cloudProvider.connectionAttempts = 1000; | |
| expect(cloudProvider.exponentialTimeout()).toEqual(31 * 1000); | |
| }); | |
| test('requestCloseConnection does not try to reconnect', () => { | |
| websocketConstructorCount = 1; // This is global, so set it back to 1 to start | |
| cloudProvider.requestCloseConnection(); | |
| expect(websocketConstructorCount).toBe(1); // No reconnection attempts | |
| }); | |
| test('close with code 4002 triggers invalid username', () => { | |
| cloudProvider.onInvalidUsername = jest.fn(); | |
| cloudProvider.onClose({code: 4002}); | |
| expect(cloudProvider.onInvalidUsername).toHaveBeenCalledTimes(1); | |
| }); | |
| test('close with normal code does not trigger invalid username', () => { | |
| cloudProvider.username = 'aaa'; | |
| cloudProvider.onInvalidUsername = jest.fn(); | |
| cloudProvider.onClose({code: 1000}); | |
| expect(cloudProvider.onInvalidUsername).not.toHaveBeenCalled(); | |
| }); | |
| }); | |
| test('username anonymization', () => { | |
| const anonymized = new CloudProvider('', null, 'player1234', ''); | |
| expect(anonymized.username).toBe('player'); | |
| const verbatim = new CloudProvider('', null, 'abcdef', ''); | |
| expect(verbatim.username).toBe('abcdef'); | |
| }); | |