cc-libs/packages/trapos-cloud/tests/cloud.lua

731 lines
30 KiB
Lua

local createLibTest = require('/apis/libtest');
local createCloud = require('/apis/libcloud');
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 cloud = createCloud();
testlib.assertEquals(cloud.traposId(fakeOs(7, 'base')), '7:base');
end);
testlib.test('traposId renders empty/nil label as trailing colon', function()
local cloud = createCloud();
testlib.assertEquals(cloud.traposId(fakeOs(7, nil)), '7:');
testlib.assertEquals(cloud.traposId(fakeOs(8, '')), '8:');
end);
-- uuid --------------------------------------------------------------------
testlib.test('uuid emits a v4-shaped string', function()
local cloud = createCloud();
local id = cloud.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 cloud = createCloud();
local frame = cloud.buildComputerRequest('7:base', 'mid-1', 'probe_server', nil);
testlib.assertEquals(frame.event, 'computer_request');
testlib.assertEquals(frame.traposSenderId, '7:base');
testlib.assertEquals(frame.messageId, 'mid-1');
testlib.assertEquals(frame.type, 'probe_server');
testlib.assertEquals(frame.payload, nil);
local withPayload = cloud.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 cloud = createCloud();
local withSecret = cloud.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 = cloud.buildHello('7:base', nil, 'mid');
testlib.assertEquals(noSecret.payload.secret, nil);
local emptySecret = cloud.buildHello('7:base', '', 'mid');
testlib.assertEquals(emptySecret.payload.secret, nil);
end);
-- onMessage dispatch ------------------------------------------------------
testlib.test('onMessage replies to a reverse ping', function()
local cloud = createCloud();
local sent = {};
cloud.onMessage(encode({ event = 'server_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 cloud = createCloud();
local sent = {};
cloud.onMessage(encode({ event = 'server_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 routes a served reverse type to trapos_cloud_request', function()
local cloud = createCloud();
local requested = {};
cloud.onMessage(encode({
event = 'server_request', type = 'exec-lua', messageId = 'g-3', payload = { code = 'x' },
}), {
traposId = '7:base',
send = function() error('served type must not reply directly', 0); end,
isServed = function(t) return t == 'exec-lua'; end,
queueRequest = function(messageId, msgType, payload)
requested[#requested + 1] = { messageId = messageId, msgType = msgType, payload = payload };
end,
});
testlib.assertEquals(#requested, 1);
testlib.assertEquals(requested[1].messageId, 'g-3');
testlib.assertEquals(requested[1].msgType, 'exec-lua');
testlib.assertEquals(requested[1].payload.code, 'x');
end);
testlib.test('onMessage replies unknown_type for a non-served reverse type', function()
local cloud = createCloud();
local sent = {};
cloud.onMessage(encode({ event = 'server_request', type = 'exec-lua', messageId = 'g-4' }), {
traposId = '7:base',
send = function(frame) sent[#sent + 1] = frame; end,
isServed = function() return false; end,
queueRequest = function() error('non-served type must not route', 0); 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 cloud = createCloud();
local calls = {};
cloud.onMessage(encode({ event = 'server_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 server_response', function()
local cloud = createCloud();
local queued = {};
cloud.onMessage(encode({
event = 'server_response', type = 'probe_server', ok = true,
messageId = 'mid-9', payload = { serverOk = true },
}), {
queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
testlib.assertEquals(#queued, 1);
testlib.assertEquals(queued[1][1], 'trapos_cloud_reply');
testlib.assertEquals(queued[1][2], 'mid-9');
testlib.assertEquals(queued[1][3], true);
testlib.assertEquals(queued[1][4].serverOk, true);
end);
testlib.test('onMessage coerces a missing payload to an empty table', function()
local cloud = createCloud();
local queued = {};
cloud.onMessage(encode({ event = 'server_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 cloud = createCloud();
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,
};
cloud.onMessage('not json', ctx);
cloud.onMessage(encode({ event = 'computer_request', type = 'ping' }), ctx);
testlib.assertEquals(sent, 0);
end);
-- startSession wiring -----------------------------------------------------
local function startSession(extra)
local cloud = createCloud();
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 = cloud.startSession(opts);
return { cloud = cloud, 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 = 'server_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
local connected = ctx.queued[#ctx.queued];
testlib.assertEquals(connected[1], 'trapos_cloud_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 = 'server_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 reconnect event reloads the secret immediately', function()
local secret = 'old';
local ctx = startSession({ getSecret = function() return secret; end });
local ws1 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws1);
local hello1 = textutils.unserializeJSON(ws1.sent[1]);
testlib.assertEquals(hello1.payload.secret, 'old');
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
secret = 'new';
ctx.el.fire('trapos_cloud_reconnect', 'login');
testlib.assertEquals(ws1.closed, true);
testlib.assertEquals(#ctx.http.connects, 2);
local ws2 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws2);
local hello2 = textutils.unserializeJSON(ws2.sent[1]);
testlib.assertEquals(hello2.payload.secret, 'new');
end);
testlib.test('startSession reconnect event suppresses the intentional close', function()
local ctx = startSession();
local ws1 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws1);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
ctx.el.fire('trapos_cloud_reconnect', 'login');
local ws2 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws2);
local timersAfterOpen = #ctx.el.timers;
ctx.el.fire('websocket_closed', ctx.url, 'manual close');
testlib.assertEquals(#ctx.el.timers, timersAfterOpen);
testlib.assertEquals(#ctx.http.connects, 2);
end);
testlib.test('startSession reconnect event recovers after unauthorized', function()
local secret = 'old';
local ctx = startSession({ getSecret = function() return secret; end });
local ws1 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws1);
ctx.el.fire('websocket_message', ctx.url, encode({
event = 'server_response', type = 'hello', ok = false, error = { code = 'unauthorized', message = 'bad secret' },
}));
testlib.assertEquals(ctx.session.lastError(), 'unauthorized');
secret = 'new';
ctx.el.fire('trapos_cloud_reconnect', 'login');
testlib.assertEquals(ctx.session.lastError(), nil);
testlib.assertEquals(#ctx.http.connects, 2);
local ws2 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws2);
local hello2 = textutils.unserializeJSON(ws2.sent[1]);
testlib.assertEquals(hello2.payload.secret, 'new');
end);
-- login verification: candidate secret + correlation token ----------------
testlib.test('a verify reconnect sends the candidate secret in the next hello', function()
local ctx = startSession({ getSecret = function() return 'saved'; end });
ctx.el.fire('trapos_cloud_reconnect', 'login-verify', 'candidate', 'tok-1');
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
testlib.assertEquals(textutils.unserializeJSON(ws.sent[1]).payload.secret, 'candidate');
end);
testlib.test('a verified hello echoes the token, clears it, and keeps the candidate secret', function()
local ctx = startSession({ getSecret = function() return 'saved'; end });
ctx.el.fire('trapos_cloud_reconnect', 'login-verify', 'candidate', 'tok-9');
local ws1 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws1);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
local connected = ctx.queued[#ctx.queued];
testlib.assertEquals(connected[1], 'trapos_cloud_connected');
testlib.assertEquals(connected[2], '7:base');
testlib.assertEquals(connected[3], 'tok-9');
-- A later natural reconnect still uses the retained candidate, but emits no token.
ctx.el.fire('websocket_closed', ctx.url);
ctx.el.fireTimer(#ctx.el.timers);
local ws2 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws2);
testlib.assertEquals(textutils.unserializeJSON(ws2.sent[1]).payload.secret, 'candidate');
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
local connected2 = ctx.queued[#ctx.queued];
testlib.assertEquals(connected2[1], 'trapos_cloud_connected');
testlib.assertEquals(connected2[3], nil);
end);
testlib.test('a rejected hello emits trapos_cloud_unauthorized with the verify token', function()
local ctx = startSession();
ctx.el.fire('trapos_cloud_reconnect', 'login-verify', 'candidate', 'tok-bad');
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
ctx.el.fire('websocket_message', ctx.url, encode({
event = 'server_response', type = 'hello', ok = false, error = { code = 'unauthorized', message = 'bad' },
}));
local evt = ctx.queued[#ctx.queued];
testlib.assertEquals(evt[1], 'trapos_cloud_unauthorized');
testlib.assertEquals(evt[2], '7:base');
testlib.assertEquals(evt[3], 'unauthorized');
testlib.assertEquals(evt[4], 'tok-bad');
testlib.assertEquals(ctx.session.lastError(), 'unauthorized');
end);
testlib.test('a plain reconnect clears the candidate override and falls back to the saved secret', function()
local ctx = startSession({ getSecret = function() return 'saved'; end });
ctx.el.fire('trapos_cloud_reconnect', 'login-verify', 'candidate', 'tok');
local ws1 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws1);
testlib.assertEquals(textutils.unserializeJSON(ws1.sent[1]).payload.secret, 'candidate');
ctx.el.fire('trapos_cloud_reconnect', 'login-restore');
local ws2 = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws2);
testlib.assertEquals(textutils.unserializeJSON(ws2.sent[1]).payload.secret, 'saved');
end);
testlib.test('the candidate override survives a websocket failure retry', function()
local ctx = startSession({ getSecret = function() return 'saved'; end });
ctx.el.fire('trapos_cloud_reconnect', 'login-verify', 'candidate', 'tok');
ctx.el.fire('websocket_failure', ctx.url); -- first attempt fails before hello
ctx.el.fireTimer(#ctx.el.timers); -- retry connect()
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
testlib.assertEquals(textutils.unserializeJSON(ws.sent[1]).payload.secret, 'candidate');
end);
testlib.test('startSession logs the reconnect loop once on entry and once on recovery', function()
local logs = {};
local function countMsg(msg)
local n = 0;
for _, entry in ipairs(logs) do
if entry.msg == msg then n = n + 1; end
end
return n;
end
local ctx = startSession({
log = function(level, msg, fields) logs[#logs + 1] = { level = level, msg = msg, fields = fields }; end,
});
-- Healthy connect: no reconnect-loop logs yet.
ctx.el.fire('websocket_success', ctx.url, fakes.fakeWs());
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
testlib.assertEquals(countMsg('reconnect loop started'), 0);
-- Drop, then fail the retry several times: the loop is announced exactly once and the
-- per-attempt chatter (connecting / connection failed) does not repeat while looping.
ctx.el.fire('websocket_closed', ctx.url);
local connectingBefore = countMsg('connecting');
for _ = 1, 3 do
ctx.el.fireTimer(#ctx.el.timers); -- reconnect timer -> connect()
ctx.el.fire('websocket_failure', ctx.url); -- retry fails, schedules the next timer
end
testlib.assertEquals(countMsg('reconnect loop started'), 1);
testlib.assertEquals(countMsg('connection closed'), 1); -- the first drop logged, not the retries
testlib.assertEquals(countMsg('connecting'), connectingBefore); -- suppressed while looping
testlib.assertEquals(countMsg('connection failed'), 0); -- suppressed while looping
-- Recovery: a single "reconnected" line, no second "loop started".
ctx.el.fireTimer(#ctx.el.timers); -- reconnect timer -> connect()
ctx.el.fire('websocket_success', ctx.url, fakes.fakeWs());
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
testlib.assertEquals(countMsg('reconnect loop ended; reconnected'), 1);
testlib.assertEquals(countMsg('reconnect loop started'), 1);
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 = 'server_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);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_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'); -- 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('startSession ignores a displacement of an already-superseded socket', function()
-- A 4000 close that arrives before our reconnect completes hello is for a socket
-- our own reconnect superseded; it must not brick the daemon into 'displaced'.
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
-- still awaiting_hello (no ack yet): a stale displacement arrives.
ctx.el.fire('websocket_closed', ctx.url, 'displaced', 4000);
testlib.assertEquals(ctx.session.lastError(), nil);
-- Not terminal: a hello ack on the live socket still brings us to ready.
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
testlib.assertEquals(ctx.session.isReady(), true);
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_cloud_send', 'mid-1', 'probe_server', nil);
local reply = ctx.queued[#ctx.queued];
testlib.assertEquals(reply[1], 'trapos_cloud_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 = 'server_response', type = 'hello', ok = true, payload = {} }));
local queuedBefore = #ctx.queued;
ctx.el.fire('trapos_cloud_send', 'mid-2', 'probe_server', 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_server');
testlib.assertEquals(frame.messageId, 'mid-2');
end);
-- request() ---------------------------------------------------------------
testlib.test('request resolves on a matching reply', function()
local cloud = createCloud();
local events = fakes.fakeEvents({
{ 'trapos_cloud_reply', 'mid', true, { serverOk = true }, n = 4 },
});
local ok, payload = cloud.request('probe_server', nil, {
messageId = 'mid', queueEvent = events.queueEvent, pullEvent = events.pullEvent,
startTimer = events.startTimer, cancelTimer = events.cancelTimer,
});
testlib.assertEquals(ok, true);
testlib.assertEquals(payload.serverOk, true);
testlib.assertEquals(events.cancelled[1], 'timer-1');
end);
testlib.test('request returns the error on a failed reply', function()
local cloud = createCloud();
local events = fakes.fakeEvents({
{ 'trapos_cloud_reply', 'mid', false, nil, { code = 'unauthorized', message = 'x' }, n = 5 },
});
local ok, err = cloud.request('probe_server', 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 cloud = createCloud();
local events = fakes.fakeEvents({
{ 'trapos_cloud_reply', 'other', true, { y = 2 }, n = 4 },
{ 'trapos_cloud_reply', 'mid', true, { x = 1 }, n = 4 },
});
local ok, payload = cloud.request('probe_server', 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_cloud_send; queued[2] is the re-queued reply.
testlib.assertEquals(events.queued[2][1], 'trapos_cloud_reply');
testlib.assertEquals(events.queued[2][2], 'other');
end);
testlib.test('request times out when the timer fires first', function()
local cloud = createCloud();
local events = fakes.fakeEvents({
{ 'timer', 'timer-1', n = 2 },
});
local ok, err = cloud.request('probe_server', 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);
-- daemon-side serve/respond wiring ----------------------------------------
local function readySession()
local ctx = startSession();
local ws = fakes.fakeWs();
ctx.el.fire('websocket_success', ctx.url, ws);
ctx.el.fire('websocket_message', ctx.url, encode({ event = 'server_response', type = 'hello', ok = true, payload = {} }));
ctx.ws = ws;
return ctx;
end
testlib.test('startSession routes an advertised reverse type to trapos_cloud_request', function()
local ctx = readySession();
ctx.el.fire('trapos_cloud_serve', 'exec-lua');
local sentBefore = #ctx.ws.sent;
ctx.el.fire('websocket_message', ctx.url, encode({
event = 'server_request', type = 'exec-lua', messageId = 'g-9', payload = { code = 'x' },
}));
-- No immediate reply on the wire; a request event is queued for the server instead.
testlib.assertEquals(#ctx.ws.sent, sentBefore);
local req = ctx.queued[#ctx.queued];
testlib.assertEquals(req[1], 'trapos_cloud_request');
testlib.assertEquals(req[2], 'g-9');
testlib.assertEquals(req[3], 'exec-lua');
testlib.assertEquals(req[4].code, 'x');
end);
testlib.test('startSession replies unknown_type for an unadvertised reverse type', function()
local ctx = readySession();
ctx.el.fire('websocket_message', ctx.url, encode({
event = 'server_request', type = 'exec-lua', messageId = 'g-10',
}));
local frame = textutils.unserializeJSON(ctx.ws.sent[#ctx.ws.sent]);
testlib.assertEquals(frame.event, 'computer_response');
testlib.assertEquals(frame.ok, false);
testlib.assertEquals(frame.error.code, 'unknown_type');
end);
testlib.test('respond handler sends a computer_response when ready', function()
local ctx = readySession();
local sentBefore = #ctx.ws.sent;
ctx.el.fire('trapos_cloud_respond', 'g-11', 'exec-lua', true, { ok = true, output = 'hi' }, nil);
testlib.assertEquals(#ctx.ws.sent, sentBefore + 1);
local frame = textutils.unserializeJSON(ctx.ws.sent[#ctx.ws.sent]);
testlib.assertEquals(frame.event, 'computer_response');
testlib.assertEquals(frame.type, 'exec-lua');
testlib.assertEquals(frame.messageId, 'g-11');
testlib.assertEquals(frame.ok, true);
testlib.assertEquals(frame.payload.output, 'hi');
end);
testlib.test('respond handler drops the reply when not connected', function()
local ctx = startSession(); -- never reaches 'ready'
-- Must not raise even though there is no live socket to send on.
ctx.el.fire('trapos_cloud_respond', 'g-12', 'exec-lua', true, { ok = true }, nil);
testlib.assertEquals(ctx.session.isReady(), false);
end);
-- serve() / respond() helpers ---------------------------------------------
testlib.test('respond queues a trapos_cloud_respond event', function()
local cloud = createCloud();
local queued = {};
cloud.respond('mid-1', 'exec-lua', true, { ok = true }, nil, {
queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
testlib.assertEquals(#queued, 1);
testlib.assertEquals(queued[1][1], 'trapos_cloud_respond');
testlib.assertEquals(queued[1][2], 'mid-1');
testlib.assertEquals(queued[1][3], 'exec-lua');
testlib.assertEquals(queued[1][4], true);
testlib.assertEquals(queued[1][5].ok, true);
end);
testlib.test('serve advertises the type and auto-responds on success', function()
local cloud = createCloud();
local el = fakes.fakeEventloop();
local queued = {};
cloud.serve('exec-lua', function(payload)
return true, { echoed = payload.code };
end, { eventloop = el, queueEvent = function(...) queued[#queued + 1] = table.pack(...); end });
testlib.assertEquals(queued[1][1], 'trapos_cloud_serve');
testlib.assertEquals(queued[1][2], 'exec-lua');
el.fire('trapos_cloud_request', 'mid-7', 'exec-lua', { code = 'print(1)' });
local respond = queued[#queued];
testlib.assertEquals(respond[1], 'trapos_cloud_respond');
testlib.assertEquals(respond[2], 'mid-7');
testlib.assertEquals(respond[4], true);
testlib.assertEquals(respond[5].echoed, 'print(1)');
end);
testlib.test('serve ignores requests for other types', function()
local cloud = createCloud();
local el = fakes.fakeEventloop();
local queued = {};
cloud.serve('exec-lua', function() return true, {}; end, {
eventloop = el, queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
local before = #queued;
el.fire('trapos_cloud_request', 'mid', 'write-file', {});
testlib.assertEquals(#queued, before); -- no respond queued
end);
testlib.test('serve responds with the error table on a failure result', function()
local cloud = createCloud();
local el = fakes.fakeEventloop();
local queued = {};
cloud.serve('exec-lua', function() return false, { code = 'internal', message = 'nope' }; end, {
eventloop = el, queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
el.fire('trapos_cloud_request', 'mid', 'exec-lua', {});
local respond = queued[#queued];
testlib.assertEquals(respond[4], false);
testlib.assertEquals(respond[6].code, 'internal');
end);
testlib.test('serve reports a thrown handler as an internal error', function()
local cloud = createCloud();
local el = fakes.fakeEventloop();
local queued = {};
cloud.serve('exec-lua', function() error('boom', 0); end, {
eventloop = el, queueEvent = function(...) queued[#queued + 1] = table.pack(...); end,
});
el.fire('trapos_cloud_request', 'mid', 'exec-lua', {});
local respond = queued[#queued];
testlib.assertEquals(respond[4], false);
testlib.assertEquals(respond[6].code, 'internal');
testlib.assertTrue(string.find(respond[6].message, 'boom', 1, true) ~= nil);
end);
testlib.run();