[Desktop][Bug fix]: Stop target sessions before unauthenticating user session (#2891)

* add stopAll invocation to ipc handler

* trigger modal on both signout btns

* refactor invalidateSession fn

* fix application controller tests

* fix failing tests settings-card/user

* add test to Acceptance | authentication

* migrate electron dialog.showMessageBoxSync to Hds

* rename channel and tracked prop, consolidate cancel fns

* combine modals, add willDestroy, refactor

* test signout and close sessions modal

* address feedback

* refactor window.electron in authentication-test

* address feedback

* refactor: 💡 Fix failing tests

---------

Co-authored-by: Zhihe Li <zhihe.li@hashicorp.com>
This commit is contained in:
LaurenandZhihe Li authored and GitHub committed 2025-08-04 16:44:27 -04:00
1 parent f776ba0617
commit 990e2d6b9d
20 files changed
+342 -84

No files matched your search

+2
View File
@@ -246,6 +246,8 @@ export default function initializeMockIPC(server, config) {
}
pauseClientAgent() {}
resumeClientAgent() {}
hasRunningSessions() {}
stopAll() {}
}
/**
@@ -343,6 +343,12 @@ session:
connected:
title: Connected
description: You can now access {targetDisplayName}
stop-sessions-and-signout:
title: Sign out of Boundary?
description: Signing out will stop all active and pending target sessions. Are you sure you want to sign out?
stop-sessions-before-quit:
title: Close sessions before quitting?
description: Quitting the app will stop all active and pending sessions. Are you sure you want to quit?
actions:
connect: Connect
end: End session
+1 -1
View File
@@ -54,7 +54,7 @@ test.describe('Scope tests', () => {
await expect(headerNavLocator).toBeVisible();
await expect(
headerNavLocator.locator('.hds-dropdown-toggle-button'),
).toHaveText('Global');
).toHaveText('Global', { ignoreCase: true });
await expect(
authedPage.getByRole('link', { name: targetA.name }),
@@ -39,7 +39,7 @@ export default class SessionTerminalTabsComponent extends Component {
id;
terminal;
removeListener;
removeTerminalListener;
// =actions
@@ -111,9 +111,7 @@ export default class SessionTerminalTabsComponent extends Component {
}
window.terminal?.remove(this.id);
window.onresize = null;
if (this.removeListener) {
this.removeListener();
}
this.removeTerminalListener?.();
}
#setupTerminal(fitAddon, xterm, termContainer) {
@@ -122,7 +120,7 @@ export default class SessionTerminalTabsComponent extends Component {
xterm.onData((data) => window.terminal.send(data, this.id));
// Save the handler to cleanup the listener on the renderer process later
this.removeListener = window.terminal.receive((event, value) => {
this.removeTerminalListener = window.terminal.receive((value) => {
xterm.write(value);
}, this.id);
@@ -36,12 +36,12 @@
<:action>
<Hds::Button
data-test-signout-button
data-test-settings-signout-btn
@text={{t 'actions.deauthenticate'}}
@color='critical'
@icon='sign-out'
@size='medium'
{{on 'click' this.invalidateSession}}
{{on 'click' @onSignout}}
/>
</:action>
</SettingsCard>
@@ -5,7 +5,6 @@
import Component from '@glimmer/component';
import { service } from '@ember/service';
import { action } from '@ember/object';
export default class SettingsUserInfoComponent extends Component {
// =services
@@ -49,14 +48,4 @@ export default class SettingsUserInfoComponent extends Component {
const formattedAuthenticatorType = authenticator.split(':')[1];
return this.intl.t(`resources.account.types.${formattedAuthenticatorType}`);
}
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
}
+58 -6
View File
@@ -7,22 +7,63 @@ import Controller from '@ember/controller';
import { service } from '@ember/service';
import { getOwner } from '@ember/application';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
export default class ApplicationController extends Controller {
// =services
@service ipc;
@service session;
@service clusterUrl;
@service flashMessages;
@service ipc;
@service session;
@service('browser/window') window;
@tracked isLoggingOut = false;
@tracked isAppQuitting = false;
// =attributes
removeOnAppQuitListener;
constructor() {
super(...arguments);
// Listen for when user attempts to quit app
// Setup removeOnAppQuitListener to destroy the listener afterwards
this.removeOnAppQuitListener = this.window.electron?.onAppQuit(() => {
this.isAppQuitting = true;
});
}
// =actions
/**
* Delegates invalidation to the session service.
* Stop all active/pending target sessions
* Logout or close app
*/
@action
invalidateSession() {
this.session.invalidate();
async confirmCloseSessions() {
await this.ipc.invoke('stopAll');
if (this.isAppQuitting) {
this.isAppQuitting = false;
this.close();
} else {
// this.session.invalidate() comes from Ember Simple Auth BaseSessionService
this.session.invalidate();
}
this.isLoggingOut = false;
}
/**
* Only renders the signout modal if target sessions are running
*/
@action
async showModalOrLogout() {
const hasRunningSessions = await this.ipc.invoke('hasRunningSessions');
if (hasRunningSessions) {
this.isLoggingOut = true;
} else {
this.session.invalidate();
}
}
/**
@@ -32,7 +73,7 @@ export default class ApplicationController extends Controller {
@action
disconnect() {
this.clusterUrl.resetClusterUrl();
this.invalidateSession();
this.session.invalidate();
}
@action
@@ -75,4 +116,15 @@ export default class ApplicationController extends Controller {
rootEl.classList.remove('rose-theme-light');
}
}
@action
cancel() {
this.isLoggingOut = false;
this.isAppQuitting = false;
}
willDestroy() {
super.willDestroy(...arguments);
this.removeOnAppQuitListener?.();
}
}
@@ -9,7 +9,8 @@ import { action } from '@ember/object';
export default class ScopesScopeProjectsSettingsIndexController extends Controller {
@controller('application') application;
//actions
// =actions
/**
* Calls the Application controller's toggleTheme method
* @param {string} theme - "light", "dark", or nullish (system default)
+39 -3
View File
@@ -63,8 +63,8 @@
/>
{{/if}}
<dd.Interactive
data-test-sign-out
{{on 'click' this.invalidateSession}}
data-test-nav-signout-btn
{{on 'click' this.showModalOrLogout}}
>
{{t 'actions.deauthenticate'}}
</dd.Interactive>
@@ -155,4 +155,40 @@
</M.Footer>
</Hds::Modal>
{{/if}}
</PendingConfirmations>
</PendingConfirmations>
{{#if (or this.isLoggingOut this.isAppQuitting)}}
<Hds::Modal @onClose={{this.cancel}} data-test-close-sessions-modal as |M|>
{{#if this.isAppQuitting}}
<M.Header>
{{t 'resources.session.messages.stop-sessions-before-quit.title'}}
</M.Header>
<M.Body>
{{t 'resources.session.messages.stop-sessions-before-quit.description'}}
</M.Body>
{{else}}
{{! If not rendering the isAppQuitting modal, render isLoggingOut modal }}
<M.Header>
{{t 'resources.session.messages.stop-sessions-and-signout.title'}}
</M.Header>
<M.Body>
{{t 'resources.session.messages.stop-sessions-and-signout.description'}}
</M.Body>
{{/if}}
<M.Footer>
<Hds::ButtonSet>
<Hds::Button
{{on 'click' this.confirmCloseSessions}}
type='button'
@text={{t 'actions.ok'}}
/>
<Hds::Button
{{on 'click' this.cancel}}
type='button'
@text={{t 'actions.cancel'}}
@color='secondary'
/>
</Hds::ButtonSet>
</M.Footer>
</Hds::Modal>
{{/if}}
@@ -3,7 +3,7 @@
SPDX-License-Identifier: BUSL-1.1
}}
<SettingsCard::User />
<SettingsCard::User @onSignout={{this.application.showModalOrLogout}} />
<SettingsCard::Server @model={{@model}} />
<SettingsCard::Application @model={{@model}} @toggle={{this.toggleTheme}} />
<SettingsCard::ClientAgent @model={{@model}} />
+16 -20
View File
@@ -13,7 +13,6 @@ const {
const {
session,
app,
dialog,
protocol,
BrowserWindow,
Menu,
@@ -22,7 +21,6 @@ const {
} = require('electron');
require('./ipc/handlers.js');
const log = require('electron-log/main');
const fs = require('fs');
const { generateCSPHeader } = require('./config/content-security-policy.js');
const runtimeSettings = require('./services/runtime-settings.js');
@@ -40,6 +38,7 @@ const { version } = require('./cli/index.js');
const isDev = require('electron-is-dev');
// Register the custom file protocol
// This will render as the default cluster URL: serve://boundary
const emberAppProtocol = 'serve';
const emberAppName = 'boundary';
const emberAppURL = `${emberAppProtocol}://${emberAppName}`;
@@ -129,7 +128,7 @@ const createWindow = async (partition, closeWindowCB) => {
const browserWindow = new BrowserWindow(browserWindowOptions);
// If the user-specified cluster URL changes, reload the page so that
// the CSP can be refreshed with the this source allowed
// the content security policy (CSP) can be refreshed with the source allowed
runtimeSettings.onClusterUrlChange(
async () => await browserWindow.loadURL(emberAppURL),
);
@@ -143,6 +142,8 @@ const createWindow = async (partition, closeWindowCB) => {
await browserWindow.loadURL(emberAppURL);
});
// Emitted when the renderer process unexpectedly disappears.
// This is normally because it was crashed or killed.
browserWindow.webContents.on('render-process-gone', () => {
console.log(
'Your Ember app (or other code) in the main window has crashed.',
@@ -160,7 +161,7 @@ const createWindow = async (partition, closeWindowCB) => {
});
// Opens external links in the host default browser.
// We allow developer.hashicorp.com domain to open on external window
// We allow developer.hashicorp.com domain to open on an external window
// and releases.hashicorp.com domain to download the desktop app or
// link to the release page for the desktop app.
browserWindow.webContents.setWindowOpenHandler(({ url }) => {
@@ -204,15 +205,15 @@ app.on('window-all-closed', () => {
app.on('ready', async () => {
// Setup a partition
// Must be prefixed with persist: in order to save things like localStorage
// Must be prefixed with "persist:" in order to save things like localStorage
// data. Without this, the session is in-memory only.
// https://www.electronjs.org/docs/api/session#sessionfrompartitionpartition-options
const partition = `persist:${emberAppName}`;
const ses = session.fromPartition(partition);
// Register custom protocol
// This file protocol is the exclusive protocol for this application. All
// other protocols are disabled.
// This file protocol is the exclusive protocol for this application.
// All other protocols are disabled.
ses.protocol.handle(emberAppProtocol, (request) => {
/* eng-disable PROTOCOL_HANDLER_JS_CHECK */
const isDir = request.url.endsWith('/');
@@ -270,8 +271,8 @@ app.on('ready', async () => {
/**
* Need for Mac OS behavior of closing the window but not the app.
* This allows to reopen the window if the user clicks Boundary icon in the dock
* while the app is not close.
* This allows a user to reopen the window when clicking the Boundary icon in the dock,
* only when the app has not been closed.
* More info: https://www.electronjs.org/docs/latest/tutorial/quick-start#open-a-window-if-none-are-open-macos
*/
app.on('activate', async () => {
@@ -284,18 +285,13 @@ app.on('ready', async () => {
});
/**
* Prompt for closing spawned processes
* Prompt user to confirm action before closing spawned processes
* This interaction lives in the ember app using Hds::Modal
*/
app.on('before-quit', (event) => {
app.on('before-quit', async (event) => {
if (sessionManager.hasRunningSessions) {
const dialogOpts = {
type: 'question',
buttons: ['Close', 'Cancel'],
detail: 'Close sessions before quitting?',
};
const buttonId = dialog.showMessageBoxSync(null, dialogOpts);
buttonId === 0 ? sessionManager.stopAll() : event.preventDefault();
mainWindow.webContents.send('onAppQuit');
event.preventDefault();
}
});
@@ -312,7 +308,7 @@ app.on('quit', () => {
// from the exception can cause additional unforeseen and unpredictable issues.
//
// Attempting to resume normally after an uncaught exception can be similar to pulling out
// of the power cord when upgrading a computer -- nine out of ten times nothing happens -
// the power cord when upgrading a computer -- nine out of ten times nothing happens -
// but the 10th time, the system becomes corrupted.
//
// The correct use of 'uncaughtException' is to perform synchronous cleanup of allocated
+12 -1
View File
@@ -76,10 +76,15 @@ handle('connect', ({ target_id, token, host_id }) =>
);
/**
* Cancel an established boundary session spawned process.
* Stop an established boundary session spawned process.
*/
handle('stop', ({ session_id }) => sessionManager.stopById(session_id));
/**
* Stop all active and pending target sessions.
*/
handle('stopAll', async () => sessionManager.stopAll());
/**
* Check for OS window chrome. Enabled on MacOS only.
* Window chrome is disabled regardless of platform for
@@ -116,6 +121,12 @@ handle('toggleFullscreenWindow', () => {
*/
handle('closeWindow', () => app.quit());
/**
* Check if session manager has running sessions
* Return boolean
*/
handle('hasRunningSessions', () => sessionManager.hasRunningSessions);
/**
* Focus the window
*/
@@ -39,8 +39,13 @@ class SessionManager {
return session?.stop?.();
}
/**
* Stop all active and pending target sessions
* Returning Promise.all() ensures all sessions in the list have been
* stopped before calling the next fn
*/
stopAll() {
this.#sessions.forEach((session) => session.stop());
return Promise.all(this.#sessions.map((session) => session.stop()));
}
}
+18 -2
View File
@@ -22,11 +22,12 @@ contextBridge.exposeInMainWorld('terminal', {
},
receive: (callback, id) => {
const incomingDataChannel = `terminalIncomingData-${id}`;
ipcRenderer.on(incomingDataChannel, callback);
const listenerCallback = (_event, value) => callback(value);
ipcRenderer.on(incomingDataChannel, listenerCallback);
// Return a function for the caller to handle cleaning up the listener
return () => {
return ipcRenderer.removeListener(incomingDataChannel, callback);
return ipcRenderer.removeListener(incomingDataChannel, listenerCallback);
};
},
create: (vars) => {
@@ -61,3 +62,18 @@ process.once('loaded', () => {
}
});
});
/**
* Listener on electron app when user triggers before-quit event
*/
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);
return () => {
return ipcRenderer.removeListener('onAppQuit', listenerCallback);
};
},
});
@@ -4,7 +4,13 @@
*/
import { module, test } from 'qunit';
import { visit, currentURL, fillIn, click, find } from '@ember/test-helpers';
import {
visit,
currentURL,
fillIn,
click,
triggerEvent,
} from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { Response } from 'miragejs';
@@ -15,6 +21,8 @@ import {
invalidateSession,
} from 'ember-simple-auth/test-support';
import WindowMockIPC from '../helpers/window-mock-ipc';
import Service from '@ember/service';
import sinon from 'sinon';
module('Acceptance | authentication', function (hooks) {
setupApplicationTest(hooks);
@@ -58,6 +66,13 @@ module('Acceptance | authentication', function (hooks) {
sessions: null,
};
const SIGNOUT_BTN = '[data-test-nav-signout-btn]';
const MODAL_CLOSE_SESSIONS = '[data-test-close-sessions-modal]';
const MODAL_CONFIRM_BTN = '.hds-modal__footer .hds-button--color-primary';
const MODAL_CANCEL_BTN = '.hds-modal__footer .hds-button--color-secondary';
const HEADER_DROPDOWN_BTN =
'.rose-header-utilities .header-dropdown-button-override button';
const setDefaultClusterUrl = (test) => {
const windowOrigin = window.location.origin;
const clusterUrl = test.owner.lookup('service:clusterUrl');
@@ -71,6 +86,9 @@ module('Acceptance | authentication', function (hooks) {
await invalidateSession();
const ipcService = this.owner.lookup('service:ipc');
this.ipcStub = sinon.stub(ipcService, 'invoke');
// create scopes
instances.scopes.global = this.server.create('scope', { id: 'global' });
stubs.global = { id: 'global', type: 'global' };
@@ -128,6 +146,10 @@ module('Acceptance | authentication', function (hooks) {
setDefaultClusterUrl(this);
});
hooks.afterEach(async function () {
sinon.restore();
});
test('visiting index while unauthenticated redirects to global authenticate method', async function (assert) {
assert.expect(2);
await visit(urls.index);
@@ -183,16 +205,11 @@ module('Acceptance | authentication', function (hooks) {
assert.ok(currentSession().isAuthenticated);
await click(
'.rose-header-utilities .header-dropdown-button-override button',
);
await click(HEADER_DROPDOWN_BTN);
assert.strictEqual(
find('[data-test-sign-out]').textContent.trim(),
'Sign Out',
);
assert.dom(SIGNOUT_BTN).includesText('Sign Out');
await click('[data-test-sign-out]');
await click(SIGNOUT_BTN);
assert.notOk(currentSession().isAuthenticated);
});
@@ -235,4 +252,95 @@ module('Acceptance | authentication', function (hooks) {
assert.dom('[data-test-no-auth-methods]').includesText('No Auth Methods');
assert.dom('.change-origin').exists();
});
test('signing out with running sessions renders signout modal', async function (assert) {
this.ipcStub.withArgs('hasRunningSessions').returns(true);
await visit(urls.authenticate.methods.global);
await authenticateSession({ username: 'test' });
assert.ok(currentSession().isAuthenticated);
await click(HEADER_DROPDOWN_BTN);
await click(SIGNOUT_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isVisible();
assert.dom(MODAL_CLOSE_SESSIONS).includesText('Sign out of Boundary?');
await click(MODAL_CANCEL_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isNotVisible();
assert.ok(currentSession().isAuthenticated);
});
test('confirming signout via modal stops sessions and logs out user', async function (assert) {
const stopAllSessions = this.ipcStub.withArgs('stopAll');
this.ipcStub.withArgs('hasRunningSessions').returns(true);
await visit(urls.authenticate.methods.global);
await authenticateSession({ username: 'test' });
assert.ok(currentSession().isAuthenticated);
await click(HEADER_DROPDOWN_BTN);
await click(SIGNOUT_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isVisible();
assert.dom(MODAL_CLOSE_SESSIONS).includesText('Sign out of Boundary?');
await click(MODAL_CONFIRM_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isNotVisible();
assert.ok(stopAllSessions.calledOnce);
assert.notOk(currentSession().isAuthenticated);
});
test('attempting to quit app when signout modal is present triggers the close sessions modal', async function (assert) {
// 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({ username: 'test' });
assert.ok(currentSession().isAuthenticated);
await click(HEADER_DROPDOWN_BTN);
await click(SIGNOUT_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isVisible();
assert.dom(MODAL_CLOSE_SESSIONS).includesText('Sign out of Boundary?');
await triggerEvent(window, 'onAppQuit');
assert
.dom(MODAL_CLOSE_SESSIONS)
.includesText('Close sessions before quitting?');
await click(MODAL_CONFIRM_BTN);
assert.ok(stopAllSessions.calledOnce);
assert.ok(quitApp.calledOnce);
});
});
@@ -20,8 +20,6 @@ import {
import WindowMockIPC from '../../../helpers/window-mock-ipc';
import setupStubs from 'api/test-support/handlers/cache-daemon-search';
const SIGNOUT_SELECTOR = '[data-test-signout-button]';
module('Acceptance | projects | settings | index', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
@@ -57,6 +55,10 @@ module('Acceptance | projects | settings | index', function (hooks) {
settings: null,
};
const SIGNOUT_BTN = '[data-test-settings-signout-btn]';
const MODAL_CLOSE_SESSIONS = '[data-test-close-sessions-modal]';
const MODAL_CONFIRM_BTN = '.hds-modal__footer .hds-button--color-primary';
const setDefaultClusterUrl = (test) => {
const windowOrigin = window.location.origin;
const clusterUrl = test.owner.lookup('service:clusterUrl');
@@ -65,17 +67,20 @@ module('Acceptance | projects | settings | index', function (hooks) {
hooks.beforeEach(async function () {
await authenticateSession();
// Generate scopes
instances.scopes.global = this.server.create('scope', {
id: 'global',
name: 'Global',
});
const globalScope = { id: 'global', type: 'global' };
instances.scopes.org = this.server.create('scope', {
type: 'org',
scope: globalScope,
});
const orgScope = { id: instances.scopes.org.id, type: 'org' };
instances.scopes.project = this.server.create('scope', {
type: 'project',
scope: orgScope,
@@ -83,7 +88,6 @@ module('Acceptance | projects | settings | index', function (hooks) {
urls.scopes.org = `/scopes/${instances.scopes.org.id}`;
urls.scopes.global = `/scopes/${instances.scopes.global.id}`;
urls.projects = `${urls.scopes.org}/projects`;
urls.settings = `${urls.projects}/settings`;
this.owner.register('service:browser/window', WindowMockIPC);
@@ -113,16 +117,19 @@ module('Acceptance | projects | settings | index', function (hooks) {
assert.notOk(currentSession().get('data.theme'));
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// toggle light mode
await select('[name="theme"]', 'light');
assert.strictEqual(currentSession().get('data.theme'), 'light');
assert.ok(getRootElement().classList.contains('rose-theme-light'));
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
// toggle dark mode
await select('[name="theme"]', 'dark');
assert.strictEqual(currentSession().get('data.theme'), 'dark');
assert.notOk(getRootElement().classList.contains('rose-theme-light'));
assert.ok(getRootElement().classList.contains('rose-theme-dark'));
// toggle system default
await select('[name="theme"]', 'system-default-theme');
assert.strictEqual(
@@ -133,12 +140,37 @@ module('Acceptance | projects | settings | index', function (hooks) {
assert.notOk(getRootElement().classList.contains('rose-theme-dark'));
});
test('clicking sign-out button logs out the user', async function (assert) {
await authenticateSession({ username: 'testuser' });
test('clicking signout button logs out the user', async function (assert) {
assert.expect(2);
await visit(urls.settings);
await authenticateSession({ username: 'testuser' });
assert.ok(currentSession().isAuthenticated);
await click(SIGNOUT_SELECTOR);
await visit(urls.settings);
await click(SIGNOUT_BTN);
assert.notOk(currentSession().isAuthenticated);
});
test('confirming signout with running sessions stops sessions and logs out user', async function (assert) {
const stopAllSessions = this.ipcStub.withArgs('stopAll');
this.ipcStub.withArgs('hasRunningSessions').returns(true);
await authenticateSession({ username: 'testuser' });
assert.ok(currentSession().isAuthenticated);
await visit(urls.settings);
await click(SIGNOUT_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isVisible();
assert.dom(MODAL_CLOSE_SESSIONS).includesText('Sign out of Boundary?');
await click(MODAL_CONFIRM_BTN);
assert.dom(MODAL_CLOSE_SESSIONS).isNotVisible();
assert.ok(stopAllSessions.calledOnce);
assert.notOk(currentSession().isAuthenticated);
});
});
@@ -55,6 +55,8 @@ class MockIPC {
}
pauseClientAgent() {}
resumeClientAgent() {}
hasRunningSessions() {}
stopAll() {}
}
/**
@@ -25,6 +25,10 @@ module('Integration | Component | settings-card/user', function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
hooks.beforeEach(function () {
this.set('application', { showModalOrLogout: () => {} });
});
test('it renders password correctly', async function (assert) {
this.owner.register(
'service:session',
@@ -34,7 +38,9 @@ module('Integration | Component | settings-card/user', function (hooks) {
},
);
await render(hbs`<SettingsCard::User />`);
await render(
hbs`<SettingsCard::User @onSignout={{this.application.showModalOrLogout}} />`,
);
assert.dom(AUTHENTICATION_BADGE).hasText('Authenticated');
assert.dom(AUTH_METHOD_TYPE).hasText('Password');
@@ -52,7 +58,9 @@ module('Integration | Component | settings-card/user', function (hooks) {
},
);
await render(hbs`<SettingsCard::User />`);
await render(
hbs`<SettingsCard::User @onSignout={{this.application.showModalOrLogout}} />`,
);
assert.dom(AUTHENTICATION_BADGE).hasText('Authenticated');
assert.dom(AUTH_METHOD_TYPE).hasText('OIDC');
@@ -70,7 +78,9 @@ module('Integration | Component | settings-card/user', function (hooks) {
},
);
await render(hbs`<SettingsCard::User />`);
await render(
hbs`<SettingsCard::User @onSignout={{this.application.showModalOrLogout}} />`,
);
assert.dom(AUTHENTICATION_BADGE).hasText('Authenticated');
assert.dom(AUTH_METHOD_TYPE).hasText('LDAP');
@@ -12,9 +12,9 @@ module('Unit | Controller | application', function (hooks) {
setupTest(hooks);
let controller;
let session;
let clusterUrl;
let mockIPC;
let session;
const setupMockIpc = (test) => {
test.owner.register('service:browser/window', WindowMockIPC);
@@ -23,10 +23,10 @@ module('Unit | Controller | application', function (hooks) {
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');
setupMockIpc(this);
});
test('it exists', function (assert) {
@@ -34,14 +34,8 @@ module('Unit | Controller | application', function (hooks) {
assert.ok(controller.minimize);
assert.ok(controller.toggleFullScreen);
assert.ok(controller.close);
});
test('invalidateSession action de-authenticates a user', async function (assert) {
assert.true(session.isAuthenticated);
await controller.invalidateSession();
assert.false(session.isAuthenticated);
assert.ok(controller.confirmCloseSessions);
assert.ok(controller.showModalOrLogout);
});
test('toggleTheme action sets theme to specified value', function (assert) {
@@ -132,7 +132,7 @@ module(
assert.deepEqual(controller.availableSessions, selectedItems);
});
test('cancelSession action action updates session status', async function (assert) {
test('cancelSession action updates session status', async function (assert) {
await visit(urls.sessions);
const session = await store.findRecord('session', instances.session.id);