// SPDX-License-Identifier: LGPL-2.1-or-later import cockpit from "cockpit"; import QUnit from "qunit-tests"; /* Set this to a regexp to ignore that warning once */ function console_ignore_log(exp) { const console_log = console.log; console.log = function() { if (!exp.exec(arguments[0])) console_log.apply(console, arguments); console.log = console_log; }; } /* The other end of the mock websocket */ function MockPeer() { const self = this; const echos = { }; const nulls = { }; const queue = []; let pending = false; cockpit.event_target(this); /* These are events */ self.onopen = function(event) { }; self.onrecv = function(event, channel, payload) { /* A rudimentary echo channel implementation */ if (channel) { if (channel in echos || channel in nulls) queue.push([channel, payload]); } else { const command = JSON.parse(payload); if (command.command === "open") { let reply; if (command.payload === "echo") { echos[command.channel] = true; reply = { command: "ready", channel: command.channel }; } else if (command.payload === "null") { nulls[command.channel] = true; reply = { command: "ready", channel: command.channel }; } else { reply = { command: "close", channel: command.channel, problem: "not-supported", }; } queue.push([null, JSON.stringify(reply)]); } else if (command.command === "close") { delete echos[command.channel]; delete nulls[command.channel]; } } if (queue.length && !pending) { pending = true; window.setTimeout(function() { while (queue.length) { self.send.apply(self, queue.shift()); } pending = false; }, 0); } }; /* Methods filled in by MockWebSocket */ self.send = function(channel, payload) { throw Error("not reached") }; self.close = function(options) { throw Error("not reached") }; } window.mock = { url: "ws://url" }; let force_default_host = null; let mock_peer = new MockPeer(); QUnit.testDone(function() { mock_peer = new MockPeer(); cockpit.transport.close(); }); /* Mock WebSocket */ function MockWebSocket(url, protocol) { if (typeof url != "string") throw Error("WebSocket(@url) is not a string: " + typeof url); if (typeof protocol != "string") throw Error("WebSocket(@protocol) is not a string: " + typeof protocol); this.onopen = function(event) { }; this.onclose = function(event) { }; this.onmessage = function(event) { }; this.onerror = function(event) { }; this.readyState = 0; this.url = url; this.protocol = protocol; this.extensions = ""; this.binaryType = null; const ws = this; const mock = mock_peer; this.send = function(data) { if (typeof data != "string") throw Error("WebSocket.send(@data) is not a string: " + typeof data); const pos = data.indexOf("\n"); if (pos === -1) throw Error("Invalid frame sent to WebSocket: " + data); const channel = data.substring(0, pos); const payload = data.substring(pos + 1); window.setTimeout(function() { mock.dispatchEvent("recv", channel, payload) }, 5); }; this.close = function(code, reason) { if (typeof code != "number" && typeof code != "undefined") throw Error("WebSocket.close(@code) is not a number: " + typeof code); if (typeof reason != "string" && typeof reason != "undefined") throw Error("WebSocket.close(@reason) is not a number: " + typeof reason); if (this.readyState > 1) throw Error("WebSocket.close() called on a closed WebSocket" + this.readyState + " " + code + reason); this.readyState = 3; this.onclose({ name: "close", code: code || 1000, reason, wasClean: true }); }; /* Instantiate the global mock peer */ const sending = []; mock.send = function(channel, payload) { if (!channel) channel = ""; const event = { name: "message", data: channel.toString() + "\n" + payload }; sending.push(event); window.setTimeout(function() { if (ws.readyState === 1) ws.onmessage(sending.shift()); }, 5); }; mock.close = function(options) { if (!options) options = { }; window.setTimeout(function() { ws.close(options.reason ? 1000 : 1011, options.reason || ""); }, 5); }; /* Open shortly */ window.setTimeout(function() { ws.readyState = 1; mock.dispatchEvent("open"); ws.onopen({ name: "open" }); const init = { command: "init", version: 1, "channel-seed": "test", "csrf-token": "the-csrf-token", user: { user: "scruffy", name: "Scruffy the Janitor" }, system: { version: "zero.point.zero", build: "nasty stuff", } }; if (force_default_host) init.host = force_default_host; force_default_host = null; ws.onmessage({ data: "\n" + JSON.stringify(init) }); }, 5); } WebSocket = MockWebSocket; // eslint-disable-line no-global-assign function check_transport (assert, base_url, application, socket, url_root) { const old_url = window.mock.url; const arr = [base_url]; if (base_url.slice(-1) === '/') arr.push(base_url + "other"); else arr.push(base_url + '/', base_url + '/other'); window.mock.url = null; window.mock.url_root = url_root; for (let i = 0; i < arr.length; i++) { window.mock.pathname = arr[i]; assert.equal(cockpit.transport.application(), application, arr[i] + " transport.application is " + socket); assert.equal(cockpit.transport.uri(), "ws://" + window.location.host + socket, arr[i] + " transport.uri is " + socket); } window.mock.url = old_url; window.mock.url_root = null; window.mock.pathname = null; } QUnit.test("public api", function (assert) { const channel = cockpit.channel({ host: "host.example.com" }); assert.equal(typeof channel, "object", "cockpit.channel() constructor"); assert.equal(channel.options.host, "host.example.com", "channel.options is dict"); assert.ok(channel.id !== undefined, "channel.id is a field"); assert.ok(channel.toString().indexOf("host.example.com") > 0, "channel.toString()"); assert.equal(typeof channel.send, "function", "channel.send() is a function"); assert.equal(typeof channel.close, "function", "channel.close() is a function"); assert.strictEqual(channel.valid, true, "channel.valid is set"); assert.equal(typeof cockpit.logout, "function", "cockpit.logout is a function"); assert.equal(typeof cockpit.transport, "object", "cockpit.transport is an object"); assert.equal(typeof cockpit.transport.close, "function", "cockpit.transport.close is a function"); assert.equal(typeof cockpit.transport.options, "object", "cockpit.transport.options is a object"); if (window.location.origin) assert.equal(cockpit.transport.origin, window.location.origin, "cockpit.transport.origin is correct"); else assert.equal(typeof cockpit.transport.origin, "string", "cockpit.transport.origin is present"); check_transport(assert, '/', 'cockpit', '/cockpit/socket'); check_transport(assert, '/cockpit', 'cockpit', '/cockpit/socket'); check_transport(assert, '/cockpitother/', 'cockpit', '/cockpit/socket'); check_transport(assert, '/cockpita+pplication/', 'cockpit', '/cockpit/socket'); check_transport(assert, '/cockpit+application', 'cockpit+application', '/cockpit+application/socket'); check_transport(assert, '/=machine', 'cockpit+=machine', '/cockpit+=machine/socket'); check_transport(assert, '/url-root', 'cockpit', '/url-root/cockpit/socket', 'url-root'); check_transport(assert, '/url-root/cockpit', 'cockpit', '/url-root/cockpit/socket', 'url-root'); check_transport(assert, '/url-root/cockpit+application', 'cockpit+application', '/url-root/cockpit+application/socket', 'url-root'); check_transport(assert, '/url-root/=machine', 'cockpit+=machine', '/url-root/cockpit+=machine/socket', 'url-root'); }); QUnit.test("open channel", function (assert) { const done = assert.async(); assert.expect(8); const channel = cockpit.channel({ host: "scruffy" }); let is_inited = false; mock_peer.addEventListener("open", function(event) { assert.ok(true, "websocket connected"); }); mock_peer.addEventListener("recv", function(event, chan, payload) { const command = JSON.parse(payload); if (!is_inited) { assert.equal(typeof command, "object", "valid json"); assert.strictEqual(chan, "", "sent with empty channel"); assert.equal(command.command, "init", "got init"); assert.equal(command.version, 1, "got init version"); is_inited = true; } else { assert.equal(command.command, "open", "right command"); assert.strictEqual(command.channel, channel.id, "contains right channel"); assert.equal(command.host, "scruffy", "host as expected"); done(); } }); }); QUnit.test("multiple", function (assert) { const done = assert.async(); assert.expect(1); const channel = cockpit.channel({ host: "scruffy" }); const channelb = cockpit.channel({ host: "amy" }); const onrecv = event => { mock_peer.removeEventListener("recv", onrecv); assert.notStrictEqual(channel.id, channelb.id, "channels have different ids"); done(); }; mock_peer.addEventListener("recv", onrecv); }); QUnit.test("open no host", function (assert) { const done = assert.async(); assert.expect(3); const channel = cockpit.channel({ }); assert.ok(channel); mock_peer.addEventListener("open", function(event) { assert.ok(true, "websocket connected"); }); mock_peer.addEventListener("recv", function(event, chan, payload) { const command = JSON.parse(payload); if (command.command === "open") { assert.strictEqual(command.host, undefined, "host not included"); done(); } }); }); QUnit.test("open auto host", function (assert) { const done = assert.async(); assert.expect(3); force_default_host = "planetexpress"; const channel = cockpit.channel({ }); assert.ok(channel); mock_peer.addEventListener("open", function(event) { assert.ok(true, "websocket connected"); }); mock_peer.addEventListener("recv", function(event, chan, payload) { const command = JSON.parse(payload); if (command.command === "open") { assert.strictEqual(command.host, "planetexpress", "host automatically chosen"); done(); } }); }); QUnit.test("send message", function (assert) { const done = assert.async(); assert.expect(2); const channel = cockpit.channel({ }); mock_peer.addEventListener("open", function(event) { channel.send("Scruffy gonna die the way he lived"); }); mock_peer.addEventListener("recv", function(event, chan, payload) { /* Ignore the open and init messages */ if (!chan) return; assert.strictEqual(chan, channel.id, "sent with correct channel"); assert.equal(payload, "Scruffy gonna die the way he lived", "sent the right payload"); done(); }); }); QUnit.test("queue messages", function (assert) { const done = assert.async(); assert.expect(1); const sentence = []; const channel = cockpit.channel({ }); channel.send("Scruffy"); channel.send("knows"); channel.send("he"); channel.send("rules"); mock_peer.addEventListener("recv", function(event, chan, payload) { if (!chan) return; /* ignore control messages */ sentence.push(payload); if (sentence.length === 4) { assert.equal(sentence.join(" "), "Scruffy knows he rules", "messages queued and sent correctly"); done(); } }); }); QUnit.test("receive message", function (assert) { const done = assert.async(); assert.expect(1); const onrecv = (event, chan, payload) => { const cmd = JSON.parse(payload); if (cmd.command === "open") { mock_peer.removeEventListener("recv", onrecv); mock_peer.send(channel.id, "Oh, marrrrmalade!"); } }; mock_peer.addEventListener("recv", onrecv); const channel = cockpit.channel({ }); channel.addEventListener("message", function(event, message) { assert.equal(message, "Oh, marrrrmalade!", "got right message in channel"); done(); }); }); QUnit.test("close channel", function (assert) { const done = assert.async(2); assert.expect(4); mock_peer.addEventListener("recv", function(event, chan, payload) { const cmd = JSON.parse(payload); if (cmd.command === "init") { return; } else if (cmd.command === "open") { channel.close(); return; } assert.equal(cmd.command, "close", "sent close command"); assert.strictEqual(cmd.channel, channel.id, "correct channel"); mock_peer.send("", payload); done(); }); const channel = cockpit.channel({ }); channel.addEventListener("close", function(event, options) { assert.ok(true, "triggered event"); assert.ok(!options.problem, "no problem"); done(); }); }); QUnit.test("close early", function (assert) { const done = assert.async(); assert.expect(3); const channel = cockpit.channel({ }); channel.addEventListener("close", function(event, options) { assert.ok(true, "triggered event"); assert.equal(options.problem, "yo", "got problem"); done(); }); channel.close("yo"); assert.strictEqual(channel.valid, false, "no longer valid"); }); QUnit.test("close problem", function (assert) { const done = assert.async(); assert.expect(5); mock_peer.addEventListener("recv", function(event, chan, payload) { const cmd = JSON.parse(payload); if (cmd.command === "init") { return; } else if (cmd.command === "open") { channel.close({ problem: "problem" }); assert.strictEqual(channel.valid, false, "no longer valid"); return; } assert.equal(cmd.command, "close", "sent close command"); assert.equal(cmd.problem, "problem", "sent reason"); done(); }); const channel = cockpit.channel({ }); channel.addEventListener("close", function(event, options) { assert.ok(true, "triggered event"); assert.equal(options.problem, "problem", "set"); }); }); QUnit.test("close problem string", function (assert) { const done = assert.async(); assert.expect(5); const channel = cockpit.channel({ }); mock_peer.addEventListener("recv", function(event, chan, payload) { const cmd = JSON.parse(payload); if (cmd.command === "init") { return; } else if (cmd.command === "open") { channel.close("testo"); assert.strictEqual(channel.valid, false, "no longer valid"); return; } assert.equal(cmd.command, "close", "sent close command"); assert.equal(cmd.problem, "testo", "sent reason"); done(); }); channel.addEventListener("close", function(event, options) { assert.ok(true, "triggered event"); assert.equal(options.problem, "testo", "set"); }); }); QUnit.test("close peer", function (assert) { const done = assert.async(); assert.expect(5); mock_peer.addEventListener("recv", function(event, chan, payload) { const msg = JSON.parse(payload); if (msg.command === "init") return; const cmd = { command: "close", channel: channel.id, problem: "marmalade", extra: 5 }; mock_peer.send("", JSON.stringify(cmd)); }); const channel = cockpit.channel({ }); const channelb = cockpit.channel({ }); channel.addEventListener("close", function(event, options) { assert.ok(true, "triggered event"); assert.equal(options.problem, "marmalade", "received reason"); assert.equal(options.extra, 5, "received extra"); assert.strictEqual(channel.valid, false, "became invalid"); assert.strictEqual(channelb.valid, true, "correct channel"); done(); }); }); QUnit.test("close socket", function (assert) { const done = assert.async(); assert.expect(4); const channel = cockpit.channel({ }); const channelb = cockpit.channel({ }); channel.addEventListener("close", function(event, options) { assert.equal(options.problem, "disconnected", "received reason"); assert.strictEqual(channel.valid, false, "channel is invalid"); if (!channel.valid && !channelb.valid) done(); }); channelb.addEventListener("close", function(event, options) { assert.equal(options.problem, "disconnected", "received reason"); assert.strictEqual(channelb.valid, false, "other channel invalid"); if (!channel.valid && !channelb.valid) done(); }); mock_peer.close(); }); QUnit.test("wait ready", function (assert) { const done = assert.async(); assert.expect(5); const channel = cockpit.channel({ payload: "echo" }); channel.wait().then(function(options) { assert.ok(true, "channel is ready"); assert.equal(typeof options, "object", "wait options"); assert.ok(!!options, "wait options not null"); assert.equal(options.command, "ready", "wait is ready"); assert.strictEqual(channel.valid, true, "when valid"); }, function() { assert.ok(false, "should not fail"); }) .always(function() { done(); }); }); QUnit.test("wait close", function (assert) { const done = assert.async(); assert.expect(6); const channel = cockpit.channel({ payload: "unsupported" }); channel.wait().then(function() { assert.ok(false, "should not succeed"); }, function(options) { assert.ok(true, "channel is closed"); assert.equal(typeof options, "object", "wait options"); assert.ok(!!options, "wait options not null"); assert.equal(options.command, "close", "wait is close"); assert.equal(options.problem, "not-supported", "wait options has fields"); assert.strictEqual(channel.valid, false, "channel not valid"); }) .always(function() { done(); }); }); QUnit.test("wait callback", function (assert) { const done = assert.async(); assert.expect(5); const channel = cockpit.channel({ payload: "unsupported" }); channel.wait(function(options) { assert.equal(typeof options, "object", "wait options"); assert.ok(!!options, "wait options not null"); assert.equal(options.command, "close", "wait is close"); assert.equal(options.problem, "not-supported", "wait options has fields"); assert.strictEqual(channel.valid, false, "channel not valid"); }).always(function() { done(); }); }); QUnit.test("logout", function (assert) { const done = assert.async(); mock_peer.addEventListener("recv", function(event, chan, payload) { const cmd = JSON.parse(payload); if (cmd.command === "logout") { mock_peer.close("disconnected"); assert.strictEqual(cmd.disconnect, true, "disconnect set"); } }); let channel = cockpit.channel({ payload: "echo" }); let channelb = cockpit.channel({ payload: "echo" }); channel.addEventListener("close", function(event, options) { assert.equal(options.problem, "disconnected", "received reason"); assert.strictEqual(channel.valid, false, "channel is invalid"); channel = null; if (channel === null && channelb === null) done(); }); channelb.addEventListener("close", function(event, options) { assert.equal(options.problem, "disconnected", "received reason"); assert.strictEqual(channelb.valid, false, "other channel invalid"); channelb = null; if (channel === null && channelb === null) done(); }); cockpit.logout(false); }); QUnit.test("droppriv", function (assert) { const done = assert.async(); assert.expect(1); mock_peer.addEventListener("recv", function(event, chan, payload) { const cmd = JSON.parse(payload); if (cmd.command === "logout") { assert.strictEqual(cmd.disconnect, false, "disconnect not set"); done(); } }); cockpit.drop_privileges(); }); QUnit.test("info", function (assert) { const done = assert.async(); assert.expect(4); let info_changed = false; const onchanged = () => { assert.strictEqual(cockpit.info.version, "zero.point.zero", "cockpit.info.version"); assert.strictEqual(cockpit.info.build, "nasty stuff", "cockpit.info.build"); info_changed = true; }; cockpit.info.addEventListener("changed", onchanged); const onrecv = (event, chan, payload) => { const cmd = JSON.parse(payload); if (cmd.command === "open") { mock_peer.removeEventListener("recv", onrecv); cockpit.info.removeEventListener("changed", onchanged); assert.strictEqual(info_changed, true, "info changed event was called"); done(); } }; mock_peer.addEventListener("recv", onrecv); const channel = cockpit.channel({ host: "scruffy" }); assert.ok(channel); }); QUnit.test("send after close", function (assert) { const done = assert.async(); assert.expect(1); console_ignore_log(/sending message on closed.*/); let received_message = false; const channel = cockpit.channel({ }); mock_peer.addEventListener("recv", function(event, chan, payload) { if (chan) received_message = true; }); channel.close(); channel.send("Dern it."); window.setTimeout(function() { assert.ok(!received_message, "didn't send message"); done(); }, 50); }); QUnit.test("ignore other commands", function (assert) { const done = assert.async(); assert.expect(1); const channel = cockpit.channel({ payload: "echo" }); console_ignore_log(/unhandled control message.*/); mock_peer.send(0, JSON.stringify({ command: "ping" })); mock_peer.send(0, JSON.stringify({ command: "unexpected" })); window.setTimeout(function() { assert.ok(channel.valid, "other messages didn't screw up channel"); done(); }, 50); }); QUnit.test("filter message in", function (assert) { const done = assert.async(); assert.expect(14); let filtered = 0; let filtering = true; cockpit.transport.filter(function(message, channelid, control) { if (!filtering) return true; if (message[0] === '\n') { assert.strictEqual(channelid, "", "control message channel"); assert.equal(typeof control, "object", "control is a JSON object"); assert.equal(typeof control.command, "string", "control has a command"); return true; } else { assert.strictEqual(channelid, channel.id, "cockpit channel id"); assert.equal(control, undefined, "control is undefined"); filtered += 1; return (filtered !== 1); } }); let received = 0; const channel = cockpit.channel({ payload: "echo" }); channel.addEventListener("message", function(data) { received += 1; if (received === 2) { assert.equal(filtered, 3, "filtered right amount"); assert.equal(received, 2, "let through right amount"); channel.close(); filtering = false; done(); } }); channel.send("one"); channel.send("two"); channel.send("three"); }); QUnit.test("inject message out", function (assert) { const done = assert.async(); assert.expect(4); const ret = cockpit.transport.inject("bree\nyellow"); assert.equal(ret, false, "failure returns false"); let first = true; mock_peer.addEventListener("recv", function(event, chan, payload) { if (first) { const ret = cockpit.transport.inject("bree\nyellow"); assert.equal(ret, true, "returned true"); first = false; return; } if (chan) { assert.equal(chan, "bree", "right channel"); assert.equal(payload, "yellow", "right payload"); channel.close(); done(); } }); const channel = cockpit.channel({ }); }); QUnit.test("inject message in", function (assert) { const done = assert.async(); assert.expect(3); const channel = cockpit.channel({ payload: "null" }); channel.addEventListener("control", function(ev, control) { if (control.command === "ready") { const payload = JSON.stringify({ command: "blah", blah: "marmalade", channel: channel.id }); const ret = cockpit.transport.inject("\n" + payload, false); assert.equal(ret, true, "returned true"); } else { assert.equal(control.command, "blah", "got right control message"); assert.equal(control.blah, "marmalade", "got right control data"); channel.close(); done(); } }); }); QUnit.test("transport options", function (assert) { const done = assert.async(); assert.expect(3); mock_peer.addEventListener("recv", function(event, chan, payload) { if (chan) { assert.equal(typeof cockpit.transport.options, "object", "is an object"); assert.deepEqual(cockpit.transport.options, { command: "init", version: 1, "channel-seed": "test", "csrf-token": "the-csrf-token", user: { user: "scruffy", name: "Scruffy the Janitor" }, system: { version: "zero.point.zero", build: "nasty stuff", } }, "is correct"); assert.equal(cockpit.transport.csrf_token, "the-csrf-token", "got csrf token"); channel.close(); done(); } }); const channel = cockpit.channel({ }); channel.send("blah"); }); QUnit.test("message", function (assert) { assert.expect(4); assert.strictEqual(cockpit.message("terminated"), "Your session has been terminated.", "problem code"); assert.strictEqual(cockpit.message({ problem: "timeout" }), "Connection has timed out.", "problem property"); assert.strictEqual(cockpit.message({ message: "The message", problem: "blah" }), "The message", "problem property"); assert.strictEqual(cockpit.message(55), "55", "invalid input"); }); window.location.hash = ""; QUnit.start();