cc-libs/tests/sandbox.lua

365 lines
14 KiB
Lua

local createLibTest = require('/apis/libtest');
local createSandbox = require('/apis/libsandbox');
local fakes = require('/tests/harness/fake-eventloop');
local testlib = createLibTest({ ... });
local fakeOs = fakes.fakeOs;
local encode = textutils.serializeJSON;
-- traposId ----------------------------------------------------------------
testlib.test('traposId joins id and label', function()
local sandbox = createSandbox();
testlib.assertEquals(sandbox.traposId(fakeOs(7, 'base')), '7:base');
end);
testlib.test('traposId renders empty/nil label as trailing colon', function()
local sandbox = createSandbox();
testlib.assertEquals(sandbox.traposId(fakeOs(7, nil)), '7:');
testlib.assertEquals(sandbox.traposId(fakeOs(8, '')), '8:');
end);
-- uuid --------------------------------------------------------------------
testlib.test('uuid emits a v4-shaped string', function()
local sandbox = createSandbox();
local id = sandbox.uuid();
testlib.assertEquals(string.len(id), 36);
testlib.assertEquals(string.sub(id, 15, 15), '4');
testlib.assertTrue(string.match(id, '^%x%x%x%x%x%x%x%x%-%x%x%x%x%-4') ~= nil);
end);
-- envelope builders -------------------------------------------------------
testlib.test('buildComputerRequest carries sender id and omits empty payload', function()
local sandbox = createSandbox();
local frame = sandbox.buildComputerRequest('7:base', 'mid-1', 'probe_gateway', nil);
testlib.assertEquals(frame.event, 'computer_request');
testlib.assertEquals(frame.traposSenderId, '7:base');
testlib.assertEquals(frame.messageId, 'mid-1');
testlib.assertEquals(frame.type, 'probe_gateway');
testlib.assertEquals(frame.payload, nil);
local withPayload = sandbox.buildComputerRequest('7:base', 'mid-2', 'x', { a = 1 });
testlib.assertEquals(withPayload.payload.a, 1);
end);
testlib.test('buildHello includes secret only when non-empty', function()
local sandbox = createSandbox();
local withSecret = sandbox.buildHello('7:base', 'hunter2', 'mid');
testlib.assertEquals(withSecret.event, 'computer_request');
testlib.assertEquals(withSecret.type, 'hello');
testlib.assertEquals(withSecret.traposSenderId, '7:base');
testlib.assertEquals(withSecret.payload.traposId, '7:base');
testlib.assertEquals(withSecret.payload.secret, 'hunter2');
local noSecret = sandbox.buildHello('7:base', nil, 'mid');
testlib.assertEquals(noSecret.payload.secret, nil);
local emptySecret = sandbox.buildHello('7:base', '', 'mid');
testlib.assertEquals(emptySecret.payload.secret, nil);
end);
-- onMessage dispatch ------------------------------------------------------
testlib.test('onMessage replies to a reverse ping', function()
local sandbox = createSandbox();
local sent = {};
sandbox.onMessage(encode({ event = 'gateway_request', type = 'ping', messageId = 'g-1' }), {
traposId = '7:base',
send = function(frame) sent[#sent + 1] = frame; end,
});
testlib.assertEquals(#sent, 1);
testlib.assertEquals(sent[1].event, 'computer_response');
testlib.assertEquals(sent[1].type, 'ping');
testlib.assertEquals(sent[1].ok, true);
testlib.assertEquals(sent[1].messageId, 'g-1');
testlib.assertEquals(sent[1].traposSenderId, '7:base');
testlib.assertEquals(sent[1].payload.traposId, '7:base');
end);
testlib.test('onMessage rejects unknown reverse type with unknown_type', function()
local sandbox = createSandbox();
local sent = {};
sandbox.onMessage(encode({ event = 'gateway_request', type = 'dance', messageId = 'g-2' }), {
traposId = '7:base',
send = function(frame) sent[#sent + 1] = frame; end,
});
testlib.assertEquals(#sent, 1);
testlib.assertEquals(sent[1].ok, false);
testlib.assertEquals(sent[1].error.code, 'unknown_type');
end);
testlib.test('onMessage drives hello via onHello', function()
local sandbox = createSandbox();
local calls = {};
sandbox.onMessage(encode({ event = 'gateway_response', type = 'hello', ok = true, payload = { accountId = 'local' } }), {
onHello = function(ok, payload) calls[#calls + 1] = { ok = ok, payload = payload }; end,
});
testlib.assertEquals(#calls, 1);
testlib.assertEquals(calls[1].ok, true);
testlib.assertEquals(calls[1].payload.accountId, 'local');
end);
testlib.test('onMessage queues a reply for a non-hello gateway_response', function()
local sandbox = createSandbox();
local queued = {};
sandbox.onMessage(encode({
event = 'gateway_response', type = 'probe_gateway', ok = true,
messageId = 'mid-9', payload = { sandboxOk = true },
}), {
queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
testlib.assertEquals(#queued, 1);
testlib.assertEquals(queued[1][1], 'trapos_sandbox_reply');
testlib.assertEquals(queued[1][2], 'mid-9');
testlib.assertEquals(queued[1][3], true);
testlib.assertEquals(queued[1][4].sandboxOk, true);
end);
testlib.test('onMessage coerces a missing payload to an empty table', function()
local sandbox = createSandbox();
local queued = {};
sandbox.onMessage(encode({ event = 'gateway_response', type = 'noop', ok = true, messageId = 'mid-x' }), {
queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
testlib.assertEquals(#queued, 1);
testlib.assertEquals(type(queued[1][4]), 'table');
testlib.assertEquals(next(queued[1][4]), nil);
end);
testlib.test('onMessage stays silent on malformed frames', function()
local sandbox = createSandbox();
local sent = 0;
local ctx = {
traposId = '7:base',
send = function() sent = sent + 1; end,
queueEvent = function() sent = sent + 1; end,
onHello = function() sent = sent + 1; end,
};
sandbox.onMessage('not json', ctx);
sandbox.onMessage(encode({ event = 'computer_request', type = 'ping' }), ctx);
testlib.assertEquals(sent, 0);
end);
-- startSession wiring -----------------------------------------------------
local function startSession(extra)
local sandbox = createSandbox();
local el = fakes.fakeEventloop();
local http = fakes.fakeHttp();
local queued = {};
local opts = {
eventloop = el,
http = http,
url = 'ws://host:4444',
os = fakeOs(7, 'base'),
queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
};
if extra then
for k, v in pairs(extra) do opts[k] = v; end
end
local session = sandbox.startSession(opts);
return { sandbox = sandbox, el = el, http = http, queued = queued, session = session, url = http.connects[1] };
end
testlib.test('startSession connects to the /gateway path', function()
local ctx = startSession();
testlib.assertEquals(ctx.url, 'ws://host:4444/gateway');
end);
testlib.test('startSession sends hello on open and goes ready on ack', function()
local ctx = startSession({ secret = 'hunter2' });
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
testlib.assertEquals(ctx.session.isReady(), false);
testlib.assertEquals(#ws.sent, 1);
local hello = textutils.unserializeJSON(ws.sent[1]);
testlib.assertEquals(hello.type, 'hello');
testlib.assertEquals(hello.payload.secret, 'hunter2');
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'gateway_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
local connected = ctx.queued[#ctx.queued];
testlib.assertEquals(connected[1], 'trapos_sandbox_connected');
testlib.assertEquals(connected[2], '7:base');
end);
testlib.test('startSession treats unauthorized hello as terminal', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
local timersAfterOpen = #ctx.el.timers; -- hello watchdog
ctx.el.fire('websocket_message', ctx.url, encode({
event = 'gateway_response', type = 'hello', ok = false, error = { code = 'unauthorized', message = 'bad secret' },
}));
testlib.assertEquals(ctx.session.isReady(), false);
testlib.assertEquals(ctx.session.lastError(), 'unauthorized');
testlib.assertEquals(ws.closed, true);
-- A subsequent close must NOT schedule a reconnect.
ctx.el.fire('websocket_closed', ctx.url);
testlib.assertEquals(#ctx.el.timers, timersAfterOpen);
end);
testlib.test('startSession reconnects on a network drop', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
testlib.assertEquals(#ctx.http.connects, 1);
ctx.el.fire('websocket_closed', ctx.url);
ctx.el.fireTimer(#ctx.el.timers); -- the reconnect timer
testlib.assertEquals(#ctx.http.connects, 2);
end);
testlib.test('startSession reconnects on an abnormal close with a non-displaced code', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
testlib.assertEquals(#ctx.http.connects, 1);
ctx.el.fire('websocket_closed', ctx.url, '', 1006); -- abnormal close, not a displacement
ctx.el.fireTimer(#ctx.el.timers);
testlib.assertEquals(#ctx.http.connects, 2);
end);
testlib.test('startSession treats a displacement close (4000) as terminal', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'gateway_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
local timersBefore = #ctx.el.timers;
ctx.el.fire('websocket_closed', ctx.url, 'displaced', 4000);
testlib.assertEquals(ctx.session.isReady(), false);
testlib.assertEquals(ctx.session.lastError(), 'displaced');
-- No reconnect timer scheduled, and no further connect attempt even if one fired.
testlib.assertEquals(#ctx.el.timers, timersBefore);
testlib.assertEquals(#ctx.http.connects, 1);
end);
testlib.test('startSession treats a displacement close by reason as terminal', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
local timersBefore = #ctx.el.timers;
ctx.el.fire('websocket_closed', ctx.url, 'displaced'); -- reason only, code absent
testlib.assertEquals(ctx.session.lastError(), 'displaced');
testlib.assertEquals(#ctx.el.timers, timersBefore);
testlib.assertEquals(#ctx.http.connects, 1);
end);
testlib.test('hello watchdog closes a socket stuck awaiting ack', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
ctx.el.fireTimer(#ctx.el.timers); -- watchdog fires while still awaiting_hello
testlib.assertEquals(ws.closed, true);
end);
testlib.test('send handler replies not_connected when not ready', function()
local ctx = startSession();
ctx.el.fire('trapos_sandbox_send', 'mid-1', 'probe_gateway', nil);
local reply = ctx.queued[#ctx.queued];
testlib.assertEquals(reply[1], 'trapos_sandbox_reply');
testlib.assertEquals(reply[2], 'mid-1');
testlib.assertEquals(reply[3], false);
testlib.assertEquals(reply[5].code, 'not_connected');
end);
testlib.test('send handler forwards a request when ready without an immediate reply', function()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'gateway_response', type = 'hello', ok = true, payload = {} }));
local queuedBefore = #ctx.queued;
ctx.el.fire('trapos_sandbox_send', 'mid-2', 'probe_gateway', nil);
testlib.assertEquals(#ctx.queued, queuedBefore); -- no immediate reply
local frame = textutils.unserializeJSON(ws.sent[#ws.sent]);
testlib.assertEquals(frame.event, 'computer_request');
testlib.assertEquals(frame.type, 'probe_gateway');
testlib.assertEquals(frame.messageId, 'mid-2');
end);
-- request() ---------------------------------------------------------------
testlib.test('request resolves on a matching reply', function()
local sandbox = createSandbox();
local events = fakes.fakeEvents({
{ 'trapos_sandbox_reply', 'mid', true, { sandboxOk = true }, n = 4 },
});
local ok, payload = sandbox.request('probe_gateway', nil, {
messageId = 'mid', queueEvent = events.queueEvent, pullEvent = events.pullEvent,
startTimer = events.startTimer, cancelTimer = events.cancelTimer,
});
testlib.assertEquals(ok, true);
testlib.assertEquals(payload.sandboxOk, true);
testlib.assertEquals(events.cancelled[1], 'timer-1');
end);
testlib.test('request returns the error on a failed reply', function()
local sandbox = createSandbox();
local events = fakes.fakeEvents({
{ 'trapos_sandbox_reply', 'mid', false, nil, { code = 'unauthorized', message = 'x' }, n = 5 },
});
local ok, err = sandbox.request('probe_gateway', nil, {
messageId = 'mid', queueEvent = events.queueEvent, pullEvent = events.pullEvent,
startTimer = events.startTimer, cancelTimer = events.cancelTimer,
});
testlib.assertEquals(ok, false);
testlib.assertEquals(err.code, 'unauthorized');
end);
testlib.test('request re-queues a mismatched reply and keeps waiting', function()
local sandbox = createSandbox();
local events = fakes.fakeEvents({
{ 'trapos_sandbox_reply', 'other', true, { y = 2 }, n = 4 },
{ 'trapos_sandbox_reply', 'mid', true, { x = 1 }, n = 4 },
});
local ok, payload = sandbox.request('probe_gateway', nil, {
messageId = 'mid', queueEvent = events.queueEvent, pullEvent = events.pullEvent,
startTimer = events.startTimer, cancelTimer = events.cancelTimer,
});
testlib.assertEquals(ok, true);
testlib.assertEquals(payload.x, 1);
-- queued[1] is the trapos_sandbox_send; queued[2] is the re-queued reply.
testlib.assertEquals(events.queued[2][1], 'trapos_sandbox_reply');
testlib.assertEquals(events.queued[2][2], 'other');
end);
testlib.test('request times out when the timer fires first', function()
local sandbox = createSandbox();
local events = fakes.fakeEvents({
{ 'timer', 'timer-1', n = 2 },
});
local ok, err = sandbox.request('probe_gateway', nil, {
messageId = 'mid', queueEvent = events.queueEvent, pullEvent = events.pullEvent,
startTimer = events.startTimer, cancelTimer = events.cancelTimer,
});
testlib.assertEquals(ok, false);
testlib.assertEquals(err.code, 'timeout');
end);
testlib.run();