From 203385e6e5084399592896da670fc9c4d4a08d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Thu, 9 May 2024 14:20:13 +0100 Subject: [PATCH 01/17] web: fix l10n client tests --- web/src/client/l10n.test.js | 107 +++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/web/src/client/l10n.test.js b/web/src/client/l10n.test.js index 590d177143..faf0c71bac 100644 --- a/web/src/client/l10n.test.js +++ b/web/src/client/l10n.test.js @@ -20,43 +20,110 @@ */ // @ts-check -// cspell:ignore Cestina -import DBusClient from "./dbus"; +import { HTTPClient } from "./http"; import { L10nClient } from "./l10n"; jest.mock("./dbus"); -const L10N_IFACE = "org.opensuse.Agama1.Locale"; +const mockJsonFn = jest.fn(); +const mockGetFn = jest.fn().mockImplementation(() => { + return { ok: true, json: mockJsonFn }; +}); +const mockPatchFn = jest.fn().mockImplementation(() => { + return { ok: true }; +}); + +jest.mock("./http", () => { + return { + HTTPClient: jest.fn().mockImplementation(() => { + return { + get: mockGetFn, + patch: mockPatchFn, + }; + }), + }; +}); -const l10nProxy = { - ListLocales: jest.fn().mockResolvedValue( - [ - ["es_ES.UTF-8", "Spanish", "Spain"], - ["en_US.UTF-8", "English", "United States"] - ] - ), +let client; + +const locales = [ + { + "id": "en_US.UTF-8", + "language": "English", + "territory": "United States", + }, + { + "id": "es_ES.UTF-8", + "language": "Spanish", + "territory": "Spain", + }, +]; + +const config = { + "locales": [ + "en_US.UTF-8", + ], + "keymap": "us", + "timezone": "Europe/Berlin", + "uiLocale": "en_US.UTF-8", + "uiKeymap": "us", }; beforeEach(() => { - // @ts-ignore - DBusClient.mockImplementation(() => { - return { - proxy: (iface) => { - if (iface === L10N_IFACE) return l10nProxy; - } - }; - }); + client = new L10nClient(new HTTPClient(new URL("http://localhost"))); }); describe("#locales", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(locales); + }); + it("returns the list of available locales", async () => { - const client = new L10nClient(); const locales = await client.locales(); expect(locales).toEqual([ + { id: "en_US.UTF-8", name: "English", territory: "United States" }, { id: "es_ES.UTF-8", name: "Spanish", territory: "Spain" }, - { id: "en_US.UTF-8", name: "English", territory: "United States" } ]); + expect(mockGetFn).toHaveBeenCalledWith("/l10n/locales"); + }); +}); + +describe("#getConfig", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(config); + }); + + it("returns the list of selected locales", async () => { + const l10nConfig = await client.getConfig(); + + expect(l10nConfig).toEqual(config); + expect(mockGetFn).toHaveBeenCalledWith("/l10n/config"); + }); +}); + +describe("#setConfig", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(config); + }); + + it("updates the l10n configuration", async () => { + await client.setConfig(config); + client.setConfig(config); + expect(mockPatchFn).toHaveBeenCalledWith("/l10n/config", config); + }); +}); + +describe("#getLocales", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(config); + }); + + it("returns the list of selected locales", async () => { + const locales = await client.getLocales(); + + expect(locales).toEqual(["en_US.UTF-8"]); + expect(mockGetFn).toHaveBeenCalledWith("/l10n/config"); }); }); From d50513b7095c786161a9e310a933f223476057eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:10:38 +0100 Subject: [PATCH 02/17] web: avoid making HTTP calls in questions constructor --- web/src/client/questions.js | 11 +++++++---- web/src/components/questions/Questions.jsx | 4 ++++ web/src/test-utils.js | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/web/src/client/questions.js b/web/src/client/questions.js index d268e6c455..1999d39ba8 100644 --- a/web/src/client/questions.js +++ b/web/src/client/questions.js @@ -55,15 +55,12 @@ class QuestionsClient { */ constructor(client) { this.client = client; + this.listening = false; this.questionIds = []; this.handlers = { added: [], removed: [], }; - this.getQuestions().then((qs) => { - this.questionIds = qs.map((q) => q.id); - }); - this.listenQuestions(); } /** @@ -127,6 +124,12 @@ class QuestionsClient { } async listenQuestions() { + if (this.listening) return; + + this.listening = true; + this.getQuestions().then((qs) => { + this.questionIds = qs.map((q) => q.id); + }); return this.client.onEvent("QuestionsChanged", () => { this.getQuestions().then((qs) => { const updatedIds = qs.map((q) => q.id); diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index 0912fc8aef..569ff12109 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -44,6 +44,10 @@ export default function Questions() { removeQuestion(question.id); }, [client.questions, removeQuestion]); + useEffect(() => client.questions.listenQuestions(), + [client.questions, cancellablePromise] + ); + useEffect(() => { cancellablePromise(client.questions.getQuestions()) .then(setPendingQuestions) diff --git a/web/src/test-utils.js b/web/src/test-utils.js index 00a6aaf4ef..5355bfa2c4 100644 --- a/web/src/test-utils.js +++ b/web/src/test-utils.js @@ -72,7 +72,7 @@ jest.mock('react-router-dom', () => ({ })); const Providers = ({ children, withL10n }) => { - const client = createClient(); + const client = createClient(new URL("https://localhost")); // FIXME: workaround to fix the tests. We should inject // the client instead of mocking `createClient`. From 787eb7eab2bfc9d6daa5d036533a9d920b309e13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:27:49 +0100 Subject: [PATCH 03/17] web: fix InstallationProgress test --- web/src/components/core/InstallationProgress.test.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/components/core/InstallationProgress.test.jsx b/web/src/components/core/InstallationProgress.test.jsx index 234caa5bbb..f21d05a0e4 100644 --- a/web/src/components/core/InstallationProgress.test.jsx +++ b/web/src/components/core/InstallationProgress.test.jsx @@ -28,6 +28,7 @@ import InstallationProgress from "./InstallationProgress"; jest.mock("~/components/core/ProgressReport", () => () =>
ProgressReport Mock
); jest.mock("~/components/core/Sidebar", () => () =>
Agama sidebar
); +jest.mock("~/components/core/Sidebar", () => () =>
Questions Mock
); describe("InstallationProgress", () => { it("uses 'Installing' as title", () => { From c7024f3517be7ff8652e9ac654771870afe868d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:28:47 +0100 Subject: [PATCH 04/17] web: display the language name instead of the id --- web/src/components/overview/L10nSection.jsx | 4 +++- web/src/components/overview/L10nSection.test.jsx | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/web/src/components/overview/L10nSection.jsx b/web/src/components/overview/L10nSection.jsx index cbdbfeb328..38b1980796 100644 --- a/web/src/components/overview/L10nSection.jsx +++ b/web/src/components/overview/L10nSection.jsx @@ -35,7 +35,9 @@ const Content = ({ locales }) => { return ( - {msg1}{`${locale.id} (${locale.territory})`}{msg2} + {msg1} + {`${locale.name} (${locale.territory})`} + {msg2} ); }; diff --git a/web/src/components/overview/L10nSection.test.jsx b/web/src/components/overview/L10nSection.test.jsx index e5da1c1088..b94a79ef99 100644 --- a/web/src/components/overview/L10nSection.test.jsx +++ b/web/src/components/overview/L10nSection.test.jsx @@ -21,7 +21,7 @@ import React from "react"; import { act, screen } from "@testing-library/react"; -import { installerRender, createCallbackMock } from "~/test-utils"; +import { createCallbackMock, installerRender } from "~/test-utils"; import { L10nSection } from "~/components/overview"; import { createClient } from "~/client"; @@ -29,27 +29,28 @@ jest.mock("~/client"); const locales = [ { id: "en_US", name: "English", territory: "United States" }, - { id: "de_DE", name: "German", territory: "Germany" } + { id: "de_DE", name: "German", territory: "Germany" }, ]; const l10nClientMock = { locales: jest.fn().mockResolvedValue(locales), getLocales: jest.fn().mockResolvedValue(["en_US"]), getUILocale: jest.fn().mockResolvedValue("en_US"), + getUIKeymap: jest.fn().mockResolvedValue("en"), keymaps: jest.fn().mockResolvedValue([]), getKeymap: jest.fn().mockResolvedValue(undefined), timezones: jest.fn().mockResolvedValue([]), getTimezone: jest.fn().mockResolvedValue(undefined), onLocalesChange: jest.fn(), onKeymapChange: jest.fn(), - onTimezoneChange: jest.fn() + onTimezoneChange: jest.fn(), }; beforeEach(() => { // if defined outside, the mock is cleared automatically createClient.mockImplementation(() => { return { - l10n: l10nClientMock + l10n: l10nClientMock, }; }); }); From e323f87d9eafc7c800a2f6a9a0f69fed5d6fb19c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:45:40 +0100 Subject: [PATCH 05/17] web: fix Page tests --- web/src/components/core/Page.test.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/components/core/Page.test.jsx b/web/src/components/core/Page.test.jsx index f78b83ba11..094555ed49 100644 --- a/web/src/components/core/Page.test.jsx +++ b/web/src/components/core/Page.test.jsx @@ -29,13 +29,14 @@ jest.mock("~/client"); const l10nClientMock = { getUILocale: jest.fn().mockResolvedValue("en_US"), + getUIKeymap: jest.fn().mockResolvedValue("en"), keymaps: jest.fn().mockResolvedValue([]), getKeymap: jest.fn().mockResolvedValue(undefined), timezones: jest.fn().mockResolvedValue([]), getTimezone: jest.fn().mockResolvedValue(undefined), onLocalesChange: jest.fn(), onKeymapChange: jest.fn(), - onTimezoneChange: jest.fn() + onTimezoneChange: jest.fn(), }; describe("Page", () => { From 609b61c4a91841ab8b7bbad171a412caa61572c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:49:51 +0100 Subject: [PATCH 06/17] web: drop NetworkManagerAdapter * It is not used anymore. --- web/src/client/network/index.js | 9 +- web/src/client/network/model.js | 59 ++- web/src/client/network/network_manager.js | 470 ------------------ .../client/network/network_manager.test.js | 436 ---------------- 4 files changed, 62 insertions(+), 912 deletions(-) delete mode 100644 web/src/client/network/network_manager.js delete mode 100644 web/src/client/network/network_manager.test.js diff --git a/web/src/client/network/index.js b/web/src/client/network/index.js index dc7313f86d..4ddb3ce1b1 100644 --- a/web/src/client/network/index.js +++ b/web/src/client/network/index.js @@ -21,8 +21,13 @@ // @ts-check -import { securityFromFlags } from "./network_manager"; -import { createConnection, ConnectionTypes, ConnectionState, createAccessPoint } from "./model"; +import { + ConnectionState, + ConnectionTypes, + createAccessPoint, + createConnection, + securityFromFlags, +} from "./model"; import { formatIp, ipPrefixFor } from "./utils"; const DeviceType = Object.freeze({ diff --git a/web/src/client/network/model.js b/web/src/client/network/model.js index c4f8ab9e76..2ad100b080 100644 --- a/web/src/client/network/model.js +++ b/web/src/client/network/model.js @@ -92,6 +92,28 @@ const SecurityProtocols = Object.freeze({ // WPA3Only: "wpa-eap-suite-b-192" // }); +const ApFlags = Object.freeze({ + NONE: 0x00000000, + PRIVACY: 0x00000001, + WPS: 0x00000002, + WPS_PBC: 0x00000004, + WPS_PIN: 0x00000008, +}); + +const ApSecurityFlags = Object.freeze({ + NONE: 0x00000000, + PAIR_WEP40: 0x00000001, + PAIR_WEP104: 0x00000002, + PAIR_TKIP: 0x00000004, + PAIR_CCMP: 0x00000008, + GROUP_WEP40: 0x00000010, + GROUP_WEP104: 0x00000020, + GROUP_TKIP: 0x00000040, + GROUP_CCMP: 0x00000080, + KEY_MGMT_PSK: 0x00000100, + KEY_MGMT_8021_X: 0x00000200, +}); + /** * @typedef {object} IPAddress * @property {string} address - like "129.168.1.2" @@ -226,13 +248,42 @@ const createAccessPoint = ({ ssid, hwAddress, strength, security }) => ( } ); +/** + * @param {number} flags - AP flags + * @param {number} wpa_flags - AP WPA1 flags + * @param {number} rsn_flags - AP WPA2 flags + * @return {string[]} security protocols supported + */ +const securityFromFlags = (flags, wpa_flags, rsn_flags) => { + const security = []; + + if ((flags & ApFlags.PRIVACY) && (wpa_flags === 0) && (rsn_flags === 0)) { + security.push(SecurityProtocols.WEP); + } + + if (wpa_flags > 0) { + security.push(SecurityProtocols.WPA); + } + if (rsn_flags > 0) { + security.push(SecurityProtocols.RSN); + } + if ( + (wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X) || (rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X) + ) { + security.push(SecurityProtocols._8021X); + } + + return security; +}; + export { - createAccessPoint, - createConnection, - createDevice, connectionHumanState, ConnectionState, ConnectionTypes, + createAccessPoint, + createConnection, + createDevice, DeviceState, - SecurityProtocols + securityFromFlags, + SecurityProtocols, }; diff --git a/web/src/client/network/network_manager.js b/web/src/client/network/network_manager.js deleted file mode 100644 index 5b480126d3..0000000000 --- a/web/src/client/network/network_manager.js +++ /dev/null @@ -1,470 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -// @ts-check -// -import DBusClient from "../dbus"; -import cockpit from "../../lib/cockpit"; -import { NetworkEventTypes } from "./index"; -import { createAccessPoint, SecurityProtocols } from "./model"; -import { ipPrefixFor } from "./utils"; - -/** - * @typedef {import("./model").NetworkSettings} NetworkSettings - * @typedef {import("./model").Connection} Connection - * @typedef {import("./model").ActiveConnection} ActiveConnection - * @typedef {import("./model").IPAddress} IPAddress - * @typedef {import("./model").AccessPoint} AccessPoint - * @typedef {import("./index").NetworkEventFn} NetworkEventFn - */ - -const SERVICE_NAME = "org.freedesktop.NetworkManager"; -const IFACE = "org.freedesktop.NetworkManager"; -const SETTINGS_IFACE = "org.freedesktop.NetworkManager.Settings"; -const CONNECTION_IFACE = "org.freedesktop.NetworkManager.Settings.Connection"; -const DEVICE_IFACE = "org.freedesktop.NetworkManager.Device"; -const DEVICES_NAMESPACE = "/org/freedesktop/NetworkManager/Devices"; -const ACTIVE_CONNECTION_IFACE = "org.freedesktop.NetworkManager.Connection.Active"; -const ACTIVE_CONNECTION_NAMESPACE = "/org/freedesktop/NetworkManager/ActiveConnection"; -const IP4CONFIG_IFACE = "org.freedesktop.NetworkManager.IP4Config"; -const IP4CONFIG_NAMESPACE = "/org/freedesktop/NetworkManager/IP4Config"; -const ACCESS_POINT_IFACE = "org.freedesktop.NetworkManager.AccessPoint"; -const ACCESS_POINT_NAMESPACE = "/org/freedesktop/NetworkManager/AccessPoint"; -const SETTINGS_NAMESPACE = "/org/freedesktop/NetworkManager/Settings"; -const NM_DEVICE_TYPE_WIFI = 2; - -const ApFlags = Object.freeze({ - NONE: 0x00000000, - PRIVACY: 0x00000001, - WPS: 0x00000002, - WPS_PBC: 0x00000004, - WPS_PIN: 0x00000008 -}); - -const ApSecurityFlags = Object.freeze({ - NONE: 0x00000000, - PAIR_WEP40: 0x00000001, - PAIR_WEP104: 0x00000002, - PAIR_TKIP: 0x00000004, - PAIR_CCMP: 0x00000008, - GROUP_WEP40: 0x00000010, - GROUP_WEP104: 0x00000020, - GROUP_TKIP: 0x00000040, - GROUP_CCMP: 0x00000080, - KEY_MGMT_PSK: 0x00000100, - KEY_MGMT_8021_X: 0x00000200, -}); - -/** -* @param {number} flags - AP flags -* @param {number} wpa_flags - AP WPA1 flags -* @param {number} rsn_flags - AP WPA2 flags -* @return {string[]} security protocols supported -*/ -const securityFromFlags = (flags, wpa_flags, rsn_flags) => { - const security = []; - - if ((flags & ApFlags.PRIVACY) && (wpa_flags === 0) && (rsn_flags === 0)) - security.push(SecurityProtocols.WEP); - - if (wpa_flags > 0) - security.push(SecurityProtocols.WPA); - if (rsn_flags > 0) - security.push(SecurityProtocols.RSN); - if ((wpa_flags & ApSecurityFlags.KEY_MGMT_8021_X) || (rsn_flags & ApSecurityFlags.KEY_MGMT_8021_X)) - security.push(SecurityProtocols._8021X); - - return security; -}; - -/** - * @param {Connection} connection - Connection to convert - */ -const connectionToCockpit = (connection) => { - const { ipv4, wireless } = connection; - const settings = { - connection: { - id: cockpit.variant("s", connection.id) - }, - ipv4: { - "address-data": cockpit.variant("aa{sv}", ipv4.addresses.map(addr => ( - { - address: cockpit.variant("s", addr.address), - prefix: cockpit.variant("u", ipPrefixFor(addr.prefix.toString())) - } - ))), - "dns-data": cockpit.variant("as", ipv4.nameServers), - method: cockpit.variant("s", ipv4.method) - } - }; - - if (ipv4.gateway && connection.ipv4.addresses.length !== 0) { - settings.ipv4.gateway = cockpit.variant("s", ipv4.gateway); - } - - if (wireless) { - settings.connection.type = cockpit.variant("s", "802-11-wireless"); - settings["802-11-wireless"] = { - mode: cockpit.variant("s", "infrastructure"), - ssid: cockpit.variant("ay", cockpit.byte_array(wireless.ssid)), - hidden: cockpit.variant("b", !!wireless.hidden) - }; - - if (wireless.security) { - settings["802-11-wireless-security"] = { - "key-mgmt": cockpit.variant("s", wireless.security), - psk: cockpit.variant("s", wireless.password) - }; - } - } - - return settings; -}; - -/** - * It merges the information from a connection into a D-Bus settings object - * - * @param {object} settings - Settings from the GetSettings D-Bus method - * @param {Connection} connection - Connection containing the information to update - * @return {object} Object to be used with the UpdateConnection D-Bus method - */ -const mergeConnectionSettings = (settings, connection) => { - // We need to delete these keys or otherwise they have precedence over the key-data ones - const { addresses, gateway, dns, ...cleanIPv4 } = settings.ipv4 || {}; - const { connection: conn, ipv4 } = connectionToCockpit(connection); - - return { - ...settings, - connection: { - ...settings.connection, - ...conn - }, - ipv4: { - ...cleanIPv4, - ...ipv4 - } - }; -}; - -/** - * NetworkClient adapter for NetworkManager - * - * This class is responsible for providing an interface to interact with NetworkManager through - * D-Bus. Its interface is modeled to serve NetworkClient requirements. - */ -class NetworkManagerAdapter { - constructor() { - this.client = new DBusClient(SERVICE_NAME); - this.proxies = { - accessPoints: {}, - activeConnections: {}, - devices: {}, - ip4Configs: {}, - manager: null, - settings: null, - connections: {} - }; - this.eventsHandler = null; - } - - /** - * Builds proxies and starts listening to them - * - * @param {NetworkEventFn} handler - Events handler - */ - async setUp(handler) { - this.eventsHandler = handler; - this.proxies = { - accessPoints: await this.client.proxies(ACCESS_POINT_IFACE, ACCESS_POINT_NAMESPACE), - activeConnections: await this.client.proxies( - ACTIVE_CONNECTION_IFACE, ACTIVE_CONNECTION_NAMESPACE - ), - devices: await this.client.proxies(DEVICE_IFACE, DEVICES_NAMESPACE), - ip4Configs: await this.client.proxies(IP4CONFIG_IFACE, IP4CONFIG_NAMESPACE), - manager: await this.client.proxy(IFACE), - settings: await this.client.proxy(SETTINGS_IFACE), - connections: await this.client.proxies(CONNECTION_IFACE, SETTINGS_NAMESPACE) - }; - - this.subscribeToEvents(); - } - - /** - * Returns the list of active connections - * - * @return {ActiveConnection[]} - * @see https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html - */ - activeConnections() { - return Object.values(this.proxies.activeConnections).map(proxy => { - return this.activeConnectionFromProxy(proxy); - }); - } - - /** - * Returns the list of configured connections - * - * @return {Promise} - * @see https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html - */ - async connections() { - return await Promise.all(Object.values(this.proxies.connections).map(async proxy => { - return await this.connectionFromProxy(proxy); - })); - } - - /** - * Returns the list of available wireless access points (AP) - * - * @return {AccessPoint[]} - */ - accessPoints() { - return Object.values(this.proxies.accessPoints).map(ap => { - return createAccessPoint({ - ssid: window.atob(ap.Ssid), - hwAddress: ap.HwAddress, - strength: ap.Strength, - security: securityFromFlags(ap.Flags, ap.WpaFlags, ap.RsnFlags) - }); - }); - } - - /** - * Returns a Connection given the nm-settings given by DBUS - * - * @param {object} settings - connection options - * @return {Connection} - * - */ - connectionFromSettings(settings) { - const { connection, ipv4, "802-11-wireless": wireless, path } = settings; - const conn = { - id: connection.id.v, - uuid: connection.uuid.v, - iface: connection.interface?.v, - type: connection.type.v - }; - - if (path) conn.path = path; - - if (ipv4) { - conn.ipv4 = { - addresses: ipv4["address-data"].v.map(({ address, prefix }) => { - return { address: address.v, prefix: prefix.v }; - }), - nameServers: ipv4["dns-data"]?.v || [], - method: ipv4.method.v, - }; - if (ipv4.gateway?.v) conn.ipv4.gateway = ipv4.gateway.v; - } - - if (wireless) { - conn.wireless = { - ssid: window.atob(wireless.ssid.v), - hidden: wireless.hidden?.v || false - }; - } - - return conn; - } - - /** - * Connects to given Wireless network - * - * @param {object} settings - connection options - */ - async connectTo(settings) { - const settingsProxy = await this.connectionSettingsObject(settings.uuid); - await this.activateConnection(settingsProxy.path); - } - - /** - * Subscribes to network events - * - * Registers a handler for changes in /org/freedesktop/NetworkManager/ActiveConnection/*. - * The handler receives a NetworkEvent object. - * - * @private - */ - async subscribeToEvents() { - const activeConnectionProxies = this.proxies.activeConnections; - const connectionProxies = this.proxies.connections; - const managerProxy = this.proxies.manager; - const settingsProxy = this.proxies.settings; - - /** @type {(eventType: string) => NetworkEventFn} */ - const handleWrapperActiveConnection = (eventType) => (_event, proxy) => { - const connection = this.activeConnectionFromProxy(proxy); - this.eventsHandler({ type: eventType, payload: connection }); - }; - - /** @type {(eventType: string) => NetworkEventFn} */ - const handleWrapperConnection = (eventType) => async (_event, proxy) => { - let connection; - - if (eventType === NetworkEventTypes.CONNECTION_REMOVED) { - connection = { id: proxy.id, path: proxy.path }; - } else { - connection = await this.connectionFromProxy(proxy); - } - - this.eventsHandler({ type: eventType, payload: connection }); - }; - - const handleWrapperSettings = (eventType) => () => { - this.eventsHandler({ type: eventType, payload: this.settings() }); - }; - - // FIXME: do not build a map (eventTypesMap), just inject the type here - connectionProxies.addEventListener("added", handleWrapperConnection(NetworkEventTypes.CONNECTION_ADDED)); - connectionProxies.addEventListener("changed", handleWrapperConnection(NetworkEventTypes.CONNECTION_UPDATED)); - connectionProxies.addEventListener("removed", handleWrapperConnection(NetworkEventTypes.CONNECTION_REMOVED)); - - // FIXME: do not build a map (eventTypesMap), just inject the type here - activeConnectionProxies.addEventListener("added", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_ADDED)); - activeConnectionProxies.addEventListener("changed", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_UPDATED)); - activeConnectionProxies.addEventListener("removed", handleWrapperActiveConnection(NetworkEventTypes.ACTIVE_CONNECTION_REMOVED)); - - managerProxy.addEventListener("changed", handleWrapperSettings(NetworkEventTypes.SETTINGS_UPDATED)); - settingsProxy.addEventListener("changed", handleWrapperSettings(NetworkEventTypes.SETTINGS_UPDATED)); - } - - /** - * Reactivate the given connection - * - * @private - * @param {string} path - connection path - * See NM API documentation for details. - * https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.html - */ - async activateConnection(path) { - const proxy = await this.client.proxy(IFACE); - return proxy.ActivateConnection(path, "/", "/"); - } - - /** - * Builds a connection object from a Cockpit's proxy object - * - * It retrieves additional information like IPv4 settings. - * - * @private - * @param {object} proxy - Proxy object from /org/freedesktop/NetworkManager/Settings/* - * @return {Promise} - */ - async connectionFromProxy(proxy) { - const settings = await proxy.GetSettings(); - settings.path = proxy.path; - return this.connectionFromSettings(settings); - } - - /** - * Builds a connection object from a Cockpit's proxy object - * - * It retrieves additional information like IPv4 settings. - * - * @private - * @param {object} proxy - Proxy object from /org/freedesktop/NetworkManager/ActiveConnection/* - * @return {ActiveConnection} - */ - activeConnectionFromProxy(proxy) { - const ip4Config = this.proxies.ip4Configs[proxy.Ip4Config]; - let addresses = []; - if (ip4Config) { - addresses = ip4Config.AddressData.map(this.connectionIPAddress); - } - - return { - id: proxy.Id, - uuid: proxy.Uuid, - addresses, - type: proxy.Type, - state: proxy.State - }; - } - - /** - * - * Returns connection settings for the given connection - * - * @private - * @param {string} uuid - Connection ID - * @return {Promise} - */ - async connectionSettingsObject(uuid) { - const proxy = await this.client.proxy(SETTINGS_IFACE); - const path = await proxy.GetConnectionByUuid(uuid); - return await this.client.proxy(CONNECTION_IFACE, path); - } - - /* - * Returns NM IP config for the particular connection - * - * @private - * See NM API documentation for details - * https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.Connection.Active.html - * https://developer-old.gnome.org/NetworkManager/stable/gdbus-org.freedesktop.NetworkManager.IP4Config.html - * - * FIXME: improve data types - * @param {object} data - * @return {Promise} - */ - connectionIPAddress(data) { - return { address: data.address.v, prefix: parseInt(data.prefix.v) }; - } - - /* - * Returns the list of WiFi devices available in the system - * - * @return {object[]} list of available WiFi devices - */ - availableWifiDevices() { - return Object.values(this.proxies.devices).filter(d => d.DeviceType === NM_DEVICE_TYPE_WIFI); - } - - /* - * Returns whether the system is able to scan wifi networks based on rfkill and the presence of - * some wifi device - * - * @return {boolean} - */ - wifiScanSupported() { - const { manager } = this.proxies; - - if (!manager) return false; - if (!(manager.WirelessEnabled && manager.WirelessHardwareEnabled)) return false; - - return this.availableWifiDevices().length > 0; - } - - /* - * Returns NetworkManager general settings - * - * @return {NetworkSettings} - */ - settings() { - return { - wifiScanSupported: this.wifiScanSupported(), - hostname: this.proxies.settings?.Hostname || "" - }; - } -} - -export { NetworkManagerAdapter, mergeConnectionSettings, securityFromFlags }; diff --git a/web/src/client/network/network_manager.test.js b/web/src/client/network/network_manager.test.js deleted file mode 100644 index fbc1248cd8..0000000000 --- a/web/src/client/network/network_manager.test.js +++ /dev/null @@ -1,436 +0,0 @@ -/* - * Copyright (c) [2022] SUSE LLC - * - * All Rights Reserved. - * - * This program is free software; you can redistribute it and/or modify it - * under the terms of version 2 of the GNU General Public License as published - * by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, contact SUSE LLC. - * - * To contact SUSE LLC about this file by physical or electronic mail, you may - * find current contact information at www.suse.com. - */ - -import { securityFromFlags, mergeConnectionSettings, NetworkManagerAdapter } from "./network_manager"; -import { createConnection } from "./model"; -import { ConnectionState, ConnectionTypes } from "./index"; -import DBusClient from "../dbus"; -import cockpit from "../../lib/cockpit"; - -jest.mock("../dbus"); - -const NM_IFACE = "org.freedesktop.NetworkManager"; -const NM_SETTINGS_IFACE = "org.freedesktop.NetworkManager.Settings"; -const IP4CONFIG_IFACE = "org.freedesktop.NetworkManager.IP4Config"; -const DEVICE_IFACE = "org.freedesktop.NetworkManager.Device"; -const NM_CONNECTION_IFACE = "org.freedesktop.NetworkManager.Settings.Connection"; -const ACTIVE_CONNECTION_IFACE = "org.freedesktop.NetworkManager.Connection.Active"; -const ACCESS_POINT_IFACE = "org.freedesktop.NetworkManager.AccessPoint"; - -let devices; -const defaultDevices = { - "/org/freedesktop/NetworkManager/Devices/17": { - ActiveConnection: "/", - NmPluginMissing: false, - Real: true, - InterfaceFlags: 1, - DeviceType: 2, - Mtu: 1500, - Ports: [], - IpInterface: "", - DriverVersion: "6.0.12-1-default", - State: 30, - Ip6Config: "/org/freedesktop/NetworkManager/IP6Config/17", - Metered: 0, - Ip4Address: 0, - LldpNeighbors: [], - Interface: "wlp0s20u5", - FirmwareMissing: false, - Ip6Connectivity: 1, - Driver: "mt7601u", - PhysicalPortId: "", - Capabilities: 1, - Dhcp4Config: "/", - AvailableConnections: [ - "/org/freedesktop/NetworkManager/Settings/3", - "/org/freedesktop/NetworkManager/Settings/4", - "/org/freedesktop/NetworkManager/Settings/5" - ], - HwAddress: "BA:FB:BE:AB:00:5B", - Managed: true, - StateReason: [ - 30, - 42 - ], - FirmwareVersion: "N/A", - Ip4Config: "/org/freedesktop/NetworkManager/IP4Config/17", - Udi: "/sys/devices/pci0000:00/0000:00:14.0/usb2/2-5/2-5:1.0/net/wlp0s20u5", - Autoconnect: true, - Ip4Connectivity: 1, - Path: "pci-0000:00:14.0-usb-0:5:1.0", - Dhcp6Config: "/" - } -}; - -const accessPoints = { - "/org/freedesktop/NetworkManager/AccessPoint/11": { - Flags: 3, - WpaFlags: 0, - RsnFlags: 392, - Ssid: "VGVzdGluZw==", - Frequency: 2432, - HwAddress: "00:31:92:25:84:FA", - Mode: 2, - MaxBitrate: 270000, - Strength: 76, - LastSeen: 96711 - } -}; - -const activeConnections = { - "/active/connection/wifi/1": { - Id: "active-wifi-connection", - Uuid: "uuid-wifi-1", - State: ConnectionState.ACTIVATED, - Type: ConnectionTypes.WIFI, - Ip4Config: "/ip4Config/2" - }, - "/active/connection/wired/1": { - Id: "active-wired-connection", - Uuid: "uuid-wired-1", - State: ConnectionState.ACTIVATED, - Type: ConnectionTypes.ETHERNET, - Ip4Config: "/ip4Config/1" - }, -}; - -const connections = { - "/org/freedesktop/NetworkManager/Settings/1": { - wait: jest.fn(), - path: "/org/freedesktop/NetworkManager/Settings/1", - GetSettings: () => ({ - connection: { - id: cockpit.variant("s", "Testing"), - uuid: cockpit.variant("s", "1f40ddb0-e6e8-4af8-8b7a-0b3898f0f57a"), - type: cockpit.variant("s", "802-11-wireless") - }, - ipv4: { - addresses: [], - "address-data": cockpit.variant("aa{sv}", []), - method: cockpit.variant("s", "auto"), - dns: [], - "dns-data": cockpit.variant("as", []), - "route-data": [] - }, - "802-11-wireless": { - ssid: cockpit.variant("ay", cockpit.byte_array("Testing")), - hidden: cockpit.variant("b", true), - mode: cockpit.variant("s", "infrastructure") - }, - "802-11-wireless-security": { - "key-mgmt": cockpit.variant("s", "wpa-psk") - } - }) - } -}; - -// Reminder: by default, properties added using Object.defineProperties() are not enumerable. -// We use #defineProperties here, so it doesn't show up as a "connection" in these objects. -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties#enumerable -Object.defineProperties(activeConnections, { - addEventListener: { value: jest.fn() } -}); - -Object.defineProperties(connections, { - addEventListener: { value: jest.fn() } -}); - -const addressesData = { - "/ip4Config/1": { - wait: jest.fn(), - AddressData: [ - { - address: { v: "10.0.0.1", t: "s" }, - prefix: { v: "22", t: "s" } - } - ] - }, - "/ip4Config/2": { - wait: jest.fn(), - AddressData: [ - { - address: { v: "10.0.0.2", t: "s" }, - prefix: { v: "22", t: "s" } - } - ] - } -}; - -const ActivateConnectionFn = jest.fn(); - -const AddConnectionFn = jest.fn(); -let networkProxy; -const networkSettingsProxy = { - wait: jest.fn(), - Hostname: "testing-machine", - GetConnectionByUuid: () => "/org/freedesktop/NetworkManager/Settings/1", - AddConnection: AddConnectionFn, - addEventListener: () => ({ value: jest.fn(), enumerable: false }) -}; - -const connectionSettingsMock = { - wait: jest.fn(), - path: "/org/freedesktop/NetworkManager/Settings/1", - GetSettings: () => ({ - connection: { - id: cockpit.variant("s", "active-wifi-connection"), - "interface-name": cockpit.variant("s", "wlp3s0"), - uuid: cockpit.variant("s", "uuid-wifi-1"), - type: cockpit.variant("s", "802-11-wireless") - }, - ipv4: { - addresses: [], - "address-data": cockpit.variant( - "aa{sv}", [{ - address: cockpit.variant("s", "192.168.122.200"), - prefix: cockpit.variant("u", 24) - }] - ), - method: cockpit.variant("s", "auto"), - gateway: cockpit.variant("s", "192.168.122.1"), - // this field is buggy and superseded by dns-data. test that it is not used. - dns: cockpit.variant("au", [67305985]), - "dns-data": cockpit.variant("as", ["192.168.122.1", "1.1.1.1"]), - "route-data": [] - } - }), - Update: jest.fn(), - Delete: jest.fn() -}; - -const connectionSettingsProxy = () => connectionSettingsMock; - -describe("NetworkManagerAdapter", () => { - beforeEach(() => { - networkProxy = { - wait: jest.fn(), - ActivateConnection: ActivateConnectionFn, - ActiveConnections: Object.keys(activeConnections), - WirelessEnabled: true, - WirelessHardwareEnabled: true, - addEventListener: jest.fn() - }; - - devices = defaultDevices; - - DBusClient.mockImplementation(() => { - return { - proxy: (iface) => { - if (iface === NM_IFACE) return networkProxy; - if (iface === NM_SETTINGS_IFACE) return networkSettingsProxy; - if (iface === NM_CONNECTION_IFACE) return connectionSettingsProxy(); - }, - proxies: (iface) => { - if (iface === ACCESS_POINT_IFACE) return accessPoints; - if (iface === ACTIVE_CONNECTION_IFACE) return activeConnections; - if (iface === DEVICE_IFACE) return devices; - if (iface === NM_CONNECTION_IFACE) return connections; - if (iface === IP4CONFIG_IFACE) return addressesData; - return {}; - } - }; - }); - }); - - describe("#accessPoints", () => { - it("returns the list of last scanned access points", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - const accessPoints = client.accessPoints(); - - expect(accessPoints.length).toEqual(1); - const [testing] = accessPoints; - expect(testing).toEqual({ - ssid: "Testing", - hwAddress: "00:31:92:25:84:FA", - strength: 76, - security: ["WPA2"] - }); - }); - }); - - describe("#activeConnections", () => { - it("returns the list of active connections", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - const availableConnections = client.activeConnections(); - - expect(availableConnections.length).toEqual(2); - const [wireless, ethernet] = availableConnections; - expect(wireless).toEqual({ - id: "active-wifi-connection", - uuid: "uuid-wifi-1", - state: ConnectionState.ACTIVATED, - type: ConnectionTypes.WIFI, - addresses: [{ address: "10.0.0.2", prefix: 22 }] - }); - - expect(ethernet).toEqual({ - id: "active-wired-connection", - uuid: "uuid-wired-1", - state: ConnectionState.ACTIVATED, - type: ConnectionTypes.ETHERNET, - addresses: [{ address: "10.0.0.1", prefix: 22 }] - }); - }); - }); - - describe("#connections", () => { - it("returns the list of settings (profiles)", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - const connections = await client.connections(); - - const [wifi] = connections; - - expect(wifi).toEqual({ - id: "Testing", - uuid: "1f40ddb0-e6e8-4af8-8b7a-0b3898f0f57a", - path: "/org/freedesktop/NetworkManager/Settings/1", - type: ConnectionTypes.WIFI, - ipv4: { method: 'auto', addresses: [], nameServers: [] }, - wireless: { ssid: "Testing", hidden: true }, - }); - }); - }); - - describe("#connectTo", () => { - it("activates the given connection", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - const [wifi] = await client.connections(); - await client.connectTo(wifi); - expect(ActivateConnectionFn).toHaveBeenCalledWith(wifi.path, "/", "/"); - }); - }); - - describe("#availableWifiDevices", () => { - it("returns the list of WiFi devices", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - - expect(client.availableWifiDevices().length).toEqual(1); - expect(client.availableWifiDevices()[0].Interface).toEqual("wlp0s20u5"); - }); - }); - - describe("#wifiScanSupported", () => { - describe("when wireless devices are disabled by software", () => { - it("returns false", async () => { - const client = new NetworkManagerAdapter(); - networkProxy.WirelessEnabled = false; - await client.setUp(); - expect(client.wifiScanSupported()).toEqual(false); - }); - }); - - describe("when wireless devices are disabled by hardware", () => { - it("returns false", async () => { - const client = new NetworkManagerAdapter(); - networkProxy.WirelessHardwareEnabled = false; - await client.setUp(); - expect(client.wifiScanSupported()).toEqual(false); - }); - }); - - describe("when wireless devices are enabled", () => { - describe("but there are no WiFi devices", () => { - it("returns false", async () => { - const client = new NetworkManagerAdapter(); - devices = {}; - await client.setUp(); - expect(client.wifiScanSupported()).toEqual(false); - }); - }); - - describe("and at least a WiFi devices is present in the system", () => { - it("returns true", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - expect(client.wifiScanSupported()).toEqual(true); - }); - }); - }); - }); - - describe("#settings", () => { - it("returns the Network Manager settings", async () => { - const client = new NetworkManagerAdapter(); - await client.setUp(); - expect(client.settings().hostname).toEqual("testing-machine"); - expect(client.settings().wifiScanSupported).toEqual(true); - }); - }); -}); - -describe("securityFromFlags", () => { - it("returns an array with the security protocols supported by the given AP flags", () => { - expect(securityFromFlags(1, 0, 0)).toEqual(["WEP"]); - expect(securityFromFlags(1, 0x00000100, 0x00000100)).toEqual(["WPA1", "WPA2"]); - expect(securityFromFlags(1, 0x00000200, 0x00000200)).toEqual(["WPA1", "WPA2", "802.1X"]); - }); -}); - -describe("mergeConnectionSettings", () => { - it("returns an object merging the original settings and the ones from the connection", () => { - const settings = { - uuid: cockpit.variant("s", "ba2b14db-fc6c-40a7-b275-77ef9341880c"), - id: cockpit.variant("s", "Wired connection 1"), - ipv4: { - addresses: cockpit.variant("aau", [[3232266754, 24, 3232266753]]), - "routes-data": cockpit.variant("aau", []) - }, - proxy: {} - - }; - - const connection = createConnection({ - name: "Wired connection 2", - ipv4: { - addresses: [{ address: "192.168.1.2", prefix: 24 }], - gateway: "192.168.1.1" - } - }); - - const newSettings = mergeConnectionSettings(settings, connection); - - expect(newSettings.connection.id).toEqual(cockpit.variant("s", connection.name)); - const expectedIpv4 = ({ - gateway: cockpit.variant("s", "192.168.1.1"), - "address-data": cockpit.variant("aa{sv}", [{ - address: cockpit.variant("s", "192.168.1.2"), - prefix: cockpit.variant("u", 24) - }]), - "dns-data": cockpit.variant("as", []), - method: cockpit.variant("s", "auto"), - "routes-data": cockpit.variant("aau", []) - }); - expect(newSettings.ipv4).toEqual(expect.objectContaining(expectedIpv4)); - expect(newSettings.proxy).not.toBeUndefined(); - }); - - it("does not set a gateway if there are not addresses", () => { - const connection = createConnection({ name: "Wired connection" }); - const settings = {}; - const newSettings = mergeConnectionSettings(settings, connection); - expect(newSettings.gateway).toBeUndefined(); - }); -}); From 495bf1b31a2a7084871a82fc6727846756d2db0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:51:42 +0100 Subject: [PATCH 07/17] web: fix App tests --- web/src/App.test.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/App.test.jsx b/web/src/App.test.jsx index bd0a963959..6364ea8d63 100644 --- a/web/src/App.test.jsx +++ b/web/src/App.test.jsx @@ -77,6 +77,7 @@ describe("App", () => { getTimezone: jest.fn().mockResolvedValue("Europe/Berlin"), keymaps: jest.fn().mockResolvedValue([]), getKeymap: jest.fn().mockResolvedValue(undefined), + getUIKeymap: jest.fn().mockResolvedValue("en"), getUILocale: jest.fn().mockResolvedValue("en_us"), setUILocale: jest.fn().mockResolvedValue("en_us"), onTimezoneChange: jest.fn(), From 4fde98459bb0ee04e611ae05286aa41ea789bfc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 06:54:06 +0100 Subject: [PATCH 08/17] web: drop an outdated LoginPage test --- web/src/components/core/LoginPage.test.jsx | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/web/src/components/core/LoginPage.test.jsx b/web/src/components/core/LoginPage.test.jsx index 4f781cba4c..d0192caf13 100644 --- a/web/src/components/core/LoginPage.test.jsx +++ b/web/src/components/core/LoginPage.test.jsx @@ -75,16 +75,4 @@ describe("LoginPage", () => { within(dialog).getByText(/About/); }); }); - - describe("when user is already authenticated", () => { - beforeEach(() => { - mockIsAuthenticated = true; - }); - - it("redirects to root route", async () => { - plainRender(); - // react-router-dom Navigate is mocked. See test-utils for more details. - await screen.findByText("Navigating to /"); - }); - }); }); From 0cc853136f3c5b36c42bedec92d13d8d33a5ae9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 07:11:35 +0100 Subject: [PATCH 09/17] web: fix manager client tests --- web/src/client/manager.js | 1 - web/src/client/manager.test.js | 114 ++++++++++++--------------------- 2 files changed, 40 insertions(+), 75 deletions(-) diff --git a/web/src/client/manager.js b/web/src/client/manager.js index 03c691d6cb..7b0951a9ff 100644 --- a/web/src/client/manager.js +++ b/web/src/client/manager.js @@ -23,7 +23,6 @@ import { WithProgress, WithStatus } from "./mixins"; -const MANAGER_PATH = "/org/opensuse/Agama/Manager1"; const MANAGER_SERVICE = "org.opensuse.Agama.Manager1"; /** diff --git a/web/src/client/manager.test.js b/web/src/client/manager.test.js index 8a621638f5..ab5cd66ae0 100644 --- a/web/src/client/manager.test.js +++ b/web/src/client/manager.test.js @@ -21,94 +21,64 @@ // @ts-check +import { HTTPClient } from "./http"; import { ManagerClient } from "./manager"; -import DBusClient from "./dbus"; import cockpit from "../lib/cockpit"; -jest.mock("../lib/cockpit"); -jest.mock("./dbus"); - -const MANAGER_IFACE = "org.opensuse.Agama.Manager1"; -const SERVICE_IFACE = "org.opensuse.Agama1.ServiceStatus"; -const PROGRESS_IFACE = "org.opensuse.Agama1.Progress"; - -const managerProxy = { - wait: jest.fn(), - Commit: jest.fn(), - Probe: jest.fn(), - Finish: jest.fn().mockReturnValue(true), - CanInstall: jest.fn(), - CollectLogs: jest.fn(), - CurrentInstallationPhase: 0 -}; +const mockJsonFn = jest.fn(); +const mockGetFn = jest.fn().mockImplementation(() => { + return { ok: true, json: mockJsonFn }; +}); +const mockPostFn = jest.fn().mockImplementation(() => { + return { ok: true }; +}); -const statusProxy = { - wait: jest.fn(), - Current: 0 -}; +jest.mock("./http", () => { + return { + HTTPClient: jest.fn().mockImplementation(() => { + return { + get: mockGetFn, + post: mockPostFn, + }; + }), + }; +}); -const progressProxy = { - wait: jest.fn(), - CurrentStep: [2, "Installing software"], - TotalSteps: 3, - Finished: false -}; +let client; -const proxies = { - [MANAGER_IFACE]: managerProxy, - [SERVICE_IFACE]: statusProxy, - [PROGRESS_IFACE]: progressProxy +const installerStatus = { + phase: 1, + busy: [], + iguana: false, + canInstall: true, }; beforeEach(() => { - // @ts-ignore - DBusClient.mockImplementation(() => { - return { proxy: (iface) => proxies[iface] }; - }); -}); - -describe("#getStatus", () => { - it("returns the installer status", async () => { - const client = new ManagerClient(); - const status = await client.getStatus(); - expect(status).toEqual(0); - }); -}); - -describe("#getProgress", () => { - it("returns the manager service progress", async () => { - const client = new ManagerClient(); - const status = await client.getProgress(); - expect(status).toEqual({ - message: "Installing software", - current: 2, - total: 3, - finished: false - }); - }); + client = new ManagerClient(new HTTPClient(new URL("http://localhost"))); }); describe("#startProbing", () => { it("(re)starts the probing process", async () => { - const client = new ManagerClient(); await client.startProbing(); - expect(managerProxy.Probe).toHaveBeenCalledWith(); + expect(mockPostFn).toHaveBeenCalledWith("/manager/probe", {}); }); }); describe("#getPhase", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(installerStatus); + }); + it("resolves to the current phase", () => { - const client = new ManagerClient(); const phase = client.getPhase(); - expect(phase).resolves.toEqual(0); + expect(phase).resolves.toEqual(1); }); }); describe("#startInstallation", () => { it("starts the installation", async () => { - const client = new ManagerClient(); await client.startInstallation(); - expect(managerProxy.Commit).toHaveBeenCalledWith(); + expect(mockPostFn).toHaveBeenCalledWith("/manager/install", {}); }); }); @@ -118,20 +88,18 @@ describe("#rebootSystem", () => { }); it("returns whether the system reboot command was called or not", async () => { - const client = new ManagerClient(); const reboot = await client.finishInstallation(); - expect(reboot).toEqual(true); + expect(mockPostFn).toHaveBeenCalledWith("/manager/finish", {}); }); }); describe("#canInstall", () => { describe("when the system can be installed", () => { beforeEach(() => { - managerProxy.CanInstall = jest.fn().mockResolvedValue(true); + mockJsonFn.mockResolvedValue(installerStatus); }); it("returns true", async () => { - const client = new ManagerClient(); const install = await client.canInstall(); expect(install).toEqual(true); }); @@ -139,25 +107,23 @@ describe("#canInstall", () => { describe("when the system cannot be installed", () => { beforeEach(() => { - managerProxy.CanInstall = jest.fn().mockResolvedValue(false); + mockJsonFn.mockResolvedValue({ ...installerStatus, canInstall: false }); }); it("returns false", async () => { - const client = new ManagerClient(); const install = await client.canInstall(); expect(install).toEqual(false); }); }); }); -describe("#fetchLogs", () => { - beforeEach(() => { - managerProxy.CollectLogs = jest.fn(() => "/tmp/y2log-hWBn95.tar.xz"); - cockpit.file = jest.fn(() => ({ read: () => "fake-binary-data" })); - }); +describe.skip("#fetchLogs", () => { + // beforeEach(() => { + // managerProxy.CollectLogs = jest.fn(() => "/tmp/y2log-hWBn95.tar.xz"); + // cockpit.file = jest.fn(() => ({ read: () => "fake-binary-data" })); + // }); it("returns the logs file binary content", async () => { - const client = new ManagerClient(); const logsContent = await client.fetchLogs(); expect(logsContent).toEqual("fake-binary-data"); expect(cockpit.file).toHaveBeenCalledWith("/tmp/y2log-hWBn95.tar.xz", { binary: true }); From 75539ca9c1b2ac58742e55f1ca663751c47404fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 07:16:18 +0100 Subject: [PATCH 10/17] web: fix installerL10n context tests --- web/src/context/installerL10n.test.jsx | 99 ++++++++++++++++---------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/web/src/context/installerL10n.test.jsx b/web/src/context/installerL10n.test.jsx index 3361c84b58..11794e002c 100644 --- a/web/src/context/installerL10n.test.jsx +++ b/web/src/context/installerL10n.test.jsx @@ -23,7 +23,7 @@ // cspell:ignore ahoj import React from "react"; -import { render, waitFor, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { InstallerL10nProvider } from "~/context/installerL10n"; import { InstallerClientProvider } from "./installer"; @@ -35,20 +35,17 @@ const setUILocaleFn = jest.fn().mockResolvedValue(); const client = { l10n: { getUILocale: getUILocaleFn, - setUILocale: setUILocaleFn + setUILocale: setUILocaleFn, + getUIKeymap: jest.fn().mockResolvedValue("en"), }, - onDisconnect: jest.fn() + onDisconnect: jest.fn(), }; jest.mock("~/languages.json", () => ({ "es-ar": "Español (Argentina)", "cs-cz": "čeština", "en-us": "English (US)", - "es-es": "Español" -})); - -jest.mock("~/lib/cockpit", () => ({ - spawn: jest.fn().mockResolvedValue() + "es-es": "Español", })); // Helper component that displays a translated message depending on the @@ -78,7 +75,7 @@ describe("InstallerL10nProvider", () => { window.navigator = { languages: ["es-es", "cs-cz"] }; }); - // remove the Cockpit language cookie after each test + // remove the language cookie after each test afterEach(() => { // setting a cookie with already expired date removes it document.cookie = "agamaLang=; path=/; expires=" + new Date(0).toUTCString(); @@ -89,7 +86,7 @@ describe("InstallerL10nProvider", () => { window.location.search = ""; }); - describe("when the Cockpit language is already set", () => { + describe("when the language is already set", () => { beforeEach(() => { document.cookie = "agamaLang=en-us; path=/;"; getUILocaleFn.mockResolvedValueOnce("en_US.UTF-8"); @@ -98,8 +95,10 @@ describe("InstallerL10nProvider", () => { it("displays the children content and does not reload", async () => { render( - - + + + + , ); // children are displayed @@ -109,7 +108,7 @@ describe("InstallerL10nProvider", () => { }); }); - describe("when the Cockpit language is set to an unsupported language", () => { + describe("when the language is set to an unsupported language", () => { beforeEach(() => { document.cookie = "agamaLang=de-de; path=/;"; getUILocaleFn.mockResolvedValueOnce("de_DE.UTF-8"); @@ -119,8 +118,10 @@ describe("InstallerL10nProvider", () => { it("uses the first supported language from the browser", async () => { render( - - + + + + , ); await waitFor(() => expect(utils.locationReload).toHaveBeenCalled()); @@ -128,8 +129,10 @@ describe("InstallerL10nProvider", () => { // renders again after reloading render( - - + + + + , ); await waitFor(() => screen.getByText("hola")); @@ -137,7 +140,7 @@ describe("InstallerL10nProvider", () => { }); }); - describe("when the Cockpit language is not set", () => { + describe("when the language is not set", () => { beforeEach(() => { // Ensure both, UI and backend mock languages, are in sync since // client.setUILocale is mocked too. @@ -148,8 +151,10 @@ describe("InstallerL10nProvider", () => { it("sets the preferred language from browser and reloads", async () => { render( - - + + + + , ); await waitFor(() => expect(utils.locationReload).toHaveBeenCalled()); @@ -157,8 +162,10 @@ describe("InstallerL10nProvider", () => { // renders again after reloading render( - - + + + + , ); await waitFor(() => screen.getByText("hola")); }); @@ -171,8 +178,10 @@ describe("InstallerL10nProvider", () => { it("sets the first which language matches", async () => { render( - - + + + + , ); await waitFor(() => expect(utils.locationReload).toHaveBeenCalled()); @@ -180,8 +189,10 @@ describe("InstallerL10nProvider", () => { // renders again after reloading render( - - + + + + , ); await waitFor(() => screen.getByText("hola!")); }); @@ -194,7 +205,7 @@ describe("InstallerL10nProvider", () => { history.replaceState(history.state, null, `http://localhost/?lang=cs-CZ`); }); - describe("when the Cockpit language is already set to 'cs-cz'", () => { + describe("when the language is already set to 'cs-cz'", () => { beforeEach(() => { document.cookie = "agamaLang=cs-cz; path=/;"; getUILocaleFn.mockResolvedValueOnce("cs_CZ.UTF-8"); @@ -203,8 +214,10 @@ describe("InstallerL10nProvider", () => { it("displays the children content and does not reload", async () => { render( - - + + + + , ); // children are displayed @@ -217,7 +230,7 @@ describe("InstallerL10nProvider", () => { }); }); - describe("when the Cockpit language is set to 'en-us'", () => { + describe("when the language is set to 'en-us'", () => { beforeEach(() => { document.cookie = "agamaLang=en-us; path=/;"; getUILocaleFn.mockResolvedValueOnce("en_US"); @@ -228,8 +241,10 @@ describe("InstallerL10nProvider", () => { it("sets the 'cs-cz' language and reloads", async () => { render( - - + + + + , ); await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); @@ -237,8 +252,10 @@ describe("InstallerL10nProvider", () => { // renders again after reloading render( - - + + + + , ); await waitFor(() => screen.getByText("ahoj")); @@ -246,7 +263,7 @@ describe("InstallerL10nProvider", () => { }); }); - describe("when the Cockpit language is not set", () => { + describe("when the language is not set", () => { beforeEach(() => { getUILocaleFn.mockResolvedValueOnce("en_US.UTF-8"); getUILocaleFn.mockResolvedValueOnce("cs_CZ.UTF-8"); @@ -256,8 +273,10 @@ describe("InstallerL10nProvider", () => { it("sets the 'cs-cz' language and reloads", async () => { render( - - + + + + , ); await waitFor(() => expect(utils.setLocationSearch).toHaveBeenCalledWith("lang=cs-cz")); @@ -265,8 +284,10 @@ describe("InstallerL10nProvider", () => { // reload the component render( - - + + + + , ); await waitFor(() => screen.getByText("ahoj")); From c9dfebf936c4a4f190c167f5b4fd4894cc66c40b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Fri, 10 May 2024 11:18:23 +0100 Subject: [PATCH 11/17] web: fix users client tests --- web/src/client/users.js | 2 +- web/src/client/users.test.js | 170 +++++++++++++++++++++-------------- 2 files changed, 103 insertions(+), 69 deletions(-) diff --git a/web/src/client/users.js b/web/src/client/users.js index a62663ba7f..9689a2069d 100644 --- a/web/src/client/users.js +++ b/web/src/client/users.js @@ -84,7 +84,7 @@ class UsersBaseClient { return false; } const config = await response.json(); - return config.password; + return config.password !== ""; } /** diff --git a/web/src/client/users.test.js b/web/src/client/users.test.js index 220a3b5313..eac042771c 100644 --- a/web/src/client/users.test.js +++ b/web/src/client/users.test.js @@ -21,114 +21,124 @@ // @ts-check -import DBusClient from "./dbus"; +import { HTTPClient } from "./http"; import { UsersClient } from "./users"; -jest.mock("./dbus"); +const mockJsonFn = jest.fn(); +const mockGetFn = jest.fn().mockImplementation(() => { + return { ok: true, json: mockJsonFn }; +}); +const mockPatchFn = jest.fn().mockImplementation(() => { + return { ok: true }; +}); +const mockPutFn = jest.fn().mockImplementation(() => { + return { ok: true }; +}); +const mockDeleteFn = jest.fn().mockImplementation(() => { + return { ok: true }; +}); -const USERS_IFACE = "org.opensuse.Agama.Users1"; +jest.mock("./http", () => { + return { + HTTPClient: jest.fn().mockImplementation(() => { + return { + get: mockGetFn, + patch: mockPatchFn, + put: mockPutFn, + delete: mockDeleteFn, + }; + }), + }; +}); -let setFirstUserResult = [true, []]; +let client; -const usersProxy = { - wait: jest.fn(), - FirstUser: ["Jane Doe", "jane", "12345", false, {}], - SetFirstUser: jest.fn().mockResolvedValue(setFirstUserResult), - RemoveFirstUser: jest.fn().mockResolvedValue(0), - SetRootPassword: jest.fn().mockResolvedValue(0), - RemoveRootPassword: jest.fn().mockResolvedValue(0), - RootPasswordSet: false, - SetRootSSHKey: jest.fn().mockResolvedValue(0), - RootSSHKey: "ssh-key" +let firstUser = { + fullName: "Jane Doe", + userName: "jane", + password: "12345", + autologin: false, }; beforeEach(() => { - // @ts-ignore - DBusClient.mockImplementation(() => { - return { - proxy: (iface) => { - if (iface === USERS_IFACE) return usersProxy; - } - }; - }); + client = new UsersClient(new HTTPClient(new URL("http://localhost"))); }); describe("#getUser", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue(firstUser); + }); + it("returns the defined first user", async () => { - const client = new UsersClient(); const user = await client.getUser(); - expect(user).toEqual({ - fullName: "Jane Doe", - userName: "jane", - password: "12345", - autologin: false - }); + expect(user).toEqual(firstUser); + expect(mockGetFn).toHaveBeenCalledWith("/users/first"); }); }); describe("#isRootPasswordSet", () => { describe("when the root password is set", () => { beforeEach(() => { - usersProxy.RootPasswordSet = true; + mockJsonFn.mockResolvedValue({ password: "12345", sshkey: "" }); }); it("returns true", async () => { - const client = new UsersClient(); - const result = await client.isRootPasswordSet(); - expect(result).toEqual(true); + expect(await client.isRootPasswordSet()).toEqual(true); + expect(mockGetFn).toHaveBeenCalledWith("/users/root"); }); }); describe("when the root password is not set", () => { beforeEach(() => { - usersProxy.RootPasswordSet = false; + mockJsonFn.mockResolvedValue({ password: "", sshkey: "" }); }); it("returns false", async () => { - const client = new UsersClient(); - const result = await client.isRootPasswordSet(); - expect(result).toEqual(false); + expect(await client.isRootPasswordSet()).toEqual(false); + expect(mockGetFn).toHaveBeenCalledWith("/users/root"); }); }); }); describe("#getRootSSHKey", () => { + beforeEach(() => { + mockJsonFn.mockResolvedValue({ password: "", sshkey: "ssh-key" }); + }); + it("returns the SSH key for the root user", async () => { - const client = new UsersClient(); - const result = await client.getRootSSHKey(); - expect(result).toEqual("ssh-key"); + const result = expect(await client.getRootSSHKey()).toEqual("ssh-key"); + expect(mockGetFn).toHaveBeenCalledWith("/users/root"); }); }); describe("#setUser", () => { it("sets the values of the first user and returns whether succeeded or not an errors found", async () => { - const client = new UsersClient(); - const result = await client.setUser({ + let user = { fullName: "Jane Doe", userName: "jane", password: "12345", - autologin: false - }); - - expect(usersProxy.SetFirstUser).toHaveBeenCalledWith("Jane Doe", "jane", "12345", false, {}); - expect(result).toEqual({ result: true, issues: [] }); + autologin: false, + }; + const result = await client.setUser(user); + expect(mockPutFn).toHaveBeenCalledWith("/users/first", user); + expect(result); }); describe("when setting the user fails because some issue", () => { beforeEach(() => { - setFirstUserResult = [false, ["There is an error"]]; - usersProxy.SetFirstUser = jest.fn().mockResolvedValue(setFirstUserResult); + mockPutFn.mockResolvedValue({ ok: false }); }); - it("returns an object with the result as false and the issues found", async () => { - const client = new UsersClient(); + // issues are not included in the response + it.skip("returns an object with the result as false and the issues found", async () => { const result = await client.setUser({ fullName: "Jane Doe", userName: "jane", password: "12345", - autologin: false + autologin: false, }); + expect(mockPutFn).toHaveBeenCalledWith("/users/first"); expect(result).toEqual({ result: false, issues: ["There is an error"] }); }); }); @@ -136,75 +146,99 @@ describe("#setUser", () => { describe("#removeUser", () => { it("removes the first user and returns true", async () => { - const client = new UsersClient(); const result = await client.removeUser(); - expect(usersProxy.RemoveFirstUser).toHaveBeenCalled(); expect(result).toEqual(true); + expect(mockDeleteFn).toHaveBeenCalledWith("/users/first"); }); describe("when removing the user fails", () => { - beforeEach(() => (usersProxy.RemoveFirstUser = jest.fn().mockResolvedValue(1))); + beforeEach(() => { + mockDeleteFn.mockResolvedValue({ ok: false }); + }); it("returns false", async () => { - const client = new UsersClient(); const result = await client.removeUser(); expect(result).toEqual(false); + expect(mockDeleteFn).toHaveBeenCalledWith("/users/first"); }); }); }); describe("#setRootPassword", () => { it("sets the root password and returns true", async () => { - const client = new UsersClient(); const result = await client.setRootPassword("12345"); - expect(usersProxy.SetRootPassword).toHaveBeenCalledWith("12345", false); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { + password: "12345", + password_encrypted: false, + }); expect(result).toEqual(true); }); describe("when setting the password fails", () => { - beforeEach(() => (usersProxy.SetRootPassword = jest.fn().mockResolvedValue(1))); + beforeEach(() => { + mockPatchFn.mockResolvedValue({ ok: false }); + }); it("returns false", async () => { - const client = new UsersClient(); const result = await client.setRootPassword("12345"); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { + password: "12345", + password_encrypted: false, + }); expect(result).toEqual(false); }); }); }); describe("#removeRootPassword", () => { + beforeEach(() => { + mockPatchFn.mockResolvedValue({ ok: true }); + }); + it("removes the root password", async () => { - const client = new UsersClient(); const result = await client.removeRootPassword(); - expect(usersProxy.RemoveRootPassword).toHaveBeenCalled(); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { + password: "", + password_encrypted: false, + }); expect(result).toEqual(true); }); describe("when setting the user fails", () => { - beforeEach(() => (usersProxy.RemoveRootPassword = jest.fn().mockResolvedValue(1))); + beforeEach(() => { + mockPatchFn.mockResolvedValue({ ok: false }); + }); it("returns false", async () => { - const client = new UsersClient(); const result = await client.removeRootPassword(); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { + password: "", + password_encrypted: false, + }); expect(result).toEqual(false); }); }); }); describe("#setRootSSHKey", () => { + beforeEach(() => { + mockPatchFn.mockResolvedValue({ ok: true }); + }); + it("sets the root password and returns true", async () => { - const client = new UsersClient(); const result = await client.setRootSSHKey("ssh-key"); - expect(usersProxy.SetRootSSHKey).toHaveBeenCalledWith("ssh-key"); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { sshkey: "ssh-key" }); expect(result).toEqual(true); }); describe("when setting the user fails", () => { - beforeEach(() => (usersProxy.SetRootSSHKey = jest.fn().mockResolvedValue(1))); + beforeEach(() => { + mockPatchFn.mockResolvedValue({ ok: false }); + }); it("returns false", async () => { - const client = new UsersClient(); const result = await client.setRootSSHKey("ssh-key"); + expect(mockPatchFn).toHaveBeenCalledWith("/users/root", { sshkey: "ssh-key" }); expect(result).toEqual(false); }); }); From 49062cb61bfe093c2be94af480c93574973c13d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:16:52 +0100 Subject: [PATCH 12/17] web: remove duplicated import from storage.test.js --- web/src/client/storage.test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 6b4d2e73d9..5324135154 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -24,7 +24,6 @@ import { HTTPClient } from "./http"; import DBusClient from "./dbus"; -import { HTTPClient } from "./http"; import { StorageClient } from "./storage"; const mockJsonFn = jest.fn(); From e01d58c8e50fbf4d6fba0a25440af6c98aa15309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:28:50 +0100 Subject: [PATCH 13/17] web: fix iSCSI tests --- web/src/client/storage.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 5324135154..9f79e55d4f 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -2277,6 +2277,7 @@ describe("#iscsi", () => { describe("#getInitiator", () => { beforeEach(() => { + mockGetFn.mockResolvedValue({ ok: true, json: mockJsonFn }); mockJsonFn.mockResolvedValue({ name: "iqn.1996-04.com.suse:01:351e6d6249", ibft: false, From 86676f87c14aa2924e5dba9c10b873dc006c7b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:30:56 +0100 Subject: [PATCH 14/17] web: fix Questions component test --- web/src/components/questions/Questions.test.jsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/components/questions/Questions.test.jsx b/web/src/components/questions/Questions.test.jsx index ced3aeaeb0..6c4d6e625b 100644 --- a/web/src/components/questions/Questions.test.jsx +++ b/web/src/components/questions/Questions.test.jsx @@ -29,10 +29,13 @@ import { Questions } from "~/components/questions"; jest.mock("~/client"); jest.mock("~/components/questions/GenericQuestion", () => () =>
A Generic question mock
); -jest.mock("~/components/questions/LuksActivationQuestion", () => () =>
A LUKS activation question mock
); +jest.mock( + "~/components/questions/LuksActivationQuestion", + () => () =>
A LUKS activation question mock
, +); const handlers = {}; -const genericQuestion = { id: 1, type: 'generic' }; +const genericQuestion = { id: 1, type: "generic" }; const luksActivationQuestion = { id: 1, class: "storage.luks_activation" }; let pendingQuestions = []; @@ -42,16 +45,17 @@ beforeEach(() => { questions: { getQuestions: () => Promise.resolve(pendingQuestions), // Capture the handler for the onQuestionAdded signal for triggering it manually - onQuestionAdded: onAddHandler => { + onQuestionAdded: (onAddHandler) => { handlers.onAdd = onAddHandler; return jest.fn; }, // Capture the handler for the onQuestionREmoved signal for triggering it manually - onQuestionRemoved: onRemoveHandler => { + onQuestionRemoved: (onRemoveHandler) => { handlers.onRemove = onRemoveHandler; return jest.fn; }, - } + listenQuestions: jest.fn(), + }, }; }); }); From c7fd738e2cbe43a9264fa17d8e20119ae9fb7640 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:32:03 +0100 Subject: [PATCH 15/17] web: fix InstallationProgress component test --- web/src/components/core/InstallationProgress.test.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/core/InstallationProgress.test.jsx b/web/src/components/core/InstallationProgress.test.jsx index f21d05a0e4..140d16981d 100644 --- a/web/src/components/core/InstallationProgress.test.jsx +++ b/web/src/components/core/InstallationProgress.test.jsx @@ -28,7 +28,7 @@ import InstallationProgress from "./InstallationProgress"; jest.mock("~/components/core/ProgressReport", () => () =>
ProgressReport Mock
); jest.mock("~/components/core/Sidebar", () => () =>
Agama sidebar
); -jest.mock("~/components/core/Sidebar", () => () =>
Questions Mock
); +jest.mock("~/components/questions/Questions", () => () =>
Questions Mock
); describe("InstallationProgress", () => { it("uses 'Installing' as title", () => { From 9756369d77a01fb666528043266c8ba3cef08932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:42:25 +0100 Subject: [PATCH 16/17] web: fix linter errors --- web/src/client/l10n.test.js | 22 +++++++++--------- web/src/client/storage.test.js | 26 +-------------------- web/src/client/users.test.js | 4 ++-- web/src/components/questions/Questions.jsx | 27 +++++++++++----------- 4 files changed, 28 insertions(+), 51 deletions(-) diff --git a/web/src/client/l10n.test.js b/web/src/client/l10n.test.js index faf0c71bac..e24dbb7192 100644 --- a/web/src/client/l10n.test.js +++ b/web/src/client/l10n.test.js @@ -49,25 +49,25 @@ let client; const locales = [ { - "id": "en_US.UTF-8", - "language": "English", - "territory": "United States", + id: "en_US.UTF-8", + language: "English", + territory: "United States", }, { - "id": "es_ES.UTF-8", - "language": "Spanish", - "territory": "Spain", + id: "es_ES.UTF-8", + language: "Spanish", + territory: "Spain", }, ]; const config = { - "locales": [ + locales: [ "en_US.UTF-8", ], - "keymap": "us", - "timezone": "Europe/Berlin", - "uiLocale": "en_US.UTF-8", - "uiKeymap": "us", + keymap: "us", + timezone: "Europe/Berlin", + uiLocale: "en_US.UTF-8", + uiKeymap: "us", }; beforeEach(() => { diff --git a/web/src/client/storage.test.js b/web/src/client/storage.test.js index 9f79e55d4f..510fe7e6b5 100644 --- a/web/src/client/storage.test.js +++ b/web/src/client/storage.test.js @@ -569,30 +569,6 @@ const contexts = { withoutISCSINodes: () => { cockpitProxies.iscsiNodes = {}; }, - withISCSINodes: () => { - cockpitProxies.iscsiNodes = { - "/org/opensuse/Agama/Storage1/iscsi_nodes/1": { - path: "/org/opensuse/Agama/Storage1/iscsi_nodes/1", - Target: "iqn.2023-01.com.example:37dac", - Address: "192.168.100.101", - Port: 3260, - Interface: "default", - IBFT: false, - Connected: false, - Startup: "" - }, - "/org/opensuse/Agama/Storage1/iscsi_nodes/2": { - path: "/org/opensuse/Agama/Storage1/iscsi_nodes/2", - Target: "iqn.2023-01.com.example:74afb", - Address: "192.168.100.102", - Port: 3260, - Interface: "default", - IBFT: true, - Connected: true, - Startup: "onboot" - } - }; - }, withISCSINodes: () => [ { id: 1, @@ -2374,7 +2350,7 @@ describe("#iscsi", () => { }); describe("#login", () => { - let auth = { + const auth = { username: "test", password: "12345", reverseUsername: "target", diff --git a/web/src/client/users.test.js b/web/src/client/users.test.js index eac042771c..fbecae44b9 100644 --- a/web/src/client/users.test.js +++ b/web/src/client/users.test.js @@ -53,7 +53,7 @@ jest.mock("./http", () => { let client; -let firstUser = { +const firstUser = { fullName: "Jane Doe", userName: "jane", password: "12345", @@ -113,7 +113,7 @@ describe("#getRootSSHKey", () => { describe("#setUser", () => { it("sets the values of the first user and returns whether succeeded or not an errors found", async () => { - let user = { + const user = { fullName: "Jane Doe", userName: "jane", password: "12345", diff --git a/web/src/components/questions/Questions.jsx b/web/src/components/questions/Questions.jsx index 569ff12109..a4ca87d02f 100644 --- a/web/src/components/questions/Questions.jsx +++ b/web/src/components/questions/Questions.jsx @@ -14,12 +14,12 @@ * * You should have received a copy of the GNU General Public License along * with this program; if not, contact SUSE LLC. - *Creating new + * * To contact SUSE LLC about this file by physical or electronic mail, you may * find current contact information at www.suse.com. */ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useCallback, useEffect, useState } from "react"; import { useInstallerClient } from "~/context/installer"; import { useCancellablePromise } from "~/utils"; @@ -31,27 +31,26 @@ export default function Questions() { const [pendingQuestions, setPendingQuestions] = useState([]); - const addQuestion = useCallback(question => { - setPendingQuestions(pending => [...pending, question]); + const addQuestion = useCallback((question) => { + setPendingQuestions((pending) => [...pending, question]); }, []); - const removeQuestion = useCallback(id => - setPendingQuestions(pending => pending.filter(q => q.id !== id)) - , []); + const removeQuestion = useCallback( + (id) => setPendingQuestions((pending) => pending.filter((q) => q.id !== id)), + [], + ); - const answerQuestion = useCallback(question => { + const answerQuestion = useCallback((question) => { client.questions.answer(question); removeQuestion(question.id); }, [client.questions, removeQuestion]); - useEffect(() => client.questions.listenQuestions(), - [client.questions, cancellablePromise] - ); + useEffect(() => client.questions.listenQuestions(), [client.questions, cancellablePromise]); useEffect(() => { cancellablePromise(client.questions.getQuestions()) .then(setPendingQuestions) - .catch(e => console.error("Something went wrong retrieving pending questions", e)); + .catch((e) => console.error("Something went wrong retrieving pending questions", e)); }, [client.questions, cancellablePromise]); useEffect(() => { @@ -59,7 +58,9 @@ export default function Questions() { unsubscribeCallbacks.push(client.questions.onQuestionAdded(addQuestion)); unsubscribeCallbacks.push(client.questions.onQuestionRemoved(removeQuestion)); - return () => { unsubscribeCallbacks.forEach(cb => cb()) }; + return () => { + unsubscribeCallbacks.forEach((cb) => cb()); + }; }, [client.questions, addQuestion, removeQuestion]); if (pendingQuestions.length === 0) return null; From 9748440e1538d61babc3d2313d04d32ddd2fb5e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Imobach=20Gonz=C3=A1lez=20Sosa?= Date: Mon, 13 May 2024 10:43:32 +0100 Subject: [PATCH 17/17] web: enable tests, linters and build on CI --- .github/workflows/ci-web.yml | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-web.yml b/.github/workflows/ci-web.yml index 223f2050be..6b15945bf1 100644 --- a/.github/workflows/ci-web.yml +++ b/.github/workflows/ci-web.yml @@ -57,24 +57,24 @@ jobs: - name: Install dependencies run: npm install -# -# - name: Build the application -# run: make -# + + - name: Build the application + run: npm run build + # - name: Run check spell # run: npm run cspell # # - name: Check types # run: npm run check-types # -# - name: Run ESLint -# run: npm run eslint -# -# - name: Run Stylelint -# run: npm run stylelint -# -# - name: Run the tests and generate coverage report -# run: npm test -- --coverage + - name: Run ESLint + run: npm run eslint + + - name: Run Stylelint + run: npm run stylelint + + - name: Run the tests and generate coverage report + run: npm test -- --coverage # # # send the code coverage for the web part to the coveralls.io # - name: Coveralls GitHub Action