752 lines
30 KiB
Lua
752 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 retries when websocketAsync throws', function()
|
|
local http = { connects = {} };
|
|
function http.websocketAsync(url)
|
|
http.connects[#http.connects + 1] = url;
|
|
error('open boom', 0);
|
|
end
|
|
function http.websocket()
|
|
return nil;
|
|
end
|
|
|
|
local ctx = startSession({ http = http });
|
|
|
|
testlib.assertEquals(ctx.session.isReady(), false);
|
|
testlib.assertEquals(#http.connects, 1);
|
|
testlib.assertEquals(#ctx.el.timers, 1);
|
|
|
|
ctx.el.fireTimer(#ctx.el.timers);
|
|
testlib.assertEquals(#http.connects, 2);
|
|
testlib.assertEquals(#ctx.el.timers, 2);
|
|
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();
|