From eed84abd19cd1af66da45c6a6860e1be98e5b987 Mon Sep 17 00:00:00 2001 From: Marek Rusinowski Date: Sat, 11 Jan 2025 19:19:28 +0100 Subject: [PATCH] Put unit tests in suites for test report clarity --- src/engineAutohostInterface.test.ts | 498 ++++++++++----------- src/engineRunner.test.ts | 202 ++++----- src/oauth2Client.test.ts | 160 +++---- src/startScriptGen.test.ts | 666 ++++++++++++++-------------- src/tachyonClient.test.ts | 125 +++--- src/tachyonServer.fake.ts | 43 ++ src/tachyonTypes.test.ts | 184 ++++---- 7 files changed, 967 insertions(+), 911 deletions(-) diff --git a/src/engineAutohostInterface.test.ts b/src/engineAutohostInterface.test.ts index d3663e8..e3e3be2 100644 --- a/src/engineAutohostInterface.test.ts +++ b/src/engineAutohostInterface.test.ts @@ -1,4 +1,4 @@ -import test from 'node:test'; +import test, { suite } from 'node:test'; import assert from 'node:assert/strict'; import { @@ -15,285 +15,287 @@ import { serializeCommandPacket, } from './engineAutohostInterface.js'; -test('parse SERVER_STARTED', () => { - const event = parsePacket(Buffer.from('00', 'hex')); - assert.equal(event.type, EventType.SERVER_STARTED); +suite('engineAutohostInterface parsing', () => { + test('parse SERVER_STARTED', () => { + const event = parsePacket(Buffer.from('00', 'hex')); + assert.equal(event.type, EventType.SERVER_STARTED); - assert.throws(() => { - parsePacket(Buffer.from('0000', 'hex')); - }, PacketParseError); -}); + assert.throws(() => { + parsePacket(Buffer.from('0000', 'hex')); + }, PacketParseError); + }); -test('parse SERVER_QUIT', () => { - const event = parsePacket(Buffer.from('01', 'hex')); - assert.equal(event.type, EventType.SERVER_QUIT); + test('parse SERVER_QUIT', () => { + const event = parsePacket(Buffer.from('01', 'hex')); + assert.equal(event.type, EventType.SERVER_QUIT); - assert.throws(() => { - parsePacket(Buffer.from('0100', 'hex')); - }, PacketParseError); -}); - -test('parse SERVER_STARTPLAYING', () => { - const event = parsePacket( - Buffer.from( - '02a40000002e9836666a18a55fbcc6228ee62217492f686f6d652f73706164732f73706164732f7661722f436c75737465724d616e616765722f5b7465685d636c75737465725553345b31305d2f64656d6f732d7365727665722f323032342d30352d30345f32302d31382d35342d3236305f48656c6c617320426173696e2076315f3130352e312e312d323434392d6766313233346139204241523130352e7364667a', - 'hex', - ), - ); - assert.deepEqual(event, { - type: EventType.SERVER_STARTPLAYING, - gameId: '2e9836666a18a55fbcc6228ee6221749', - demoPath: - '/home/spads/spads/var/ClusterManager/[teh]clusterUS4[10]/demos-server/2024-05-04_20-18-54-260_Hellas Basin v1_105.1.1-2449-gf1234a9 BAR105.sdfz', + assert.throws(() => { + parsePacket(Buffer.from('0100', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('0200', 'hex')); - }, PacketParseError); - - assert.throws(() => { - parsePacket( + test('parse SERVER_STARTPLAYING', () => { + const event = parsePacket( Buffer.from( - '02a40000002e9836666a18a55fbcc6228ee62217492f686f6d652f73706164732f73706164732f7661722f436c75737465724d616e616765722f5b7465685d636c75737465725553345b3130', + '02a40000002e9836666a18a55fbcc6228ee62217492f686f6d652f73706164732f73706164732f7661722f436c75737465724d616e616765722f5b7465685d636c75737465725553345b31305d2f64656d6f732d7365727665722f323032342d30352d30345f32302d31382d35342d3236305f48656c6c617320426173696e2076315f3130352e312e312d323434392d6766313233346139204241523130352e7364667a', 'hex', ), ); - }, PacketParseError); -}); - -test('parse SERVER_GAMEOVER', () => { - const event = parsePacket(Buffer.from('03040601', 'hex')); - assert.deepEqual(event, { - type: EventType.SERVER_GAMEOVER, - player: 6, - winningAllyTeams: [1], + assert.deepEqual(event, { + type: EventType.SERVER_STARTPLAYING, + gameId: '2e9836666a18a55fbcc6228ee6221749', + demoPath: + '/home/spads/spads/var/ClusterManager/[teh]clusterUS4[10]/demos-server/2024-05-04_20-18-54-260_Hellas Basin v1_105.1.1-2449-gf1234a9 BAR105.sdfz', + }); + + assert.throws(() => { + parsePacket(Buffer.from('0200', 'hex')); + }, PacketParseError); + + assert.throws(() => { + parsePacket( + Buffer.from( + '02a40000002e9836666a18a55fbcc6228ee62217492f686f6d652f73706164732f73706164732f7661722f436c75737465724d616e616765722f5b7465685d636c75737465725553345b3130', + 'hex', + ), + ); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('030406', 'hex')); - }, PacketParseError); -}); - -test('parse SERVER_MESSAGE', () => { - const event = parsePacket( - Buffer.from( - '04436f6e6e656374696e6720746f206175746f686f7374206f6e20706f7274203533313232', - 'hex', - ), - ); - assert.deepEqual(event, { - type: EventType.SERVER_MESSAGE, - message: 'Connecting to autohost on port 53122', + test('parse SERVER_GAMEOVER', () => { + const event = parsePacket(Buffer.from('03040601', 'hex')); + assert.deepEqual(event, { + type: EventType.SERVER_GAMEOVER, + player: 6, + winningAllyTeams: [1], + }); + + assert.throws(() => { + parsePacket(Buffer.from('030406', 'hex')); + }, PacketParseError); }); -}); -test('parse SERVER_WARNING', () => { - const event = parsePacket(Buffer.from('054f6e6c696e65207761726e696e67206c6f6c', 'hex')); - assert.deepEqual(event, { - type: EventType.SERVER_WARNING, - message: 'Online warning lol', + test('parse SERVER_MESSAGE', () => { + const event = parsePacket( + Buffer.from( + '04436f6e6e656374696e6720746f206175746f686f7374206f6e20706f7274203533313232', + 'hex', + ), + ); + assert.deepEqual(event, { + type: EventType.SERVER_MESSAGE, + message: 'Connecting to autohost on port 53122', + }); }); -}); -test('parse PLAYER_JOINED', () => { - const event = parsePacket(Buffer.from('0a0b417865', 'hex')); - assert.deepEqual(event, { - type: EventType.PLAYER_JOINED, - player: 11, - name: 'Axe', + test('parse SERVER_WARNING', () => { + const event = parsePacket(Buffer.from('054f6e6c696e65207761726e696e67206c6f6c', 'hex')); + assert.deepEqual(event, { + type: EventType.SERVER_WARNING, + message: 'Online warning lol', + }); }); - assert.throws(() => { - parsePacket(Buffer.from('0a0b', 'hex')); - }, PacketParseError); -}); -test('parse PLAYER_LEFT', () => { - assert.deepEqual(parsePacket(Buffer.from('0b1201', 'hex')), { - type: EventType.PLAYER_LEFT, - player: 18, - reason: LeaveReason.LEFT, - }); - assert.deepEqual(parsePacket(Buffer.from('0b0400', 'hex')), { - type: EventType.PLAYER_LEFT, - player: 4, - reason: LeaveReason.LOST_CONNECTION, - }); - assert.deepEqual(parsePacket(Buffer.from('0b1202', 'hex')), { - type: EventType.PLAYER_LEFT, - player: 18, - reason: LeaveReason.KICKED, + test('parse PLAYER_JOINED', () => { + const event = parsePacket(Buffer.from('0a0b417865', 'hex')); + assert.deepEqual(event, { + type: EventType.PLAYER_JOINED, + player: 11, + name: 'Axe', + }); + assert.throws(() => { + parsePacket(Buffer.from('0a0b', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('0b12', 'hex')); - }, PacketParseError); - assert.throws(() => { - parsePacket(Buffer.from('0b1203', 'hex')); - }, PacketParseError); -}); -test('parse PLAYER_READY', () => { - assert.deepEqual(parsePacket(Buffer.from('0c0200', 'hex')), { - type: EventType.PLAYER_READY, - player: 2, - state: ReadyState.NOT_READY, - }); - assert.deepEqual(parsePacket(Buffer.from('0c0d01', 'hex')), { - type: EventType.PLAYER_READY, - player: 13, - state: ReadyState.READY, + test('parse PLAYER_LEFT', () => { + assert.deepEqual(parsePacket(Buffer.from('0b1201', 'hex')), { + type: EventType.PLAYER_LEFT, + player: 18, + reason: LeaveReason.LEFT, + }); + assert.deepEqual(parsePacket(Buffer.from('0b0400', 'hex')), { + type: EventType.PLAYER_LEFT, + player: 4, + reason: LeaveReason.LOST_CONNECTION, + }); + assert.deepEqual(parsePacket(Buffer.from('0b1202', 'hex')), { + type: EventType.PLAYER_LEFT, + player: 18, + reason: LeaveReason.KICKED, + }); + assert.throws(() => { + parsePacket(Buffer.from('0b12', 'hex')); + }, PacketParseError); + assert.throws(() => { + parsePacket(Buffer.from('0b1203', 'hex')); + }, PacketParseError); }); - assert.deepEqual(parsePacket(Buffer.from('0c0d02', 'hex')), { - type: EventType.PLAYER_READY, - player: 13, - state: ReadyState.FORCED, - }); - assert.throws(() => { - parsePacket(Buffer.from('0c0d', 'hex')); - }, PacketParseError); - assert.throws(() => { - parsePacket(Buffer.from('0b1204', 'hex')); - }, PacketParseError); -}); -test('parse PLAYER_CHAT', () => { - assert.deepEqual(parsePacket(Buffer.from('0d08fc6e696365', 'hex')), { - type: EventType.PLAYER_CHAT, - fromPlayer: 8, - destination: ChatDestination.TO_ALLIES, - message: 'nice', - }); - assert.deepEqual(parsePacket(Buffer.from('0d0bfe72657369676e', 'hex')), { - type: EventType.PLAYER_CHAT, - fromPlayer: 11, - destination: ChatDestination.TO_EVERYONE, - message: 'resign', - }); - assert.deepEqual(parsePacket(Buffer.from('0d11fd6c6f6c', 'hex')), { - type: EventType.PLAYER_CHAT, - fromPlayer: 17, - destination: ChatDestination.TO_SPECTATORS, - message: 'lol', - }); - assert.deepEqual(parsePacket(Buffer.from('0d11016c6f6c', 'hex')), { - type: EventType.PLAYER_CHAT, - fromPlayer: 17, - toPlayer: 1, - destination: ChatDestination.TO_PLAYER, - message: 'lol', + test('parse PLAYER_READY', () => { + assert.deepEqual(parsePacket(Buffer.from('0c0200', 'hex')), { + type: EventType.PLAYER_READY, + player: 2, + state: ReadyState.NOT_READY, + }); + assert.deepEqual(parsePacket(Buffer.from('0c0d01', 'hex')), { + type: EventType.PLAYER_READY, + player: 13, + state: ReadyState.READY, + }); + assert.deepEqual(parsePacket(Buffer.from('0c0d02', 'hex')), { + type: EventType.PLAYER_READY, + player: 13, + state: ReadyState.FORCED, + }); + assert.throws(() => { + parsePacket(Buffer.from('0c0d', 'hex')); + }, PacketParseError); + assert.throws(() => { + parsePacket(Buffer.from('0b1204', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('0d11', 'hex')); - }, PacketParseError); -}); -test('parse PLAYER_DEFEATED', () => { - assert.deepEqual(parsePacket(Buffer.from('0e0b', 'hex')), { - type: EventType.PLAYER_DEFEATED, - player: 11, + test('parse PLAYER_CHAT', () => { + assert.deepEqual(parsePacket(Buffer.from('0d08fc6e696365', 'hex')), { + type: EventType.PLAYER_CHAT, + fromPlayer: 8, + destination: ChatDestination.TO_ALLIES, + message: 'nice', + }); + assert.deepEqual(parsePacket(Buffer.from('0d0bfe72657369676e', 'hex')), { + type: EventType.PLAYER_CHAT, + fromPlayer: 11, + destination: ChatDestination.TO_EVERYONE, + message: 'resign', + }); + assert.deepEqual(parsePacket(Buffer.from('0d11fd6c6f6c', 'hex')), { + type: EventType.PLAYER_CHAT, + fromPlayer: 17, + destination: ChatDestination.TO_SPECTATORS, + message: 'lol', + }); + assert.deepEqual(parsePacket(Buffer.from('0d11016c6f6c', 'hex')), { + type: EventType.PLAYER_CHAT, + fromPlayer: 17, + toPlayer: 1, + destination: ChatDestination.TO_PLAYER, + message: 'lol', + }); + assert.throws(() => { + parsePacket(Buffer.from('0d11', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('0e', 'hex')); - }, PacketParseError); -}); -test('parse GAME_LUAMSG', () => { - const event1 = parsePacket(Buffer.from('14320c000a640000407a683630', 'hex')); - assert.deepEqual(event1, { - type: EventType.GAME_LUAMSG, - script: LuaMsgScript.RULES, - player: 10, - data: Buffer.from('407a683630', 'hex'), + test('parse PLAYER_DEFEATED', () => { + assert.deepEqual(parsePacket(Buffer.from('0e0b', 'hex')), { + type: EventType.PLAYER_DEFEATED, + player: 11, + }); + assert.throws(() => { + parsePacket(Buffer.from('0e', 'hex')); + }, PacketParseError); }); - const event2 = parsePacket( - Buffer.from('1432180000d0070044726166744f726465725f52616e646f6d', 'hex'), - ); - assert.deepEqual(event2, { - type: EventType.GAME_LUAMSG, - script: LuaMsgScript.UI, - uiMode: LuaMsgUIMode.ALL, - player: 0, - data: Buffer.from('DraftOrder_Random'), + test('parse GAME_LUAMSG', () => { + const event1 = parsePacket(Buffer.from('14320c000a640000407a683630', 'hex')); + assert.deepEqual(event1, { + type: EventType.GAME_LUAMSG, + script: LuaMsgScript.RULES, + player: 10, + data: Buffer.from('407a683630', 'hex'), + }); + + const event2 = parsePacket( + Buffer.from('1432180000d0070044726166744f726465725f52616e646f6d', 'hex'), + ); + assert.deepEqual(event2, { + type: EventType.GAME_LUAMSG, + script: LuaMsgScript.UI, + uiMode: LuaMsgUIMode.ALL, + player: 0, + data: Buffer.from('DraftOrder_Random'), + }); + + assert.throws(() => { + parsePacket(Buffer.from('143200', 'hex')); + }, PacketParseError); + + assert.throws(() => { + parsePacket(Buffer.from('14330c000a640000407a683630', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('143200', 'hex')); - }, PacketParseError); - - assert.throws(() => { - parsePacket(Buffer.from('14330c000a640000407a683630', 'hex')); - }, PacketParseError); -}); - -test('parse GAME_TEAMSTAT', () => { - const event = parsePacket( - Buffer.from( - '3c0700e10000ade0ad470abc944a8c38d747b4138f4a000000004a052144a2208244d308ca48f19406461fdc5148da6d3f478c976947f50100000e010000010000000600000000000000000000004c000000', - 'hex', - ), - ); - assert.deepEqual(event, { - type: EventType.GAME_TEAMSTAT, - teamNumber: 7, - // I hope that floating point numbers in decimal are precise enough - // for this test to work reliably... - stats: { - frame: 57600, - metalUsed: 89025.3515625, - energyUsed: 4873733, - metalProduced: 110193.09375, - energyProduced: 4688346, - metalExcess: 0, - energyExcess: 644.0826416015625, - metalReceived: 1041.019775390625, - energyReceived: 413766.59375, - metalSent: 8613.2353515625, - energySent: 214896.484375, - damageDealt: 49005.8515625, - damageReceived: 59799.546875, - unitsProduced: 501, - unitsDied: 270, - unitsReceived: 1, - unitsSent: 6, - unitsCaptured: 0, - unitsOutCaptured: 0, - unitsKilled: 76, - }, + test('parse GAME_TEAMSTAT', () => { + const event = parsePacket( + Buffer.from( + '3c0700e10000ade0ad470abc944a8c38d747b4138f4a000000004a052144a2208244d308ca48f19406461fdc5148da6d3f478c976947f50100000e010000010000000600000000000000000000004c000000', + 'hex', + ), + ); + assert.deepEqual(event, { + type: EventType.GAME_TEAMSTAT, + teamNumber: 7, + // I hope that floating point numbers in decimal are precise enough + // for this test to work reliably... + stats: { + frame: 57600, + metalUsed: 89025.3515625, + energyUsed: 4873733, + metalProduced: 110193.09375, + energyProduced: 4688346, + metalExcess: 0, + energyExcess: 644.0826416015625, + metalReceived: 1041.019775390625, + energyReceived: 413766.59375, + metalSent: 8613.2353515625, + energySent: 214896.484375, + damageDealt: 49005.8515625, + damageReceived: 59799.546875, + unitsProduced: 501, + unitsDied: 270, + unitsReceived: 1, + unitsSent: 6, + unitsCaptured: 0, + unitsOutCaptured: 0, + unitsKilled: 76, + }, + }); + assert.throws(() => { + parsePacket(Buffer.from('3c0700e10000ade0', 'hex')); + }, PacketParseError); }); - assert.throws(() => { - parsePacket(Buffer.from('3c0700e10000ade0', 'hex')); - }, PacketParseError); -}); -test('serialize message', () => { - assert.deepEqual(serializeMessagePacket('msg'), Buffer.from('msg')); - assert.deepEqual(serializeMessagePacket('/asdasd'), Buffer.from('//asdasd')); - assert.deepEqual(serializeMessagePacket('//asd'), Buffer.from('///asd')); - assert.deepEqual(serializeMessagePacket(''), Buffer.from('')); - assert.throws(() => { - serializeMessagePacket('a'.repeat(200)); - }, PacketSerializeError); -}); + test('serialize message', () => { + assert.deepEqual(serializeMessagePacket('msg'), Buffer.from('msg')); + assert.deepEqual(serializeMessagePacket('/asdasd'), Buffer.from('//asdasd')); + assert.deepEqual(serializeMessagePacket('//asd'), Buffer.from('///asd')); + assert.deepEqual(serializeMessagePacket(''), Buffer.from('')); + assert.throws(() => { + serializeMessagePacket('a'.repeat(200)); + }, PacketSerializeError); + }); -test('serialize command', () => { - assert.deepEqual(serializeCommandPacket('cmd', []), Buffer.from('/cmd')); - assert.deepEqual(serializeCommandPacket('a', ['1', '2', 'asd']), Buffer.from('/a 1 2 asd')); - assert.deepEqual( - serializeCommandPacket('b', ['1', '2', 'some text with stuff']), - Buffer.from('/b 1 2 some text with stuff'), - ); - assert.throws(() => { - serializeCommandPacket('', ['1', '2']); - }, PacketSerializeError); - assert.throws(() => { - serializeCommandPacket('a', ['', '2']); - }, PacketSerializeError); - assert.throws(() => { - serializeCommandPacket('cmd', ['asd asd', 'asd asd']); - }, PacketSerializeError); - assert.throws(() => { - serializeCommandPacket('cmd', ['asd', 'asd //asd']); - }, PacketSerializeError); + test('serialize command', () => { + assert.deepEqual(serializeCommandPacket('cmd', []), Buffer.from('/cmd')); + assert.deepEqual(serializeCommandPacket('a', ['1', '2', 'asd']), Buffer.from('/a 1 2 asd')); + assert.deepEqual( + serializeCommandPacket('b', ['1', '2', 'some text with stuff']), + Buffer.from('/b 1 2 some text with stuff'), + ); + assert.throws(() => { + serializeCommandPacket('', ['1', '2']); + }, PacketSerializeError); + assert.throws(() => { + serializeCommandPacket('a', ['', '2']); + }, PacketSerializeError); + assert.throws(() => { + serializeCommandPacket('cmd', ['asd asd', 'asd asd']); + }, PacketSerializeError); + assert.throws(() => { + serializeCommandPacket('cmd', ['asd', 'asd //asd']); + }, PacketSerializeError); + }); }); /** diff --git a/src/engineRunner.test.ts b/src/engineRunner.test.ts index 2bd217c..59344f2 100644 --- a/src/engineRunner.test.ts +++ b/src/engineRunner.test.ts @@ -1,4 +1,4 @@ -import test from 'node:test'; +import test, { suite } from 'node:test'; import assert from 'node:assert/strict'; import dgram from 'node:dgram'; import events from 'node:events'; @@ -63,117 +63,119 @@ function getEnv(spawnMock?: typeof spawn) { const origCwd = process.cwd(); let testDir: string; -test.beforeEach(async () => { - testDir = await mkdtemp(join(tmpdir(), 'engine-runner-test-')); - chdir(testDir); - await mkdir('engines/test', { recursive: true }); -}); - -test.afterEach(async () => { - chdir(origCwd); - await rm(testDir, { recursive: true }); -}); - -test('runEngine quick close works', async () => { - const er = runEngine(getEnv(), optsBase); - er.close(); - await events.once(er, 'exit'); -}); - -test('engineRunner emits error on server start', async () => { - const er = new EngineRunnerImpl( - getEnv((() => { - const cp = new ChildProcess(); - process.nextTick(() => { - cp.emit('error', new Error('test error')); - }); - return cp; - }) as typeof spawn), - ); - er._run(optsBase); - await assert.rejects(events.once(er, 'start'), /test error/); -}); - -test('engineRunner spawns process correctly', async () => { - const er = new EngineRunnerImpl( - getEnv(((cmd: string, args: string[], opts: SpawnOptions) => { - assert.match(cmd, /.*\/engines\/test\/spring-dedicated$/); - return spawn('echo', args, opts); - }) as typeof spawn), - ); - er._run(optsBase); - await events.once(er, 'exit'); -}); - -test('engineRunner close before spawn works', async () => { - const er = new EngineRunnerImpl( - getEnv((() => { - process.nextTick(() => { - er.close(); - }); - return spawn('sleep', ['1000'], { stdio: 'ignore' }); - }) as typeof spawn), - ); - er._run(optsBase); - await events.once(er, 'exit'); -}); +suite('engineRunner', () => { + test.beforeEach(async () => { + testDir = await mkdtemp(join(tmpdir(), 'engine-runner-test-')); + chdir(testDir); + await mkdir('engines/test', { recursive: true }); + }); -test('engineRunner multi start, multi close', async () => { - const er = new EngineRunnerImpl(getEnv()); - er._run(optsBase); - assert.throws(() => er._run(optsBase)); - er.close(); - er.close(); - await events.once(er, 'exit'); -}); + test.afterEach(async () => { + chdir(origCwd); + await rm(testDir, { recursive: true }); + }); -test('engineRunner full run simulated engine', async () => { - const er = new EngineRunnerImpl( - getEnv((() => { - const cp = new ChildProcess(); + test('runEngine quick close works', async () => { + const er = runEngine(getEnv(), optsBase); + er.close(); + await events.once(er, 'exit'); + }); - cp.kill = (() => { - assert.fail('kill should not be called'); - }) as typeof ChildProcess.prototype.kill; + test('engineRunner emits error on server start', async () => { + const er = new EngineRunnerImpl( + getEnv((() => { + const cp = new ChildProcess(); + process.nextTick(() => { + cp.emit('error', new Error('test error')); + }); + return cp; + }) as typeof spawn), + ); + er._run(optsBase); + await assert.rejects(events.once(er, 'start'), /test error/); + }); - process.nextTick(() => { - cp.emit('spawn'); - }); + test('engineRunner spawns process correctly', async () => { + const er = new EngineRunnerImpl( + getEnv(((cmd: string, args: string[], opts: SpawnOptions) => { + assert.match(cmd, /.*\/engines\/test\/spring-dedicated$/); + return spawn('echo', args, opts); + }) as typeof spawn), + ); + er._run(optsBase); + await events.once(er, 'exit'); + }); - setImmediate(() => simulateEngine(cp)); + test('engineRunner close before spawn works', async () => { + const er = new EngineRunnerImpl( + getEnv((() => { + process.nextTick(() => { + er.close(); + }); + return spawn('sleep', ['1000'], { stdio: 'ignore' }); + }) as typeof spawn), + ); + er._run(optsBase); + await events.once(er, 'exit'); + }); - return cp; - }) as typeof spawn), - ); - er._run(optsBase); + test('engineRunner multi start, multi close', async () => { + const er = new EngineRunnerImpl(getEnv()); + er._run(optsBase); + assert.throws(() => er._run(optsBase)); + er.close(); + er.close(); + await events.once(er, 'exit'); + }); - async function simulateEngine(cp: ChildProcess) { - const s = dgram.createSocket('udp4'); - s.connect(testPort); - await events.once(s, 'connect'); + test('engineRunner full run simulated engine', async () => { + const er = new EngineRunnerImpl( + getEnv((() => { + const cp = new ChildProcess(); + + cp.kill = (() => { + assert.fail('kill should not be called'); + }) as typeof ChildProcess.prototype.kill; + + process.nextTick(() => { + cp.emit('spawn'); + }); + + setImmediate(() => simulateEngine(cp)); + + return cp; + }) as typeof spawn), + ); + er._run(optsBase); + + async function simulateEngine(cp: ChildProcess) { + const s = dgram.createSocket('udp4'); + s.connect(testPort); + await events.once(s, 'connect'); + + for (const packet of [ + Buffer.from('00', 'hex'), + Buffer.from('054f6e6c696e65207761726e696e67206c6f6c', 'hex'), + Buffer.from('01', 'hex'), + ]) { + await asyncSetImmediate(); + s.send(packet); + const msg = (await events.once(s, 'message')) as [Buffer, dgram.RemoteInfo]; + assert.equal(msg[0].toString('utf8'), `test${packet[0]}`); + } - for (const packet of [ - Buffer.from('00', 'hex'), - Buffer.from('054f6e6c696e65207761726e696e67206c6f6c', 'hex'), - Buffer.from('01', 'hex'), - ]) { await asyncSetImmediate(); - s.send(packet); - const msg = (await events.once(s, 'message')) as [Buffer, dgram.RemoteInfo]; - assert.equal(msg[0].toString('utf8'), `test${packet[0]}`); + cp.emit('exit', 0, 'exit'); + s.close(); } - await asyncSetImmediate(); - cp.emit('exit', 0, 'exit'); - s.close(); - } + assert.rejects(er.sendPacket(Buffer.from('asd')), /not running/); - assert.rejects(er.sendPacket(Buffer.from('asd')), /not running/); + er.on('packet', async (packet) => { + await er.sendPacket(Buffer.from(`test${packet.type}`)); + }); - er.on('packet', async (packet) => { - await er.sendPacket(Buffer.from(`test${packet.type}`)); + await events.once(er, 'start'); + await events.once(er, 'exit'); }); - - await events.once(er, 'start'); - await events.once(er, 'exit'); }); diff --git a/src/oauth2Client.test.ts b/src/oauth2Client.test.ts index c2d0bad..77b06eb 100644 --- a/src/oauth2Client.test.ts +++ b/src/oauth2Client.test.ts @@ -1,4 +1,4 @@ -import test, { after, beforeEach } from 'node:test'; +import { test, after, beforeEach, suite } from 'node:test'; import { equal } from 'node:assert/strict'; import { getAccessToken } from './oauth2Client.js'; @@ -30,93 +30,95 @@ server.post('/oauth2/token', (req, resp) => tokenHandler.call(server, req, resp) await server.listen(); const PORT = server.addresses()[0].port; -after(() => server.close()); +suite('oauth2client', () => { + after(() => server.close()); -test('simple full example', async () => { - metadataHandler = async () => { - return { - issuer: `http://localhost:${PORT}`, - token_endpoint: `http://localhost:${PORT}/oauth2/token`, - response_types_supported: ['token'], + test('simple full example', async () => { + metadataHandler = async () => { + return { + issuer: `http://localhost:${PORT}`, + token_endpoint: `http://localhost:${PORT}/oauth2/token`, + response_types_supported: ['token'], + }; }; - }; - let tokenErr: Error | undefined; - tokenHandler = async (req) => { - try { - equal(req.headers.authorization, 'Basic dXNlcjE6cGFzczE='); - equal(req.headers['content-type'], 'application/x-www-form-urlencoded'); - const params = new URLSearchParams(req.body as string); - equal(params.get('grant_type'), 'client_credentials'); - equal(params.get('scope'), 'tachyon.lobby'); - } catch (error) { - tokenErr = error as Error; - } - return { - access_token: 'token_value', - token_type: 'Bearer', - expires_in: 60, + let tokenErr: Error | undefined; + tokenHandler = async (req) => { + try { + equal(req.headers.authorization, 'Basic dXNlcjE6cGFzczE='); + equal(req.headers['content-type'], 'application/x-www-form-urlencoded'); + const params = new URLSearchParams(req.body as string); + equal(params.get('grant_type'), 'client_credentials'); + equal(params.get('scope'), 'tachyon.lobby'); + } catch (error) { + tokenErr = error as Error; + } + return { + access_token: 'token_value', + token_type: 'Bearer', + expires_in: 60, + }; }; - }; - const token = await getAccessToken( - `http://localhost:${PORT}`, - 'user1', - 'pass1', - 'tachyon.lobby', - ); - if (tokenErr) throw tokenErr; - equal(token, 'token_value'); -}); + const token = await getAccessToken( + `http://localhost:${PORT}`, + 'user1', + 'pass1', + 'tachyon.lobby', + ); + if (tokenErr) throw tokenErr; + equal(token, 'token_value'); + }); -test('wrong oauth2 metadata', async () => { - metadataHandler = async () => { - return { - issuer: `http://localhost:${PORT}`, + test('wrong oauth2 metadata', async () => { + metadataHandler = async () => { + return { + issuer: `http://localhost:${PORT}`, + }; }; - }; - await rejects( - getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), - /Invalid.*object/, - ); -}); + await rejects( + getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), + /Invalid.*object/, + ); + }); -test('propagates OAuth2 error message', async () => { - metadataHandler = async () => { - return { - issuer: `http://localhost:${PORT}`, - token_endpoint: `http://localhost:${PORT}/oauth2/token`, - response_types_supported: ['token'], + test('propagates OAuth2 error message', async () => { + metadataHandler = async () => { + return { + issuer: `http://localhost:${PORT}`, + token_endpoint: `http://localhost:${PORT}/oauth2/token`, + response_types_supported: ['token'], + }; }; - }; - tokenHandler = async (_req, resp) => { - resp.code(400); - return { - error: 'invalid_scope', - error_description: 'Invalid scope', + tokenHandler = async (_req, resp) => { + resp.code(400); + return { + error: 'invalid_scope', + error_description: 'Invalid scope', + }; }; - }; - await rejects( - getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), - /invalid_scope.*Invalid scope/, - ); -}); + await rejects( + getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), + /invalid_scope.*Invalid scope/, + ); + }); -test('bad access token response', async () => { - metadataHandler = async () => { - return { - issuer: `http://localhost:${PORT}`, - token_endpoint: `http://localhost:${PORT}/oauth2/token`, - response_types_supported: ['token'], + test('bad access token response', async () => { + metadataHandler = async () => { + return { + issuer: `http://localhost:${PORT}`, + token_endpoint: `http://localhost:${PORT}/oauth2/token`, + response_types_supported: ['token'], + }; }; - }; - tokenHandler = async () => { - return { - access_token: 'token_value', - token_type: 'CustomType', - expires_in: 60, + tokenHandler = async () => { + return { + access_token: 'token_value', + token_type: 'CustomType', + expires_in: 60, + }; }; - }; - await rejects( - getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), - /expected Bearer/, - ); + await rejects( + getAccessToken(`http://localhost:${PORT}`, 'user1', 'pass1', 'tachyon.lobby'), + /expected Bearer/, + ); + }); }); diff --git a/src/startScriptGen.test.ts b/src/startScriptGen.test.ts index 8997506..7aaa814 100644 --- a/src/startScriptGen.test.ts +++ b/src/startScriptGen.test.ts @@ -1,85 +1,235 @@ -import test from 'node:test'; +import { test, suite } from 'node:test'; import assert from 'node:assert/strict'; import { AutohostStartRequestData } from 'tachyon-protocol/types'; import { scriptGameFromStartRequest, StartScriptGenError } from './startScriptGen.js'; -test('simple full example', () => { - const startReq: AutohostStartRequestData = { - battleId: 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', - engineVersion: 'recoil 2024.08.15-gdefse23', - gameName: 'Game 22', - mapName: 'de_duck 1.2', - gameArchiveHash: - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - mapArchiveHash: - 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', - startPosType: 'ingame', - startDelay: 10, - allyTeams: [ - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - countryCode: 'NA', +suite('Start script generation', () => { + test('simple full example', () => { + const startReq: AutohostStartRequestData = { + battleId: 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', + engineVersion: 'recoil 2024.08.15-gdefse23', + gameName: 'Game 22', + mapName: 'de_duck 1.2', + gameArchiveHash: + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + mapArchiveHash: + 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + startPosType: 'ingame', + startDelay: 10, + allyTeams: [ + { + teams: [ + { + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + countryCode: 'NA', + }, + ], + faction: 'ARM', + color: { + r: 1, + g: 0, + b: 0.5, + }, + customProperties: { + 'specialModOption': 'asd', }, - ], - faction: 'ARM', - color: { - r: 1, - g: 0, - b: 0.5, - }, - customProperties: { - 'specialModOption': 'asd', }, + ], + startBox: { + top: 0, + left: 0, + bottom: 0, + right: 0.2, }, - ], - startBox: { - top: 0, - left: 0, - bottom: 0, - right: 0.2, }, - }, - { - teams: [ - { - faction: 'CORE', - advantage: 0.5, - incomeMultiplier: 1.2, - startPos: { - x: 100, - y: 100, - }, - bots: [ - { - hostUserId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - aiShortName: 'BARb', - aiVersion: '3.2', - name: 'AI 1', - aiOptions: { - 'difficulty': 'op', + { + teams: [ + { + faction: 'CORE', + advantage: 0.5, + incomeMultiplier: 1.2, + startPos: { + x: 100, + y: 100, + }, + bots: [ + { + hostUserId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + aiShortName: 'BARb', + aiVersion: '3.2', + name: 'AI 1', + aiOptions: { + 'difficulty': 'op', + }, + customProperties: { + 'x': 'y', + }, }, - customProperties: { - 'x': 'y', + ], + }, + ], + startBox: { + top: 0, + left: 0.8, + bottom: 0, + right: 1, + }, + }, + { + allies: [0], + teams: [ + { + players: [ + { + userId: '441a8dde-4a7a-4baf-9a3f-f51015fa61c4', + name: 'Player X', + password: 'X', + countryCode: 'DE', }, - }, - ], + ], + }, + ], + }, + ], + spectators: [ + { + userId: '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', + name: 'Player 2', + password: 'asd', + countryCode: 'PL', + rank: 1, + customProperties: { + 'key': 'value', }, - ], - startBox: { - top: 0, - left: 0.8, - bottom: 0, - right: 1, }, + ], + mapOptions: { + 'waterLevel': '1000', + }, + gameOptions: { + 'bigGun': 'asdasd', + }, + restrictions: { + 'unitname': 20, + 'anotherunit': 30, + }, + }; + + const expected = { + 'GameID': 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', + 'GameType': 'Game 22', + 'MapName': 'de_duck 1.2', + 'ModHash': + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', + 'MapHash': + 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', + 'StartPosType': 2, + 'GameStartDelay': 10, + 'MODOPTIONS': { + 'bigGun': 'asdasd', + }, + 'MAPOPTIONS': { + 'waterLevel': '1000', }, + 'NumRestrictions': 2, + 'RESTRICT': { + 'Unit0': 'unitname', + 'Limit0': 20, + 'Unit1': 'anotherunit', + 'Limit1': 30, + }, + 'ALLYTEAM0': { + 'StartRectTop': 0, + 'StartRectLeft': 0, + 'StartRectBottom': 0, + 'StartRectRight': 0.2, + }, + 'TEAM0': { + 'AllyTeam': 0, + 'TeamLeader': 0, + 'Side': 'ARM', + 'RgbColor': '1 0 0.5', + 'specialModOption': 'asd', + }, + 'PLAYER0': { + 'Name': 'Player 1', + 'UserID': '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + 'Password': '87dw9cnqr86437w', + 'Team': 0, + 'CountryCode': 'NA', + }, + 'ALLYTEAM1': { + 'StartRectTop': 0, + 'StartRectLeft': 0.8, + 'StartRectBottom': 0, + 'StartRectRight': 1, + }, + 'TEAM1': { + 'AllyTeam': 1, + 'TeamLeader': 0, + 'IncomeMultiplier': 1.2, + 'Side': 'CORE', + 'Advantage': 0.5, + 'StartPosX': 100, + 'StartPosZ': 100, + }, + 'AI0': { + 'Name': 'AI 1', + 'ShortName': 'BARb', + 'Host': 0, + 'Team': 1, + 'Version': '3.2', + 'OPTIONS': { + 'difficulty': 'op', + }, + 'x': 'y', + }, + 'ALLYTEAM2': { + 'NumAllies': 1, + 'Ally0': 0, + }, + 'TEAM2': { + 'AllyTeam': 2, + 'TeamLeader': 1, + }, + 'PLAYER1': { + 'Name': 'Player X', + 'UserID': '441a8dde-4a7a-4baf-9a3f-f51015fa61c4', + 'Password': 'X', + 'Team': 2, + 'CountryCode': 'DE', + }, + 'PLAYER2': { + 'Name': 'Player 2', + 'UserID': '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', + 'Password': 'asd', + 'Spectator': 1, + 'CountryCode': 'PL', + 'Rank': 1, + 'key': 'value', + }, + 'NumAllyTeams': 3, + 'NumTeams': 3, + 'NumPlayers': 3, + }; + + const actual = scriptGameFromStartRequest(startReq); + + assert.deepStrictEqual(actual, expected); + }); + + const throwStartReqBase: AutohostStartRequestData = { + battleId: 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', + engineVersion: 'recoil 2024.08.15-gdefse23', + gameName: 'Game 22', + mapName: 'de_duck 1.2', + startPosType: 'ingame', + allyTeams: [ { - allies: [0], teams: [ { players: [ @@ -94,282 +244,134 @@ test('simple full example', () => { ], }, ], - spectators: [ - { - userId: '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', - name: 'Player 2', - password: 'asd', - countryCode: 'PL', - rank: 1, - customProperties: { - 'key': 'value', - }, - }, - ], - mapOptions: { - 'waterLevel': '1000', - }, - gameOptions: { - 'bigGun': 'asdasd', - }, - restrictions: { - 'unitname': 20, - 'anotherunit': 30, - }, - }; - - const expected = { - 'GameID': 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', - 'GameType': 'Game 22', - 'MapName': 'de_duck 1.2', - 'ModHash': - 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', - 'MapHash': - 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd', - 'StartPosType': 2, - 'GameStartDelay': 10, - 'MODOPTIONS': { - 'bigGun': 'asdasd', - }, - 'MAPOPTIONS': { - 'waterLevel': '1000', - }, - 'NumRestrictions': 2, - 'RESTRICT': { - 'Unit0': 'unitname', - 'Limit0': 20, - 'Unit1': 'anotherunit', - 'Limit1': 30, - }, - 'ALLYTEAM0': { - 'StartRectTop': 0, - 'StartRectLeft': 0, - 'StartRectBottom': 0, - 'StartRectRight': 0.2, - }, - 'TEAM0': { - 'AllyTeam': 0, - 'TeamLeader': 0, - 'Side': 'ARM', - 'RgbColor': '1 0 0.5', - 'specialModOption': 'asd', - }, - 'PLAYER0': { - 'Name': 'Player 1', - 'UserID': '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - 'Password': '87dw9cnqr86437w', - 'Team': 0, - 'CountryCode': 'NA', - }, - 'ALLYTEAM1': { - 'StartRectTop': 0, - 'StartRectLeft': 0.8, - 'StartRectBottom': 0, - 'StartRectRight': 1, - }, - 'TEAM1': { - 'AllyTeam': 1, - 'TeamLeader': 0, - 'IncomeMultiplier': 1.2, - 'Side': 'CORE', - 'Advantage': 0.5, - 'StartPosX': 100, - 'StartPosZ': 100, - }, - 'AI0': { - 'Name': 'AI 1', - 'ShortName': 'BARb', - 'Host': 0, - 'Team': 1, - 'Version': '3.2', - 'OPTIONS': { - 'difficulty': 'op', - }, - 'x': 'y', - }, - 'ALLYTEAM2': { - 'NumAllies': 1, - 'Ally0': 0, - }, - 'TEAM2': { - 'AllyTeam': 2, - 'TeamLeader': 1, - }, - 'PLAYER1': { - 'Name': 'Player X', - 'UserID': '441a8dde-4a7a-4baf-9a3f-f51015fa61c4', - 'Password': 'X', - 'Team': 2, - 'CountryCode': 'DE', - }, - 'PLAYER2': { - 'Name': 'Player 2', - 'UserID': '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', - 'Password': 'asd', - 'Spectator': 1, - 'CountryCode': 'PL', - 'Rank': 1, - 'key': 'value', - }, - 'NumAllyTeams': 3, - 'NumTeams': 3, - 'NumPlayers': 3, }; - const actual = scriptGameFromStartRequest(startReq); - - assert.deepStrictEqual(actual, expected); -}); - -const throwStartReqBase: AutohostStartRequestData = { - battleId: 'e4f9f751-3626-48eb-bb8b-1ff8f25e12f9', - engineVersion: 'recoil 2024.08.15-gdefse23', - gameName: 'Game 22', - mapName: 'de_duck 1.2', - startPosType: 'ingame', - allyTeams: [ - { - teams: [ + test('throw on non-unique players', () => { + // Players in different teams. + const startReq1: AutohostStartRequestData = { + ...throwStartReqBase, + allyTeams: [ { - players: [ + teams: [ { - userId: '441a8dde-4a7a-4baf-9a3f-f51015fa61c4', - name: 'Player X', - password: 'X', - countryCode: 'DE', + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], + }, + ], + }, + { + teams: [ + { + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], }, ], }, ], - }, - ], -}; + }; + assert.throws(() => scriptGameFromStartRequest(startReq1), StartScriptGenError); -test('throw on non-unique players', () => { - // Players in different teams. - const startReq1: AutohostStartRequestData = { - ...throwStartReqBase, - allyTeams: [ - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - }, - ], - }, - ], - }, - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - }, - ], - }, - ], - }, - ], - }; - assert.throws(() => scriptGameFromStartRequest(startReq1), StartScriptGenError); - - // Also in spectators. - const startReq2: AutohostStartRequestData = { - ...throwStartReqBase, - allyTeams: [ - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - }, - ], - }, - ], - }, - ], - spectators: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - }, - ], - }; - assert.throws(() => scriptGameFromStartRequest(startReq2), StartScriptGenError); -}); + // Also in spectators. + const startReq2: AutohostStartRequestData = { + ...throwStartReqBase, + allyTeams: [ + { + teams: [ + { + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], + }, + ], + }, + ], + spectators: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], + }; + assert.throws(() => scriptGameFromStartRequest(startReq2), StartScriptGenError); + }); -test('at least one ai/player is required', () => { - const startReq: AutohostStartRequestData = { - ...throwStartReqBase, - allyTeams: [ - { - teams: [{}], - }, - ], - }; - assert.throws(() => scriptGameFromStartRequest(startReq)); -}); + test('at least one ai/player is required', () => { + const startReq: AutohostStartRequestData = { + ...throwStartReqBase, + allyTeams: [ + { + teams: [{}], + }, + ], + }; + assert.throws(() => scriptGameFromStartRequest(startReq)); + }); -test("custom opts can't override built-in fields", () => { - const startReq: AutohostStartRequestData = { - ...throwStartReqBase, - allyTeams: [ - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', + test("custom opts can't override built-in fields", () => { + const startReq: AutohostStartRequestData = { + ...throwStartReqBase, + allyTeams: [ + { + teams: [ + { + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], + customProperties: { + 'AllyTeam': '1', }, - ], - customProperties: { - 'AllyTeam': '1', }, - }, - ], - }, - ], - }; - assert.throws(() => scriptGameFromStartRequest(startReq), StartScriptGenError); -}); + ], + }, + ], + }; + assert.throws(() => scriptGameFromStartRequest(startReq), StartScriptGenError); + }); -test('ai must reference existing player', () => { - const startReq: AutohostStartRequestData = { - ...throwStartReqBase, - allyTeams: [ - { - teams: [ - { - players: [ - { - userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', - name: 'Player 1', - password: '87dw9cnqr86437w', - }, - ], - bots: [ - { - hostUserId: '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', - aiShortName: 'BARb', - }, - ], - }, - ], - }, - ], - }; - assert.throws(() => scriptGameFromStartRequest(startReq), StartScriptGenError); + test('ai must reference existing player', () => { + const startReq: AutohostStartRequestData = { + ...throwStartReqBase, + allyTeams: [ + { + teams: [ + { + players: [ + { + userId: '730c874d-e5a3-4c24-a053-fcb2cfb23b32', + name: 'Player 1', + password: '87dw9cnqr86437w', + }, + ], + bots: [ + { + hostUserId: '7cd7fbda-44c8-4986-afc9-1f5d8b68e59e', + aiShortName: 'BARb', + }, + ], + }, + ], + }, + ], + }; + assert.throws(() => scriptGameFromStartRequest(startReq), StartScriptGenError); + }); }); diff --git a/src/tachyonClient.test.ts b/src/tachyonClient.test.ts index 95f27d3..4c9e2fb 100644 --- a/src/tachyonClient.test.ts +++ b/src/tachyonClient.test.ts @@ -1,4 +1,4 @@ -import test, { after, afterEach } from 'node:test'; +import { test, after, afterEach, suite } from 'node:test'; import { equal } from 'node:assert/strict'; import { once } from 'node:events'; @@ -11,74 +11,77 @@ import { TachyonMessage } from './tachyonTypes.js'; const server = await createTachyonServer({ clientId: 'c', clientSecret: 's' }); await server.start(); const port = server.fastifyServer.addresses()[0].port; -after(() => server.close()); -afterEach(() => server.removeAllListeners()); -const connectionParams = { - clientId: 'c', - clientSecret: 's', - hostname: 'localhost', - port, -}; +suite('tachyon client', () => { + after(() => server.close()); + afterEach(() => server.removeAllListeners()); -test('simple full example', async () => { - server.on('connection', (conn) => { - conn.on('message', (msg) => { - equal(msg.type, 'request'); - equal(msg.commandId, 'autohost/sendMessage'); - deepEqual((msg as unknown as { data: string }).data, { - battleId: 'id', - message: 'msg', - }); - conn.send({ - type: 'response', - commandId: msg.commandId, - messageId: msg.messageId, - status: 'success', + const connectionParams = { + clientId: 'c', + clientSecret: 's', + hostname: 'localhost', + port, + }; + + test('simple full example', async () => { + server.on('connection', (conn) => { + conn.on('message', (msg) => { + equal(msg.type, 'request'); + equal(msg.commandId, 'autohost/sendMessage'); + deepEqual((msg as unknown as { data: string }).data, { + battleId: 'id', + message: 'msg', + }); + conn.send({ + type: 'response', + commandId: msg.commandId, + messageId: msg.messageId, + status: 'success', + }); }); }); - }); - const client = new TachyonClient(connectionParams); - await once(client, 'connected'); - client.send({ - type: 'request', - commandId: 'autohost/sendMessage', - messageId: 'test-message1', - data: { battleId: 'id', message: 'msg' }, - }); - const msg = (await once(client, 'message')) as [TachyonMessage]; - deepEqual(msg[0], { - type: 'response', - commandId: 'autohost/sendMessage', - messageId: 'test-message1', - status: 'success', + const client = new TachyonClient(connectionParams); + await once(client, 'connected'); + client.send({ + type: 'request', + commandId: 'autohost/sendMessage', + messageId: 'test-message1', + data: { battleId: 'id', message: 'msg' }, + }); + const msg = (await once(client, 'message')) as [TachyonMessage]; + deepEqual(msg[0], { + type: 'response', + commandId: 'autohost/sendMessage', + messageId: 'test-message1', + status: 'success', + }); + client.close(); }); - client.close(); -}); -test("doesn't emit bad tachyon messages", async () => { - server.on('connection', (conn) => { - conn.on('message', () => { - conn.send({ - type: 'asdasdasd', + test("doesn't emit bad tachyon messages", async () => { + server.on('connection', (conn) => { + conn.on('message', () => { + conn.send({ + type: 'asdasdasd', + }); }); }); + const client = new TachyonClient(connectionParams); + await once(client, 'connected'); + client.send({ + type: 'request', + commandId: 'autohost/sendMessage', + messageId: 'test-message1', + data: { battleId: 'id', message: 'msg' }, + }); + let gotMessages = 0; + client.on('message', () => { + ++gotMessages; + }); + await once(client, 'close'); + equal(gotMessages, 0); }); - const client = new TachyonClient(connectionParams); - await once(client, 'connected'); - client.send({ - type: 'request', - commandId: 'autohost/sendMessage', - messageId: 'test-message1', - data: { battleId: 'id', message: 'msg' }, - }); - let gotMessages = 0; - client.on('message', () => { - ++gotMessages; - }); - await once(client, 'close'); - equal(gotMessages, 0); -}); -// TODO: Add more tests then only a simple happy path. + // TODO: Add more tests then only a simple happy path. +}); diff --git a/src/tachyonServer.fake.ts b/src/tachyonServer.fake.ts index 8553478..a7be7ae 100644 --- a/src/tachyonServer.fake.ts +++ b/src/tachyonServer.fake.ts @@ -17,6 +17,10 @@ * curl http://localhost:8084/request/0/kill \ * --json '{"battleId": "24b72b50-ef8c-4899-8372-d2b3a0ca3d7b"}' * + * Alternative endpoint is /rawCommand/:connIdx which takes a full tachyon command in body, sets a + * new unique `messageId` (controlled by `fixMessageId` query param) and if present, drops `$schema` + * key from the message if present (This is to better support objects authored in Visual Studio Code: + * https://code.visualstudio.com/Docs/languages/json). */ import { randomUUID } from 'node:crypto'; import Fastify, { FastifyBaseLogger } from 'fastify'; @@ -343,5 +347,44 @@ if (import.meta.filename == process.argv[1]) { }, ); + srv.fastifyServer.post( + '/rawCommand/:connIdx', + { + schema: { + params: { + type: 'object', + properties: { + connIdx: { type: 'integer' }, + }, + required: ['connIdx'], + }, + body: { + type: 'object', + }, + querystring: { + type: 'object', + properties: { + fixMessageId: { type: 'boolean', default: true }, + }, + }, + }, + }, + async (req, resp) => { + const conn = connections.get(req.params.connIdx); + if (!conn) { + resp.code(404); + return `Couldn't find the open connection ${req.params.connIdx}`; + } + if (req.query.fixMessageId) { + req.body.messageId = randomUUID(); + } + if ('$schema' in req.body) { + delete req.body['$schema']; + } + conn.send(req.body); + return `send request ${req.body.messageId}`; + }, + ); + await srv.start(); } diff --git a/src/tachyonTypes.test.ts b/src/tachyonTypes.test.ts index 18d7d40..9d4c8d1 100644 --- a/src/tachyonTypes.test.ts +++ b/src/tachyonTypes.test.ts @@ -1,4 +1,4 @@ -import test from 'node:test'; +import { test, suite } from 'node:test'; import assert from 'node:assert/strict'; import { callTachyonAutohost, @@ -9,104 +9,106 @@ import { } from './tachyonTypes.js'; import { AutohostKillRequestData } from 'tachyon-protocol/types'; -test('parsing correct tachyon message succeeds', () => { - const message = { - messageId: 'someid', - commandId: 'some/command', - type: 'request', - data: {}, - }; - const parsed = parseTachyonMessage(JSON.stringify(message)); - assert.deepStrictEqual(parsed, message); -}); +suite('tachyon types', () => { + test('parsing correct tachyon message succeeds', () => { + const message = { + messageId: 'someid', + commandId: 'some/command', + type: 'request', + data: {}, + }; + const parsed = parseTachyonMessage(JSON.stringify(message)); + assert.deepStrictEqual(parsed, message); + }); -test('parsing incorrect tachyon message fails', () => { - const message = { - messageId: 'someid', - commandId: 'some/command', - type: 'reqwest', - data: {}, - }; - assert.throws(() => { - parseTachyonMessage(JSON.stringify({ ...message, extra: 'field' })); + test('parsing incorrect tachyon message fails', () => { + const message = { + messageId: 'someid', + commandId: 'some/command', + type: 'reqwest', + data: {}, + }; + assert.throws(() => { + parseTachyonMessage(JSON.stringify({ ...message, extra: 'field' })); + }); }); -}); -test('creating a tachyon event succeeds', () => { - const event = createTachyonEvent('autohost/status', { currentBattles: 2, maxBattles: 4 }); - assert.deepStrictEqual(event, { - type: 'event', - messageId: event.messageId, - commandId: 'autohost/status', - data: { currentBattles: 2, maxBattles: 4 }, + test('creating a tachyon event succeeds', () => { + const event = createTachyonEvent('autohost/status', { currentBattles: 2, maxBattles: 4 }); + assert.deepStrictEqual(event, { + type: 'event', + messageId: event.messageId, + commandId: 'autohost/status', + data: { currentBattles: 2, maxBattles: 4 }, + }); }); -}); -test('calling tachyon autohost succeeds', async () => { - const killData: AutohostKillRequestData = { - battleId: '873bf189-d659-4527-befd-e9d63b308955', - }; - const req = { - messageId: 'some-message-id', - commandId: 'autohost/kill', - type: 'request', - data: killData, - } as TachyonMessage; - let called = 0; - const autohost = { - kill: async (data: AutohostKillRequestData) => { - assert.deepStrictEqual(data, killData); - ++called; - }, - } as TachyonAutohost; - const response = await callTachyonAutohost(req, autohost); - assert.deepStrictEqual(response, { - type: 'response', - status: 'success', - messageId: req.messageId, - commandId: req.commandId, - data: undefined, + test('calling tachyon autohost succeeds', async () => { + const killData: AutohostKillRequestData = { + battleId: '873bf189-d659-4527-befd-e9d63b308955', + }; + const req = { + messageId: 'some-message-id', + commandId: 'autohost/kill', + type: 'request', + data: killData, + } as TachyonMessage; + let called = 0; + const autohost = { + kill: async (data: AutohostKillRequestData) => { + assert.deepStrictEqual(data, killData); + ++called; + }, + } as TachyonAutohost; + const response = await callTachyonAutohost(req, autohost); + assert.deepStrictEqual(response, { + type: 'response', + status: 'success', + messageId: req.messageId, + commandId: req.commandId, + data: undefined, + }); + assert.strictEqual(called, 1); }); - assert.strictEqual(called, 1); -}); -test('calling tachyon autohost catches bad commands', async () => { - const req = { - messageId: 'some-message-id', - commandId: 'autohost/killss', - type: 'request', - data: {}, - } as TachyonMessage; - const response = await callTachyonAutohost(req, {} as TachyonAutohost); - assert(response.status === 'failed'); - assert.deepStrictEqual(response, { - type: 'response', - status: 'failed', - messageId: req.messageId, - commandId: req.commandId, - reason: 'command_unimplemented', - details: response.details, + test('calling tachyon autohost catches bad commands', async () => { + const req = { + messageId: 'some-message-id', + commandId: 'autohost/killss', + type: 'request', + data: {}, + } as TachyonMessage; + const response = await callTachyonAutohost(req, {} as TachyonAutohost); + assert(response.status === 'failed'); + assert.deepStrictEqual(response, { + type: 'response', + status: 'failed', + messageId: req.messageId, + commandId: req.commandId, + reason: 'command_unimplemented', + details: response.details, + }); }); -}); -test('calling tachyon autohost validates data', async () => { - const killData: AutohostKillRequestData = { - battleId: '873bf189-d659-4527-befd-e9d63b308', // invalid uuid - }; - const req = { - messageId: 'some-message-id', - commandId: 'autohost/kill', - type: 'request', - data: killData, - } as TachyonMessage; - const response = await callTachyonAutohost(req, {} as TachyonAutohost); - assert(response.status === 'failed'); - assert.deepStrictEqual(response, { - type: 'response', - status: 'failed', - messageId: req.messageId, - commandId: req.commandId, - reason: 'invalid_request', - details: response.details, + test('calling tachyon autohost validates data', async () => { + const killData: AutohostKillRequestData = { + battleId: '873bf189-d659-4527-befd-e9d63b308', // invalid uuid + }; + const req = { + messageId: 'some-message-id', + commandId: 'autohost/kill', + type: 'request', + data: killData, + } as TachyonMessage; + const response = await callTachyonAutohost(req, {} as TachyonAutohost); + assert(response.status === 'failed'); + assert.deepStrictEqual(response, { + type: 'response', + status: 'failed', + messageId: req.messageId, + commandId: req.commandId, + reason: 'invalid_request', + details: response.details, + }); }); });