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); 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();