From 392e49bf49d33abfe29079c707edd2a5850a2950 Mon Sep 17 00:00:00 2001 From: Priya Patel Date: Fri, 13 Mar 2026 15:14:38 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20window=20postmessage?= =?UTF-8?q?=20pattern=20->=20contextbridge=20boundary=20api=20(#3167)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 postmessage pattern -> contextbridge boundary api * refactor: 💡 use sinon stub for boundary api mock test helper * refactor: 💡 updated tests * refactor: 💡 renamed boundary mock api * chore: 🤖 addressed feedback * refactor: 💡 updated context bridge name, added nested objects --- .../handlers/cache-daemon-search.js | 69 ++-- .../addon/handlers/cache-daemon-handler.js | 20 +- addons/api/app/mirage/config.js | 8 +- addons/api/app/mirage/scenarios/ipc.js | 296 ---------------- .../ipc/desktop-context-bridge-api.js | 318 ++++++++++++++++++ .../app/components/session/tabs/index.js | 10 +- .../settings-card/client-agent/index.js | 5 +- .../components/settings-card/logs/index.js | 6 +- ui/desktop/app/controllers/application.js | 14 +- .../scopes/scope/authenticate/method/index.js | 3 +- .../scopes/scope/projects/sessions/index.js | 3 +- .../scopes/scope/projects/targets/index.js | 8 +- ui/desktop/app/routes/application.js | 10 +- ui/desktop/app/routes/scopes.js | 4 +- .../app/routes/scopes/scope/projects.js | 8 +- .../scopes/scope/projects/sessions/index.js | 3 +- .../scopes/scope/projects/sessions/session.js | 3 +- .../scopes/scope/projects/settings/index.js | 13 +- .../scopes/scope/projects/targets/index.js | 4 +- .../app/services/client-agent-sessions.js | 7 +- ui/desktop/app/services/cluster-url.js | 7 +- ui/desktop/app/services/ipc.js | 147 -------- ui/desktop/app/services/rdp.js | 16 +- ui/desktop/app/services/session.js | 3 +- ui/desktop/electron-app/src/preload.js | 108 ++++-- .../tests/acceptance/application-test.js | 15 +- .../tests/acceptance/authentication-test.js | 41 +-- .../tests/acceptance/cluster-url-test.js | 25 +- ui/desktop/tests/acceptance/projects-test.js | 5 +- .../projects/sessions/index-test.js | 14 +- .../projects/sessions/session-test.js | 50 +-- .../projects/settings/index-test.js | 57 ++-- .../acceptance/projects/targets/index-test.js | 38 +-- .../projects/targets/target-test.js | 43 +-- ui/desktop/tests/acceptance/scopes-test.js | 16 +- .../desktop-context-bridge-api-mock.js | 119 +++++++ ui/desktop/tests/helpers/window-mock-ipc.js | 106 ------ .../unit/controllers/application-test.js | 14 +- .../unit/controllers/cluster-url-test.js | 13 +- .../scope/authenticate/method/index-test.js | 4 +- .../scope/projects/sessions/index-test.js | 4 +- .../scope/projects/targets/index-test.js | 7 +- .../tests/unit/services/cluster-url-test.js | 13 +- ui/desktop/tests/unit/services/ipc-test.js | 103 ------ ui/desktop/tests/unit/services/rdp-test.js | 26 +- 45 files changed, 744 insertions(+), 1062 deletions(-) delete mode 100644 addons/api/app/mirage/scenarios/ipc.js create mode 100644 addons/api/app/mirage/scenarios/ipc/desktop-context-bridge-api.js delete mode 100644 ui/desktop/app/services/ipc.js create mode 100644 ui/desktop/tests/helpers/desktop-context-bridge-api-mock.js delete mode 100644 ui/desktop/tests/helpers/window-mock-ipc.js delete mode 100644 ui/desktop/tests/unit/services/ipc-test.js diff --git a/addons/api/addon-test-support/handlers/cache-daemon-search.js b/addons/api/addon-test-support/handlers/cache-daemon-search.js index cf1ca489b..3d5207580 100644 --- a/addons/api/addon-test-support/handlers/cache-daemon-search.js +++ b/addons/api/addon-test-support/handlers/cache-daemon-search.js @@ -11,7 +11,7 @@ import { underscore } from '@ember/string'; /** * This test helper can be used to help setup your sinon stubs in your tests. - * Must be called after setupMirage. + * Must be called after setupMirage and setupDesktopContextBridgeApiMock in tests. * * ```js * import setupStubs from 'api/test-support/handlers/cache-daemon-search'; @@ -19,14 +19,15 @@ import { underscore } from '@ember/string'; * module('Acceptance | my test', function(hooks) { * setupApplicationTest(hooks); * setupMirage(hooks); + * setupDesktopContextBridgeApiMock(hooks); * setupStubs(hooks); * * // add your actual tests here * }); * ``` * - * This will setup a stub for the IPC service and include helper methods to - * help setup what the cache daemon search command returns. + * This will setup a stub for window.desktop.daemon.searchCacheDaemon and include + * helper methods to help setup what the cache daemon search command returns. * * @module Test Helpers * @public @@ -35,9 +36,6 @@ export default function setupStubs(hooks) { let stubTypes = []; hooks.beforeEach(function () { - const ipcService = this.owner.lookup('service:ipc'); - this.ipcStub = sinon.stub(ipcService, 'invoke'); - /** * We aim to still use mirage data when stubbing out the cache daemon * search. However, the handler expects the search data to be raw JSON from @@ -56,34 +54,32 @@ export default function setupStubs(hooks) { stubTypes = types; types.forEach((type, i) => { - this.ipcStub - .withArgs('searchCacheDaemon') - .onCall(i) - .callsFake(() => { - let models; - let resourceName; - if (typeOf(type) === 'object') { - models = type.func(); - resourceName = type.resource; - } else { - models = this.server.schema[type].all().models; - resourceName = type; - } + this.searchCacheDaemonStub.onCall(i).callsFake(() => { + let models; + let resourceName; + if (typeOf(type) === 'object') { + models = type.func(); + resourceName = type.resource; + } else { + models = this.server.schema[type].all().models; + resourceName = type; + } - return { - [underscore(resourceNames[singularize(resourceName)])]: - models.map((model) => { - // Use internal serializer to serialize the model correctly - // according to our mirage serializers - const modelData = - this.server.serializerOrRegistry.serialize(model); + return { + [underscore(resourceNames[singularize(resourceName)])]: models.map( + (model) => { + // Use internal serializer to serialize the model correctly + // according to our mirage serializers + const modelData = + this.server.serializerOrRegistry.serialize(model); - // Serialize the data properly to standard JSON as that is what - // we're expecting from the cache daemon response - return JSON.parse(JSON.stringify(modelData)); - }), - }; - }); + // Serialize the data properly to standard JSON as that is what + // we're expecting from the cache daemon response + return JSON.parse(JSON.stringify(modelData)); + }, + ), + }; + }); }); }; }); @@ -91,22 +87,19 @@ export default function setupStubs(hooks) { hooks.afterEach(function () { // Verify the number of mocks we set up match with how // many times the cache daemon was called with - sinon.assert.callCount( - this.ipcStub.withArgs('searchCacheDaemon'), - stubTypes.length, - ); + sinon.assert.callCount(this.searchCacheDaemonStub, stubTypes.length); stubTypes.forEach((type, i) => { // Assert that the stub was called with the resource we're trying to mock // so we don't mistakenly mock the wrong resource - const ipcCall = this.ipcStub.withArgs('searchCacheDaemon').getCall(i); + const call = this.searchCacheDaemonStub.getCall(i); let resource = type; if (typeOf(type) === 'object') { resource = type.resource; } const mappedResourceName = resourceNames[singularize(resource)]; - sinon.assert.match(ipcCall.args[1], { resource: mappedResourceName }); + sinon.assert.match(call.args[0], { resource: mappedResourceName }); }); sinon.restore(); diff --git a/addons/api/addon/handlers/cache-daemon-handler.js b/addons/api/addon/handlers/cache-daemon-handler.js index 851a827e8..3f406c3f4 100644 --- a/addons/api/addon/handlers/cache-daemon-handler.js +++ b/addons/api/addon/handlers/cache-daemon-handler.js @@ -59,7 +59,6 @@ const fetchControllerData = async (context, next) => { */ export default class CacheDaemonHandler { @service session; - @service ipc; constructor(context) { setOwner(this, getOwner(context)); @@ -70,9 +69,8 @@ export default class CacheDaemonHandler { case 'query': { const { store, data } = context.request; const { type, query, options: { pushToStore = true } = {} } = data; - const isCacheDaemonRunning = await this.ipc.invoke( - 'isCacheDaemonRunning', - ); + const isCacheDaemonRunning = + await window.desktop.daemon.isCacheDaemonRunning(); // eslint-disable-next-line no-unused-vars let { recursive, scope_id, page, pageSize, ...remainingQuery } = query; @@ -119,23 +117,19 @@ export default class CacheDaemonHandler { let cacheDaemonResults = {}; try { - cacheDaemonResults = await this.ipc.invoke( - 'searchCacheDaemon', - remainingQuery, - ); + cacheDaemonResults = + await window.desktop.daemon.searchCacheDaemon(remainingQuery); } catch (e) { // If we got a 403, most likely the cache daemon was restarted and our token is no longer valid // I'm not sure if we can get a 401 since we always send a token but we'll handle it in the same way if (e.statusCode === 403 || e.statusCode === 401) { try { - await this.ipc.invoke('addTokenToDaemons', { + await window.desktop.daemon.addTokenToDaemons({ tokenId: auth_token_id, token, }); - cacheDaemonResults = await this.ipc.invoke( - 'searchCacheDaemon', - remainingQuery, - ); + cacheDaemonResults = + await window.desktop.daemon.searchCacheDaemon(remainingQuery); } catch (err) { // If it fails again just fall back to fetching controller data __electronLog?.error( diff --git a/addons/api/app/mirage/config.js b/addons/api/app/mirage/config.js index a9eb58499..be86b60f8 100644 --- a/addons/api/app/mirage/config.js +++ b/addons/api/app/mirage/config.js @@ -7,7 +7,7 @@ import { createServer, Response } from 'miragejs'; import { authHandler, deauthHandler } from './route-handlers/auth'; import { targetHandler } from './route-handlers/target'; import { pickRandomStatusString } from './factories/session'; -import initializeMockIPC from './scenarios/ipc'; +import initializeMockDesktopContextBridgeAPI from './scenarios/ipc/desktop-context-bridge-api'; import makeBooleanFilter from './helpers/bexpr-filter'; import { faker } from '@faker-js/faker'; import { asciicasts } from './data/asciicasts'; @@ -66,7 +66,7 @@ import workerSerializer from './serializers/worker'; // mirage scenarios (alphabetical) import defaultScenario from './scenarios/default'; -import ipcScenario from './scenarios/ipc'; +import desktopContextBridgeApiScenario from './scenarios/ipc/desktop-context-bridge-api'; // mirage factories (alphabetical) import accountFactory from './factories/account'; @@ -103,7 +103,7 @@ export default function (mirageConfig) { scenarios: { default: defaultScenario, - ipcScenario: ipcScenario, + desktopContextBridgeApiScenario: desktopContextBridgeApiScenario, }, factories: { @@ -190,7 +190,7 @@ export default function (mirageConfig) { // Only routes are defined here function routes() { - initializeMockIPC(this, environmentConfig); + initializeMockDesktopContextBridgeAPI(this, environmentConfig); this.passthrough(); // make this `http://localhost:8080`, for example, if your API is on a different server diff --git a/addons/api/app/mirage/scenarios/ipc.js b/addons/api/app/mirage/scenarios/ipc.js deleted file mode 100644 index 0cd614b4d..000000000 --- a/addons/api/app/mirage/scenarios/ipc.js +++ /dev/null @@ -1,296 +0,0 @@ -/** - * Copyright IBM Corp. 2021, 2026 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { faker } from '@faker-js/faker'; - -export default function initializeMockIPC(server, config) { - const isTesting = config.environment === 'test'; - - /** - * We strive to make this application runnable in a regular web browser, since - * it is a convenient environment for development and testing. But only an - * Electron environment has true IPC. Outside of Electron or with mirage, - * we mock the handling of the message-based IPC requests originating from the - * renderer (the Ember app). - */ - class MockIPC { - async invoke(method, payload) { - return await this[method](payload); - } - - clusterUrl = null; - - checkOS() { - return { - isLinux: false, - isMac: true, - isWindows: false, - }; - } - - getClusterUrl() { - return this.clusterUrl; - } - - setClusterUrl(clusterUrl) { - this.clusterUrl = clusterUrl; - return this.clusterUrl; - } - - resetClusterUrl() { - this.clusterUrl = null; - } - - openExternal(href) { - window.open(href); - } - - cliExists() { - return true; - } - - /** - * Creates a new session instance associated with the specified target. - * @param connect_details - * @param - */ - connect({ target_id }) { - const target = server.schema.targets.find(target_id); - const { scope, type } = target; - const newSession = server.schema.sessions.create({ - scope, - target, - userId: 'authenticateduser', - type, - created_time: new Date().toISOString(), - expiration_time: faker.date.soon(), - }); - const decoded = { - key1: 'value 1', - key2: true, - key3: 'test', - }; - const raw = btoa(JSON.stringify(decoded)); - return { - address: window.location.hostname, - port: faker.internet.port(), - protocol: type, - session_id: newSession.id, - credentials: [ - { - credential_source: { - id: 'clvlt_4cvscMTl0N', - name: 'Credential Library 1', - description: 'Source Description', - credential_store_id: 'csvlt_Q1HFGt7Jpm', - type: 'vault', - }, - secret: { - raw: btoa('just-a-random-string'), - }, - }, - { - credential_source: { - id: 'clvlt_4cvscMTl0N', - name: 'Credential Library 0', - description: 'Source Description', - credential_store_id: 'csvlt_Q1HFGt7Jpm', - type: 'vault-generic', - }, - secret: { - raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ', - decoded: { - data: { - backslash: 'password\\withslash\\', - backslash1: 'password\\fwithslash\\f', - password: '123', - username: 'test', - email: { - address: 'test.com', - }, - }, - metadata: { - created_time: '2024-04-12T18:38:36.226715555Z', - custom_metadata: null, - deletion_time: '', - destroyed: false, - version: 8, - }, - }, - }, - }, - - { - credential_source: { - id: 'credjson_7cKBbBEkC3', - credential_store_id: 'csst_yzlsot2pum', - type: 'static', - credential_type: 'json', - }, - secret: { - raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ==', - decoded: { - secret_key: 'QWERTYUIOP', - secret_access_key: 'QWERT.YUIOP234567890', - nested_secret: { - session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321', - complex_nest: { - blackslash: 'password\\withslash\\', - hash: 'qazxswedcvfrtgbnjhyujm.1234567890', - }, - }, - }, - }, - credential: { - secret_key: 'QWERTYUIOP', - secret_access_key: 'QWERT.YUIOP234567890', - nested_secret: { - session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321', - complex_nest: { - hash: 'qazxswedcvfrtgbnjhyujm.1234567890', - }, - }, - }, - }, - { - credential_source: { - id: 'clvlt_4cvscMTl0N', - name: 'Credential Library 2', - description: 'Source Description', - credential_store_id: 'csvlt_Q1HFGt7Jpm', - type: 'vault', - }, - secret: { - raw, - decoded, - }, - }, - { - credential_source: { - id: 'clvlt_9KWscxpcY7', - name: 'Credential Library 3', - description: 'Source Description', - credential_store_id: 'csvlt_Q1HFGt7Jpm', - type: 'vault', - }, - secret: { - raw: btoa('just-a-random-string'), - }, - }, - { - credential_source: { - id: 'clvlt_9KWscxpcY7', - name: 'Credential Library 4', - description: 'Source Description', - credential_store_id: 'csvlt_Q1HFGt7Jpm', - type: 'vault', - }, - secret: { - raw, - decoded, - }, - }, - ], - }; - } - - /** - * Check for window chrome on MacOS - */ - hasMacOSChrome() { - return true; - } - - /** - * Check for OS chrome state - */ - showWindowActions() { - if (navigator.userAgent.includes('Mac')) { - return false; - } - return true; - } - - /** - * Do nothing when attempting to minimize, toggle fullscreen, - * and close a browser window - * * Do nothing when canceling a session invokes the stop method - */ - minimizeWindow() {} - closeWindow() {} - toggleFullscreenWindow() {} - stop() {} - checkCommand() { - return faker.system.filePath(); - } - addTokenToDaemons() {} - searchCacheDaemon() {} - isCacheDaemonRunning() { - return false; - } - isClientAgentRunning() { - return false; - } - cacheDaemonStatus() { - return { version: 'Boundary v1.0.0' }; - } - getCliVersion() { - return { versionNumber: '1.0.0' }; - } - getDesktopVersion() { - return { desktopVersion: '1.0.0' }; - } - getLogLevel() { - return 'info'; - } - getLogPath() { - return '~/.config/Boundary/logs/desktop-client.log'; - } - setLogLevel() {} - clientAgentStatus() { - return { version: '0.0.1-dev', status: 'running' }; - } - pauseClientAgent() {} - resumeClientAgent() {} - hasRunningSessions() {} - stopAll() {} - getRdpClients() { - return ['windows-app', 'none', 'mstsc']; - } - getPreferredRdpClient() { - return 'windows-app'; - } - getRecommendedRdpClient() { - return { - name: 'windows-app', - link: 'https://apps.apple.com/us/app/windows-app/id1295203466', - }; - } - setPreferredRdpClient() {} - launchRdpClient() {} - } - - /** - * Establishes a mock IPC handler, which mimics the behavior of the Electron - * main process, and routes messages to it in a way similar to our - * preload.js script. - * - * Initializes mock IPC only in a non-testing context and when mirage is turned on. - * We mock certain functions even in electron (e.g. hasMacOSChrome) when running - * locally which will force a certain appearance regardless of platform - */ - if (config.mirage?.enabled && !isTesting) { - const mockIPC = new MockIPC(); - - window.addEventListener('message', async function (event) { - if (event.origin !== window.location.origin) return; - const { method, payload } = event?.data ?? {}; - if (method) { - const response = await mockIPC.invoke(method, payload); - event.ports[0].postMessage(response); - } - }); - } -} diff --git a/addons/api/app/mirage/scenarios/ipc/desktop-context-bridge-api.js b/addons/api/app/mirage/scenarios/ipc/desktop-context-bridge-api.js new file mode 100644 index 000000000..b06cae8b9 --- /dev/null +++ b/addons/api/app/mirage/scenarios/ipc/desktop-context-bridge-api.js @@ -0,0 +1,318 @@ +/** + * Copyright IBM Corp. 2021, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { faker } from '@faker-js/faker'; + +export default function initializeMockDesktopContextBridgeAPI(server, config) { + const isTesting = config.environment === 'test'; + + /** + * We strive to make this application runnable in a regular web browser, since + * it is a convenient environment for development and testing. But only an + * Electron environment has the desktop API exposed via contextBridge. + * Outside of Electron or with mirage, we mock the desktop API object to + * simulate the Electron preload script's exposed API. + */ + class MockDesktopContextBridgeAPI { + clusterUrl = null; + + cluster = { + getClusterUrl: () => { + return this.clusterUrl; + }, + + setClusterUrl: (clusterUrl) => { + this.clusterUrl = clusterUrl; + return this.clusterUrl; + }, + + resetClusterUrl: () => { + this.clusterUrl = null; + }, + }; + + system = { + checkOS: () => { + return { + isLinux: false, + isMac: true, + isWindows: false, + }; + }, + + openExternal: (href) => { + window.open(href); + }, + + cliExists: () => { + return true; + }, + + checkCommand: () => { + return faker.system.filePath(); + }, + + getCliVersion: () => { + return { versionNumber: '1.0.0' }; + }, + + getDesktopVersion: () => { + return { desktopVersion: '1.0.0' }; + }, + }; + + session = { + /** + * Creates a new session instance associated with the specified target. + * @param connect_details + * @param + */ + connectSession: ({ target_id }) => { + const target = server.schema.targets.find(target_id); + const { scope, type } = target; + const newSession = server.schema.sessions.create({ + scope, + target, + userId: 'authenticateduser', + type, + created_time: new Date().toISOString(), + expiration_time: faker.date.soon(), + }); + const decoded = { + key1: 'value 1', + key2: true, + key3: 'test', + }; + const raw = btoa(JSON.stringify(decoded)); + return { + address: window.location.hostname, + port: faker.internet.port(), + protocol: type, + session_id: newSession.id, + credentials: [ + { + credential_source: { + id: 'clvlt_4cvscMTl0N', + name: 'Credential Library 1', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault', + }, + secret: { + raw: btoa('just-a-random-string'), + }, + }, + { + credential_source: { + id: 'clvlt_4cvscMTl0N', + name: 'Credential Library 0', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault-generic', + }, + secret: { + raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ', + decoded: { + data: { + backslash: 'password\\withslash\\', + backslash1: 'password\\fwithslash\\f', + password: '123', + username: 'test', + email: { + address: 'test.com', + }, + }, + metadata: { + created_time: '2024-04-12T18:38:36.226715555Z', + custom_metadata: null, + deletion_time: '', + destroyed: false, + version: 8, + }, + }, + }, + }, + + { + credential_source: { + id: 'credjson_7cKBbBEkC3', + credential_store_id: 'csst_yzlsot2pum', + type: 'static', + credential_type: 'json', + }, + secret: { + raw: 'eyJhcnJheSI6WyJvbmUiLCJ0d28iLCJ0aHJlZSIsIm9uZSIsInR3byIsInRocmVlIiwib25lIiwidHdvIiwidGhyZWUiLCJvbmUiLCJ0d28iLCJ0aHJlZSJdLCJuZXN0ZWQiOnsiYm9vbCI6dHJ1ZSwibG9uZyI6IjEyMjM1MzQ1NmFzZWRmYTQzd3J0ZjIzNGYyM2FzZGdmYXNkZnJnYXdzZWZhd3NlZnNkZjQiLCJzZWNlcmV0Ijoic28gbmVzdGVkIn0sInRlc3QiOiJwaHJhc2UifQ==', + decoded: { + secret_key: 'QWERTYUIOP', + secret_access_key: 'QWERT.YUIOP234567890', + nested_secret: { + session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321', + complex_nest: { + blackslash: 'password\\withslash\\', + hash: 'qazxswedcvfrtgbnjhyujm.1234567890', + }, + }, + }, + }, + credential: { + secret_key: 'QWERTYUIOP', + secret_access_key: 'QWERT.YUIOP234567890', + nested_secret: { + session_token: 'ZXCVBNMLKJHGFDSAQWERTYUIOP0987654321', + complex_nest: { + hash: 'qazxswedcvfrtgbnjhyujm.1234567890', + }, + }, + }, + }, + { + credential_source: { + id: 'clvlt_4cvscMTl0N', + name: 'Credential Library 2', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault', + }, + secret: { + raw, + decoded, + }, + }, + { + credential_source: { + id: 'clvlt_9KWscxpcY7', + name: 'Credential Library 3', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault', + }, + secret: { + raw: btoa('just-a-random-string'), + }, + }, + { + credential_source: { + id: 'clvlt_9KWscxpcY7', + name: 'Credential Library 4', + description: 'Source Description', + credential_store_id: 'csvlt_Q1HFGt7Jpm', + type: 'vault', + }, + secret: { + raw, + decoded, + }, + }, + ], + }; + }, + + stopSession: () => {}, + hasRunningSessions: () => {}, + stopAllSessions: () => {}, + }; + + windowAction = { + /** + * Check for window chrome on MacOS + */ + hasMacOSChrome: () => { + return true; + }, + + /** + * Check for OS chrome state + */ + showWindowActions: () => { + if (navigator.userAgent.includes('Mac')) { + return false; + } + return true; + }, + + /** + * Do nothing when attempting to minimize, toggle fullscreen, + * and close a browser window + */ + minimizeWindow: () => {}, + closeWindow: () => {}, + toggleFullscreenWindow: () => {}, + focusWindow: () => {}, + }; + + daemon = { + addTokenToDaemons: () => {}, + searchCacheDaemon: () => {}, + isCacheDaemonRunning: () => { + return false; + }, + cacheDaemonStatus: () => { + return { version: 'Boundary v1.0.0' }; + }, + }; + + clientAgent = { + getClientAgentSessions: () => {}, + isClientAgentRunning: () => { + return false; + }, + clientAgentStatus: () => { + return { version: '0.0.1-dev', status: 'running' }; + }, + pauseClientAgent: () => {}, + resumeClientAgent: () => {}, + }; + + logging = { + getLogLevel: () => { + return 'info'; + }, + getLogPath: () => { + return '~/.config/Boundary/logs/desktop-client.log'; + }, + setLogLevel: () => {}, + }; + + rdp = { + getRdpClients: () => { + return ['windows-app', 'none', 'mstsc']; + }, + getPreferredRdpClient: () => { + return 'windows-app'; + }, + getRecommendedRdpClient: () => { + return { + name: 'windows-app', + link: 'https://apps.apple.com/us/app/windows-app/id1295203466', + }; + }, + setPreferredRdpClient: () => {}, + launchRdpClient: () => {}, + }; + + app = { + onAppQuit: (callback) => { + const listenerCallback = () => callback(); + window.addEventListener('onAppQuit', listenerCallback); + return () => { + window.removeEventListener('onAppQuit', listenerCallback); + }; + }, + }; + } + + /** + * Initializes mock only in a non-testing environment and when mirage is enabled. + * In browser mode, creates the API. In Electron mode, the API already exists + * from contextBridge and we can't override it + */ + if (config.mirage?.enabled && !isTesting) { + const desktopContextBridgeAPIMock = new MockDesktopContextBridgeAPI(); + // Only in browser mode, we create the desktop API object not in Electron mode because they already exist from contextBridge + if (!window.desktop) { + window.desktop = desktopContextBridgeAPIMock; + } + } +} diff --git a/ui/desktop/app/components/session/tabs/index.js b/ui/desktop/app/components/session/tabs/index.js index 74e8f0893..ba06148c9 100644 --- a/ui/desktop/app/components/session/tabs/index.js +++ b/ui/desktop/app/components/session/tabs/index.js @@ -5,7 +5,6 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import { service } from '@ember/service'; import { Terminal } from '@xterm/xterm'; import { FitAddon } from '@xterm/addon-fit'; import { CanvasAddon } from '@xterm/addon-canvas'; @@ -31,10 +30,6 @@ const calculateTerminalContainerHeight = (termContainer) => { }; export default class SessionTerminalTabsComponent extends Component { - // =services - - @service ipc; - // =attributes id; @@ -50,7 +45,7 @@ export default class SessionTerminalTabsComponent extends Component { return; } - const { isWindows } = await this.ipc.invoke('checkOS'); + const { isWindows } = await window.desktop.system.checkOS(); const xterm = new Terminal(terminalOptions); this.terminal = xterm; const fitAddon = new FitAddon(); @@ -83,7 +78,8 @@ export default class SessionTerminalTabsComponent extends Component { this.id = uuidv4(); this.#setupTerminal(fitAddon, xterm, termContainer); - const isSSHCommandAvailable = await this.ipc.invoke('checkCommand', 'ssh'); + const isSSHCommandAvailable = + await window.desktop.system.checkCommand('ssh'); const { model } = this.args; const { proxy_address, proxy_port, started_desktop_client, target } = model; diff --git a/ui/desktop/app/components/settings-card/client-agent/index.js b/ui/desktop/app/components/settings-card/client-agent/index.js index 476d43c77..730ec80d4 100644 --- a/ui/desktop/app/components/settings-card/client-agent/index.js +++ b/ui/desktop/app/components/settings-card/client-agent/index.js @@ -11,7 +11,6 @@ import { notifyError } from 'core/decorators/notify'; export default class SettingsCardClientAgentComponent extends Component { // =services - @service ipc; @service router; @service loading; @@ -59,9 +58,9 @@ export default class SettingsCardClientAgentComponent extends Component { @notifyError(({ message }) => message) async changeClientAgentState() { if (this.isClientAgentStatusRunning) { - await this.ipc.invoke('pauseClientAgent'); + await window.desktop.clientAgent.pauseClientAgent(); } else { - await this.ipc.invoke('resumeClientAgent'); + await window.desktop.clientAgent.resumeClientAgent(); } // Refresh projects page in order to stop or start // polling when client agent status changes. diff --git a/ui/desktop/app/components/settings-card/logs/index.js b/ui/desktop/app/components/settings-card/logs/index.js index 673163148..11750248e 100644 --- a/ui/desktop/app/components/settings-card/logs/index.js +++ b/ui/desktop/app/components/settings-card/logs/index.js @@ -5,12 +5,8 @@ import Component from '@glimmer/component'; import { action } from '@ember/object'; -import { service } from '@ember/service'; export default class SettingsCardLogsComponent extends Component { - // =services - @service ipc; - // =attributes /** @@ -30,6 +26,6 @@ export default class SettingsCardLogsComponent extends Component { */ @action async selectLogLevel({ target: { value } }) { - await this.ipc.invoke('setLogLevel', value); + await window.desktop.logging.setLogLevel(value); } } diff --git a/ui/desktop/app/controllers/application.js b/ui/desktop/app/controllers/application.js index c9d64b536..4bf6622a4 100644 --- a/ui/desktop/app/controllers/application.js +++ b/ui/desktop/app/controllers/application.js @@ -16,7 +16,6 @@ export default class ApplicationController extends Controller { @service clusterUrl; @service flashMessages; - @service ipc; @service session; @service('browser/window') window; @service router; @@ -34,7 +33,7 @@ export default class ApplicationController extends Controller { super(...arguments); // Listen for when user attempts to quit app // Setup removeOnAppQuitListener to destroy the listener afterwards - this.removeOnAppQuitListener = this.window.electron?.onAppQuit(() => { + this.removeOnAppQuitListener = this.window.desktop?.app.onAppQuit(() => { this.isAppQuitting = true; }); } @@ -71,7 +70,7 @@ export default class ApplicationController extends Controller { */ @action async confirmCloseSessions() { - await this.ipc.invoke('stopAll'); + await window.desktop.session.stopAllSessions(); if (this.isAppQuitting) { this.isAppQuitting = false; this.close(); @@ -88,7 +87,8 @@ export default class ApplicationController extends Controller { */ @action async showModalOrLogout() { - const hasRunningSessions = await this.ipc.invoke('hasRunningSessions'); + const hasRunningSessions = + await window.desktop.session.hasRunningSessions(); if (hasRunningSessions) { this.isLoggingOut = true; } else { @@ -108,17 +108,17 @@ export default class ApplicationController extends Controller { @action minimize() { - this.ipc.invoke('minimizeWindow'); + window.desktop.windowAction.minimizeWindow(); } @action toggleFullScreen() { - this.ipc.invoke('toggleFullscreenWindow'); + window.desktop.windowAction.toggleFullscreenWindow(); } @action close() { - this.ipc.invoke('closeWindow'); + window.desktop.windowAction.closeWindow(); } /** diff --git a/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js b/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js index 39ac3f188..6c7a2cb34 100644 --- a/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js +++ b/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js @@ -14,7 +14,6 @@ export default class ScopesScopeAuthenticateMethodIndexController extends Contro // =services @service session; - @service ipc; @service intl; @service router; @@ -39,7 +38,7 @@ export default class ScopesScopeAuthenticateMethodIndexController extends Contro * @param {string} url */ async openExternalOIDCFlow(url) { - await this.ipc.invoke('openExternal', url); + await window.desktop.system.openExternal(url); } // =actions diff --git a/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js b/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js index 7ed18522e..b46d827bc 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/sessions/index.js @@ -22,7 +22,6 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll @service intl; @service store; - @service ipc; @service session; @service router; @service abilities; @@ -157,7 +156,7 @@ export default class ScopesScopeProjectsSessionsIndexController extends Controll await updatedSession.cancelSession(); - await this.ipc.invoke('stop', { session_id: session.id }); + await window.desktop.session.stopSession({ session_id: session.id }); if ( this.router.currentRoute.name === 'scopes.scope.projects.sessions.session.index' diff --git a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js index 3cc19b0ee..3d1cd1254 100644 --- a/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/controllers/scopes/scope/projects/targets/index.js @@ -20,7 +20,6 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle // =services @service confirm; - @service ipc; @service router; @service session; @service store; @@ -189,7 +188,7 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle @loading async connect(target, host) { // Check for CLI - const cliExists = await this.ipc.invoke('cliExists'); + const cliExists = await window.desktop.system.cliExists(); if (!cliExists) throw new Error('Cannot find Boundary CLI.'); const options = { @@ -201,7 +200,8 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle if (host) options.host_id = host.id; // Create target session - const connectionDetails = await this.ipc.invoke('connect', options); + const connectionDetails = + await window.desktop.session.connectSession(options); // Associate the connection details with the session let session; @@ -307,7 +307,7 @@ export default class ScopesScopeProjectsTargetsIndexController extends Controlle } await updatedSession.cancelSession(); - await this.ipc.invoke('stop', { session_id: session.id }); + await window.desktop.session.stopSession({ session_id: session.id }); this.router.refresh(); } diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 4814e8660..5bd9c8bf6 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -14,7 +14,6 @@ export default class ApplicationRoute extends Route { @service session; @service clusterUrl; - @service ipc; @service intl; @service rdp; @@ -46,7 +45,7 @@ export default class ApplicationRoute extends Route { // Add token to cache daemon after a successful authentication restoration if (this.session.isAuthenticated) { const sessionData = this.session.data?.authenticated; - await this.ipc.invoke('addTokenToDaemons', { + await window.desktop.daemon.addTokenToDaemons({ tokenId: sessionData?.id, token: sessionData?.token, }); @@ -62,10 +61,13 @@ export default class ApplicationRoute extends Route { * Add window frame config on controller. */ async setupController(controller) { - controller.set('hasMacOSChrome', await this.ipc.invoke('hasMacOSChrome')); + controller.set( + 'hasMacOSChrome', + await window.desktop.windowAction.hasMacOSChrome(), + ); controller.set( 'showWindowActions', - await this.ipc.invoke('showWindowActions'), + await window.desktop.windowAction.showWindowActions(), ); } diff --git a/ui/desktop/app/routes/scopes.js b/ui/desktop/app/routes/scopes.js index 8692c868f..63606337e 100644 --- a/ui/desktop/app/routes/scopes.js +++ b/ui/desktop/app/routes/scopes.js @@ -11,7 +11,6 @@ export default class ScopesRoute extends Route { // =services @service store; - @service ipc; // =methods @@ -65,7 +64,8 @@ export default class ScopesRoute extends Route { if (!this.isPaginationSupported) { const metaDataUrl = 'https://api.releases.hashicorp.com/v1/releases/boundary-desktop/1.7.1'; - const { isWindows, isMac, isLinux } = await this.ipc.invoke('checkOS'); + const { isWindows, isMac, isLinux } = + await window.desktop.system.checkOS(); try { const metaDataResponse = await fetch(metaDataUrl); diff --git a/ui/desktop/app/routes/scopes/scope/projects.js b/ui/desktop/app/routes/scopes/scope/projects.js index 455b9110d..2dd6fb467 100644 --- a/ui/desktop/app/routes/scopes/scope/projects.js +++ b/ui/desktop/app/routes/scopes/scope/projects.js @@ -33,7 +33,6 @@ export default class ScopesScopeProjectsRoute extends Route { @service router; @service store; @service intl; - @service ipc; @service clientAgentSessions; @service flashMessages; @@ -51,7 +50,8 @@ export default class ScopesScopeProjectsRoute extends Route { * @return {Promise} */ async model() { - const isClientAgentRunning = await this.ipc.invoke('isClientAgentRunning'); + const isClientAgentRunning = + await window.desktop.clientAgent.isClientAgentRunning(); if (isClientAgentRunning) { this.poller.perform(); // start polling task @@ -95,7 +95,7 @@ export default class ScopesScopeProjectsRoute extends Route { const token = sessionData?.token; try { - await this.ipc.invoke('addTokenToDaemons', { + await window.desktop.daemon.addTokenToDaemons({ tokenId: auth_token_id, token, }); @@ -156,7 +156,7 @@ export default class ScopesScopeProjectsRoute extends Route { } window.location.href = `serve://boundary/#/scopes/${orgScope}/projects/sessions/${session.session_authorization.session_id}`; - await this.ipc.invoke('focusWindow'); + await window.desktop.windowAction.focusWindow(); }; }); await timeout(POLL_TIMEOUT_SECONDS * 1000); diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js b/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js index 30b96f090..535a0d1f2 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions/index.js @@ -18,7 +18,6 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { @service session; @service store; - @service ipc; @service router; // =attributes @@ -137,7 +136,7 @@ export default class ScopesScopeProjectsSessionsIndexRoute extends Route { allSessions: this.allSessions, associatedTargets, totalItems, - isCacheDaemonRunning: await this.ipc.invoke('isCacheDaemonRunning'), + isCacheDaemonRunning: await window.desktop.daemon.isCacheDaemonRunning(), }; } diff --git a/ui/desktop/app/routes/scopes/scope/projects/sessions/session.js b/ui/desktop/app/routes/scopes/scope/projects/sessions/session.js index 35ebcda74..bc0049d79 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/sessions/session.js +++ b/ui/desktop/app/routes/scopes/scope/projects/sessions/session.js @@ -12,7 +12,6 @@ export default class ScopesScopeProjectsSessionsSessionRoute extends Route { // =services @service store; - @service ipc; @service clientAgentSessions; @service flashMessages; @service intl; @@ -57,7 +56,7 @@ export default class ScopesScopeProjectsSessionsSessionRoute extends Route { // was initiated through the client agent. if ( !session.credentials.length && - (await this.ipc.invoke('isClientAgentRunning')) + (await window.desktop.clientAgent.isClientAgentRunning()) ) { try { const clientAgentSession = diff --git a/ui/desktop/app/routes/scopes/scope/projects/settings/index.js b/ui/desktop/app/routes/scopes/scope/projects/settings/index.js index 0961d5984..400a51677 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/settings/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/settings/index.js @@ -8,21 +8,20 @@ import { service } from '@ember/service'; export default class ScopesScopeProjectsSettingsIndexRoute extends Route { // =services - @service ipc; @service session; @service clusterUrl; // =methods async model() { const { versionNumber: cliVersion } = - await this.ipc.invoke('getCliVersion'); + await window.desktop.system.getCliVersion(); - const { desktopVersion } = await this.ipc.invoke('getDesktopVersion'); + const { desktopVersion } = await window.desktop.system.getDesktopVersion(); const cacheDaemonStatus = await this.getCacheStatus(); const clientAgentStatus = await this.getClientAgentStatus(); - const logLevel = await this.ipc.invoke('getLogLevel'); - const logPath = await this.ipc.invoke('getLogPath'); + const logLevel = await window.desktop.logging.getLogLevel(); + const logPath = await window.desktop.logging.getLogPath(); const serverInformation = this.clusterUrl.rendererClusterUrl; return { @@ -40,7 +39,7 @@ export default class ScopesScopeProjectsSettingsIndexRoute extends Route { let cacheDaemonStatus; let cacheDaemonStatusError = []; try { - cacheDaemonStatus = await this.ipc.invoke('cacheDaemonStatus'); + cacheDaemonStatus = await window.desktop.daemon.cacheDaemonStatus(); } catch (e) { cacheDaemonStatusError.push(e); } @@ -66,7 +65,7 @@ export default class ScopesScopeProjectsSettingsIndexRoute extends Route { let clientAgentStatus; let clientAgentStatusError = []; try { - clientAgentStatus = await this.ipc.invoke('clientAgentStatus'); + clientAgentStatus = await window.desktop.clientAgent.clientAgentStatus(); } catch (e) { clientAgentStatusError.push(e); } diff --git a/ui/desktop/app/routes/scopes/scope/projects/targets/index.js b/ui/desktop/app/routes/scopes/scope/projects/targets/index.js index e80d35d98..dd4e9d9da 100644 --- a/ui/desktop/app/routes/scopes/scope/projects/targets/index.js +++ b/ui/desktop/app/routes/scopes/scope/projects/targets/index.js @@ -23,7 +23,6 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route { @service router; @service session; @service store; - @service ipc; // =attributes @@ -113,7 +112,8 @@ export default class ScopesScopeProjectsTargetsIndexRoute extends Route { await timeout(250); } - const isCacheRunningPromise = this.ipc.invoke('isCacheDaemonRunning'); + const isCacheRunningPromise = + window.desktop.daemon.isCacheDaemonRunning(); const orgScope = this.modelFor('scopes.scope'); const projects = this.modelFor('scopes.scope.projects'); diff --git a/ui/desktop/app/services/client-agent-sessions.js b/ui/desktop/app/services/client-agent-sessions.js index 71bc0c7ef..9495f9dff 100644 --- a/ui/desktop/app/services/client-agent-sessions.js +++ b/ui/desktop/app/services/client-agent-sessions.js @@ -3,12 +3,9 @@ * SPDX-License-Identifier: BUSL-1.1 */ -import Service, { inject as service } from '@ember/service'; +import Service from '@ember/service'; export default class ClientAgentSessionsService extends Service { - // =services - @service ipc; - // =attributes #sessionsSet; @@ -19,7 +16,7 @@ export default class ClientAgentSessionsService extends Service { * @returns {Promise<*>} */ async getClientAgentSessions() { - return this.ipc.invoke('getClientAgentSessions'); + return window.desktop.clientAgent.getClientAgentSessions(); } /** diff --git a/ui/desktop/app/services/cluster-url.js b/ui/desktop/app/services/cluster-url.js index 5042dc523..0480e3e67 100644 --- a/ui/desktop/app/services/cluster-url.js +++ b/ui/desktop/app/services/cluster-url.js @@ -11,7 +11,6 @@ import { notifyError } from 'core/decorators/notify'; export default class ClusterUrlService extends Service { // =services - @service ipc; @service store; @service storage; @@ -42,7 +41,7 @@ export default class ClusterUrlService extends Service { * @type {Promise{?string}} */ get mainClusterUrl() { - return this.ipc.invoke('getClusterUrl'); + return window.desktop.cluster.getClusterUrl(); } // =methods @@ -76,7 +75,7 @@ export default class ClusterUrlService extends Service { this.adapter.host = clusterUrl; this.rendererClusterUrl = clusterUrl; if (clusterUrl !== (await this.mainClusterUrl)) { - await this.ipc.invoke('setClusterUrl', clusterUrl); + await window.desktop.cluster.setClusterUrl(clusterUrl); } } catch (e) { this.adapter.host = originalHost; @@ -94,6 +93,6 @@ export default class ClusterUrlService extends Service { }) async resetClusterUrl() { this.rendererClusterUrl = null; - await this.ipc.invoke('resetClusterUrl'); + await window.desktop.cluster.resetClusterUrl(); } } diff --git a/ui/desktop/app/services/ipc.js b/ui/desktop/app/services/ipc.js deleted file mode 100644 index 3f81ef6f6..000000000 --- a/ui/desktop/app/services/ipc.js +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Copyright IBM Corp. 2021, 2026 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Service from '@ember/service'; -import { service } from '@ember/service'; -import { Promise } from 'rsvp'; - -/** - * An IPC request is a promise-like object that issues a request via - * `window.postMessage` and resolves to a response received via a response - * port. IPC requests are used by an Electron renderer process to communicate - * with the main process when the renderer is fully isolated. - * - * Some of the design goals of this "safe IPC" are to respect the confines - * of process isolation and security, without using node integration, while - * retaining the ability to run the Ember application within a browser - * (sans main process) by using only APIs available in a standard - * (non-Electron) browser. This is achieved by posting messages to the - * renderer's `window.postMessage` interface. Thus, the renderer requires no - * knowledge of the main process or the Electron context. In fact a main - * process and Electron context are not even required. - * - * In an Electron context, the `preload.js` script sets up a `window.postMessage` - * listener which acts as proxy layer between the main and renderer processes. - * It waits for incoming requests from the renderer and forwards them to main. - * Responses from the main process are forwarded back via a response port - * (sent with the original request). - * - * Local development in a non-Electron browser context or test context should - * mock up IPC requests. See the ipc service test for an example of mocking. - * - * TODO: add utilities for quickly mocking IPC responses (like Mirage) - * - */ -export class IPCRequest { - // =attributes - - /** - * @type {?MessageChannel} - * @private - */ - #channel = null; - - /** - * @type {?Promise} - * @private - */ - #promise = null; - - // =methods - - /** - * Posts a message to the window object, along with a MessagePort - * with which to receive a response. An internal promise is generated which - * resolves to the response. - * @param {string} method - * @param {object} payload - * @param {Window} window - A reference to the window object - */ - constructor(method, payload, window) { - this.method = method; - this.payload = payload; - this.origin = window?.location?.origin; - this.#channel = new window.MessageChannel(); - this.#promise = new Promise((resolve, reject) => { - this.#channel.port1.onmessage = (event) => { - this.close(); - if (event.data instanceof Error) { - reject(JSON.parse(event.data.message)); - } else { - resolve(event.data); - } - }; - }); - window.postMessage( - { - method: this.method, - payload: this.payload, - }, - origin, - [this.#channel.port2], - ); - } - - /** - * Closes the message ports and drops the channel reference. - * This is called upon receiving a response for cleanup purposes. - */ - close() { - this.#channel.port1.close(); - this.#channel.port2.close(); - this.#channel = null; - } - - /** - * Proxies to the underlying promise. - */ - then() { - return this.#promise.then(...arguments); - } - - /** - * Proxies to the underlying promise. - */ - catch() { - return this.#promise.catch(...arguments); - } - - /** - * Proxies to the underlying promise. - */ - finally() { - return this.#promise.finally(...arguments); - } -} - -/** - * A simple factory for generating IPCRequests in Ember. - * @example - * - * class MyClass { - * @service ipc; - * - * makeRequest { - * return this.ipc.invoke('getFoobars', { hello: 'world' }); - * } - * } - */ -export default class IpcService extends Service { - // =services - - @service('browser/window') window; - - // =methods - - /** - * Creates a new IPC request and returns it. - * @param {string} method - * @param {object} payload - * @return {IPCRequest} - */ - invoke(method, payload) { - return new IPCRequest(method, payload, this.window); - } -} diff --git a/ui/desktop/app/services/rdp.js b/ui/desktop/app/services/rdp.js index 38a653050..a731bf20a 100644 --- a/ui/desktop/app/services/rdp.js +++ b/ui/desktop/app/services/rdp.js @@ -4,7 +4,6 @@ */ import Service from '@ember/service'; -import { service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; export const RDP_CLIENT_NONE = 'none'; @@ -26,10 +25,6 @@ const windowsRecommendedRdpClient = { }; export default class RdpService extends Service { - // =services - - @service ipc; - // =properties /** @@ -84,7 +79,7 @@ export default class RdpService extends Service { return this.rdpClients; } try { - this.rdpClients = await this.ipc.invoke('getRdpClients'); + this.rdpClients = await window.desktop.rdp.getRdpClients(); return this.rdpClients; } catch (error) { __electronLog?.error('Failed to fetch RDP clients', error.message); @@ -103,7 +98,7 @@ export default class RdpService extends Service { if (this.recommendedRdpClient !== null) { return this.recommendedRdpClient; } - const { isWindows, isMac } = await this.ipc.invoke('checkOS'); + const { isWindows, isMac } = await window.desktop.system.checkOS(); if (isWindows) { this.recommendedRdpClient = windowsRecommendedRdpClient; } else if (isMac) { @@ -128,7 +123,8 @@ export default class RdpService extends Service { return this.preferredRdpClient; } try { - this.preferredRdpClient = await this.ipc.invoke('getPreferredRdpClient'); + this.preferredRdpClient = + await window.desktop.rdp.getPreferredRdpClient(); return this.preferredRdpClient; } catch (error) { __electronLog?.error( @@ -148,7 +144,7 @@ export default class RdpService extends Service { */ async setPreferredRdpClient(rdpClient) { try { - await this.ipc.invoke('setPreferredRdpClient', rdpClient); + await window.desktop.rdp.setPreferredRdpClient(rdpClient); this.preferredRdpClient = rdpClient; return this.preferredRdpClient; } catch (error) { @@ -165,6 +161,6 @@ export default class RdpService extends Service { * @param {string} sessionId - The ID of the active session */ async launchRdpClient(sessionId) { - await this.ipc.invoke('launchRdpClient', sessionId); + await window.desktop.rdp.launchRdpClient(sessionId); } } diff --git a/ui/desktop/app/services/session.js b/ui/desktop/app/services/session.js index 5f59da92e..977596355 100644 --- a/ui/desktop/app/services/session.js +++ b/ui/desktop/app/services/session.js @@ -9,7 +9,6 @@ import { notifyError } from 'core/decorators/notify'; import { tracked } from '@glimmer/tracking'; export default class SessionService extends BaseSessionService { - @service ipc; @service store; @tracked username; @@ -24,7 +23,7 @@ export default class SessionService extends BaseSessionService { if (this.session.isAuthenticated) { const sessionData = this.data?.authenticated; - await this.ipc.invoke('addTokenToDaemons', { + await window.desktop.daemon.addTokenToDaemons({ tokenId: sessionData?.id, token: sessionData?.token, }); diff --git a/ui/desktop/electron-app/src/preload.js b/ui/desktop/electron-app/src/preload.js index bbec1657a..7820f7810 100644 --- a/ui/desktop/electron-app/src/preload.js +++ b/ui/desktop/electron-app/src/preload.js @@ -5,9 +5,6 @@ const { ipcRenderer, contextBridge } = require('electron'); -// Messages must originate from this origin -const emberAppOrigin = window.location.origin; - /** * Exposing terminal creation to an isolated context (Ember) * More information about contextBridge https://www.electronjs.org/docs/latest/api/context-bridge @@ -41,39 +38,82 @@ contextBridge.exposeInMainWorld('terminal', { }, }); -process.once('loaded', () => { - /** - * Ember-land has no access to the renderer or node modules, and thus - * cannot call into the main process or make invocations directly. - * In order to communicate with the main process, the Ember app uses - * the `postMessage` interface to send messages to its browser window. - * These messages include message ports with which to receive responses from - * the main process. - * - * This preload script simply wires up the forwarding of - * messages received on the window and responses received from main. - */ - window.addEventListener('message', async function (event) { - if (event.origin !== emberAppOrigin) return; - const { method, payload } = event?.data ?? {}; - if (method) { - const response = await ipcRenderer.invoke(method, payload); - event.ports[0].postMessage(response); - } - }); -}); - /** - * Listener on electron app when user triggers before-quit event + * Exposes `desktop` via Electron ContextBridge to the renderer process. It is grouped into nested objects based on the type of functionality. + * use example: window.desktop.cluster.getClusterUrl(). */ -contextBridge.exposeInMainWorld('electron', { - onAppQuit: (callback) => { - // Don't pass in callback directly so users can't access passed in event for security - const listenerCallback = () => callback(); - ipcRenderer.on('onAppQuit', listenerCallback); +contextBridge.exposeInMainWorld('desktop', { + cluster: { + getClusterUrl: () => ipcRenderer.invoke('getClusterUrl'), + setClusterUrl: (url) => ipcRenderer.invoke('setClusterUrl', url), + resetClusterUrl: () => ipcRenderer.invoke('resetClusterUrl'), + }, - return () => { - return ipcRenderer.removeListener('onAppQuit', listenerCallback); - }; + system: { + openExternal: (href) => ipcRenderer.invoke('openExternal', href), + cliExists: () => ipcRenderer.invoke('cliExists'), + getCliVersion: () => ipcRenderer.invoke('getCliVersion'), + checkCommand: (command) => ipcRenderer.invoke('checkCommand', command), + checkOS: () => ipcRenderer.invoke('checkOS'), + getDesktopVersion: () => ipcRenderer.invoke('getDesktopVersion'), + }, + + session: { + connectSession: (params) => ipcRenderer.invoke('connect', params), + stopSession: (params) => ipcRenderer.invoke('stop', params), + stopAllSessions: () => ipcRenderer.invoke('stopAll'), + hasRunningSessions: () => ipcRenderer.invoke('hasRunningSessions'), + }, + + windowAction: { + hasMacOSChrome: () => ipcRenderer.invoke('hasMacOSChrome'), + showWindowActions: () => ipcRenderer.invoke('showWindowActions'), + minimizeWindow: () => ipcRenderer.invoke('minimizeWindow'), + toggleFullscreenWindow: () => ipcRenderer.invoke('toggleFullscreenWindow'), + closeWindow: () => ipcRenderer.invoke('closeWindow'), + focusWindow: () => ipcRenderer.invoke('focusWindow'), + }, + + daemon: { + addTokenToDaemons: (data) => ipcRenderer.invoke('addTokenToDaemons', data), + searchCacheDaemon: (request) => + ipcRenderer.invoke('searchCacheDaemon', request), + isCacheDaemonRunning: () => ipcRenderer.invoke('isCacheDaemonRunning'), + cacheDaemonStatus: () => ipcRenderer.invoke('cacheDaemonStatus'), + }, + + clientAgent: { + getClientAgentSessions: () => ipcRenderer.invoke('getClientAgentSessions'), + isClientAgentRunning: () => ipcRenderer.invoke('isClientAgentRunning'), + clientAgentStatus: () => ipcRenderer.invoke('clientAgentStatus'), + pauseClientAgent: () => ipcRenderer.invoke('pauseClientAgent'), + resumeClientAgent: () => ipcRenderer.invoke('resumeClientAgent'), + }, + + logging: { + getLogLevel: () => ipcRenderer.invoke('getLogLevel'), + setLogLevel: (logLevel) => ipcRenderer.invoke('setLogLevel', logLevel), + getLogPath: () => ipcRenderer.invoke('getLogPath'), + }, + + rdp: { + getRdpClients: () => ipcRenderer.invoke('getRdpClients'), + getPreferredRdpClient: () => ipcRenderer.invoke('getPreferredRdpClient'), + setPreferredRdpClient: (client) => + ipcRenderer.invoke('setPreferredRdpClient', client), + launchRdpClient: (sessionId) => + ipcRenderer.invoke('launchRdpClient', sessionId), + }, + + app: { + onAppQuit: (callback) => { + // Don't pass in callback directly so users can't access passed in event for security + const listenerCallback = () => callback(); + ipcRenderer.on('onAppQuit', listenerCallback); + + return () => { + return ipcRenderer.removeListener('onAppQuit', listenerCallback); + }; + }, }, }); diff --git a/ui/desktop/tests/acceptance/application-test.js b/ui/desktop/tests/acceptance/application-test.js index ead61cede..db4c70b9b 100644 --- a/ui/desktop/tests/acceptance/application-test.js +++ b/ui/desktop/tests/acceptance/application-test.js @@ -9,6 +9,7 @@ import { setupApplicationTest } from 'desktop/tests/helpers'; import sinon from 'sinon'; import { invalidateSession } from 'ember-simple-auth/test-support'; import { setupBrowserFakes } from 'ember-browser-services/test-support'; +import { setupDesktopContextBridgeApiMock } from '../helpers/desktop-context-bridge-api-mock'; module('Acceptance | Application', function (hooks) { setupApplicationTest(hooks); @@ -19,9 +20,7 @@ module('Acceptance | Application', function (hooks) { '[data-test-toast-notification] button'; const TOAST_DISMISS_BUTTON = '[aria-label="Dismiss"]'; - const stubs = { - ipcService: null, - }; + setupDesktopContextBridgeApiMock(hooks); const urls = { clusterUrl: null, @@ -32,9 +31,6 @@ module('Acceptance | Application', function (hooks) { urls.clusterUrl = '/cluster-url'; - const ipcService = this.owner.lookup('service:ipc'); - stubs.ipcService = sinon.stub(ipcService, 'invoke'); - // mock RDP service calls let rdpService = this.owner.lookup('service:rdp'); sinon.stub(rdpService, 'initialize').resolves(); @@ -46,7 +42,6 @@ module('Acceptance | Application', function (hooks) { test('visiting index with chrome disabled hides custom window actions', async function (assert) { assert.expect(3); - stubs.ipcService.withArgs('showWindowActions').returns(false); await visit(urls.clusterUrl); assert.notOk(find('.button-window-close'), 'Window close button'); assert.notOk(find('.button-window-minimize'), 'Window minimize button'); @@ -55,7 +50,7 @@ module('Acceptance | Application', function (hooks) { test('visiting index with chrome enabled shows custom window actions', async function (assert) { assert.expect(3); - stubs.ipcService.withArgs('showWindowActions').returns(true); + window.desktop.windowAction.showWindowActions.resolves(true); await visit(urls.clusterUrl); assert.ok(find('.button-window-close'), 'Window close button'); assert.ok(find('.button-window-minimize'), 'Window minimize button'); @@ -64,7 +59,7 @@ module('Acceptance | Application', function (hooks) { test('visiting index with MacOS chrome enabled', async function (assert) { assert.expect(1); - stubs.ipcService.withArgs('hasMacOSChrome').returns(true); + window.desktop.windowAction.hasMacOSChrome.resolves(true); await visit(urls.clusterUrl); assert.ok( find('.app-header.header-cushion'), @@ -74,7 +69,7 @@ module('Acceptance | Application', function (hooks) { test('visiting index with MacOS chrome disabled', async function (assert) { assert.expect(1); - stubs.ipcService.withArgs('hasMacOSChrome').returns(false); + window.desktop.windowAction.hasMacOSChrome.resolves(false); await visit(urls.clusterUrl); assert.notOk( find('.app-header.header-cushion'), diff --git a/ui/desktop/tests/acceptance/authentication-test.js b/ui/desktop/tests/acceptance/authentication-test.js index a1ffd0c49..75f8049d5 100644 --- a/ui/desktop/tests/acceptance/authentication-test.js +++ b/ui/desktop/tests/acceptance/authentication-test.js @@ -18,13 +18,13 @@ import { authenticateSession, invalidateSession, } from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../helpers/window-mock-ipc'; -import Service from '@ember/service'; +import { setupDesktopContextBridgeApiMock } from '../helpers/desktop-context-bridge-api-mock'; import sinon from 'sinon'; import { setRunOptions } from 'ember-a11y-testing/test-support'; module('Acceptance | authentication', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); const instances = { scopes: { @@ -80,9 +80,6 @@ module('Acceptance | authentication', function (hooks) { hooks.beforeEach(async function () { await invalidateSession(); - const ipcService = this.owner.lookup('service:ipc'); - this.ipcStub = sinon.stub(ipcService, 'invoke'); - // create scopes instances.scopes.global = this.server.schema.scopes.find('global'); stubs.global = { id: 'global', type: 'global' }; @@ -133,8 +130,6 @@ module('Acceptance | authentication', function (hooks) { urls.targets = `${urls.projects}/targets`; urls.sessions = `${urls.projects}/sessions`; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); }); @@ -196,6 +191,7 @@ module('Acceptance | authentication', function (hooks) { }, }, }); + window.desktop.session.hasRunningSessions.resolves(false); assert.expect(3); await visit(urls.authenticate.methods.global); @@ -274,8 +270,6 @@ module('Acceptance | authentication', function (hooks) { }, }); - this.ipcStub.withArgs('hasRunningSessions').returns(true); - await visit(urls.authenticate.methods.global); await authenticateSession({ account_id: instances.account.id }); @@ -305,9 +299,6 @@ module('Acceptance | authentication', function (hooks) { }, }); - const stopAllSessions = this.ipcStub.withArgs('stopAll'); - this.ipcStub.withArgs('hasRunningSessions').returns(true); - await visit(urls.authenticate.methods.global); await authenticateSession({ account_id: instances.account.id }); @@ -324,7 +315,7 @@ module('Acceptance | authentication', function (hooks) { await click(MODAL_CONFIRM_BTN); assert.dom(MODAL_CLOSE_SESSIONS).isNotVisible(); - assert.ok(stopAllSessions.calledOnce); + assert.ok(window.desktop.session.stopAllSessions.calledOnce); assert.notOk(currentSession().isAuthenticated); }); @@ -338,26 +329,6 @@ module('Acceptance | authentication', function (hooks) { }, }); - // We need to encapsulate the event listener inside a mocked window service to ensure - // the entire event is torn down (including the mocked window), since "window" exists - // globally across all tests, and we don't want tests impacting one another - const mockElectronEvent = class WindowElectronMock extends Service { - electron = { - onAppQuit: (callback) => { - window.addEventListener('onAppQuit', callback); - return () => { - window.removeEventListener('onAppQuit', callback); - }; - }, - }; - }; - - this.owner.register('service:browser/window', mockElectronEvent); - const stopAllSessions = this.ipcStub.withArgs('stopAll'); - const quitApp = this.ipcStub.withArgs('closeWindow'); - - this.ipcStub.withArgs('hasRunningSessions').returns(true); - await visit(urls.authenticate.methods.global); await authenticateSession({ account_id: instances.account.id }); @@ -378,7 +349,7 @@ module('Acceptance | authentication', function (hooks) { .includesText('Close sessions before quitting?'); await click(MODAL_CONFIRM_BTN); - assert.ok(stopAllSessions.calledOnce); - assert.ok(quitApp.calledOnce); + assert.ok(window.desktop.session.stopAllSessions.calledOnce); + assert.ok(window.desktop.windowAction.closeWindow.calledOnce); }); }); diff --git a/ui/desktop/tests/acceptance/cluster-url-test.js b/ui/desktop/tests/acceptance/cluster-url-test.js index 89cb7a99a..8e39b391d 100644 --- a/ui/desktop/tests/acceptance/cluster-url-test.js +++ b/ui/desktop/tests/acceptance/cluster-url-test.js @@ -9,7 +9,7 @@ import { setupApplicationTest } from 'desktop/tests/helpers'; import sinon from 'sinon'; import { invalidateSession } from 'ember-simple-auth/test-support'; import { setupBrowserFakes } from 'ember-browser-services/test-support'; -import WindowMockIPC from '../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../helpers/desktop-context-bridge-api-mock'; import config from '../../config/environment'; module('Acceptance | clusterUrl', function (hooks) { @@ -17,7 +17,6 @@ module('Acceptance | clusterUrl', function (hooks) { setupBrowserFakes(hooks, { window: true }); const currentOrigin = window.location.origin; - let mockIPC; const instances = { scopes: { @@ -36,7 +35,6 @@ module('Acceptance | clusterUrl', function (hooks) { const stubs = { global: null, org: null, - ipcService: null, }; const urls = { @@ -55,10 +53,7 @@ module('Acceptance | clusterUrl', function (hooks) { projects: null, targets: null, }; - const setupMockIpc = (test) => { - test.owner.register('service:browser/window', WindowMockIPC); - mockIPC = test.owner.lookup('service:browser/window').mockIPC; - }; + setupDesktopContextBridgeApiMock(hooks); hooks.beforeEach(async function () { await invalidateSession(); @@ -113,41 +108,37 @@ module('Acceptance | clusterUrl', function (hooks) { test('visiting index without a clusterUrl specified redirects to clusterUrl route', async function (assert) { assert.expect(2); - setupMockIpc(this); await visit(urls.index); - assert.notOk(mockIPC.clusterUrl); + assert.notOk(window.desktop.clusterUrl); assert.strictEqual(currentURL(), urls.clusterUrl); }); test('can set clusterUrl', async function (assert) { assert.expect(3); - setupMockIpc(this); - assert.notOk(mockIPC.clusterUrl); + assert.notOk(window.desktop.clusterUrl); await visit(urls.clusterUrl); await fillIn('[name="host"]', currentOrigin); await click('[type="submit"]'); assert.strictEqual(currentURL(), urls.authenticate.methods.global); - assert.strictEqual(mockIPC.clusterUrl, currentOrigin); + assert.strictEqual(window.desktop.clusterUrl, currentOrigin); }); test('can reset clusterUrl before authentication', async function (assert) { assert.expect(4); - setupMockIpc(this); - assert.notOk(mockIPC.clusterUrl); + assert.notOk(window.desktop.clusterUrl); await visit(urls.clusterUrl); await fillIn('[name="host"]', currentOrigin); await click('[type="submit"]'); assert.strictEqual(currentURL(), urls.authenticate.methods.global); - assert.strictEqual(mockIPC.clusterUrl, currentOrigin); + assert.strictEqual(window.desktop.clusterUrl, currentOrigin); await click('.change-origin a'); assert.strictEqual(currentURL(), urls.clusterUrl); }); test('captures error on clusterUrl update', async function (assert) { assert.expect(2); - setupMockIpc(this); - assert.notOk(mockIPC.clusterUrl); + assert.notOk(window.desktop.clusterUrl); sinon .stub(this.owner.lookup('service:clusterUrl'), 'setClusterUrl') .throws(); diff --git a/ui/desktop/tests/acceptance/projects-test.js b/ui/desktop/tests/acceptance/projects-test.js index 00ddfc07e..da5be4cfc 100644 --- a/ui/desktop/tests/acceptance/projects-test.js +++ b/ui/desktop/tests/acceptance/projects-test.js @@ -10,10 +10,11 @@ import { currentSession, invalidateSession, } from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../helpers/desktop-context-bridge-api-mock'; module('Acceptance | projects', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); const instances = { scopes: { @@ -86,8 +87,6 @@ module('Acceptance | projects', function (hooks) { urls.authenticate.methods.global = `${urls.authenticate.global}/${instances.authMethods.global.id}`; urls.projects = `${urls.scopes.org}/projects`; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); }); diff --git a/ui/desktop/tests/acceptance/projects/sessions/index-test.js b/ui/desktop/tests/acceptance/projects/sessions/index-test.js index 77a6e27e4..c5b6a91fb 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/index-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/index-test.js @@ -7,7 +7,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'desktop/tests/helpers'; import { Response } from 'miragejs'; -import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../helpers/desktop-context-bridge-api-mock'; import { currentSession, invalidateSession, @@ -24,6 +24,7 @@ import sinon from 'sinon'; module('Acceptance | projects | sessions | index', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); const APP_STATE_TITLE = @@ -147,11 +148,9 @@ module('Acceptance | projects | sessions | index', function (hooks) { urls.sessions2 = `${urls.projects2}/sessions`; urls.session = `${urls.projects}/sessions/${instances.session.id}`; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'sessions', 'targets'); // mock RDP service calls @@ -441,7 +440,6 @@ module('Acceptance | projects | sessions | index', function (hooks) { }, }); - this.ipcStub.withArgs('stop'); await visit(urls.projects); await click(`[href="${urls.sessions}"]`); @@ -462,7 +460,6 @@ module('Acceptance | projects | sessions | index', function (hooks) { }, }); - this.ipcStub.withArgs('stop'); await visit(urls.projects); await click(`[href="${urls.sessions}"]`); @@ -502,7 +499,7 @@ module('Acceptance | projects | sessions | index', function (hooks) { }, }); - this.ipcStub.withArgs('stop').throws(); + window.desktop.session.stopSession.throws(); await visit(urls.projects); await click(`[href="${urls.sessions}"]`); @@ -572,7 +569,8 @@ module('Acceptance | projects | sessions | index', function (hooks) { }, }); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(false); + window.desktop.daemon.isCacheDaemonRunning.resolves(false); + this.stubCacheDaemonSearch(); const sessionsCount = this.server.schema.sessions.all().models.length; diff --git a/ui/desktop/tests/acceptance/projects/sessions/session-test.js b/ui/desktop/tests/acceptance/projects/sessions/session-test.js index 7b8915334..cccfd9295 100644 --- a/ui/desktop/tests/acceptance/projects/sessions/session-test.js +++ b/ui/desktop/tests/acceptance/projects/sessions/session-test.js @@ -9,7 +9,7 @@ import { module, test } from 'qunit'; import { visit, currentURL, click } from '@ember/test-helpers'; import { setupApplicationTest } from 'desktop/tests/helpers'; import { Response } from 'miragejs'; -import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../helpers/desktop-context-bridge-api-mock'; import { STATUS_SESSION_ACTIVE } from 'api/models/session'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; @@ -19,6 +19,7 @@ import { RDP_CLIENT_WINDOWS_APP, RDP_CLIENT_NONE } from 'desktop/services/rdp'; module('Acceptance | projects | sessions | session', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); const TARGET_CONNECT_BUTTON = '[data-test-target-detail-connect-button]'; @@ -137,11 +138,8 @@ module('Acceptance | projects | sessions | session', function (hooks) { urls.sessions = `${urls.projects}/sessions`; urls.session = `${urls.projects}/sessions/${instances.session.id}`; urls.rdpSession = `${urls.projects}/sessions/${instances.rdpSession.id}`; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); - setDefaultClusterUrl(this); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(false); + setDefaultClusterUrl(this); // mock RDP service calls this.rdpService = this.owner.lookup('service:rdp'); @@ -181,8 +179,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }); assert.expect(4); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -210,8 +207,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -283,8 +279,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -341,8 +336,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, host_id: 'h_123', address: 'a_123', @@ -378,8 +372,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }); // First RDP Session visit should show the toast notification - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, protocol: 'rdp', }); @@ -426,8 +419,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { assert.expect(1); this.server.get('/hosts/:id', () => new Response(403)); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, host_id: 'h_123', address: 'a_123', @@ -459,8 +451,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }; this.server.get('/sessions/:id', () => new Response(403)); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, host_id: 'h_123', address: 'a_123', @@ -585,7 +576,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { }); assert.expect(1); - this.ipcStub.withArgs('stop').throws(); + window.desktop.session.stopSession.throws(); await visit(urls.session); @@ -606,12 +597,10 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, host_id: 'h_123', address: 'a_123', @@ -628,7 +617,7 @@ module('Acceptance | projects | sessions | session', function (hooks) { await click(RDP_OPEN_BUTTON); assert.ok( - this.ipcStub.calledWith('launchRdpClient', instances.rdpSession.id), + window.desktop.rdp.launchRdpClient.calledWith(instances.rdpSession.id), ); }); @@ -642,12 +631,10 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.rdpService.preferredRdpClient = RDP_CLIENT_NONE; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, host_id: 'h_123', address: 'a_123', @@ -672,19 +659,17 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, host_id: 'h_123', address: 'a_123', port: 'p_123', protocol: 'rdp', }); - this.ipcStub.withArgs('launchRdpClient', instances.rdpSession.id).rejects(); + window.desktop.rdp.launchRdpClient.rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; @@ -710,12 +695,11 @@ module('Acceptance | projects | sessions | session', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); instances.rdpSession.update({ authorized_actions: [] }); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.rdpSession.id, host_id: 'h_123', address: 'a_123', diff --git a/ui/desktop/tests/acceptance/projects/settings/index-test.js b/ui/desktop/tests/acceptance/projects/settings/index-test.js index 809394b2a..28bd7d033 100644 --- a/ui/desktop/tests/acceptance/projects/settings/index-test.js +++ b/ui/desktop/tests/acceptance/projects/settings/index-test.js @@ -16,7 +16,7 @@ import { authenticateSession, currentSession, } from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../helpers/desktop-context-bridge-api-mock'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { @@ -28,6 +28,7 @@ import { module('Acceptance | projects | settings | index', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); const instances = { @@ -95,25 +96,15 @@ module('Acceptance | projects | settings | index', function (hooks) { urls.projects = `${urls.scopes.org}/projects`; urls.settings = `${urls.projects}/settings`; - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub - .withArgs('getDesktopVersion') - .returns({ desktopVersion: '0.1.0' }); - this.ipcStub - .withArgs('getCliVersion') - .returns({ versionNumber: 'Boundary CLI v0.1.0' }); - this.ipcStub - .withArgs('cacheDaemonStatus') - .returns({ version: 'Boundary CLI v0.1.0' }); - // mock RDP client data - this.ipcStub - .withArgs('getRdpClients') - .returns([RDP_CLIENT_MSTSC, RDP_CLIENT_NONE]); - this.ipcStub.withArgs('getPreferredRdpClient').returns(RDP_CLIENT_MSTSC); - this.ipcStub.withArgs('checkOS').returns({ isWindows: true, isMac: false }); + window.desktop.rdp.getRdpClients.resolves([ + RDP_CLIENT_MSTSC, + RDP_CLIENT_NONE, + ]); + window.desktop.rdp.getPreferredRdpClient.resolves(RDP_CLIENT_MSTSC); + window.desktop.system.checkOS.resolves({ isWindows: true, isMac: false }); }); test('can navigate to the settings page', async function (assert) { @@ -195,6 +186,8 @@ module('Acceptance | projects | settings | index', function (hooks) { }, }); + window.desktop.session.hasRunningSessions.resolves(false); + await authenticateSession({ account_id: instances.account.id }); assert.expect(2); @@ -223,9 +216,6 @@ module('Acceptance | projects | settings | index', function (hooks) { }, }); - const stopAllSessions = this.ipcStub.withArgs('stopAll'); - this.ipcStub.withArgs('hasRunningSessions').returns(true); - await authenticateSession({ account_id: instances.account.id }); assert.ok(currentSession().isAuthenticated); @@ -239,7 +229,7 @@ module('Acceptance | projects | settings | index', function (hooks) { await click(MODAL_CONFIRM_BTN); assert.dom(MODAL_CLOSE_SESSIONS).isNotVisible(); - assert.ok(stopAllSessions.calledOnce); + assert.ok(window.desktop.session.stopAllSessions.calledOnce); assert.notOk(currentSession().isAuthenticated); }); @@ -278,14 +268,13 @@ module('Acceptance | projects | settings | index', function (hooks) { }, }); - // update IPC stub fo mac - this.ipcStub.withArgs('checkOS').returns({ isWindows: false, isMac: true }); - this.ipcStub - .withArgs('getRdpClients') - .returns([RDP_CLIENT_WINDOWS_APP, RDP_CLIENT_NONE]); - this.ipcStub - .withArgs('getPreferredRdpClient') - .returns(RDP_CLIENT_WINDOWS_APP); + // update window bounday mock fo mac + window.desktop.system.checkOS.resolves({ isWindows: false, isMac: true }); + window.desktop.rdp.getRdpClients.resolves([ + RDP_CLIENT_WINDOWS_APP, + RDP_CLIENT_NONE, + ]); + window.desktop.rdp.getPreferredRdpClient.resolves(RDP_CLIENT_WINDOWS_APP); await visit(urls.settings); assert @@ -309,9 +298,9 @@ module('Acceptance | projects | settings | index', function (hooks) { }, }); - // update IPC stub for no RDP clients - this.ipcStub.withArgs('getRdpClients').returns([RDP_CLIENT_NONE]); - this.ipcStub.withArgs('getPreferredRdpClient').returns(RDP_CLIENT_NONE); + // update window boundary mock for no RDP clients + window.desktop.rdp.getRdpClients.resolves([RDP_CLIENT_NONE]); + window.desktop.rdp.getPreferredRdpClient.resolves(RDP_CLIENT_NONE); await visit(urls.settings); assert.dom(RDP_RECOMMENDED_CLIENT).isVisible(); @@ -337,13 +326,13 @@ module('Acceptance | projects | settings | index', function (hooks) { }); const rdpService = this.owner.lookup('service:rdp'); - this.ipcStub.withArgs('setPreferredRdpClient').resolves(); + await visit(urls.settings); await visit(urls.settings); await select(RDP_PREFERRED_CLIENT, RDP_CLIENT_NONE); assert.ok( - this.ipcStub.calledWith('setPreferredRdpClient', RDP_CLIENT_NONE), + window.desktop.rdp.setPreferredRdpClient.calledWith(RDP_CLIENT_NONE), ); assert.strictEqual(rdpService.preferredRdpClient, RDP_CLIENT_NONE); }); diff --git a/ui/desktop/tests/acceptance/projects/targets/index-test.js b/ui/desktop/tests/acceptance/projects/targets/index-test.js index 0375c57a6..2e2e50992 100644 --- a/ui/desktop/tests/acceptance/projects/targets/index-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/index-test.js @@ -11,7 +11,7 @@ import { click, } from '@ember/test-helpers'; import { setupApplicationTest } from 'desktop/tests/helpers'; -import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../helpers/desktop-context-bridge-api-mock'; import { invalidateSession, currentSession, @@ -28,6 +28,7 @@ import { RDP_CLIENT_NONE, RDP_CLIENT_WINDOWS_APP } from 'desktop/services/rdp'; module('Acceptance | projects | targets | index', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); let getTargetCount; @@ -164,11 +165,9 @@ module('Acceptance | projects | targets | index', function (hooks) { // Generate resource counter getTargetCount = () => this.server.schema.targets.all().models.length; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); // mock RDP service calls @@ -321,8 +320,7 @@ module('Acceptance | projects | targets | index', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').rejects(); + window.desktop.session.connectSession.rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; await visit(urls.projects); @@ -348,8 +346,7 @@ module('Acceptance | projects | targets | index', function (hooks) { instances.target.authorized_actions = instances.target.authorized_actions.filter((item) => item !== 'read'); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -380,8 +377,7 @@ module('Acceptance | projects | targets | index', function (hooks) { instances.target.authorized_actions = instances.target.authorized_actions.filter((item) => item !== 'read'); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').rejects(); + window.desktop.session.connectSession.rejects(); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; @@ -625,7 +621,7 @@ module('Acceptance | projects | targets | index', function (hooks) { }, }); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(false); + window.desktop.daemon.isCacheDaemonRunning.resolves(false); this.stubCacheDaemonSearch(); const targetsCount = getTargetCount(); @@ -795,18 +791,15 @@ module('Acceptance | projects | targets | index', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', protocol: 'rdp', }); - this.ipcStub.withArgs('launchRdpClient').resolves(); // visit targets page await visit(urls.targets); @@ -815,7 +808,9 @@ module('Acceptance | projects | targets | index', function (hooks) { await click(TARGET_OPEN_BUTTON(instances.target.id)); - assert.ok(this.ipcStub.calledWith('launchRdpClient', instances.session.id)); + assert.ok( + window.desktop.rdp.launchRdpClient.calledWith(instances.session.id), + ); }); test('clicking `Connect` button for RDP target without preferred client calls connect IPC', async function (assert) { @@ -828,12 +823,10 @@ module('Acceptance | projects | targets | index', function (hooks) { }, }); - this.ipcStub.withArgs('cliExists').returns(true); - this.rdpService.preferredRdpClient = RDP_CLIENT_NONE; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('connect').returns({ + const connectSessionStub = window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -847,7 +840,7 @@ module('Acceptance | projects | targets | index', function (hooks) { await click(TARGET_CONNECT_BUTTON(instances.target.id)); - assert.ok(this.ipcStub.calledWith('connect')); + assert.ok(connectSessionStub.called); }); test('shows confirm modal when connection error occurs on launching rdp client', async function (assert) { @@ -863,15 +856,14 @@ module('Acceptance | projects | targets | index', function (hooks) { this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('cliExists').returns(true); // target quick connection is a success but launching RDP client fails - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', protocol: 'rdp', }); - this.ipcStub.withArgs('launchRdpClient').rejects(); + window.desktop.rdp.launchRdpClient.rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; diff --git a/ui/desktop/tests/acceptance/projects/targets/target-test.js b/ui/desktop/tests/acceptance/projects/targets/target-test.js index 5e103594a..bff8331f0 100644 --- a/ui/desktop/tests/acceptance/projects/targets/target-test.js +++ b/ui/desktop/tests/acceptance/projects/targets/target-test.js @@ -14,7 +14,7 @@ import { waitFor, } from '@ember/test-helpers'; import { setupApplicationTest } from 'desktop/tests/helpers'; -import WindowMockIPC from '../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../helpers/desktop-context-bridge-api-mock'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import { TYPE_TARGET_RDP } from 'api/models/target'; @@ -23,6 +23,7 @@ import { RDP_CLIENT_WINDOWS_APP } from 'desktop/services/rdp'; module('Acceptance | projects | targets | target', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); const TARGET_RESOURCE_LINK = (id) => `[data-test-visit-target="${id}"]`; @@ -149,11 +150,9 @@ module('Acceptance | projects | targets | target', function (hooks) { urls.targetWithTwoHosts = `${urls.targets}/${instances.targetWithTwoHosts.id}`; urls.targetWithManyHosts = `${urls.targets}/${instances.targetWithManyHosts.id}`; - // Mock the postMessage interface used by IPC. - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); // mock RDP service calls @@ -172,8 +171,7 @@ module('Acceptance | projects | targets | target', function (hooks) { }); assert.expect(3); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'localhost', port: 'p_123', @@ -202,7 +200,6 @@ module('Acceptance | projects | targets | target', function (hooks) { }); assert.expect(4); - this.ipcStub.withArgs('cliExists').returns(true); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; await visit(urls.targets); @@ -227,8 +224,7 @@ module('Acceptance | projects | targets | target', function (hooks) { }); assert.expect(4); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').rejects(); + window.desktop.session.connectSession.rejects(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; await visit(urls.targets); @@ -244,7 +240,7 @@ module('Acceptance | projects | targets | target', function (hooks) { test('user can retry on error', async function (assert) { assert.expect(1); - this.ipcStub.withArgs('cliExists').rejects(); + window.desktop.system.cliExists.rejects(); this.stubCacheDaemonSearch(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; @@ -273,8 +269,7 @@ module('Acceptance | projects | targets | target', function (hooks) { }); assert.expect(2); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -411,8 +406,7 @@ module('Acceptance | projects | targets | target', function (hooks) { instances.targetWithTwoHosts.hostSets.models[0].hosts.models[0].id; assert.expect(2); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -591,8 +585,7 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.expect(1); this.server.get('/host-sets/:id', () => new Response(403)); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -619,8 +612,7 @@ module('Acceptance | projects | targets | target', function (hooks) { assert.expect(1); this.server.get('/hosts/:id', () => new Response(403)); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -674,15 +666,13 @@ module('Acceptance | projects | targets | target', function (hooks) { this.rdpService.preferredRdpClient = RDP_CLIENT_WINDOWS_APP; instances.target.update({ type: TYPE_TARGET_RDP }); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', protocol: 'rdp', }); this.stubCacheDaemonSearch(); - this.ipcStub.withArgs('launchRdpClient').resolves(); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; @@ -691,7 +681,9 @@ module('Acceptance | projects | targets | target', function (hooks) { await click(TARGET_OPEN_BUTTON); - assert.ok(this.ipcStub.calledWith('launchRdpClient', instances.session.id)); + assert.ok( + window.desktop.rdp.launchRdpClient.calledWith(instances.session.id), + ); }); test('shows `Connect` button for rdp target without preferred client', async function (assert) { @@ -708,8 +700,7 @@ module('Acceptance | projects | targets | target', function (hooks) { instances.target.update({ type: TYPE_TARGET_RDP }); this.stubCacheDaemonSearch(); - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns({ + const connectSessionStub = window.desktop.session.connectSession.resolves({ session_id: instances.session.id, address: 'a_123', port: 'p_123', @@ -723,8 +714,8 @@ module('Acceptance | projects | targets | target', function (hooks) { await click(TARGET_CONNECT_BUTTON); - assert.ok(this.ipcStub.calledWith('connect')); - assert.notOk(this.ipcStub.calledWith('launchRdpClient')); + assert.ok(connectSessionStub.called); + assert.notOk(window.desktop.rdp.launchRdpClient.called); }); test('shows confirm modal when quickConnectAndLaunchRdp fails', async function (assert) { diff --git a/ui/desktop/tests/acceptance/scopes-test.js b/ui/desktop/tests/acceptance/scopes-test.js index 406cc008d..2e273c00e 100644 --- a/ui/desktop/tests/acceptance/scopes-test.js +++ b/ui/desktop/tests/acceptance/scopes-test.js @@ -18,13 +18,14 @@ import { currentSession, invalidateSession, } from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../helpers/desktop-context-bridge-api-mock'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { setRunOptions } from 'ember-a11y-testing/test-support'; import sinon from 'sinon'; module('Acceptance | scopes', function (hooks) { setupApplicationTest(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); const APP_STATE_TITLE = '.hds-application-state__title'; @@ -134,10 +135,9 @@ module('Acceptance | scopes', function (hooks) { urls.globalTargets = `${urls.globalProjects}/targets`; urls.target = `${urls.targets}/${instances.target.id}`; - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); // mock RDP service calls @@ -319,7 +319,7 @@ module('Acceptance | scopes', function (hooks) { }); test.skip('pagination is not supported - windows build', async function (assert) { - this.ipcStub.withArgs('checkOS').returns({ + window.desktop.system.checkOS.resolves({ isWindows: true, isMac: false, isLinux: false, @@ -342,7 +342,7 @@ module('Acceptance | scopes', function (hooks) { }); test.skip('pagination is not supported - mac build', async function (assert) { - this.ipcStub.withArgs('checkOS').returns({ + window.desktop.system.checkOS.resolves({ isWindows: false, isMac: true, isLinux: false, @@ -365,7 +365,7 @@ module('Acceptance | scopes', function (hooks) { }); test.skip('pagination is not supported - linux build', async function (assert) { - this.ipcStub.withArgs('checkOS').returns({ + window.desktop.system.checkOS.resolves({ isWindows: false, isMac: false, isLinux: true, @@ -388,7 +388,7 @@ module('Acceptance | scopes', function (hooks) { }); test.skip('pagination is not supported - failed to fetch metaData', async function (assert) { - this.ipcStub.withArgs('checkOS').returns({ + window.desktop.system.checkOS.resolves({ isWindows: true, isMac: false, isLinux: false, @@ -413,7 +413,7 @@ module('Acceptance | scopes', function (hooks) { test('pagination is not supported - navigate to cluster url page', async function (assert) { await invalidateSession(); this.stubCacheDaemonSearch(); - this.ipcStub.withArgs('checkOS').returns({ + window.desktop.system.checkOS.resolves({ isWindows: true, isMac: false, isLinux: false, diff --git a/ui/desktop/tests/helpers/desktop-context-bridge-api-mock.js b/ui/desktop/tests/helpers/desktop-context-bridge-api-mock.js new file mode 100644 index 000000000..9f8f641ab --- /dev/null +++ b/ui/desktop/tests/helpers/desktop-context-bridge-api-mock.js @@ -0,0 +1,119 @@ +/** + * Copyright IBM Corp. 2021, 2026 + * SPDX-License-Identifier: BUSL-1.1 + */ +import sinon from 'sinon'; + +/** + * Helper to mock the `window.desktop` API in tests. + * Use `setupDesktopContextBridgeApiMock(hooks)` in tests and access via `window.desktop`. + */ +export class DesktopContextBridgeAPIMock { + constructor() { + this.sandbox = sinon.createSandbox(); + this.clusterUrl = null; + + this.cluster = { + getClusterUrl: this.sandbox.stub().callsFake(() => { + return Promise.resolve(this.clusterUrl); + }), + setClusterUrl: this.sandbox.stub().callsFake((clusterUrl) => { + this.clusterUrl = clusterUrl; + return this.clusterUrl; + }), + resetClusterUrl: this.sandbox.stub().callsFake(() => { + this.clusterUrl = null; + }), + }; + + this.system = { + openExternal: this.sandbox.stub().resolves(), + cliExists: this.sandbox.stub().resolves(true), + checkCommand: this.sandbox.stub().resolves(false), + checkOS: this.sandbox.stub().resolves({ isMac: true }), + getCliVersion: this.sandbox + .stub() + .resolves({ versionNumber: 'Boundary CLI v0.1.0' }), + getDesktopVersion: this.sandbox + .stub() + .resolves({ desktopVersion: '1.0.0' }), + }; + + this.session = { + connectSession: this.sandbox.stub(), + stopSession: this.sandbox.stub().resolves(true), + stopAllSessions: this.sandbox.stub().resolves(), + hasRunningSessions: this.sandbox.stub().resolves(true), + }; + + this.windowAction = { + hasMacOSChrome: this.sandbox.stub().resolves(false), + showWindowActions: this.sandbox.stub().resolves(false), + minimizeWindow: this.sandbox.stub().resolves(), + toggleFullscreenWindow: this.sandbox.stub().resolves(), + closeWindow: this.sandbox.stub().resolves(), + focusWindow: this.sandbox.stub().resolves(), + }; + + this.daemon = { + addTokenToDaemons: this.sandbox.stub().resolves(), + searchCacheDaemon: this.sandbox.stub().resolves({}), + isCacheDaemonRunning: this.sandbox.stub().resolves(false), + cacheDaemonStatus: this.sandbox + .stub() + .resolves({ version: 'Boundary CLI v1.0.0' }), + }; + + this.clientAgent = { + getClientAgentSessions: this.sandbox.stub().resolves([]), + isClientAgentRunning: this.sandbox.stub().resolves(false), + clientAgentStatus: this.sandbox + .stub() + .resolves({ version: '0.0.1-dev', status: 'running' }), + pauseClientAgent: this.sandbox.stub().resolves(), + resumeClientAgent: this.sandbox.stub().resolves(), + }; + + this.logging = { + getLogLevel: this.sandbox.stub().resolves('info'), + setLogLevel: this.sandbox.stub().resolves(), + getLogPath: this.sandbox + .stub() + .resolves('~/.config/Boundary/logs/desktop-client.log'), + }; + + this.rdp = { + getRdpClients: this.sandbox.stub().resolves([]), + getPreferredRdpClient: this.sandbox.stub().resolves('none'), + setPreferredRdpClient: this.sandbox.stub().callsFake((client) => { + return Promise.resolve(client); + }), + launchRdpClient: this.sandbox.stub().resolves(true), + }; + + this.app = { + onAppQuit: this.sandbox.stub().callsFake((callback) => { + const listenerCallback = () => callback(); + window.addEventListener('onAppQuit', listenerCallback); + return () => { + window.removeEventListener('onAppQuit', listenerCallback); + }; + }), + }; + } +} + +export function setupDesktopContextBridgeApiMock(hooks) { + let mockDesktop; + + hooks.beforeEach(function () { + mockDesktop = new DesktopContextBridgeAPIMock(); + window.desktop = mockDesktop; + this.searchCacheDaemonStub = window.desktop.daemon.searchCacheDaemon; + }); + + hooks.afterEach(function () { + mockDesktop.sandbox.restore(); + delete window.desktop; + }); +} diff --git a/ui/desktop/tests/helpers/window-mock-ipc.js b/ui/desktop/tests/helpers/window-mock-ipc.js deleted file mode 100644 index 5b571235f..000000000 --- a/ui/desktop/tests/helpers/window-mock-ipc.js +++ /dev/null @@ -1,106 +0,0 @@ -/** - * Copyright IBM Corp. 2021, 2026 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import Service from '@ember/service'; - -class MockIPC { - clusterUrl = null; - - invoke(method, payload) { - return this[method](payload); - } - - getClusterUrl() { - return this.clusterUrl; - } - - setClusterUrl(clusterUrl) { - this.clusterUrl = clusterUrl; - return this.clusterUrl; - } - - resetClusterUrl() { - this.clusterUrl = null; - } - hasMacOSChrome() {} - showWindowActions() {} - addTokenToDaemons() {} - searchCacheDaemon() {} - isCacheDaemonRunning() { - return false; - } - isClientAgentRunning() { - return false; - } - cacheDaemonStatus() { - return { version: 'Boundary v1.0.0' }; - } - getCliVersion() { - return { versionNumber: '1.0.0' }; - } - getDesktopVersion() { - return { desktopVersion: '1.0.0' }; - } - getLogLevel() { - return 'info'; - } - getLogPath() { - return '~/.config/Boundary/logs/desktop-client.log'; - } - setLogLevel() {} - clientAgentStatus() { - return { version: '0.0.1-dev', status: 'running' }; - } - pauseClientAgent() {} - resumeClientAgent() {} - hasRunningSessions() {} - stopAll() {} -} - -/** - * Mock window service with support for simplified postMessage and - * MessageChannel APIs. This mock implements just enough functionality - * to support the IPC mechanism. - * - * This mock is necessary to guarantee synchronous and deterministic - * operation. The window's own API is evented and asynchronous, which - * cannot be relied on for testing purposes. - * - * @todo refactor mock postMessage and MessageChannel for reusability - * outside of tests - */ -export default class extends Service { - // =attributes - - mockIPC = new MockIPC(); - - MessageChannel = class MessageChannel { - constructor() { - this.port1.postMessage = this.port1.postMessage.bind(this); - this.port2.postMessage = this.port2.postMessage.bind(this); - } - port1 = { - postMessage() {}, - close() {}, - }; - port2 = { - postMessage(data) { - this.port1.onmessage({ data }); - }, - close() {}, - }; - }; - - // =methods - - postMessage(data, clusterUrl, ports) { - if (clusterUrl !== window.location.origin) return; - const { method, payload } = data; - if (method) { - const response = this.mockIPC.invoke(method, payload); - ports[0].postMessage(response); - } - } -} diff --git a/ui/desktop/tests/unit/controllers/application-test.js b/ui/desktop/tests/unit/controllers/application-test.js index 767365a4f..d44aa473b 100644 --- a/ui/desktop/tests/unit/controllers/application-test.js +++ b/ui/desktop/tests/unit/controllers/application-test.js @@ -6,24 +6,18 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { authenticateSession } from 'ember-simple-auth/test-support'; -import WindowMockIPC from '../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../helpers/desktop-context-bridge-api-mock'; module('Unit | Controller | application', function (hooks) { setupTest(hooks); + setupDesktopContextBridgeApiMock(hooks); let controller; let clusterUrl; - let mockIPC; let session; - const setupMockIpc = (test) => { - test.owner.register('service:browser/window', WindowMockIPC); - mockIPC = test.owner.lookup('service:browser/window').mockIPC; - }; - hooks.beforeEach(async function () { await authenticateSession({}); - setupMockIpc(this); controller = this.owner.lookup('controller:application'); session = this.owner.lookup('service:session'); clusterUrl = this.owner.lookup('service:cluster-url'); @@ -52,12 +46,12 @@ module('Unit | Controller | application', function (hooks) { assert.true(session.isAuthenticated); assert.strictEqual(clusterUrl.rendererClusterUrl, url); - assert.strictEqual(mockIPC.clusterUrl, url); + assert.strictEqual(await window.desktop.cluster.getClusterUrl(), url); await controller.disconnect(); assert.false(session.isAuthenticated); assert.notOk(clusterUrl.rendererClusterUrl); - assert.notOk(mockIPC.clusterUrl); + assert.notOk(await window.desktop.cluster.getClusterUrl()); }); }); diff --git a/ui/desktop/tests/unit/controllers/cluster-url-test.js b/ui/desktop/tests/unit/controllers/cluster-url-test.js index eeacf78d6..d733e91cb 100644 --- a/ui/desktop/tests/unit/controllers/cluster-url-test.js +++ b/ui/desktop/tests/unit/controllers/cluster-url-test.js @@ -7,7 +7,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { authenticateSession } from 'ember-simple-auth/test-support'; import { setupIntl } from 'ember-intl/test-support'; -import WindowMockIPC from '../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../helpers/desktop-context-bridge-api-mock'; module('Unit | Controller | cluster-url', function (hooks) { setupTest(hooks); @@ -15,18 +15,13 @@ module('Unit | Controller | cluster-url', function (hooks) { let controller; let clusterUrl; - let mockIPC; - const setupMockIpc = (test) => { - test.owner.register('service:browser/window', WindowMockIPC); - mockIPC = test.owner.lookup('service:browser/window').mockIPC; - }; + setupDesktopContextBridgeApiMock(hooks); hooks.beforeEach(async function () { await authenticateSession({}); controller = this.owner.lookup('controller:cluster-url'); clusterUrl = this.owner.lookup('service:cluster-url'); - setupMockIpc(this); }); test('it exists', function (assert) { @@ -36,11 +31,11 @@ module('Unit | Controller | cluster-url', function (hooks) { test('setClusterUrl action de-authenticates a user and resets cluster url', async function (assert) { const newClusterUrl = 'http://localhost:9200'; assert.notEqual(clusterUrl.rendererClusterUrl, newClusterUrl); - assert.notEqual(mockIPC.clusterUrl, newClusterUrl); + assert.notEqual(window.desktop.clusterUrl, newClusterUrl); await controller.setClusterUrl(newClusterUrl); assert.strictEqual(clusterUrl.rendererClusterUrl, newClusterUrl); - assert.strictEqual(mockIPC.clusterUrl, newClusterUrl); + assert.strictEqual(window.desktop.clusterUrl, newClusterUrl); }); }); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js index 5d2f7f58e..76c98c51e 100644 --- a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js +++ b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js @@ -8,7 +8,7 @@ import { setupTest } from 'ember-qunit'; import { setupMirage } from 'desktop/tests/helpers/mirage'; import { visit, currentURL, settled } from '@ember/test-helpers'; import { setupIntl } from 'ember-intl/test-support'; -import WindowMockIPC from '../../../../../../helpers/window-mock-ipc'; +import { setupDesktopContextBridgeApiMock } from '../../../../../../helpers/desktop-context-bridge-api-mock'; module( 'Unit | Controller | scopes/scope/authenticate/method/index', @@ -16,6 +16,7 @@ module( setupTest(hooks); setupMirage(hooks); setupIntl(hooks, 'en-us'); + setupDesktopContextBridgeApiMock(hooks); let controller; let store; @@ -57,7 +58,6 @@ module( urls.targets = '/scopes/global/projects/targets'; - this.owner.register('service:browser/window', WindowMockIPC); setDefaultClusterUrl(this); }); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/projects/sessions/index-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/projects/sessions/index-test.js index 13ebbec34..c86495ec0 100644 --- a/ui/desktop/tests/unit/controllers/scopes/scope/projects/sessions/index-test.js +++ b/ui/desktop/tests/unit/controllers/scopes/scope/projects/sessions/index-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { setupIntl } from 'ember-intl/test-support'; import { setupMirage } from 'desktop/tests/helpers/mirage'; +import { setupDesktopContextBridgeApiMock } from 'desktop/tests/helpers/desktop-context-bridge-api-mock'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { visit } from '@ember/test-helpers'; import { authenticateSession } from 'ember-simple-auth/test-support'; @@ -22,6 +23,7 @@ module( function (hooks) { setupTest(hooks); setupMirage(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); setupIntl(hooks, 'en-us'); @@ -75,7 +77,7 @@ module( urls.projectScope = `/scopes/${instances.scopes.org.id}/projects`; urls.sessions = `${urls.projectScope}/sessions`; - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'sessions', 'targets'); await authenticateSession({ account_id: instances.account.id }); }); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/projects/targets/index-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/projects/targets/index-test.js index e5348d017..3a96986f6 100644 --- a/ui/desktop/tests/unit/controllers/scopes/scope/projects/targets/index-test.js +++ b/ui/desktop/tests/unit/controllers/scopes/scope/projects/targets/index-test.js @@ -7,6 +7,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; import { setupIntl } from 'ember-intl/test-support'; import { setupMirage } from 'desktop/tests/helpers/mirage'; +import { setupDesktopContextBridgeApiMock } from 'desktop/tests/helpers/desktop-context-bridge-api-mock'; import setupStubs from 'api/test-support/handlers/cache-daemon-search'; import { waitUntil, visit } from '@ember/test-helpers'; import { authenticateSession } from 'ember-simple-auth/test-support'; @@ -26,6 +27,7 @@ module( function (hooks) { setupTest(hooks); setupMirage(hooks); + setupDesktopContextBridgeApiMock(hooks); setupStubs(hooks); setupIntl(hooks, 'en-us'); @@ -79,7 +81,7 @@ module( urls.projectScope = `/scopes/${instances.scopes.org.id}/projects`; urls.targets = `${urls.projectScope}/targets`; - this.ipcStub.withArgs('isCacheDaemonRunning').returns(true); + window.desktop.daemon.isCacheDaemonRunning.resolves(true); this.stubCacheDaemonSearch('sessions', 'targets', 'aliases', 'sessions'); await authenticateSession({ account_id: instances.account.id }); }); @@ -237,8 +239,7 @@ module( address: 'a_123', port: 'p_123', }; - this.ipcStub.withArgs('cliExists').returns(true); - this.ipcStub.withArgs('connect').returns(attrs); + window.desktop.session.connectSession.resolves(attrs); await visit(urls.targets); const target = await store.findRecord('target', instances.target.id); const session = await store.findRecord('session', instances.session.id); diff --git a/ui/desktop/tests/unit/services/cluster-url-test.js b/ui/desktop/tests/unit/services/cluster-url-test.js index 50ad148bb..f97b85065 100644 --- a/ui/desktop/tests/unit/services/cluster-url-test.js +++ b/ui/desktop/tests/unit/services/cluster-url-test.js @@ -5,25 +5,26 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import sinon from 'sinon'; +import { setupDesktopContextBridgeApiMock } from '../../helpers/desktop-context-bridge-api-mock'; module('Unit | Service | clusterUrl', function (hooks) { setupTest(hooks); + setupDesktopContextBridgeApiMock(hooks); - let service, ipcService; + let service; hooks.beforeEach(function () { service = this.owner.lookup('service:clusterUrl'); - ipcService = this.owner.lookup('service:ipc'); }); test('resets clusterUrl on error', async function (assert) { assert.expect(4); - const ipcServiceStubbed = sinon.stub(ipcService, 'invoke'); + await service.setClusterUrl(window.location.origin); + assert.strictEqual(service.rendererClusterUrl, window.location.origin); assert.strictEqual(service.adapter.host, window.location.origin); - ipcServiceStubbed.withArgs('setClusterUrl').rejects(); + window.desktop.cluster.setClusterUrl.rejects(); service.setClusterUrl('invalid-origin').catch(() => { assert.notOk(service.rendererClusterUrl); assert.strictEqual(service.adapter.host, window.location.origin); @@ -32,7 +33,6 @@ module('Unit | Service | clusterUrl', function (hooks) { test('drops trailing slashes from clusterUrl on setClusterUrl', async function (assert) { assert.expect(4); - sinon.stub(ipcService, 'invoke'); await service.setClusterUrl(`${window.location.origin}/`); assert.strictEqual(service.rendererClusterUrl, window.location.origin); assert.strictEqual(service.adapter.host, window.location.origin); @@ -43,7 +43,6 @@ module('Unit | Service | clusterUrl', function (hooks) { test('trim spaces from clusterUrl on setClusterUrl', async function (assert) { assert.expect(4); - sinon.stub(ipcService, 'invoke'); await service.setClusterUrl(` ${window.location.origin}/ `); assert.strictEqual(service.rendererClusterUrl, window.location.origin); assert.strictEqual(service.adapter.host, window.location.origin); diff --git a/ui/desktop/tests/unit/services/ipc-test.js b/ui/desktop/tests/unit/services/ipc-test.js deleted file mode 100644 index ef97b50c7..000000000 --- a/ui/desktop/tests/unit/services/ipc-test.js +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright IBM Corp. 2021, 2026 - * SPDX-License-Identifier: BUSL-1.1 - */ - -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; -import { IPCRequest } from 'desktop/services/ipc'; -import sinon from 'sinon'; - -module('Unit | Service | ipc', function (hooks) { - setupTest(hooks); - - class MessageChannelMock { - constructor() { - this.port1.postMessage = this.port1.postMessage.bind(this); - this.port2.postMessage = this.port2.postMessage.bind(this); - } - port1 = { - postMessage() {}, - close() {}, - }; - port2 = { - postMessage(data) { - this.port1.onmessage({ data }); - }, - close() {}, - }; - } - - hooks.beforeEach(() => { - // IPC requests talk to the main process indirectly via window.postMessage - // proxy layer (mediated by preload.js). - // But we don't want to send these for real in tests. - sinon.stub(window, 'postMessage'); - }); - - hooks.afterEach(() => { - sinon.restore(); - }); - - test('it creates new IPCRequest objects', function (assert) { - assert.expect(1); - const service = this.owner.lookup('service:ipc'); - const request = service.invoke('hello', 'world', 'http://localhost:4200'); - assert.strictEqual(request.constructor, IPCRequest); - }); - - test('IPCRequest objects post messages on their window objects', function (assert) { - assert.expect(2); - const myWindow = { - postMessage(request) { - assert.strictEqual(request.method, 'hello'); - assert.strictEqual(request.payload, 'world'); - }, - MessageChannel: MessageChannelMock, - }; - new IPCRequest('hello', 'world', myWindow); - }); - - test('IPCRequest objects resolve to the value posted via a response message port', async function (assert) { - assert.expect(1); - const myWindow = { - postMessage(request, origin, [port]) { - // Post back a message on the response port - port.postMessage({ - foo: 'bar', - test: 42, - }); - }, - MessageChannel: MessageChannelMock, - }; - const request = new IPCRequest('hello', 'world', myWindow); - const response = await request; - assert.deepEqual(response, { - foo: 'bar', - test: 42, - }); - }); - - test('IPCRequests reject if receiving an Error instance, where the rejection value is the parsed JSON Error message', async function (assert) { - assert.expect(1); - const stringifiedPayload = JSON.stringify({ - foo: 'bar', - test: 42, - }); - const myWindow = { - postMessage(request, origin, [port]) { - // Post back an error on the response port - port.postMessage(new Error(stringifiedPayload)); - }, - MessageChannel: MessageChannelMock, - }; - try { - await new IPCRequest('hello', 'world', myWindow); - } catch (e) { - assert.deepEqual(e, { - foo: 'bar', - test: 42, - }); - } - }); -}); diff --git a/ui/desktop/tests/unit/services/rdp-test.js b/ui/desktop/tests/unit/services/rdp-test.js index 2dd3f19bf..49cb3ae15 100644 --- a/ui/desktop/tests/unit/services/rdp-test.js +++ b/ui/desktop/tests/unit/services/rdp-test.js @@ -13,15 +13,16 @@ import { RDP_CLIENT_WINDOWS_APP_LINK, RDP_CLIENT_MSTSC_LINK, } from 'desktop/services/rdp'; +import { setupDesktopContextBridgeApiMock } from '../../helpers/desktop-context-bridge-api-mock'; module('Unit | Service | rdp', function (hooks) { setupTest(hooks); + setupDesktopContextBridgeApiMock(hooks); - let service, ipcService; + let service; hooks.beforeEach(function () { service = this.owner.lookup('service:rdp'); - ipcService = this.owner.lookup('service:ipc'); }); hooks.afterEach(function () { @@ -29,9 +30,7 @@ module('Unit | Service | rdp', function (hooks) { }); test('getRdpClients sets to fallback value on error', async function (assert) { - const ipcStub = sinon.stub(ipcService, 'invoke'); - ipcStub.withArgs('getRdpClients').rejects(); - ipcStub.withArgs('checkOS').resolves({ isMac: true }); + window.desktop.rdp.getRdpClients.rejects(); await service.getRdpClients(); assert.deepEqual( @@ -42,10 +41,7 @@ module('Unit | Service | rdp', function (hooks) { }); test('getPreferredRdpClient sets to fallback value on error', async function (assert) { - sinon - .stub(ipcService, 'invoke') - .withArgs('getPreferredRdpClient') - .rejects(); + window.desktop.rdp.getPreferredRdpClient.rejects(); await service.getPreferredRdpClient(); assert.strictEqual( @@ -56,10 +52,7 @@ module('Unit | Service | rdp', function (hooks) { }); test('setPreferredRdpClient sets to fallback value on error', async function (assert) { - sinon - .stub(ipcService, 'invoke') - .withArgs('setPreferredRdpClient', RDP_CLIENT_MSTSC) - .rejects(); + window.desktop.rdp.setPreferredRdpClient.rejects(); await service.setPreferredRdpClient(RDP_CLIENT_MSTSC); assert.strictEqual( @@ -70,10 +63,7 @@ module('Unit | Service | rdp', function (hooks) { }); test('sets recommendedRdpClient correctly based on OS', async function (assert) { - sinon - .stub(ipcService, 'invoke') - .withArgs('checkOS') - .resolves({ isWindows: true }); + window.desktop.system.checkOS.resolves({ isWindows: true }); await service.getRecommendedRdpClient(); assert.deepEqual( @@ -85,7 +75,7 @@ module('Unit | Service | rdp', function (hooks) { 'recommendedRdpClient is set correctly for windows', ); - ipcService.invoke.withArgs('checkOS').resolves({ isMac: true }); + window.desktop.system.checkOS.resolves({ isMac: true }); service.recommendedRdpClient = null; await service.getRecommendedRdpClient();