refactor: 💡 window postmessage pattern -> contextbridge boundary api (#3167)

* 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
This commit is contained in:
Priya Patel authored and GitHub committed 2026-03-13 15:14:38 -07:00
1 parent eba17d3c71
commit 392e49bf49
45 files changed
+744 -1062

No files matched your search

@@ -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();
@@ -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(
+4 -4
View File
@@ -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
-296
View File
@@ -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);
}
});
}
}
@@ -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;
}
}
}
@@ -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;
@@ -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.
@@ -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);
}
}
+7 -7
View File
@@ -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();
}
/**
@@ -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
@@ -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'
@@ -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();
}
+6 -4
View File
@@ -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(),
);
}
+2 -2
View File
@@ -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);
@@ -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<ScopeModel>}
*/
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);
@@ -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(),
};
}
@@ -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 =
@@ -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);
}
@@ -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');
@@ -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();
}
/**
+3 -4
View File
@@ -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();
}
}
-147
View File
@@ -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);
}
}
+6 -10
View File
@@ -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);
}
}
+1 -2
View File
@@ -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,
});
+74 -34
View File
@@ -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);
};
},
},
});
@@ -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'),
@@ -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);
});
});
@@ -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();
+2 -3
View File
@@ -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);
});
@@ -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;
@@ -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',
@@ -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);
});
@@ -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;
@@ -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) {
+8 -8
View File
@@ -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,
@@ -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;
});
}
-106
View File
@@ -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);
}
}
}
@@ -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());
});
});
@@ -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);
});
});
@@ -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);
});
@@ -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 });
});
@@ -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);
@@ -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);
-103
View File
@@ -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,
});
}
});
});
+8 -18
View File
@@ -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();