// SPDX-License-Identifier: LGPL-2.1-or-later import cockpit from "cockpit"; import QUnit, { skipWithPybridge } from "qunit-tests"; import { common_dbus_tests, dbus_track_tests } from "./test-dbus-common.js"; /* with a name */ const options = { bus: "session" }; common_dbus_tests(options, "com.redhat.Cockpit.DBusTests.Test"); dbus_track_tests(options, "com.redhat.Cockpit.DBusTests.Test"); QUnit.test("proxy no stutter", function (assert) { const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); const proxy = dbus.proxy(); assert.equal(proxy.iface, "com.redhat.Cockpit.DBusTests.Test", "interface auto chosen"); assert.equal(proxy.path, "/com/redhat/Cockpit/DBusTests/Test", "path auto chosen"); }); QUnit.test("proxies no stutter", function (assert) { const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); const proxies = dbus.proxies(); assert.equal(proxies.iface, "com.redhat.Cockpit.DBusTests.Test", "interface auto chosen"); assert.equal(proxies.path_namespace, "/", "path auto chosen"); }); QUnit.test("exposed client and options", function (assert) { const options = { host: "localhost", bus: "session" }; const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", options); const proxy = dbus.proxy("com.redhat.Cockpit.DBusTests.Frobber", "/otree/frobber"); const proxies = dbus.proxies("com.redhat.Cockpit.DBusTests.Frobber"); assert.deepEqual(dbus.options, options, "client object exposes options"); assert.strictEqual(proxy.client, dbus, "proxy object exposes client"); assert.strictEqual(proxies.client, dbus, "proxies object exposes client"); }); QUnit.test("subscriptions on closed client", function (assert) { function on_signal() { } const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); dbus.close(); const subscription = dbus.subscribe({ interface: "com.redhat.Cockpit.DBusTests.Frobber", path: "/otree/frobber" }, on_signal); assert.ok(subscription, "can subscribe"); subscription.remove(); assert.ok(true, "can unsubscribe"); }); QUnit.test("watch promise recursive", function (assert) { assert.expect(7); const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); const promise = dbus.watch("/otree/frobber"); const target = { }; const promise2 = promise.promise(target); assert.strictEqual(promise2, target, "used target"); assert.equal(typeof promise2.done, "function", "promise2.done()"); assert.equal(typeof promise2.promise, "function", "promise2.promise()"); assert.equal(typeof promise2.remove, "function", "promise2.remove()"); const promise3 = promise2.promise(); assert.equal(typeof promise3.done, "function", "promise3.done()"); assert.equal(typeof promise3.promise, "function", "promise3.promise()"); assert.equal(typeof promise3.remove, "function", "promise3.remove()"); }); QUnit.test("owned messages", function (assert) { const done = assert.async(); assert.expect(9); const name = "yo.x" + new Date().getTime(); let times_changed = 0; const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); let other = null; let org_owner = null; function on_owner (event, owner) { if (times_changed === 0) { assert.strictEqual(typeof owner, "string", "initial owner string"); assert.ok(owner.length > 1, "initial owner not empty"); org_owner = owner; } else if (times_changed === 1) { assert.strictEqual(owner, null, "no owner"); } else if (times_changed === 2) { // owner is the same because the server // dbus connection is too. assert.strictEqual(owner, org_owner, "has owner again"); } times_changed++; } function acquire_name () { dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ClaimOtherName", [name]) .always(function() { assert.equal(this.state(), "resolved", "name claimed"); if (!other) { other = cockpit.dbus(name, { bus: "session" }); other.addEventListener("owner", on_owner); release_name(); } else { assert.strictEqual(times_changed, 3, "owner changed three times"); done(); } }); } function release_name () { other.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["test"]) .always(function() { assert.equal(this.state(), "resolved", "called on other name"); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "ReleaseOtherName", [name]) .always(function() { assert.equal(this.state(), "resolved", "name released"); acquire_name(); }); }); } acquire_name(); }); QUnit.test("owned message for absent service", assert => { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.NotExisting", { bus: "session" }); dbus.addEventListener("owner", (_event, owner) => { assert.strictEqual(owner, null, "no owner"); done(); }); }); skipWithPybridge("bad dbus address", function (assert) { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus(null, { bus: "none", address: "bad" }); dbus.addEventListener("close", (event, options) => { assert.equal(options.problem, "protocol-error", "bad address closed"); done(); }); }); skipWithPybridge("bad dbus bus", function (assert) { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus(null, { bus: "bad" }); dbus.addEventListener("close", (event, options) => { assert.equal(options.problem, "protocol-error", "bad bus format"); done(); }); }); QUnit.test("wait ready", function (assert) { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); dbus.wait().then(function(options) { assert.ok(!!dbus.unique_name, "wait fills unique_name"); }, function() { assert.ok(false, "shouldn't fail"); }) .always(function() { done(); }); }); QUnit.test("wait fail", function (assert) { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.NotExisting", { bus: "session" }); dbus.wait().then(function(options) { assert.ok(false, "shouldn't succeed"); }, function() { assert.ok(true, "should fail"); }) .always(function() { done(); }); }); skipWithPybridge("no default name", function (assert) { const done = assert.async(); assert.expect(1); const dbus = cockpit.dbus(null, { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"], { name: "com.redhat.Cockpit.DBusTests.Test" }) .then(function(reply) { assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"], "replied"); }, function(ex) { assert.ok(false, "shouldn't fail"); }) .always(function() { done(); }); }); skipWithPybridge("no default name bad", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"], { name: 5 }) .then(function(reply) { assert.ok(false, "shouldn't succeed"); }, function(ex) { assert.equal(ex.problem, "protocol-error", "error problem"); assert.equal(ex.message, "the \"name\" field is invalid in dbus call", "error message"); }) .always(function() { done(); }); }); skipWithPybridge("no default name invalid", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"], { name: "!invalid!" }) .then(function(reply) { assert.ok(false, "shouldn't succeed"); }, function(ex) { assert.equal(ex.problem, "protocol-error", "error problem"); assert.equal(ex.message, "the \"name\" field in dbus call is not a valid bus name: !invalid!", "error message"); }) .always(function() { done(); }); }); skipWithPybridge("no default name missing", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"]) .then(function(reply) { assert.ok(false, "shouldn't succeed"); }, function(ex) { assert.equal(ex.problem, "protocol-error", "error problem"); assert.equal(ex.message, "the \"name\" field is missing in dbus call", "error message"); }) .always(function() { done(); }); }); skipWithPybridge("no default name second", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [], { name: "com.redhat.Cockpit.DBusTests.Test" }) .then(function(reply) { assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Test"], "right name"); return dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [], { name: "com.redhat.Cockpit.DBusTests.Second" }) .then(function(reply) { assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Second"], "second name"); }, function(ex) { assert.ok(false, "shouldn't fail"); }); }, function(ex) { console.log(ex); assert.ok(false, "shouldn't fail"); }) .always(function() { done(); }); }); skipWithPybridge("override default name", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus("com.redhat.Cockpit.DBusTests.Test", { bus: "session" }); dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", []) .then(function(reply) { assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Test"], "right name"); return dbus.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "TellMeYourName", [], { name: "com.redhat.Cockpit.DBusTests.Second" }) .then(function(reply) { assert.deepEqual(reply, ["com.redhat.Cockpit.DBusTests.Second"], "second name"); }, function(ex) { assert.ok(false, "shouldn't fail"); }); }, function(ex) { console.log(ex); assert.ok(false, "shouldn't fail"); }) .always(function() { done(); }); }); skipWithPybridge("watch no default name", function (assert) { const done = assert.async(); assert.expect(1); const cache = { }; const dbus = cockpit.dbus(null, { bus: "session" }); dbus.addEventListener("notify", function(event, data) { Object.assign(cache, data); }); dbus.watch({ path: "/otree/frobber", name: "com.redhat.Cockpit.DBusTests.Second" }) .then(function() { assert.equal(typeof cache["/otree/frobber"], "object", "has path"); }, function(ex) { assert.ok(false, "shouldn't fail"); }) .always(function() { dbus.close(); done(); }); }); skipWithPybridge("watch missing name", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session", other: "option" }); dbus.watch("/otree/frobber") .then(function() { assert.ok(false, "shouldn't succeed"); }, function(ex) { assert.equal(ex.problem, "protocol-error", "error problem"); assert.equal(ex.message, "session: no \"name\" specified in match", "error message"); }) .always(function() { dbus.close(); done(); }); }); skipWithPybridge("shared client", function (assert) { const done = assert.async(); assert.expect(2); const dbus1 = cockpit.dbus(null, { bus: "session" }); const dbus2 = cockpit.dbus(null, { bus: "session" }); /* Is identical */ assert.strictEqual(dbus1, dbus2, "shared bus returned"); /* Closing shouldn't close shared */ dbus1.close(); dbus2.call("/otree/frobber", "com.redhat.Cockpit.DBusTests.Frobber", "HelloWorld", ["Browser-side JS"], { name: "com.redhat.Cockpit.DBusTests.Test" }) .then(function(reply) { assert.deepEqual(reply, ["Word! You said `Browser-side JS'. I'm Skeleton, btw!"], "call still works"); }, function(ex) { assert.ok(false, "shouldn't fail"); }) .always(function() { done(); }); }); QUnit.test("not shared option", function (assert) { assert.expect(1); const dbus1 = cockpit.dbus(null, { bus: "session" }); const dbus2 = cockpit.dbus(null, { bus: "session", other: "option" }); /* Should not be identical */ assert.notStrictEqual(dbus1, dbus2, "shared bus returned"); /* Closing shouldn't close shared */ dbus1.close(); dbus2.close(); }); skipWithPybridge("emit signal type", function (assert) { const done = assert.async(); assert.expect(4); let received = false; const dbus = cockpit.dbus(null, { bus: "session", other: "option" }); dbus.wait(function() { dbus.subscribe({ path: "/bork", name: dbus.unique_name }, function(path, iface, signal, args) { assert.equal(path, "/bork", "reflected path"); assert.equal(iface, "borkety.Bork", "reflected interface"); assert.equal(signal, "Bork", "reflected signal"); assert.deepEqual(args, [1, 2, 3, 4, "Bork"], "reflected arguments"); received = true; dbus.close(); done(); }); dbus.addEventListener("close", function(event, ex) { if (!received) { console.log(ex); assert.ok(false, "shouldn't fail"); done(); } }); dbus.signal("/bork", "borkety.Bork", "Bork", [1, 2, 3, 4, "Bork"], { type: "iiiis" }); }); }); skipWithPybridge("emit signal no meta", function (assert) { const done = assert.async(); assert.expect(2); const dbus = cockpit.dbus(null, { bus: "session", other: "option" }); function closed(event, ex) { assert.equal(ex.problem, "protocol-error", "correct problem"); assert.equal(ex.message, "signal argument types for signal borkety.Bork Bork unknown", "correct message"); dbus.removeEventListener("close", closed); dbus.close(); done(); } dbus.addEventListener("close", closed); dbus.signal("/bork", "borkety.Bork", "Bork", [1, 2, 3, 4, "Bork"]); }); async function internal_test(assert, options) { const dbus = cockpit.dbus(null, options); const resp = await dbus.call("/", "org.freedesktop.DBus.Introspectable", "Introspect"); assert.ok(String(resp[0]).indexOf(" internal_test(assert, { bus: "internal" })); skipWithPybridge("internal dbus bus none", async assert => internal_test(assert, { bus: "none" })); skipWithPybridge("internal dbus bus none with address", async assert => internal_test(assert, { bus: "none", address: "internal" })); skipWithPybridge("separate dbus connections for channel groups", function (assert) { const done = assert.async(); assert.expect(4); const channel1 = cockpit.channel({ payload: 'dbus-json3', group: 'foo', bus: 'session' }); const channel2 = cockpit.channel({ payload: 'dbus-json3', group: 'bar', bus: 'session' }); const channel3 = cockpit.channel({ payload: 'dbus-json3', group: 'foo', bus: 'session' }); const channel4 = cockpit.channel({ payload: 'dbus-json3', group: 'baz', bus: 'session' }); Promise.all([ channel1.wait(), channel2.wait(), channel3.wait(), channel4.wait() ]).then(function ([ready1, ready2, ready3, ready4]) { assert.equal(ready1['unique-name'], ready3['unique-name']); assert.notEqual(ready1['unique-name'], ready2['unique-name']); assert.notEqual(ready1['unique-name'], ready4['unique-name']); assert.notEqual(ready2['unique-name'], ready4['unique-name']); done(); }); }); QUnit.test("cockpit.Config internal D-Bus API", async assert => { const dbus = cockpit.dbus(null, { bus: "internal" }); // Get temp config dir to see where to place our test config const reply = await dbus.call("/environment", "org.freedesktop.DBus.Properties", "Get", ["cockpit.Environment", "Variables"]); const configDir = reply[0].v.XDG_CONFIG_DIRS; await cockpit.file(configDir + "/cockpit/cockpit.conf").replace(` [SomeSection] SomeA = one SomethingElse = 2 LargeNum = 12345 [Other] Flavor=chocolate Empty= `); const proxy = dbus.proxy("cockpit.Config", "/config"); await proxy.wait(); await proxy.Reload(); // test GetString() assert.equal(await proxy.GetString("SomeSection", "SomeA"), "one"); assert.equal(await proxy.GetString("Other", "Empty"), ""); // test GetUInt() // this key exists, ignores default assert.equal(await proxy.GetUInt("SomeSection", "SomethingElse", 10, 100, 0), 2); // this key does not exist, return default assert.equal(await proxy.GetUInt("SomeSection", "NotExisting", 10, 100, 0), 10); // out of bounds, clamp to minimum assert.equal(await proxy.GetUInt("SomeSection", "SomethingElse", 42, 50, 5), 5); // out of bounds, clamp to maximum assert.equal(await proxy.GetUInt("SomeSection", "LargeNum", 42, 50, 5), 50); // not an integer value, returns default assert.equal(await proxy.GetUInt("SomeSection", "SomeA", 10, 100, 0), 10); // test GetString with non-existing section assert.rejects(proxy.GetString("UnknownSection", "SomeKey"), /key.*UnknownSection.*not exist/, "unknown section raises an error"); // test GetString with non-existing key in existing section assert.rejects(proxy.GetString("SomeSection", "UnknownKey"), /key.*UnknownKey.*not exist/, "unknown key raises an error"); // empty config await cockpit.file(configDir + "/cockpit/cockpit.conf").replace(""); await proxy.Reload(); assert.rejects(proxy.GetString("SomeSection", "SomeA"), /key.*SomeSection.*not exist/, "query in empty config raises an error"); // broken config (missing section header) await cockpit.file(configDir + "/cockpit/cockpit.conf").replace("SomeA = two"); await proxy.Reload(); assert.rejects(proxy.GetString("SomeSection", "SomeA"), /key.*SomeSection.*not exist/, "query in broken config raises an error"); }); QUnit.test("nonexisting address", async assert => { const dbus = cockpit.dbus("org.freedesktop.DBus", { address: "unix:path=/nonexisting", bus: "none" }); try { await dbus.call("/org/freedesktop/DBus", "org.freedesktop.DBus", "Hello", []); assert.ok(false, "should not be reached"); } catch (ex) { assert.equal(ex.problem, "protocol-error", "got right close code"); assert.equal(ex.message, "failed to connect to none bus: [Errno 2] sd_bus_start: No such file or directory", "error message"); } }); QUnit.start();