diff --git a/addons/core/translations/resources/en-us.yaml b/addons/core/translations/resources/en-us.yaml
index ff864c25a..05d3c67f3 100644
--- a/addons/core/translations/resources/en-us.yaml
+++ b/addons/core/translations/resources/en-us.yaml
@@ -586,6 +586,9 @@ role:
manage-custom-scopes:
title: Manage Custom Scopes
description: Select which specific orgs you want to apply to this role's grant sets.
+ manage-org-projects:
+ title: 'Add Projects in {orgDisplayName}'
+ description: Select which projects in this org you want to apply to this role's grant sets.
no-org-scopes:
title: No Org Scopes Available
description: Org scopes are not available for assignment.
@@ -603,6 +606,7 @@ role:
add-custom-scopes:
text: Add custom scopes
help: Customize which scopes are added to this role.
+ view-projects: '{selected}/{total} total projects'
form:
this:
label: Add this scope
diff --git a/addons/rose/addon/components/rose/header/brand/index.hbs b/addons/rose/addon/components/rose/header/brand/index.hbs
index 3a4078c5e..88b5162d2 100644
--- a/addons/rose/addon/components/rose/header/brand/index.hbs
+++ b/addons/rose/addon/components/rose/header/brand/index.hbs
@@ -7,8 +7,7 @@
...attributes
class='rose-header-brand'
title={{@text}}
- @route={{if @route @route 'scopes.scope'}}
- @model={{if @model @model 'global'}}
+ @route={{if @route @route 'index'}}
>
{{#if @logo}}
diff --git a/addons/rose/addon/components/rose/pagination/index.hbs b/addons/rose/addon/components/rose/pagination/index.hbs
index 9c66516d3..80f4144dc 100644
--- a/addons/rose/addon/components/rose/pagination/index.hbs
+++ b/addons/rose/addon/components/rose/pagination/index.hbs
@@ -8,5 +8,6 @@
@currentPage={{@currentPage}}
@currentPageSize={{@currentPageSize}}
@route={{this.router.currentRouteName}}
+ @model={{@model}}
@queryFunction={{this.paginationQueryParams}}
/>
\ No newline at end of file
diff --git a/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs
index a36217400..b7cd2495c 100644
--- a/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs
+++ b/ui/admin/app/components/form/role/manage-custom-scopes/index.hbs
@@ -20,7 +20,7 @@
{{on 'input' @handleSearchInput}}
/>
- {{#if @model.subScopes}}
+ {{#if @model.orgScopes}}
{{#if this.selectedItems.length}}
@@ -38,7 +38,7 @@
(hash label=(t 'form.id.label'))
(hash label=(t 'resources.role.scope.titles.projects_selected'))
}}
- @model={{@model.subScopes}}
+ @model={{@model.orgScopes}}
@isSelectable={{true}}
@onSelectionChange={{this.selectionChange}}
@valign='middle'
@@ -62,12 +62,27 @@
/>
- {{! TODO: Add link to projects selected page}}
+ {{#let (get @model.projectTotals B.data.id) as |projectTotals|}}
+ {{#if projectTotals}}
+
+ {{/if}}
+ {{/let}}
{{else}}
-
+
+
+
+
+ {{#if @model.projectScopes}}
+ {{#if this.selectedItems.length}}
+
+
+ {{t
+ 'resources.role.scope.titles.selected'
+ items=this.selectedItems.length
+ total=@model.totalItemsCount
+ }}
+
+
+ {{/if}}
+
+ <:body as |B|>
+
+
+
+ {{B.data.displayName}}
+
+
+
+
+
+
+
+
+
+
+
+ {{else}}
+
+
+
+
+ {{/if}}
+
+{{else}}
+
+
+
+
+
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui/admin/app/components/form/role/manage-org-projects/index.js b/ui/admin/app/components/form/role/manage-org-projects/index.js
new file mode 100644
index 000000000..2ef0c9d93
--- /dev/null
+++ b/ui/admin/app/components/form/role/manage-org-projects/index.js
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Component from '@glimmer/component';
+import { action } from '@ember/object';
+import { tracked } from '@glimmer/tracking';
+
+export default class FormRoleManageOrgProjectsIndexComponent extends Component {
+ // =attributes
+
+ @tracked selectedItems = [...this.args.model.selectedProjectIDs];
+
+ get allGrantScopes() {
+ return [
+ ...this.args.model.role.grantScopeKeywords,
+ ...this.args.model.role.grantScopeOrgIDs,
+ ...this.args.model.remainingProjectIDs,
+ ...this.selectedItems,
+ ];
+ }
+
+ // =actions
+
+ /**
+ * Handles the selection changes for the paginated table.
+ * @param {object} selectableRowsStates
+ */
+ @action
+ selectionChange({ selectableRowsStates }) {
+ selectableRowsStates.forEach((row) => {
+ const { isSelected, selectionKey: key } = row;
+ const includesId = this.selectedItems.includes(key);
+ if (isSelected) {
+ if (!includesId) this.selectedItems = [...this.selectedItems, key];
+ } else {
+ if (includesId)
+ this.selectedItems = this.selectedItems.filter(
+ (item) => item !== key,
+ );
+ }
+ });
+ }
+}
diff --git a/ui/admin/app/components/form/role/manage-scopes/index.hbs b/ui/admin/app/components/form/role/manage-scopes/index.hbs
index b5e7afdb3..2842cd267 100644
--- a/ui/admin/app/components/form/role/manage-scopes/index.hbs
+++ b/ui/admin/app/components/form/role/manage-scopes/index.hbs
@@ -75,8 +75,9 @@
@route={{if
@model.scope.isGlobal
'scopes.scope.roles.role.manage-scopes.manage-custom-scopes'
- 'scopes.scope.roles.role.manage-scopes'
+ 'scopes.scope.roles.role.manage-scopes.manage-org-projects'
}}
+ @model={{if @model.scope.isOrg @model.scope.id}}
/>
message, { catch: true })
+ @notifySuccess('notifications.add-success')
+ async setGrantScopes(role, grantScopeIDs) {
+ await role.setGrantScopes(grantScopeIDs);
+ if (role.scope.isGlobal) {
+ this.router.replaceWith(
+ 'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
+ );
+ } else {
+ this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
+ }
+ }
+
+ /**
+ * Redirect to previous as if nothing ever happened.
+ */
+ @action
+ cancel(role) {
+ if (role.scope.isGlobal) {
+ this.router.replaceWith(
+ 'scopes.scope.roles.role.manage-scopes.manage-custom-scopes',
+ );
+ } else {
+ this.router.replaceWith('scopes.scope.roles.role.manage-scopes');
+ }
+ }
+}
diff --git a/ui/admin/app/router.js b/ui/admin/app/router.js
index 960a1802e..ecac55a4e 100644
--- a/ui/admin/app/router.js
+++ b/ui/admin/app/router.js
@@ -55,6 +55,7 @@ Router.map(function () {
this.route('scopes');
this.route('manage-scopes', function () {
this.route('manage-custom-scopes');
+ this.route('manage-org-projects', { path: ':org_id' });
});
});
this.route('new');
diff --git a/ui/admin/app/routes/scopes/scope/roles/role/index.js b/ui/admin/app/routes/scopes/scope/roles/role/index.js
index 989e8def0..df3f4783f 100644
--- a/ui/admin/app/routes/scopes/scope/roles/role/index.js
+++ b/ui/admin/app/routes/scopes/scope/roles/role/index.js
@@ -4,5 +4,25 @@
*/
import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
-export default class ScopesScopeRolesRoleIndexRoute extends Route {}
+export default class ScopesScopeRolesRoleIndexRoute extends Route {
+ // =services
+
+ @service store;
+
+ // =methods
+
+ /**
+ * Pre-loads all scopes since they will be necessary in sub-routes.
+ * @param {RoleModel} role
+ */
+ async afterModel(role) {
+ if (!role.scope.isProject) {
+ await this.store.query('scope', {
+ scope_id: role.scope.id,
+ recursive: true,
+ });
+ }
+ }
+}
diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js
index 070c975c8..807a7f271 100644
--- a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js
+++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-custom-scopes.js
@@ -5,6 +5,7 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
+import { TYPE_SCOPE_PROJECT } from 'api/models/scope';
export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute extends Route {
// =attributes
@@ -30,7 +31,7 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext
/**
* Loads sub scopes for the current scope.
- * @returns {Promise<{role: RoleModel, subScopes: [ScopeModel], totalItems: number, totalItemsCount: number}> }
+ * @returns {Promise<{role: RoleModel, orgScopes: [ScopeModel], projectTotals: object, totalItems: number, totalItemsCount: number}> }
*/
async model({ search, page, pageSize }) {
const role = this.modelFor('scopes.scope.roles.role');
@@ -40,27 +41,63 @@ export default class ScopesScopeRolesRoleManageScopesManageCustomScopesRoute ext
scope_id: [{ equals: scope_id }],
};
- const subScopes = await this.store.query('scope', {
+ const orgScopes = await this.store.query('scope', {
scope_id,
query: { search, filters },
page,
pageSize,
});
- const totalItems = subScopes.meta?.totalItems;
+ const totalItems = orgScopes.meta?.totalItems;
const totalItemsCount = await this.getTotalItemsCount(
scope_id,
search,
totalItems,
);
+ const projectTotals = await this.getProjectTotals(
+ role.grantScopeProjectIDs,
+ );
+
return {
role,
- subScopes,
+ orgScopes,
+ projectTotals,
totalItems,
totalItemsCount,
};
}
+ /**
+ * Creates an object that contains the number of selected projects
+ * and the total number of projects for each org scope.
+ * @param {[string]} projectIDs
+ * @returns {object}
+ */
+ async getProjectTotals(projectIDs) {
+ const options = { pushToStore: false, peekIndexedDB: true };
+ const projects = await this.store.query(
+ 'scope',
+ {
+ scope_id: 'global',
+ query: { filters: { type: [{ equals: TYPE_SCOPE_PROJECT }] } },
+ },
+ options,
+ );
+
+ const projectTotals = {};
+ projects.forEach(({ id, scope }) => {
+ if (!projectTotals[scope.id]) {
+ projectTotals[scope.id] = { selected: 0, total: 0 };
+ }
+ if (projectIDs.includes(id)) {
+ projectTotals[scope.id].selected++;
+ }
+ projectTotals[scope.id].total++;
+ });
+
+ return projectTotals;
+ }
+
/**
* Sets scopesExist to true if there exists any scopes and
* sets totalItemsCount to the number of scopes that exist.
diff --git a/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js
new file mode 100644
index 000000000..82f38b0b8
--- /dev/null
+++ b/ui/admin/app/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects.js
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import Route from '@ember/routing/route';
+import { inject as service } from '@ember/service';
+
+export default class ScopesScopeRolesRoleManageScopesManageOrgProjectsRoute extends Route {
+ // =attributes
+
+ queryParams = {
+ search: {
+ refreshModel: true,
+ replace: true,
+ },
+ page: {
+ refreshModel: true,
+ },
+ pageSize: {
+ refreshModel: true,
+ },
+ };
+
+ // =services
+
+ @service store;
+
+ // =methods
+
+ /**
+ * Loads projects for current org scope.
+ * @return {Promise<{role: RoleModel, orgScope: ScopeModel, projectScopes: [ScopeModel], totalItems: number, totalItemsCount: number, selectedProjectIDs: [string], remainingProjectIDs: [string]}> }
+ */
+ async model({ org_id, search, page, pageSize }) {
+ const role = this.modelFor('scopes.scope.roles.role');
+ const orgScope = this.store.peekRecord('scope', org_id);
+ const filters = {
+ scope_id: [{ equals: org_id }],
+ };
+
+ const projectScopes = await this.store.query('scope', {
+ scope_id: org_id,
+ query: { search, filters },
+ page,
+ pageSize,
+ });
+ const totalItems = projectScopes.meta?.totalItems;
+ const totalItemsCount = await this.getTotalItemsCount(
+ org_id,
+ search,
+ totalItems,
+ );
+
+ const { selectedProjectIDs, remainingProjectIDs } =
+ await this.getSelectedProjects(role, org_id);
+
+ return {
+ role,
+ orgScope,
+ projectScopes,
+ totalItems,
+ totalItemsCount,
+ selectedProjectIDs,
+ remainingProjectIDs,
+ };
+ }
+
+ /**
+ * Sets scopesExist to true if there exists any scopes and
+ * sets totalItemsCount to the number of scopes that exist.
+ * @param {string} scope_id
+ * @param {string} search
+ * @param {number} totalItems
+ * @returns {Promise}
+ */
+ async getTotalItemsCount(scope_id, search, totalItems) {
+ if (!search) {
+ return totalItems;
+ }
+ const options = { pushToStore: false, peekIndexedDB: true };
+ const scopes = await this.store.query(
+ 'scope',
+ {
+ scope_id,
+ query: {
+ filters: {
+ scope_id: [{ equals: scope_id }],
+ },
+ },
+ page: 1,
+ pageSize: 1,
+ },
+ options,
+ );
+ return scopes.meta?.totalItems;
+ }
+
+ /**
+ * Extract project type grant scope ids that belong to this org scope.
+ * @param {RoleModel} role
+ * @param {string} org_id
+ * @returns {object}
+ */
+ async getSelectedProjects(role, org_id) {
+ let selectedProjectIDs = role.grantScopeProjectIDs;
+ let remainingProjectIDs = [];
+ if (role.scope.isGlobal && selectedProjectIDs.length) {
+ const id = [];
+ selectedProjectIDs.forEach((projectID) => id.push({ equals: projectID }));
+ const options = { pushToStore: false, peekIndexedDB: true };
+ const projects = await this.store.query(
+ 'scope',
+ { scope_id: 'global', query: { filters: { id } } },
+ options,
+ );
+ selectedProjectIDs = projects.reduce((selectedIDs, project) => {
+ if (project.scope.id === org_id) {
+ selectedIDs.push(project.id);
+ } else {
+ remainingProjectIDs.push(project.id);
+ }
+ return selectedIDs;
+ }, []);
+ }
+ return { selectedProjectIDs, remainingProjectIDs };
+ }
+}
diff --git a/ui/admin/app/routes/scopes/scope/roles/role/scopes.js b/ui/admin/app/routes/scopes/scope/roles/role/scopes.js
index bf43ea083..bff216285 100644
--- a/ui/admin/app/routes/scopes/scope/roles/role/scopes.js
+++ b/ui/admin/app/routes/scopes/scope/roles/role/scopes.js
@@ -1,23 +1,9 @@
import Route from '@ember/routing/route';
-import { inject as service } from '@ember/service';
-import { GRANT_SCOPE_KEYWORDS } from 'api/models/role';
export default class ScopesScopeRolesRoleScopesRoute extends Route {
- // =services
+ // =methods
- @service store;
-
- async model() {
- const role = this.modelFor('scopes.scope.roles.role');
- if (
- role.grant_scope_ids &&
- !GRANT_SCOPE_KEYWORDS.some((id) => role.grant_scope_ids.includes(id))
- ) {
- await this.store.query('scope', {
- scope_id: role.scope.id,
- recursive: true,
- });
- }
- return role;
+ model() {
+ return this.modelFor('scopes.scope.roles.role');
}
}
diff --git a/ui/admin/app/styles/app.scss b/ui/admin/app/styles/app.scss
index 88fb5deaa..7bf29b5c2 100644
--- a/ui/admin/app/styles/app.scss
+++ b/ui/admin/app/styles/app.scss
@@ -1059,7 +1059,10 @@
.table-selected-items-text {
display: flex;
justify-content: end;
- margin-bottom: 0.5rem;
+ }
+
+ .hds-table {
+ margin-top: 0.5rem;
}
.hds-pagination {
diff --git a/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs
new file mode 100644
index 000000000..39cd0d146
--- /dev/null
+++ b/ui/admin/app/templates/scopes/scope/roles/role/manage-scopes/manage-org-projects.hbs
@@ -0,0 +1,56 @@
+{{!
+ Copyright (c) HashiCorp, Inc.
+ SPDX-License-Identifier: BUSL-1.1
+}}
+
+{{page-title
+ (t
+ 'resources.role.scope.messages.manage-org-projects.title'
+ orgDisplayName=@model.orgScope.displayName
+ )
+}}
+{{#if @model.role.scope.isGlobal}}
+
+{{/if}}
+
+
+
+
+
+
+
+
+
+
+ {{t
+ 'resources.role.scope.messages.manage-org-projects.title'
+ orgDisplayName=@model.orgScope.displayName
+ }}
+ {{! TODO: Add doclink }}
+
+ {{t 'resources.role.scope.messages.manage-org-projects.description'}}
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ui/admin/tests/acceptance/roles/global-scope-test.js b/ui/admin/tests/acceptance/roles/global-scope-test.js
new file mode 100644
index 000000000..cad512abf
--- /dev/null
+++ b/ui/admin/tests/acceptance/roles/global-scope-test.js
@@ -0,0 +1,533 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ click,
+ findAll,
+ fillIn,
+ waitUntil,
+} from '@ember/test-helpers';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import { authenticateSession } from 'ember-simple-auth/test-support';
+import {
+ GRANT_SCOPE_THIS,
+ GRANT_SCOPE_CHILDREN,
+ GRANT_SCOPE_DESCENDANTS,
+} from 'api/models/role';
+
+module('Acceptance | roles | global-scope', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+ setupIndexedDb(hooks);
+
+ const SCOPE_TOGGLE_SELECTOR = (name) =>
+ `.hds-form-toggle input[name="${name}"]`;
+ const SCOPE_CHECKBOX_SELECTOR = (type, id) =>
+ `tbody [data-test-${type}-scopes-table-row="${id}"] input`;
+ const TABLE_ROW_SELECTOR = 'tbody tr';
+ const TABLE_SCOPE_SELECTOR = 'tbody tr:nth-child(2) a';
+ const TABLE_PARENT_SCOPE_SELECTOR = 'tbody tr:nth-child(2) td:nth-child(3) a';
+ const SAVE_BTN_SELECTOR = 'form [type="submit"]';
+ const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
+ const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
+ const TOAST_SELECTOR = '[role="alert"]';
+ const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
+ const NO_RESULTS_MSG_SELECTOR = '[data-test-no-grant-scope-results]';
+ const NO_SCOPES_MSG_SELECTOR = '.role-grant-scopes div';
+ const NO_SCOPES_MSG_LINK_SELECTOR =
+ '.role-grant-scopes div div:nth-child(3) a';
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ role: null,
+ };
+ const urls = {
+ orgScope: null,
+ roles: null,
+ role: null,
+ roleScopes: null,
+ manageScopes: null,
+ manageCustomScopes: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ instances.role = this.server.create(
+ 'role',
+ {
+ scope: instances.scopes.global,
+ },
+ 'withScopes',
+ );
+
+ urls.roles = `/scopes/global/roles`;
+ urls.role = `${urls.roles}/${instances.role.id}`;
+ urls.roleScopes = `${urls.role}/scopes`;
+ urls.manageScopes = `${urls.role}/manage-scopes`;
+ urls.manageCustomScopes = `${urls.manageScopes}/manage-custom-scopes`;
+ });
+
+ test('visiting role scopes', async function (assert) {
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await a11yAudit();
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert
+ .dom(TABLE_ROW_SELECTOR)
+ .exists({ count: instances.role.grant_scope_ids.length });
+ });
+
+ test('user can naviage to scope from role grant scopes', async function (assert) {
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await click(TABLE_SCOPE_SELECTOR);
+
+ assert.strictEqual(
+ currentURL(),
+ `/scopes/${instances.scopes.global.id}/scopes`,
+ );
+ });
+
+ test('user can naviage to parent scope from role grant scopes', async function (assert) {
+ instances.role.update({
+ grant_scope_ids: ['this', instances.scopes.org.id],
+ });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await click(TABLE_PARENT_SCOPE_SELECTOR);
+
+ assert.strictEqual(
+ currentURL(),
+ `/scopes/${instances.scopes.global.id}/scopes`,
+ );
+ });
+
+ test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.dom(NO_SCOPES_MSG_SELECTOR).includesText('No scopes added');
+ assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).isVisible();
+ });
+
+ test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
+ instances.role.update({
+ grant_scope_ids: [],
+ authorized_actions: instances.role.authorized_actions.filter(
+ (action) => action !== 'set-grant-scopes',
+ ),
+ });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).doesNotExist();
+ });
+
+ test('correct toggles are visible for global level role on manage scopes page', async function (assert) {
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).isVisible();
+ });
+
+ test('user can save scope keywords to add on manage scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await a11yAudit();
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
+ });
+
+ test('user can cancel scope keywords to add on manage scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+ });
+
+ test('shows error message on scope save on manage scopes page', async function (assert) {
+ this.server.post('/roles/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ },
+ );
+ });
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.dom(TOAST_SELECTOR).isVisible();
+ });
+
+ test('user can save custom scopes to add on manage custom scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await a11yAudit();
+
+ assert.strictEqual(currentURL(), urls.manageCustomScopes);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
+ });
+
+ test('user can cancel custom scopes to add on manage custom scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(`[href="${urls.manageCustomScopes}"]`);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+ });
+
+ test('shows error message on custom scope save on manage custom scopes page', async function (assert) {
+ this.server.post('/roles/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ },
+ );
+ });
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.dom(TOAST_SELECTOR).isVisible();
+ });
+
+ test('user can search for a specifc org scope by id on manage custom scopes page', async function (assert) {
+ const anotherOrg = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ await visit(urls.manageScopes);
+
+ await click(`[href="${urls.manageCustomScopes}"]`);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org.id);
+ await waitUntil(
+ () => findAll(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).length === 0,
+ );
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).doesNotExist();
+ });
+
+ test('user can search for org scopes and get no results on manage custom scopes page', async function (assert) {
+ const anotherOrg = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ await visit(urls.manageScopes);
+
+ await click(`[href="${urls.manageCustomScopes}"]`);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
+ await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('org', instances.scopes.org.id))
+ .doesNotExist();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('org', anotherOrg.id)).doesNotExist();
+ assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
+ });
+
+ test('user can save custom scopes to add on manage org projects page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(
+ `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
+ );
+ await a11yAudit();
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageCustomScopes);
+
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
+ });
+
+ test('user can cancel custom scopes to add on manage org projects page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(
+ `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
+ );
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageCustomScopes);
+
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+ });
+
+ test('shows error message on custom scope save on manage org projects page', async function (assert) {
+ this.server.post('/roles/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ },
+ );
+ });
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(
+ `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.dom(TOAST_SELECTOR).isVisible();
+ });
+
+ test('user can search for a specifc project scope by id on manage org projects page', async function (assert) {
+ const anotherProject = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(
+ `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
+ );
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project.id);
+ await waitUntil(
+ () =>
+ findAll(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .length === 0,
+ );
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .doesNotExist();
+ });
+
+ test('user can search for project scopes and get no results on manage org projects page', async function (assert) {
+ const anotherProject = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageCustomScopes}"]`);
+ await click(
+ `tbody [href="${urls.manageScopes}/${instances.scopes.org.id}"]`,
+ );
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
+ await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .doesNotExist();
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .doesNotExist();
+ assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
+ });
+});
diff --git a/ui/admin/tests/acceptance/roles/org-scope-test.js b/ui/admin/tests/acceptance/roles/org-scope-test.js
new file mode 100644
index 000000000..c863cbd8a
--- /dev/null
+++ b/ui/admin/tests/acceptance/roles/org-scope-test.js
@@ -0,0 +1,376 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import {
+ visit,
+ currentURL,
+ click,
+ findAll,
+ fillIn,
+ waitUntil,
+} from '@ember/test-helpers';
+import { setupApplicationTest } from 'ember-qunit';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
+import a11yAudit from 'ember-a11y-testing/test-support/audit';
+import { authenticateSession } from 'ember-simple-auth/test-support';
+import {
+ GRANT_SCOPE_THIS,
+ GRANT_SCOPE_CHILDREN,
+ GRANT_SCOPE_DESCENDANTS,
+} from 'api/models/role';
+
+module('Acceptance | roles | org-scope', function (hooks) {
+ setupApplicationTest(hooks);
+ setupMirage(hooks);
+ setupIndexedDb(hooks);
+
+ const SCOPE_TOGGLE_SELECTOR = (name) =>
+ `.hds-form-toggle input[name="${name}"]`;
+ const SCOPE_CHECKBOX_SELECTOR = (type, id) =>
+ `tbody [data-test-${type}-scopes-table-row="${id}"] input`;
+ const TABLE_ROW_SELECTOR = 'tbody tr';
+ const TABLE_SCOPE_SELECTOR = 'tbody tr:nth-child(2) a';
+ const TABLE_PARENT_SCOPE_SELECTOR = 'tbody tr:nth-child(2) td:nth-child(3) a';
+ const SAVE_BTN_SELECTOR = 'form [type="submit"]';
+ const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
+ const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
+ const TOAST_SELECTOR = '[role="alert"]';
+ const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
+ const NO_RESULTS_MSG_SELECTOR = '[data-test-no-grant-scope-results]';
+ const NO_SCOPES_MSG_SELECTOR = '.role-grant-scopes div';
+ const NO_SCOPES_MSG_LINK_SELECTOR =
+ '.role-grant-scopes div div:nth-child(3) a';
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ role: null,
+ };
+ const urls = {
+ orgScope: null,
+ roles: null,
+ role: null,
+ roleScopes: null,
+ manageScopes: null,
+ manageOrgProjects: null,
+ };
+
+ hooks.beforeEach(function () {
+ authenticateSession({});
+ instances.scopes.global = this.server.create('scope', { id: 'global' });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ instances.role = this.server.create(
+ 'role',
+ {
+ scope: instances.scopes.org,
+ },
+ 'withScopes',
+ );
+
+ urls.roles = `/scopes/${instances.scopes.org.id}/roles`;
+ urls.role = `${urls.roles}/${instances.role.id}`;
+ urls.roleScopes = `${urls.role}/scopes`;
+ urls.manageScopes = `${urls.role}/manage-scopes`;
+ urls.manageOrgProjects = `${urls.role}/manage-scopes/${instances.scopes.org.id}`;
+ });
+
+ test('visiting role scopes', async function (assert) {
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await a11yAudit();
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert
+ .dom(TABLE_ROW_SELECTOR)
+ .exists({ count: instances.role.grant_scope_ids.length });
+ });
+
+ test('user can naviage to scope from role grant scopes', async function (assert) {
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await click(TABLE_SCOPE_SELECTOR);
+
+ assert.strictEqual(
+ currentURL(),
+ `/scopes/${instances.scopes.org.id}/scopes`,
+ );
+ });
+
+ test('user can naviage to parent scope from role grant scopes', async function (assert) {
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+ await click(TABLE_PARENT_SCOPE_SELECTOR);
+
+ assert.strictEqual(
+ currentURL(),
+ `/scopes/${instances.scopes.global.id}/scopes`,
+ );
+ });
+
+ test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.dom(NO_SCOPES_MSG_SELECTOR).includesText('No scopes added');
+ assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).isVisible();
+ });
+
+ test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
+ instances.role.update({
+ grant_scope_ids: [],
+ authorized_actions: instances.role.authorized_actions.filter(
+ (action) => action !== 'set-grant-scopes',
+ ),
+ });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.dom(NO_SCOPES_MSG_LINK_SELECTOR).doesNotExist();
+ });
+
+ test('correct toggles are visible for org level role on manage scopes page', async function (assert) {
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
+ assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).doesNotExist();
+ });
+
+ test('user can save scope keywords to add on manage scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await a11yAudit();
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
+ });
+
+ test('user can cancel scope keywords to add on manage scopes page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+ });
+
+ test('shows error message on scope save on manage scopes page', async function (assert) {
+ this.server.post('/roles/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ },
+ );
+ });
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.dom(TOAST_SELECTOR).isVisible();
+ });
+
+ test('user can save custom scopes to add on manage org projects page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageOrgProjects}"]`);
+ await a11yAudit();
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
+ });
+
+ test('user can cancel custom scopes to add on manage org projects page', async function (assert) {
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(`[href="${urls.roleScopes}"]`);
+
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageOrgProjects}"]`);
+ await a11yAudit();
+
+ // Click three times to select, unselect, then reselect (for coverage)
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.manageScopes);
+
+ await click(CANCEL_BTN_SELECTOR);
+
+ assert.strictEqual(currentURL(), urls.roleScopes);
+ assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
+ });
+
+ test('shows error message on custom scope save on manage org projects page', async function (assert) {
+ this.server.post('/roles/:idMethod', () => {
+ return new Response(
+ 400,
+ {},
+ {
+ status: 400,
+ code: 'invalid_argument',
+ message: 'The request was invalid.',
+ details: {},
+ },
+ );
+ });
+ instances.role.update({ grant_scope_ids: [] });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageOrgProjects}"]`);
+ await click(
+ SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id),
+ );
+ await click(SAVE_BTN_SELECTOR);
+
+ assert.dom(TOAST_SELECTOR).isVisible();
+ });
+
+ test('user can search for a specifc project scope by id on manage org projects page', async function (assert) {
+ const anotherProject = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageOrgProjects}"]`);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.project.id);
+ await waitUntil(
+ () =>
+ findAll(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .length === 0,
+ );
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .doesNotExist();
+ });
+
+ test('user can search for project scopes and get no results on manage org projects page', async function (assert) {
+ const anotherProject = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ await visit(urls.role);
+
+ await click(MANAGE_SCOPES_SELECTOR);
+ await click(`[href="${urls.manageOrgProjects}"]`);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .exists();
+ assert.dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id)).exists();
+
+ await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
+ await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
+
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', instances.scopes.project.id))
+ .doesNotExist();
+ assert
+ .dom(SCOPE_CHECKBOX_SELECTOR('project', anotherProject.id))
+ .doesNotExist();
+ assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
+ });
+});
diff --git a/ui/admin/tests/acceptance/roles/scopes-test.js b/ui/admin/tests/acceptance/roles/scopes-test.js
deleted file mode 100644
index 88ad21f06..000000000
--- a/ui/admin/tests/acceptance/roles/scopes-test.js
+++ /dev/null
@@ -1,351 +0,0 @@
-/**
- * Copyright (c) HashiCorp, Inc.
- * SPDX-License-Identifier: BUSL-1.1
- */
-
-import { module, test } from 'qunit';
-import {
- visit,
- currentURL,
- click,
- findAll,
- fillIn,
- waitUntil,
-} from '@ember/test-helpers';
-import { setupApplicationTest } from 'ember-qunit';
-import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
-import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
-import a11yAudit from 'ember-a11y-testing/test-support/audit';
-import { authenticateSession } from 'ember-simple-auth/test-support';
-import {
- GRANT_SCOPE_THIS,
- GRANT_SCOPE_CHILDREN,
- GRANT_SCOPE_DESCENDANTS,
-} from 'api/models/role';
-
-module('Acceptance | roles | scopes', function (hooks) {
- setupApplicationTest(hooks);
- setupMirage(hooks);
- setupIndexedDb(hooks);
-
- const SCOPE_TOGGLE_SELECTOR = (name) =>
- `.hds-form-toggle input[name="${name}"]`;
- const SCOPE_CHECKBOX_SELECTOR = (id) =>
- `tbody [data-test-org-scopes-table-row="${id}"] input`;
- const TABLE_ROW_SELECTOR = 'tbody tr';
- const SAVE_BTN_SELECTOR = 'form [type="submit"]';
- const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
- const MANAGE_SCOPES_SELECTOR = '[data-test-manage-dropdown-scopes]';
- const TOAST_SELECTOR = '[role="alert"]';
- const SEARCH_INPUT_SELECTOR = '.search-filtering [type="search"]';
- const NO_RESULTS_MSG_SELECTOR = '[data-test-no-scope-results]';
-
- const instances = {
- scopes: {
- global: null,
- org: null,
- },
- globalRole: null,
- orgRole: null,
- };
- const urls = {
- orgScope: null,
- globalRoles: null,
- globalRole: null,
- globalRoleScopes: null,
- globalManageScopes: null,
- orgRoles: null,
- orgRole: null,
- orgRoleScopes: null,
- orgManageScopes: null,
- manageCustomScopes: null,
- };
-
- hooks.beforeEach(function () {
- authenticateSession({});
- instances.scopes.global = this.server.create('scope', { id: 'global' });
- instances.scopes.org = this.server.create('scope', {
- type: 'org',
- scope: { id: 'global', type: 'global' },
- });
- instances.globalRole = this.server.create(
- 'role',
- {
- scope: instances.scopes.global,
- },
- 'withScopes',
- );
- instances.orgRole = this.server.create(
- 'role',
- {
- scope: instances.scopes.org,
- },
- 'withScopes',
- );
-
- urls.globalRoles = `/scopes/global/roles`;
- urls.globalRole = `${urls.globalRoles}/${instances.globalRole.id}`;
- urls.globalRoleScopes = `${urls.globalRole}/scopes`;
- urls.globalManageScopes = `${urls.globalRole}/manage-scopes`;
- urls.manageCustomScopes = `${urls.globalManageScopes}/manage-custom-scopes`;
- urls.orgRoles = `/scopes/${instances.scopes.org.id}/roles`;
- urls.orgRole = `${urls.orgRoles}/${instances.orgRole.id}`;
- urls.orgRoleScopes = `${urls.orgRole}/scopes`;
- urls.orgManageScopes = `${urls.orgRole}/manage-scopes`;
- });
-
- test('visiting role scopes', async function (assert) {
- await visit(urls.orgRoleScopes);
- await a11yAudit();
- assert.strictEqual(currentURL(), urls.orgRoleScopes);
- assert
- .dom(TABLE_ROW_SELECTOR)
- .exists({ count: instances.orgRole.grant_scope_ids.length });
- });
-
- test('user can naviage to scope from role grant scopes', async function (assert) {
- await visit(urls.orgRoleScopes);
- await click('tbody tr:nth-child(2) a');
- assert.strictEqual(
- currentURL(),
- `/scopes/${instances.scopes.org.id}/scopes`,
- );
- });
-
- test('user can naviage to parent scope from role grant scopes', async function (assert) {
- await visit(urls.orgRoleScopes);
- await click('tbody tr:nth-child(2) td:nth-child(3) a');
- assert.strictEqual(
- currentURL(),
- `/scopes/${instances.scopes.global.id}/scopes`,
- );
- });
-
- test('user sees no scopes message and action when role has no grant scopes', async function (assert) {
- instances.orgRole.update({ grant_scope_ids: [] });
- await visit(urls.orgRoleScopes);
- assert.dom('.role-grant-scopes div').includesText('No scopes added');
- assert.dom('.role-grant-scopes div div:nth-child(3) a').isVisible();
- });
-
- test('user does not see action to add scopes when role has no grant scopes without proper permissions', async function (assert) {
- instances.orgRole.update({
- grant_scope_ids: [],
- authorized_actions: instances.orgRole.authorized_actions.filter(
- (action) => action !== 'set-grant-scopes',
- ),
- });
- await visit(urls.orgRoleScopes);
- assert.dom('.role-grant-scopes div div:nth-child(3) a').doesNotExist();
- });
-
- test('user can toggle and save scope keywords to add on manage scopes page', async function (assert) {
- instances.orgRole.update({ grant_scope_ids: [] });
- await visit(urls.orgRole);
-
- await click(`[href="${urls.orgRoleScopes}"]`);
-
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.orgManageScopes);
-
- // Click three times to select, unselect, then reselect (for coverage)
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SAVE_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.orgRoleScopes);
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
- });
-
- test('correct toggles are visible for global level role on manage scopes page', async function (assert) {
- await visit(urls.globalRole);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalManageScopes);
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).isVisible();
- });
-
- test('correct toggles are visible for org level role on manage scopes page', async function (assert) {
- await visit(urls.orgRole);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.orgManageScopes);
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS)).isVisible();
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_CHILDREN)).isVisible();
- assert.dom(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_DESCENDANTS)).doesNotExist();
- });
-
- test('user can toggle and cancel scope keywords to add on manage scopes page', async function (assert) {
- instances.orgRole.update({ grant_scope_ids: [] });
- await visit(urls.orgRole);
-
- await click(`[href="${urls.orgRoleScopes}"]`);
-
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.orgManageScopes);
-
- // Click three times to select, unselect, then reselect (for coverage)
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(CANCEL_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.orgRoleScopes);
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
- });
-
- test('shows error message on scope save on manage scopes page', async function (assert) {
- this.server.post('/roles/:idMethod', () => {
- return new Response(
- 400,
- {},
- {
- status: 400,
- code: 'invalid_argument',
- message: 'The request was invalid.',
- details: {},
- },
- );
- });
- instances.orgRole.update({ grant_scope_ids: [] });
- await visit(urls.orgRole);
-
- await click(MANAGE_SCOPES_SELECTOR);
- await click(SCOPE_TOGGLE_SELECTOR(GRANT_SCOPE_THIS));
- await click(SAVE_BTN_SELECTOR);
-
- assert.dom(TOAST_SELECTOR).isVisible();
- });
-
- test('user can select and save custom scopes to add on manage custom scopes page', async function (assert) {
- instances.globalRole.update({ grant_scope_ids: [] });
- await visit(urls.globalRole);
-
- await click(`[href="${urls.globalRoleScopes}"]`);
-
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalManageScopes);
-
- await click(`[href="${urls.manageCustomScopes}"]`);
-
- // Click three times to select, unselect, then reselect (for coverage)
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SAVE_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalManageScopes);
-
- await click(SAVE_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalRoleScopes);
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 1);
- });
-
- test('user can select and cancel custom scopes to add on manage custom scopes page', async function (assert) {
- instances.globalRole.update({ grant_scope_ids: [] });
- await visit(urls.globalRole);
-
- await click(`[href="${urls.globalRoleScopes}"]`);
-
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
-
- await click(MANAGE_SCOPES_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalManageScopes);
-
- await click(`[href="${urls.manageCustomScopes}"]`);
-
- // Click three times to select, unselect, then reselect (for coverage)
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(CANCEL_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalManageScopes);
-
- await click(CANCEL_BTN_SELECTOR);
-
- assert.strictEqual(currentURL(), urls.globalRoleScopes);
- assert.strictEqual(findAll(TABLE_ROW_SELECTOR).length, 0);
- });
-
- test('shows error message on custom scope save on manage custom scopes page', async function (assert) {
- this.server.post('/roles/:idMethod', () => {
- return new Response(
- 400,
- {},
- {
- status: 400,
- code: 'invalid_argument',
- message: 'The request was invalid.',
- details: {},
- },
- );
- });
- instances.globalRole.update({ grant_scope_ids: [] });
- await visit(urls.globalRole);
-
- await click(MANAGE_SCOPES_SELECTOR);
- await click(`[href="${urls.manageCustomScopes}"]`);
- await click(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id));
- await click(SAVE_BTN_SELECTOR);
-
- assert.dom(TOAST_SELECTOR).isVisible();
- });
-
- test('user can search for a specifc org scope by id on manage custom scopes page', async function (assert) {
- const anotherOrg = this.server.create('scope', {
- type: 'org',
- scope: { id: 'global', type: 'global' },
- });
- await visit(urls.globalManageScopes);
-
- await click(`[href="${urls.manageCustomScopes}"]`);
-
- assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
- assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists();
-
- await fillIn(SEARCH_INPUT_SELECTOR, instances.scopes.org.id);
- await waitUntil(
- () => findAll(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).length === 0,
- );
-
- assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
- assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).doesNotExist();
- });
-
- test('user can search for org scopes and get no results on manage custom scopes page', async function (assert) {
- const anotherOrg = this.server.create('scope', {
- type: 'org',
- scope: { id: 'global', type: 'global' },
- });
- await visit(urls.globalManageScopes);
-
- await click(`[href="${urls.manageCustomScopes}"]`);
-
- assert.dom(SCOPE_CHECKBOX_SELECTOR(instances.scopes.org.id)).exists();
- assert.dom(SCOPE_CHECKBOX_SELECTOR(anotherOrg.id)).exists();
-
- await fillIn(SEARCH_INPUT_SELECTOR, 'fake scope that does not exist');
- await waitUntil(() => findAll(NO_RESULTS_MSG_SELECTOR).length === 1);
-
- assert.dom(`[href="${urls.role1}"]`).doesNotExist();
- assert.dom(`[href="${urls.role2}"]`).doesNotExist();
- assert.dom(NO_RESULTS_MSG_SELECTOR).includesText('No results found');
- });
-});
diff --git a/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js
new file mode 100644
index 000000000..3390e78d7
--- /dev/null
+++ b/ui/admin/tests/unit/controllers/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+import { currentURL, visit, waitUntil } from '@ember/test-helpers';
+import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
+import { setupIndexedDb } from 'api/test-support/helpers/indexed-db';
+import { authenticateSession } from 'ember-simple-auth/test-support';
+
+module(
+ 'Unit | Controller | scopes/scope/roles/role/manage-scopes/manage-org-projects',
+ function (hooks) {
+ setupTest(hooks);
+ setupMirage(hooks);
+ setupIndexedDb(hooks);
+
+ let store;
+ let controller;
+
+ const instances = {
+ scopes: {
+ global: null,
+ org: null,
+ project: null,
+ },
+ role: null,
+ };
+
+ const urls = {
+ globalScope: null,
+ manageOrgProjects: null,
+ manageCustomScopes: null,
+ };
+
+ hooks.beforeEach(async function () {
+ authenticateSession({});
+ store = this.owner.lookup('service:store');
+ controller = this.owner.lookup(
+ 'controller:scopes/scope/roles/role/manage-scopes/manage-org-projects',
+ );
+
+ instances.scopes.global = this.server.create('scope', {
+ id: 'global',
+ type: 'global',
+ });
+ instances.scopes.org = this.server.create('scope', {
+ type: 'org',
+ scope: { id: 'global', type: 'global' },
+ });
+ instances.scopes.project = this.server.create('scope', {
+ type: 'project',
+ scope: { id: instances.scopes.org.id, type: 'org' },
+ });
+ instances.role = this.server.create('role', {
+ scope: instances.scopes.global,
+ grant_scope_ids: [],
+ });
+
+ urls.globalScope = `/scopes/global`;
+ urls.manageOrgProjects = `${urls.globalScope}/roles/${instances.role.id}/manage-scopes/${instances.scopes.org.id}`;
+ urls.manageCustomScopes = `${urls.globalScope}/roles/${instances.role.id}/manage-scopes/manage-custom-scopes`;
+ });
+
+ test('it exists', function (assert) {
+ assert.ok(controller);
+ });
+
+ test('handleSearchInput action sets expected values correctly', async function (assert) {
+ const searchValue = 'test';
+ controller.handleSearchInput({ target: { value: searchValue } });
+ await waitUntil(() => controller.search === searchValue);
+
+ assert.strictEqual(controller.page, 1);
+ assert.strictEqual(controller.search, searchValue);
+ });
+
+ test('setGrantScopes action adds scopes to specified model', async function (assert) {
+ await visit(urls.manageOrgProjects);
+ const role = await store.findRecord('role', instances.role.id);
+
+ assert.deepEqual(role.grant_scope_ids, []);
+
+ await controller.setGrantScopes(role, [instances.scopes.project.id]);
+
+ assert.deepEqual(role.grant_scope_ids, [instances.scopes.project.id]);
+
+ await waitUntil(() => currentURL() === urls.manageCustomScopes);
+
+ assert.strictEqual(currentURL(), urls.manageCustomScopes);
+ });
+
+ test('cancel action causes transition to expected route', async function (assert) {
+ await visit(urls.manageOrgProjects);
+ const role = await store.findRecord('role', instances.role.id);
+
+ await controller.cancel(role);
+ await waitUntil(() => currentURL() === urls.manageCustomScopes);
+
+ assert.strictEqual(currentURL(), urls.manageCustomScopes);
+ });
+ },
+);
diff --git a/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js
new file mode 100644
index 000000000..a0362528b
--- /dev/null
+++ b/ui/admin/tests/unit/routes/scopes/scope/roles/role/manage-scopes/manage-org-projects-test.js
@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) HashiCorp, Inc.
+ * SPDX-License-Identifier: BUSL-1.1
+ */
+
+import { module, test } from 'qunit';
+import { setupTest } from 'ember-qunit';
+
+module(
+ 'Unit | Route | scopes/scope/roles/role/manage-scopes/manage-org-projects',
+ function (hooks) {
+ setupTest(hooks);
+
+ test('it exists', function (assert) {
+ let route = this.owner.lookup(
+ 'route:scopes/scope/roles/role/manage-scopes/manage-org-projects',
+ );
+ assert.ok(route);
+ });
+ },
+);