Created
December 30, 2017 15:28
-
-
Save Darkhogg/0cda635462eaa3de07c1da6ffcb3be19 to your computer and use it in GitHub Desktop.
Factorio RCON tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const net = require('net'); | |
function connect () { | |
return new Promise((accept, reject) => { | |
const socket = net.createConnection(27015, '127.0.0.1'); | |
socket.on('connect', () => accept(socket)); | |
socket.on('error', (err) => reject(err)); | |
}); | |
} | |
function disconnect (socket) { | |
return new Promise((accept, reject) => { | |
socket.end(); | |
socket.on('end', () => accept()); | |
socket.on('error', () => reject()); | |
}) | |
} | |
function buildMessage (id, type, data) { | |
const length = data.length; | |
const buf = Buffer.alloc(length + 14) | |
buf.writeInt32LE(length + 10, 0); | |
buf.writeInt32LE(id, 4); | |
buf.writeInt32LE(type, 8); | |
buf.write(data, 12, 'utf-8'); | |
buf.writeInt16LE(0, length + 12); | |
return buf; | |
} | |
function buildAuth (id, password) { | |
return buildMessage(id, 3, password); | |
} | |
function buildCommand (id, command) { | |
return buildMessage(id, 2, command); | |
} | |
function collectResponses (socket, timeout=1000) { | |
return new Promise((accept, reject) => { | |
socket.on('end', () => accept(collector)); | |
socket.on('close', () => reject(new Error('socket closed'))); | |
socket.on('error', (err) => reject(err)); | |
setTimeout(() => {socket.destroy(); reject(new Error('timeout'))}, timeout) | |
const collector = []; | |
let oldData = null; | |
const onData = (data) => { | |
const allData = oldData ? Buffer.concat(oldData, data) : data; | |
const dataLength = (allData.length < 4) ? Infinity : allData.readInt32LE(0); | |
if (dataLength + 4 > allData.length) { | |
oldData = allData; | |
return; | |
} | |
oldData = null; | |
const dataId = allData.readInt32LE(4); | |
const dataType = allData.readInt32LE(8); | |
const dataStr = allData.toString('utf8', 12, 12 + dataLength - 10); | |
collector.push({'id': dataId, 'type': dataType, 'data': dataStr}); | |
if (allData.length > dataLength + 4) { | |
onData(allData.slice(dataLength + 4)); | |
} | |
}; | |
socket.on('data', onData); | |
}); | |
} | |
function sendThenExpect (packetGroups, timeout = 100, expected = null) { | |
return connect().then((socket) => { | |
let messagesSent = 0; | |
const collectProm = collectResponses(socket, timeout + 50); | |
let sendProm = Promise.resolve(); | |
for (const group of packetGroups) { | |
messagesSent += group.length; | |
sendProm = sendProm.then(() => { | |
socket.write(Buffer.concat(group)); | |
return new Promise((accept, reject) => setTimeout(accept, 1)); | |
}); | |
} | |
const expectedResponses = expected || messagesSent | |
setTimeout(() => disconnect(socket), timeout); | |
return sendProm.then(() => collectProm).then(result => { | |
return { | |
'expected': expectedResponses, | |
'responded': result.length, | |
'passes': expectedResponses == result.length | |
}; | |
}).catch(err => ({ | |
'expected': expectedResponses, | |
'responded': 'E', | |
'passes': false | |
})); | |
}); | |
} | |
function makeTest (text, packets, expected = null) { | |
return function () { | |
return sendThenExpect(packets, 300, expected) | |
.then(result => { | |
console.log(' [%s] (%s/%s) %s', result.passes ? ' OK ' : 'FAIL', result.responded, result.expected, text); | |
return result.passes; | |
}); | |
}; | |
} | |
const TESTS = [ | |
makeTest('should authenticate', [ | |
[buildAuth(0, 'rcon')], | |
]), | |
makeTest('should respond to single command', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed')], | |
]), | |
makeTest('should respond to multiple separate commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed')], | |
[buildCommand(2, '/seed')], | |
]), | |
makeTest('should respond to multiple combined commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed'), buildCommand(2, '/seed')], | |
]), | |
makeTest('should respond to commands after grouped commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed'), buildCommand(2, '/seed')], | |
[buildCommand(3, '/seed')], | |
]), | |
makeTest('should respond to split commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed').slice(0, 10)], | |
[buildCommand(1, '/seed').slice(10)], | |
], 2), | |
makeTest('should respond to commands after split commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed').slice(0, 10)], | |
[buildCommand(1, '/seed').slice(10)], | |
[buildCommand(2, '/seed')], | |
], 3), | |
makeTest('should respond to commands grouped before split commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed'), buildCommand(2, '/seed').slice(0, 10)], | |
[buildCommand(2, '/seed').slice(10)], | |
], 3), | |
makeTest('should respond to commands grouped after split commands', [ | |
[buildAuth(0, 'rcon')], | |
[buildCommand(1, '/seed').slice(0, 10)], | |
[buildCommand(1, '/seed').slice(10), buildCommand(2, '/seed')], | |
], 3), | |
]; | |
function main () { | |
let lastPromise = Promise.resolve(); | |
for (const test of TESTS) { | |
lastPromise = lastPromise.then(() => test()); | |
} | |
return lastPromise; | |
} | |
main().catch((err) => console.error(err.stack || err)); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment