From 8605a5422148f50da33059be7b76ff13ad5f530f Mon Sep 17 00:00:00 2001 From: Lisbet Alvarez <107949262+lisbet-alvarez@users.noreply.github.com> Date: Tue, 17 Sep 2024 16:36:47 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20=F0=9F=92=A1=20remove=20route-actio?= =?UTF-8?q?n=20usage=20for=20scopes=20in=20desktop=20(#2447)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 💡 remove route-action usage for scopes in desktop ✅ Closes: https://hashicorp.atlassian.net/browse/ICU-12621 --- ui/desktop/app/controllers/application.js | 38 +++++++++ ui/desktop/app/controllers/cluster-url.js | 34 ++++++++ ui/desktop/app/controllers/error.js | 10 +++ .../scopes/scope/authenticate/method/index.js | 81 +++++++++++++++++++ .../scopes/scope/authenticate/method/oidc.js | 2 +- .../scopes/scope/projects/error.js | 10 +++ ui/desktop/app/routes/application.js | 33 -------- ui/desktop/app/routes/cluster-url.js | 30 +------ .../scopes/scope/authenticate/method.js | 66 --------------- ui/desktop/app/templates/application.hbs | 8 +- ui/desktop/app/templates/cluster-url.hbs | 2 +- ui/desktop/app/templates/error.hbs | 2 +- .../scope/authenticate/method/index.hbs | 2 +- .../templates/scopes/scope/projects/error.hbs | 2 +- ui/desktop/tests/helpers/window-mock-ipc.js | 6 +- .../unit/controllers/application-test.js | 48 ++++++++++- .../unit/controllers/cluster-url-test.js | 46 +++++++++++ .../tests/unit/controllers/error-test.js | 17 ++++ .../scope/authenticate/method/index-test.js | 79 ++++++++++++++++++ .../scope/authenticate/method/oidc-test.js | 1 - .../scopes/scope/projects/error-test.js | 19 +++++ 21 files changed, 395 insertions(+), 141 deletions(-) create mode 100644 ui/desktop/app/controllers/cluster-url.js create mode 100644 ui/desktop/app/controllers/error.js create mode 100644 ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js create mode 100644 ui/desktop/app/controllers/scopes/scope/projects/error.js create mode 100644 ui/desktop/tests/unit/controllers/cluster-url-test.js create mode 100644 ui/desktop/tests/unit/controllers/error-test.js create mode 100644 ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js create mode 100644 ui/desktop/tests/unit/controllers/scopes/scope/projects/error-test.js diff --git a/ui/desktop/app/controllers/application.js b/ui/desktop/app/controllers/application.js index 4f5936263..511049c93 100644 --- a/ui/desktop/app/controllers/application.js +++ b/ui/desktop/app/controllers/application.js @@ -5,10 +5,48 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; export default class ApplicationController extends Controller { // =services + @service ipc; @service session; + @service clusterUrl; @service flashMessages; + + // =actions + + /** + * Delegates invalidation to the session service. + */ + @action + invalidateSession() { + this.session.invalidate(); + } + + /** + * Disconnects from clusterUrl and invalidates session, thereby resetting + * the client and reloading to the onboarding clusterUrl screen. + */ + @action + disconnect() { + this.clusterUrl.resetClusterUrl(); + this.invalidateSession(); + } + + @action + minimize() { + this.ipc.invoke('minimizeWindow'); + } + + @action + toggleFullScreen() { + this.ipc.invoke('toggleFullscreenWindow'); + } + + @action + close() { + this.ipc.invoke('closeWindow'); + } } diff --git a/ui/desktop/app/controllers/cluster-url.js b/ui/desktop/app/controllers/cluster-url.js new file mode 100644 index 000000000..6da120b79 --- /dev/null +++ b/ui/desktop/app/controllers/cluster-url.js @@ -0,0 +1,34 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; +import { loading } from 'ember-loading'; +import { notifyError } from 'core/decorators/notify'; + +export default class ClusterUrlController extends Controller { + // =services + + @service router; + @service('cluster-url') clusterUrlService; + + // =actions + + /** + * Points the API to the specified clusterUrl. When the main process receives + * the clusterUrl, it is expected that the renderer will be restarted. + * @param {string} clusterUrl + */ + @action + @loading + @notifyError(() => 'errors.cluster-url-verification-failed.description', { + catch: true, + }) + async setClusterUrl(clusterUrl) { + await this.clusterUrlService.setClusterUrl(clusterUrl); + this.router.replaceWith('index'); + } +} diff --git a/ui/desktop/app/controllers/error.js b/ui/desktop/app/controllers/error.js new file mode 100644 index 000000000..b22f8e996 --- /dev/null +++ b/ui/desktop/app/controllers/error.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller, { inject as controller } from '@ember/controller'; + +export default class ErrorController extends Controller { + @controller('application') appUtilities; +} diff --git a/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js b/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js new file mode 100644 index 000000000..de28152b3 --- /dev/null +++ b/ui/desktop/app/controllers/scopes/scope/authenticate/method/index.js @@ -0,0 +1,81 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; +import { getOwner } from '@ember/application'; +import { action } from '@ember/object'; +import { loading } from 'ember-loading'; +import { notifyError } from 'core/decorators/notify'; + +export default class ScopesScopeAuthenticateMethodIndexController extends Controller { + // =services + + @service session; + @service ipc; + @service intl; + @service router; + + // =methods + + /** + * Kicks off the OIDC auth flow by opening a new window to the provider. + * @param {string} authenticatorName + * @param {object} options + */ + async startOIDCAuthentication(authenticatorName, options) { + const oidc = getOwner(this).lookup(authenticatorName); + // TODO: delegate this call from the session service so that we don't have + // to look up the authenticator directly + const json = await oidc.startAuthentication(options); + await this.openExternalOIDCFlow(json.attributes.auth_url); + } + + /** + * Opens the specified URL in a new tab or window. By default this uses + * `window.open`, but may be overridden. + * @param {string} url + */ + async openExternalOIDCFlow(url) { + await this.ipc.invoke('openExternal', url); + } + + // =actions + + /** + * Delegates authentication to an authenticator of type matching the current + * auth method. If authentication fails, notifies with an alert. + */ + @action + @loading + @notifyError(() => 'errors.authentication-failed.title', { + sticky: false, + catch: true, + }) + async authenticate(authMethod, creds) { + const scope = authMethod.scope; + const authenticatorName = `authenticator:${authMethod.type}`; + const requestCookies = false; + switch (authMethod.type) { + case 'password': + case 'ldap': + await this.session.authenticate( + authenticatorName, + creds, + requestCookies, + { scope, authMethod }, + ); + this.router.transitionTo('index'); + break; + case 'oidc': + await this.startOIDCAuthentication(authenticatorName, { + scope, + authMethod, + }); + this.router.transitionTo('scopes.scope.authenticate.method.oidc'); + break; + } + } +} diff --git a/ui/desktop/app/controllers/scopes/scope/authenticate/method/oidc.js b/ui/desktop/app/controllers/scopes/scope/authenticate/method/oidc.js index c34971a14..c64b230c3 100644 --- a/ui/desktop/app/controllers/scopes/scope/authenticate/method/oidc.js +++ b/ui/desktop/app/controllers/scopes/scope/authenticate/method/oidc.js @@ -14,7 +14,7 @@ export default class ScopesScopeAuthenticateMethodOidcController extends Control // =attributes /** - * Authetnication URL for the pending OIDC flow, if any. + * Authentication URL for the pending OIDC flow, if any. * @type {?string} */ get authURL() { diff --git a/ui/desktop/app/controllers/scopes/scope/projects/error.js b/ui/desktop/app/controllers/scopes/scope/projects/error.js new file mode 100644 index 000000000..41b736690 --- /dev/null +++ b/ui/desktop/app/controllers/scopes/scope/projects/error.js @@ -0,0 +1,10 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller, { inject as controller } from '@ember/controller'; + +export default class ScopesScopeProjectsErrorController extends Controller { + @controller('application') appUtilities; +} diff --git a/ui/desktop/app/routes/application.js b/ui/desktop/app/routes/application.js index 176429211..4d9e491b4 100644 --- a/ui/desktop/app/routes/application.js +++ b/ui/desktop/app/routes/application.js @@ -61,24 +61,6 @@ export default class ApplicationRoute extends Route { // =actions - /** - * Delegates invalidation to the session service. - */ - @action - invalidateSession() { - this.session.invalidate(); - } - - /** - * Disconnects from clusterUrl and invalidates session, thereby resetting - * the client and reloading to the onboarding clusterUrl screen. - */ - @action - disconnect() { - this.clusterUrl.resetClusterUrl(); - this.invalidateSession(); - } - /** * Invalidates the session if a 401 error occurs and returns false to * prevent further error handling. @@ -121,19 +103,4 @@ export default class ApplicationRoute extends Route { rootEl.classList.remove('rose-theme-light'); } } - - @action - minimize() { - this.ipc.invoke('minimizeWindow'); - } - - @action - toggleFullScreen() { - this.ipc.invoke('toggleFullscreenWindow'); - } - - @action - close() { - this.ipc.invoke('closeWindow'); - } } diff --git a/ui/desktop/app/routes/cluster-url.js b/ui/desktop/app/routes/cluster-url.js index dc0576f8d..3e8985eb5 100644 --- a/ui/desktop/app/routes/cluster-url.js +++ b/ui/desktop/app/routes/cluster-url.js @@ -5,8 +5,6 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; -import { action } from '@ember/object'; -import { loading } from 'ember-loading'; import config from '../config/environment'; export default class ClusterUrlRoute extends Route { @@ -47,37 +45,13 @@ export default class ClusterUrlRoute extends Route { super.setupController(...arguments); let clusterUrl = this.clusterUrl.rendererClusterUrl; // If clusterUrl is unset and this is a development environment, - // autoset the clusterUrl field of the UI for better DX. + // auto-set the clusterUrl field of the UI for better DX. // The controller URL is almost always the same as the current window when // using mocks, and this makes development more rapid since developers // do not need to fill a clusterUrl on every session. if (!clusterUrl && config.autoOrigin) { clusterUrl = this.window.location.origin; } - controller.setProperties({ clusterUrl: clusterUrl }); - } - - /** - * Points the API to the specified clusterUrl. When the main process receives - * the clusterUrl, it is expected that the renderer will be restarted. - * @param {string} clusterUrl - */ - @action - @loading - async setClusterUrl(clusterUrl) { - try { - await this.clusterUrl.setClusterUrl(clusterUrl); - this.router.replaceWith('index'); - } catch (e) { - // If scopes do not load, we assume this is not a Boundary API - const errorMessage = this.intl.t( - 'errors.cluster-url-verification-failed.description', - ); - this.flashMessages.danger(errorMessage, { - notificationType: 'error', - sticky: true, - dismiss: (flash) => flash.destroyMessage(), - }); - } + controller.setProperties({ clusterUrl }); } } diff --git a/ui/desktop/app/routes/scopes/scope/authenticate/method.js b/ui/desktop/app/routes/scopes/scope/authenticate/method.js index 9f39de8b3..20070347c 100644 --- a/ui/desktop/app/routes/scopes/scope/authenticate/method.js +++ b/ui/desktop/app/routes/scopes/scope/authenticate/method.js @@ -5,19 +5,11 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; -import { getOwner } from '@ember/application'; -import { action } from '@ember/object'; -import { loading } from 'ember-loading'; -import { notifyError } from 'core/decorators/notify'; export default class ScopesScopeAuthenticateMethodRoute extends Route { // =services @service store; - @service session; - @service ipc; - @service intl; - @service router; // =methods @@ -30,62 +22,4 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { const adapterOptions = { scopeID: this.modelFor('scopes.scope').id }; return this.store.peekRecord('auth-method', id, adapterOptions); } - - /** - * - */ - async startOIDCAuthentication(authenticatorName, options) { - const oidc = getOwner(this).lookup(authenticatorName); - // TODO: delegate this call from the session service so that we don't have - // to look up the authenticator directly - const json = await oidc.startAuthentication(options); - await this.openExternalOIDCFlow(json.attributes.auth_url); - } - - /** - * Opens the specified URL in a new tab or window. By default this uses - * `window.open`, but may be overriden. - * @param {string} url - */ - async openExternalOIDCFlow(url) { - await this.ipc.invoke('openExternal', url); - } - - // =actions - - /** - * Delegates authentication to an authenticator of type matching the current - * auth method. If authentication fails, notifies with an alert. - */ - @action - @loading - @notifyError(() => 'errors.authentication-failed.title', { - sticky: false, - catch: true, - }) - async authenticate(creds) { - const scope = this.modelFor('scopes.scope'); - const authMethod = this.modelFor('scopes.scope.authenticate.method'); - const authenticatorName = `authenticator:${authMethod.type}`; - const requestCookies = false; - switch (authMethod.type) { - case 'password': - case 'ldap': - await this.session.authenticate( - authenticatorName, - creds, - requestCookies, - { scope, authMethod }, - ); - this.router.transitionTo('index'); - break; - case 'oidc': - await this.startOIDCAuthentication(authenticatorName, { - scope, - authMethod, - }); - this.router.transitionTo('scopes.scope.authenticate.method.oidc'); - break; - } - } } diff --git a/ui/desktop/app/templates/application.hbs b/ui/desktop/app/templates/application.hbs index 37908434b..3b4913ed3 100644 --- a/ui/desktop/app/templates/application.hbs +++ b/ui/desktop/app/templates/application.hbs @@ -26,21 +26,21 @@ @style='warning' class='button-window-close' title={{t 'actions.close'}} - {{on 'click' (route-action 'close')}} + {{on 'click' this.close}} /> {{/if}} @@ -50,7 +50,7 @@ @icon='flight-icons/svg/user-circle-16' as |dropdown| > - + {{t 'actions.deauthenticate'}} diff --git a/ui/desktop/app/templates/cluster-url.hbs b/ui/desktop/app/templates/cluster-url.hbs index 56fa81b6c..91f9975e0 100644 --- a/ui/desktop/app/templates/cluster-url.hbs +++ b/ui/desktop/app/templates/cluster-url.hbs @@ -10,7 +10,7 @@ > - + {{t 'actions.disconnect'}} diff --git a/ui/desktop/app/templates/scopes/scope/authenticate/method/index.hbs b/ui/desktop/app/templates/scopes/scope/authenticate/method/index.hbs index 59c42f585..4b6dd00c0 100644 --- a/ui/desktop/app/templates/scopes/scope/authenticate/method/index.hbs +++ b/ui/desktop/app/templates/scopes/scope/authenticate/method/index.hbs @@ -5,6 +5,6 @@ \ No newline at end of file diff --git a/ui/desktop/app/templates/scopes/scope/projects/error.hbs b/ui/desktop/app/templates/scopes/scope/projects/error.hbs index 2d5250b08..169983495 100644 --- a/ui/desktop/app/templates/scopes/scope/projects/error.hbs +++ b/ui/desktop/app/templates/scopes/scope/projects/error.hbs @@ -21,7 +21,7 @@ - + {{t 'actions.disconnect'}} diff --git a/ui/desktop/tests/helpers/window-mock-ipc.js b/ui/desktop/tests/helpers/window-mock-ipc.js index 6cff14ed8..5f1244e29 100644 --- a/ui/desktop/tests/helpers/window-mock-ipc.js +++ b/ui/desktop/tests/helpers/window-mock-ipc.js @@ -21,7 +21,9 @@ class MockIPC { return this.clusterUrl; } - resetClusterUrl() {} + resetClusterUrl() { + this.clusterUrl = null; + } hasMacOSChrome() {} showWindowActions() {} addTokenToDaemons() {} @@ -40,7 +42,7 @@ class MockIPC { * to support the IPC mechanism. * * This mock is necessary to guarantee synchronous and deterministic - * operation. The window's own API is evented and asychronous, which + * 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 diff --git a/ui/desktop/tests/unit/controllers/application-test.js b/ui/desktop/tests/unit/controllers/application-test.js index bf49338b2..2c5ef8593 100644 --- a/ui/desktop/tests/unit/controllers/application-test.js +++ b/ui/desktop/tests/unit/controllers/application-test.js @@ -5,13 +5,57 @@ 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'; module('Unit | Controller | application', function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. + let controller; + let session; + let clusterUrl; + let mockIPC; + + const setupMockIpc = (test) => { + test.owner.register('service:browser/window', WindowMockIPC); + mockIPC = test.owner.lookup('service:browser/window').mockIPC; + }; + + hooks.beforeEach(async function () { + authenticateSession({}); + 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) { - let controller = this.owner.lookup('controller:application'); assert.ok(controller); + 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); + }); + + test('disconnect action de-authenticates a user and resets cluster url', async function (assert) { + const url = 'http://localhost:9200'; + await clusterUrl.setClusterUrl(url); + + assert.true(session.isAuthenticated); + assert.strictEqual(clusterUrl.rendererClusterUrl, url); + assert.strictEqual(mockIPC.clusterUrl, url); + + await controller.disconnect(); + + assert.false(session.isAuthenticated); + assert.notOk(clusterUrl.rendererClusterUrl); + assert.notOk(mockIPC.clusterUrl); }); }); diff --git a/ui/desktop/tests/unit/controllers/cluster-url-test.js b/ui/desktop/tests/unit/controllers/cluster-url-test.js new file mode 100644 index 000000000..7b889735c --- /dev/null +++ b/ui/desktop/tests/unit/controllers/cluster-url-test.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +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'; + +module('Unit | Controller | cluster-url', function (hooks) { + setupTest(hooks); + setupIntl(hooks, 'en-us'); + + let controller; + let clusterUrl; + let mockIPC; + + const setupMockIpc = (test) => { + test.owner.register('service:browser/window', WindowMockIPC); + mockIPC = test.owner.lookup('service:browser/window').mockIPC; + }; + + hooks.beforeEach(async function () { + authenticateSession({}); + controller = this.owner.lookup('controller:cluster-url'); + clusterUrl = this.owner.lookup('service:cluster-url'); + setupMockIpc(this); + }); + + test('it exists', function (assert) { + assert.ok(controller); + }); + + 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); + + await controller.setClusterUrl(newClusterUrl); + + assert.strictEqual(clusterUrl.rendererClusterUrl, newClusterUrl); + assert.strictEqual(mockIPC.clusterUrl, newClusterUrl); + }); +}); diff --git a/ui/desktop/tests/unit/controllers/error-test.js b/ui/desktop/tests/unit/controllers/error-test.js new file mode 100644 index 000000000..595941787 --- /dev/null +++ b/ui/desktop/tests/unit/controllers/error-test.js @@ -0,0 +1,17 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | error', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let controller = this.owner.lookup('controller:error'); + assert.ok(controller); + assert.ok(controller.appUtilities); + }); +}); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js new file mode 100644 index 000000000..1303957be --- /dev/null +++ b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/index-test.js @@ -0,0 +1,79 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +import { authenticateSession } from 'ember-simple-auth/test-support'; +import { setupIntl } from 'ember-intl/test-support'; +import { TYPE_AUTH_METHOD_PASSWORD } from 'api/models/auth-method'; + +module( + 'Unit | Controller | scopes/scope/authenticate/method/index', + function (hooks) { + setupTest(hooks); + setupMirage(hooks); + setupIntl(hooks, 'en-us'); + + let controller; + let store; + let session; + + const instances = { + scopes: { + global: null, + }, + authMethod: null, + }; + + hooks.beforeEach(async function () { + authenticateSession({}); + controller = this.owner.lookup( + 'controller:scopes/scope/authenticate/method/index', + ); + store = this.owner.lookup('service:store'); + session = this.owner.lookup('service:session'); + + instances.scopes.global = this.server.create('scope', { + id: 'global', + type: 'global', + }); + instances.authMethod = this.server.create('auth-method', { + scope: instances.scopes.global, + type: TYPE_AUTH_METHOD_PASSWORD, + }); + }); + + test('it exists', function (assert) { + assert.ok(controller); + }); + + test('authenticate action saves login information', async function (assert) { + const identification = 'admin123'; + const authMethod = await store.findRecord( + 'auth-method', + instances.authMethod.id, + ); + const { authenticator: authBefore, username: usernameBefore } = + session.data.authenticated; + + assert.notOk(usernameBefore); + assert.strictEqual(authBefore, 'authenticator:test'); + + await controller.authenticate(authMethod, { + identification, + password: 'password', + }); + const { authenticator: authAfter, username: usernameAfter } = + session.data.authenticated; + + assert.strictEqual( + authAfter, + `authenticator:${instances.authMethod.type}`, + ); + assert.strictEqual(usernameAfter, identification); + }); + }, +); diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/oidc-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/oidc-test.js index 1b63d123c..a615dc9a6 100644 --- a/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/oidc-test.js +++ b/ui/desktop/tests/unit/controllers/scopes/scope/authenticate/method/oidc-test.js @@ -11,7 +11,6 @@ module( function (hooks) { setupTest(hooks); - // TODO: Replace this with your real tests. test('it exists', function (assert) { let controller = this.owner.lookup( 'controller:scopes/scope/authenticate/method/oidc', diff --git a/ui/desktop/tests/unit/controllers/scopes/scope/projects/error-test.js b/ui/desktop/tests/unit/controllers/scopes/scope/projects/error-test.js new file mode 100644 index 000000000..5ac8ad873 --- /dev/null +++ b/ui/desktop/tests/unit/controllers/scopes/scope/projects/error-test.js @@ -0,0 +1,19 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import { module, test } from 'qunit'; +import { setupTest } from 'desktop/tests/helpers'; + +module('Unit | Controller | scopes/scope/projects/error', function (hooks) { + setupTest(hooks); + + test('it exists', function (assert) { + let controller = this.owner.lookup( + 'controller:scopes/scope/projects/error', + ); + assert.ok(controller); + assert.ok(controller.appUtilities); + }); +});