From 7350c0e08c4c53464166c537534864a0629b1875 Mon Sep 17 00:00:00 2001 From: Randall Morey Date: Fri, 31 Mar 2023 17:38:17 -0700 Subject: [PATCH] Feature metadata (#1661) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 🎸 load edition from controller-metadata.json * refactor: 💡 rename edition and add licensed features support * chore: 🤖 support dev toggling of licensed features * chore: 🤖 rename ent edition * Update ui/admin/app/services/feature-edition.js * Update ui/admin/app/instance-initializers/feature-edition.js Co-authored-by: Zhihe Li * chore: 🤖 cleanup * chore: 🤖 feature edition disabled in tests / fix test cases * test: 💍 remove feature disable calls in tests * test: 💍 fix broken tests * fix: 🐛 fix config bug * fix: 🐛 fix broken test due to feature flags --------- Co-authored-by: Zhihe Li --- addons/api/mirage/config.js | 15 ++++++ addons/core/translations/actions/en-us.yaml | 1 + addons/core/translations/en-us.yaml | 5 -- .../instance-initializers/feature-edition.js | 32 +++++++++++- ui/admin/app/routes/application.js | 10 ++++ ui/admin/app/services/feature-edition.js | 26 +++++++--- ui/admin/app/templates/application.hbs | 18 ++++++- ui/admin/config/environment.js | 48 +++++++++-------- ui/admin/config/features.js | 27 +++++----- .../credential-library/create-test.js | 10 ++-- .../credential-library/read-test.js | 7 ++- .../credential-store/create-test.js | 4 ++ .../credentials/create-test.js | 10 ++-- .../credential-store/credentials/read-test.js | 9 ++-- .../acceptance/credential-store/read-test.js | 5 ++ ui/admin/tests/acceptance/develop-test.js | 2 + .../brokered-credential-sources-test.js | 5 +- .../tests/acceptance/targets/create-test.js | 51 +++++++++++++------ .../tests/acceptance/targets/read-test.js | 16 +++--- .../tests/acceptance/targets/update-test.js | 10 ++-- .../tests/acceptance/workers/create-test.js | 8 +-- .../tests/acceptance/workers/list-test.js | 8 ++- .../dual-filter/hcp/index-test.js | 2 - .../worker-diagram/dual-filter/index-test.js | 2 - .../components/worker-diagram/index-test.js | 19 +++---- .../unit/abilities/credential-library-test.js | 11 ++-- .../tests/unit/abilities/credential-test.js | 16 +++--- 27 files changed, 250 insertions(+), 127 deletions(-) diff --git a/addons/api/mirage/config.js b/addons/api/mirage/config.js index cbf0b574f..58e47471c 100644 --- a/addons/api/mirage/config.js +++ b/addons/api/mirage/config.js @@ -35,6 +35,21 @@ function routes() { // make this `http://localhost:8080`, for example, if your API is on a different server // this.urlPrefix = ''; + + this.get('/metadata.json', () => { + // this configuration is optional + const licensedFeatures = environmentConfig?.features?.licensedFeatures; + const edition = licensedFeatures ? 'enterprise' : 'oss'; + const features = licensedFeatures ? Object.keys(licensedFeatures) : []; + const metadata = { + license: { + edition, + features, + }, + }; + return metadata; + }); + // make this `/api`, for example, if your API is namespaced this.namespace = environmentConfig.api.namespace; // delay for each request, automatically set to 0 during testing diff --git a/addons/core/translations/actions/en-us.yaml b/addons/core/translations/actions/en-us.yaml index 36cb5dffe..eb8fe0e4c 100644 --- a/addons/core/translations/actions/en-us.yaml +++ b/addons/core/translations/actions/en-us.yaml @@ -47,3 +47,4 @@ remove-resources: Remove resources and save do-this-later: Do this later toggle-color-theme: Toggle Color Theme toggle-feature-edition: Toggle Feature Edition +toggle-licensed-features: Toggle Licensed Features diff --git a/addons/core/translations/en-us.yaml b/addons/core/translations/en-us.yaml index 277e0dd3b..a482f52fc 100644 --- a/addons/core/translations/en-us.yaml +++ b/addons/core/translations/en-us.yaml @@ -38,11 +38,6 @@ titles: create-resources: Create Resources choose-an-option: Choose an option feature-edition: 'Edition: {edition}' -editions: - oss: OSS - ent: Enterprise - enterprise: Enterprise - hcp: HCP descriptions: empty-set: There are no items to display yet. You may be able to add items or try back later. cluster-url-initialization: To get started, please enter your cluster URL diff --git a/ui/admin/app/instance-initializers/feature-edition.js b/ui/admin/app/instance-initializers/feature-edition.js index 9a356fb46..957f7ceea 100644 --- a/ui/admin/app/instance-initializers/feature-edition.js +++ b/ui/admin/app/instance-initializers/feature-edition.js @@ -3,9 +3,37 @@ * SPDX-License-Identifier: MPL-2.0 */ -export function initialize(owner) { +import fetch from 'fetch'; + +async function autoInitializeFeatureEdition(owner) { const service = owner.lookup('service:feature-edition'); - service.initialize(); + const url = '/metadata.json'; + let edition; + let features; + + // Attempt to load edition for the metadata JSON + try { + const response = await fetch(url); + const json = await response.json(); + edition = json?.license?.edition; + features = json?.license?.features; + } catch (e) { + // if the request fails, the service will initialize the default edition + // as specified in the config + } + + // Initialize the feature edition and its associated features + service.initialize(edition, features); +} + +export async function initialize(owner) { + const config = owner.resolveRegistration('config:environment'); + const isTesting = config.environment === 'test'; + // Do not auto-init in a test environment. + // Tests should explicitly enable editions and features as needed. + if (!isTesting) { + await autoInitializeFeatureEdition(owner); + } } export default { diff --git a/ui/admin/app/routes/application.js b/ui/admin/app/routes/application.js index b5ca65df4..cbdbc109e 100644 --- a/ui/admin/app/routes/application.js +++ b/ui/admin/app/routes/application.js @@ -20,6 +20,7 @@ export default class ApplicationRoute extends Route { @service confirm; @service router; @service intl; + @service features; @service featureEdition; // =attributes @@ -132,4 +133,13 @@ export default class ApplicationRoute extends Route { toggleEdition(edition) { this.featureEdition.setEdition(edition); } + + @action + toggleFeature(feature) { + if (this.features.isEnabled(feature)) { + this.features.disable(feature); + } else { + this.features.enable(feature); + } + } } diff --git a/ui/admin/app/services/feature-edition.js b/ui/admin/app/services/feature-edition.js index 840a8ea8f..294bcc680 100644 --- a/ui/admin/app/services/feature-edition.js +++ b/ui/admin/app/services/feature-edition.js @@ -8,8 +8,6 @@ import { inject as service } from '@ember/service'; import { tracked } from '@glimmer/tracking'; import config from 'admin/config/environment'; -const { selectedEdition, featureEditions } = config; - export default class FeatureEditionService extends Service { // =services @@ -17,24 +15,34 @@ export default class FeatureEditionService extends Service { // =attributes + licensedFeatures = Object.keys(config.features.licensedFeatures); + @tracked edition; // array of edition strings get editions() { - return Object.keys(featureEditions); + return Object.keys(config.features.featureEditions); } // =methods /** * Initializes the selected edition from the config. + * @param {string} edition + * @param {?string[]} enabledFeatures - list of extra features to enable */ - initialize() { - this.setEdition(selectedEdition); + initialize(edition, enabledFeatures) { + this.setEdition(edition || config.features.defaultEdition, enabledFeatures); } - setEdition(edition) { - const editionFlags = featureEditions[edition]; + /** + * Enables the specified edition. + * @param {string} edition + * @param {?string[]} enabledFeatures - list of extra features to enable + */ + setEdition(edition, enabledFeatures) { + this.edition = edition; + const editionFlags = config.features.featureEditions[edition]; Object.keys(editionFlags).forEach((flag) => { if (editionFlags[flag]) { this.features.enable(flag); @@ -42,6 +50,8 @@ export default class FeatureEditionService extends Service { this.features.disable(flag); } }); - this.edition = edition; + if (enabledFeatures) { + enabledFeatures.forEach((flag) => this.features.enable(flag)); + } } } diff --git a/ui/admin/app/templates/application.hbs b/ui/admin/app/templates/application.hbs index 4c4b34805..8bd7a7574 100644 --- a/ui/admin/app/templates/application.hbs +++ b/ui/admin/app/templates/application.hbs @@ -73,7 +73,7 @@ data-test-dev-edition-toggle @text={{t 'titles.feature-edition' - edition=(t (concat 'editions.' this.featureEdition.edition)) + edition=this.featureEdition.edition }} @icon='flight-icons/svg/layers-16' as |dropdown| @@ -89,7 +89,7 @@ {{#each this.featureEdition.editions as |edition|}} @@ -97,6 +97,20 @@ + + + + {{#each this.featureEdition.licensedFeatures as |feature|}} + + + + {{/each}} + + {{/if}} diff --git a/ui/admin/config/environment.js b/ui/admin/config/environment.js index 1638b2c99..577b7b6e3 100644 --- a/ui/admin/config/environment.js +++ b/ui/admin/config/environment.js @@ -5,12 +5,27 @@ 'use strict'; -const features = require('./features'); +const v8 = require('v8'); +const { + defaultEdition, + featureEditions, + licensedFeatures, +} = require('./features'); const APP_NAME = process.env.APP_NAME || 'Boundary'; const API_HOST = process.env.API_HOST || ''; +const clone = (obj) => v8.deserialize(v8.serialize(obj)); + module.exports = function (environment) { + // Start with a fresh copy of the features config every run, since it + // could be modified by an environment. + const features = { + defaultEdition, + featureEditions: clone(featureEditions), + licensedFeatures: clone(licensedFeatures), + }; + let ENV = { modulePrefix: 'admin', environment, @@ -98,8 +113,7 @@ module.exports = function (environment) { timeout: 4000, }, - selectedEdition: features.selectedEdition, - featureEditions: features.featureEditions, + features: features, featureFlags: {}, }; @@ -125,14 +139,16 @@ module.exports = function (environment) { }; // Default edition in development - ENV.selectedEdition = 'enterprise'; - features.enableFeaturesInAllEditions( - { - // Show edition toggle in UI in development - 'dev-edition-toggle': true, - }, - ENV - ); + ENV.features.defaultEdition = 'enterprise'; + // Enable development-only features + ENV.features.featureEditions.oss['dev-edition-toggle'] = true; + ENV.features.featureEditions.enterprise['dev-edition-toggle'] = true; + ENV.features.featureEditions.hcp['dev-edition-toggle'] = true; + // Enable licensed features by default in enterprise and hcp + Object.keys(ENV.features.licensedFeatures).forEach((feature) => { + ENV.features.featureEditions.enterprise[feature] = true; + ENV.features.featureEditions.hcp[feature] = true; + }); } if (environment === 'test') { @@ -150,16 +166,6 @@ module.exports = function (environment) { ENV.flashMessageDefaults.timeout = 0; ENV.enableConfirmService = false; - - /** - * Enable additional features for testing purposes. - * [TODO] Tests should be refactored such that each test case - * explicitly enables the features and/or edition that it needs. - * By default, tests should have no features enabled. This explicit - * approach ensures that test cases are easy to read and understand, - * since their feature requirements are embedded. - */ - ENV.selectedEdition = 'enterprise'; } if (environment === 'production') { diff --git a/ui/admin/config/features.js b/ui/admin/config/features.js index ed7f0b151..9a4382ec9 100644 --- a/ui/admin/config/features.js +++ b/ui/admin/config/features.js @@ -5,14 +5,22 @@ const EDITION = process.env.EDITION || 'oss'; // Default edition is OSS -const selectedEdition = EDITION; +const defaultEdition = EDITION; +const devFeatures = { + 'dev-edition-toggle': false, +}; +const licensedFeatures = { + 'multi-hop-sessions': false, + 'ssh-target': false, +}; // Base edition declares available features, disabled by default. const baseEdition = { + ...devFeatures, + ...licensedFeatures, byow: false, 'byow-pki-hcp-cluster-id': false, 'json-credentials': false, - 'ssh-target': false, 'static-credentials': false, 'target-worker-filters-v2': false, 'target-worker-filters-v2-ingress': false, @@ -32,7 +40,6 @@ featureEditions.oss = { }; featureEditions.enterprise = { ...featureEditions.oss, - 'ssh-target': true, 'target-worker-filters-v2-ingress': true, 'vault-worker-filter': true, }; @@ -42,18 +49,8 @@ featureEditions.hcp = { 'target-worker-filters-v2-hcp': true, }; -/** - * Takes a set of features and enables them in all editions. - * For use in development and testing only. - */ -const enableFeaturesInAllEditions = (features = {}, ENV) => { - Object.entries(featureEditions).forEach( - ([key, edition]) => (ENV.featureEditions[key] = { ...features, ...edition }) - ); -}; - module.exports = { featureEditions, - selectedEdition, - enableFeaturesInAllEditions, + defaultEdition, + licensedFeatures, }; diff --git a/ui/admin/tests/acceptance/credential-library/create-test.js b/ui/admin/tests/acceptance/credential-library/create-test.js index 7f0343574..dc9cfe2d1 100644 --- a/ui/admin/tests/acceptance/credential-library/create-test.js +++ b/ui/admin/tests/acceptance/credential-library/create-test.js @@ -23,6 +23,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; let getCredentialLibraryCount; const instances = { @@ -76,6 +77,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { getCredentialLibraryCount = () => this.server.schema.credentialLibraries.all().models.length; authenticateSession({}); + featuresService = this.owner.lookup('service:features'); }); test('visiting credential libraries', async function (assert) { assert.expect(2); @@ -98,6 +100,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { test('can create a new credential library of type vault ssh cert', async function (assert) { assert.expect(12); + featuresService.enable('ssh-target'); const count = getCredentialLibraryCount(); await visit(urls.newCredentialLibrary); await click('[value="vault-ssh-certificate"]'); @@ -156,6 +159,7 @@ module('Acceptance | credential-libraries | create', function (hooks) { test('ecdsa and rsa key types bring up a key bits field', async function (assert) { assert.expect(3); + featuresService.enable('ssh-target'); await visit(urls.newCredentialLibrary); await click('[value="vault-ssh-certificate"]'); await select('[name="key_type"]', 'ed25519'); @@ -224,11 +228,9 @@ module('Acceptance | credential-libraries | create', function (hooks) { }); test('cannot select vault ssh cert when feature is disabled', async function (assert) { - const featuresService = this.owner.lookup('service:features'); - featuresService.disable('ssh-target'); - assert.expect(1); + assert.expect(2); await visit(urls.newCredentialLibrary); - + assert.false(featuresService.isEnabled('ssh-target')); assert.dom('[value="vault-ssh-certificate"]').doesNotExist(); }); }); diff --git a/ui/admin/tests/acceptance/credential-library/read-test.js b/ui/admin/tests/acceptance/credential-library/read-test.js index 38cf3f406..3194db43a 100644 --- a/ui/admin/tests/acceptance/credential-library/read-test.js +++ b/ui/admin/tests/acceptance/credential-library/read-test.js @@ -85,7 +85,7 @@ module('Acceptance | credential-libraries | read', function (hooks) { }); test('cannot navigate to vault ssh cert form when feature is not enabled', async function (assert) { - assert.expect(1); + assert.expect(2); instances.credentialLibrary = this.server.create('credential-library', { scope: instances.scopes.project, credentialStore: instances.credentialStore, @@ -94,10 +94,9 @@ module('Acceptance | credential-libraries | read', function (hooks) { await visit( `${urls.credentialLibraries}/${instances.credentialLibrary.id}` ); - const featuresService = this.owner.lookup('service:features'); - featuresService.disable('ssh-target'); await visit(urls.credentialLibraries); - + const featuresService = this.owner.lookup('service:features'); + assert.false(featuresService.isEnabled('ssh-target')); assert.dom('.rose-table-body tr:nth-of-type(2) a').doesNotExist(); }); diff --git a/ui/admin/tests/acceptance/credential-store/create-test.js b/ui/admin/tests/acceptance/credential-store/create-test.js index 780023fdd..6fc90f3f9 100644 --- a/ui/admin/tests/acceptance/credential-store/create-test.js +++ b/ui/admin/tests/acceptance/credential-store/create-test.js @@ -14,6 +14,7 @@ module('Acceptance | credential-stores | create', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; let getCredentialStoresCount; let getStaticCredentialStoresCount; let getVaultCredentialStoresCount; @@ -62,10 +63,12 @@ module('Acceptance | credential-stores | create', function (hooks) { .length; }; authenticateSession({}); + featuresService = this.owner.lookup('service:features'); }); test('Users can create a new credential store of default type static', async function (assert) { assert.expect(1); + featuresService.enable('static-credentials'); const count = getStaticCredentialStoresCount(); await visit(urls.newCredentialStore); await fillIn('[name="name"]', 'random string'); @@ -75,6 +78,7 @@ module('Acceptance | credential-stores | create', function (hooks) { test('Users can create a new credential store of type vault', async function (assert) { assert.expect(1); + featuresService.enable('static-credentials'); const count = getVaultCredentialStoresCount(); await visit(urls.newCredentialStore); await fillIn('[name="name"]', 'random string'); diff --git a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js index 1276bf965..67133fb92 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/create-test.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/create-test.js @@ -117,6 +117,7 @@ module( test('users can create a new json credential', async function (assert) { assert.expect(2); + featuresService.enable('json-credentials'); const credentialsCount = getCredentialsCount(); const jsonCredentialCount = getJsonCredentialCount(); await visit(urls.credentials); @@ -159,6 +160,7 @@ module( test('users can cancel create new json credential', async function (assert) { assert.expect(2); + featuresService.enable('json-credentials'); const credentialsCount = getCredentialsCount(); await visit(urls.credentials); @@ -173,6 +175,7 @@ module( test('users can switch away from JSON type credentials and the json_object value will be cleared', async function (assert) { assert.expect(2); + featuresService.enable('json-credentials'); const editorSelector = '[data-test-code-editor-field-editor]'; const newSecret = '{"test": "value"}'; @@ -191,6 +194,7 @@ module( test('users cannot navigate to new credential route without proper authorization', async function (assert) { assert.expect(2); + featuresService.enable('static-credentials'); instances.staticCredentialStore.authorized_collection_actions.credentials = instances.staticCredentialStore.authorized_collection_actions.credentials.filter( (item) => { @@ -282,6 +286,7 @@ module( test('saving a new json credential with invalid fields displays error messages', async function (assert) { assert.expect(1); + featuresService.enable('json-credentials'); await visit(urls.credentials); this.server.post('/credentials', () => { return new Response( @@ -313,12 +318,11 @@ module( }); test('cannot navigate to json credential when feature is disabled', async function (assert) { - featuresService.disable('json-credentials'); - assert.expect(4); + assert.expect(5); await visit(urls.credentials); await click(`[href="${urls.newCredential}"]`); - + assert.false(featuresService.isEnabled('json-credentials')); assert.true( instances.staticCredentialStore.authorized_collection_actions.credentials.includes( 'create' diff --git a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js index 6739b54ea..09ccfb301 100644 --- a/ui/admin/tests/acceptance/credential-store/credentials/read-test.js +++ b/ui/admin/tests/acceptance/credential-store/credentials/read-test.js @@ -14,6 +14,8 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; + const instances = { scopes: { org: null, @@ -75,6 +77,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`; urls.unknownCredential = `${urls.credentials}/foo`; authenticateSession({}); + featuresService = this.owner.lookup('service:features'); }); test('visiting username & password credential', async function (assert) { @@ -101,6 +104,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { test('visiting JSON credential', async function (assert) { assert.expect(2); + featuresService.enable('json-credentials'); await visit(urls.staticCredentialStore); await click(`[href="${urls.credentials}"]`); await a11yAudit(); @@ -141,10 +145,9 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) { }); test('cannot navigate to a JSON credential form when feature not enabled', async function (assert) { - assert.expect(1); - const featuresService = this.owner.lookup('service:features'); - featuresService.disable('json-credentials'); + assert.expect(2); await visit(urls.credentials); + assert.false(featuresService.isEnabled('json-credentials')); assert.dom('.rose-table-body tr:nth-child(3) a').doesNotExist(); }); diff --git a/ui/admin/tests/acceptance/credential-store/read-test.js b/ui/admin/tests/acceptance/credential-store/read-test.js index b7661aef7..f8d7bc38f 100644 --- a/ui/admin/tests/acceptance/credential-store/read-test.js +++ b/ui/admin/tests/acceptance/credential-store/read-test.js @@ -14,6 +14,8 @@ module('Acceptance | credential-stores | read', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; + const instances = { scopes: { global: null, @@ -63,10 +65,12 @@ module('Acceptance | credential-stores | read', function (hooks) { urls.unknownCredentialStore = `${urls.credentialStores}/foo`; authenticateSession({}); + featuresService = this.owner.lookup('service:features'); }); test('visiting static credential store', async function (assert) { assert.expect(2); + featuresService.enable('static-credentials'); await visit(urls.credentialStores); await a11yAudit(); assert.strictEqual(currentURL(), urls.credentialStores); @@ -104,6 +108,7 @@ module('Acceptance | credential-stores | read', function (hooks) { test('cannot navigate to a vault credential store form without proper authorization', async function (assert) { assert.expect(2); + featuresService.enable('static-credentials'); await visit(urls.projectScope); instances.vaultCredentialStore.authorized_actions = instances.vaultCredentialStore.authorized_actions.filter( diff --git a/ui/admin/tests/acceptance/develop-test.js b/ui/admin/tests/acceptance/develop-test.js index 3148ff3d8..f70fc1992 100644 --- a/ui/admin/tests/acceptance/develop-test.js +++ b/ui/admin/tests/acceptance/develop-test.js @@ -20,12 +20,14 @@ module('Acceptance | develop', function (hooks) { }); test('edition toggle is hidden when `dev-edition-toggle` is false', async function (assert) { + assert.expect(2); await visit('/'); assert.false(featuresService.isEnabled('dev-edition-toggle')); assert.dom('data-dev-edition-toggle').doesNotExist(); }); test('edition toggle is visible when `dev-edition-toggle` is true', async function (assert) { + assert.expect(2); assert.false(featuresService.isEnabled('dev-edition-toggle')); featuresService.enable('dev-edition-toggle'); await visit('/'); diff --git a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js index 5f556fd6f..eb6e47021 100644 --- a/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js +++ b/ui/admin/tests/acceptance/targets/brokered-credential-sources-test.js @@ -148,14 +148,13 @@ module('Acceptance | targets | brokered credential sources', function (hooks) { }); test('cannot navigate to a json type credential when feature is disabled', async function (assert) { - featuresService.disable('json-credentials'); - assert.expect(2); + assert.expect(3); const jsonCredential = instances.credentials[2]; instances.target.update({ brokeredCredentialSourceIds: [...randomlySelectedCredentials], }); await visit(urls.brokeredCredentialSources); - + assert.false(featuresService.isEnabled('json-credentials')); assert .dom('.rose-table-row:nth-child(3)') .includesText(jsonCredential.name); diff --git a/ui/admin/tests/acceptance/targets/create-test.js b/ui/admin/tests/acceptance/targets/create-test.js index d5ff06eab..86f3ced0d 100644 --- a/ui/admin/tests/acceptance/targets/create-test.js +++ b/ui/admin/tests/acceptance/targets/create-test.js @@ -85,13 +85,17 @@ module('Acceptance | targets | create', function (hooks) { test('defaults to type `ssh` when no query param provided', async function (assert) { assert.expect(1); + featuresService.enable('ssh-target'); await visit(urls.newTarget); assert.strictEqual(find('[name="type"]:checked').value, TYPE_TARGET_SSH); }); test('can create type `ssh` when `target-worker-filters-v2` is disabled', async function (assert) { - featuresService.disable('target-worker-filters-v2'); - assert.expect(4); + assert.expect(5); + featuresService.enable('ssh-target'); + + assert.false(featuresService.isEnabled('target-worker-filters-v2')); + const targetCount = getTargetCount(); const sshTargetCount = getSSHTargetCount(); await visit(urls.targets); @@ -116,8 +120,10 @@ module('Acceptance | targets | create', function (hooks) { }); test('can create type `ssh` when `target-worker-filters-v2` is enabled', async function (assert) { - featuresService.enable('target-worker-filters-v2'); assert.expect(4); + featuresService.enable('ssh-target'); + featuresService.enable('target-worker-filters-v2'); + const targetCount = getTargetCount(); const sshTargetCount = getSSHTargetCount(); await visit(urls.targets); @@ -146,8 +152,11 @@ module('Acceptance | targets | create', function (hooks) { }); test('can create type `tcp` when `target-worker-filters-v2` is disabled', async function (assert) { - featuresService.disable('target-worker-filters-v2'); - assert.expect(4); + assert.expect(5); + featuresService.enable('ssh-target'); + + assert.false(featuresService.isEnabled('target-worker-filters-v2')); + const targetCount = getTargetCount(); const tcpTargetCount = getTCPTargetCount(); await visit(urls.targets); @@ -172,8 +181,10 @@ module('Acceptance | targets | create', function (hooks) { }); test('can create type `tcp` when `target-worker-filters-v2` is enabled', async function (assert) { - featuresService.enable('target-worker-filters-v2'); assert.expect(4); + featuresService.enable('ssh-target'); + featuresService.enable('target-worker-filters-v2'); + const targetCount = getTargetCount(); const tcpTargetCount = getTCPTargetCount(); await visit(urls.targets); @@ -203,9 +214,11 @@ module('Acceptance | targets | create', function (hooks) { }); test('can create type `tcp` when `target-worker-filters-v2` and `target-worker-filters-v2-ingress` is enabled', async function (assert) { + assert.expect(4); + featuresService.enable('ssh-target'); featuresService.enable('target-worker-filters-v2'); featuresService.enable('target-worker-filters-v2-ingress'); - assert.expect(4); + const targetCount = getTargetCount(); const tcpTargetCount = getTCPTargetCount(); await visit(urls.targets); @@ -239,15 +252,16 @@ module('Acceptance | targets | create', function (hooks) { test('default port is not marked required for SSH targets', async function (assert) { assert.expect(1); + featuresService.enable('ssh-target'); await visit(urls.newTarget); assert.dom('[data-test-default-port-label]').includesText('Optional'); }); test('default port is marked required for TCP targets', async function (assert) { assert.expect(1); + featuresService.enable('ssh-target'); await visit(urls.newTarget); await click('[value="tcp"]'); - assert.dom('[data-test-default-port-label]').includesText('Required'); }); @@ -284,12 +298,11 @@ module('Acceptance | targets | create', function (hooks) { }); test('cannot navigate to new SSH targets route when ssh feature is disabled', async function (assert) { - assert.expect(3); - featuresService.disable('ssh-target'); + assert.expect(4); await visit(urls.targets); await click(`[href="${urls.newTarget}"]`); - + assert.false(featuresService.isEnabled('ssh-target')); assert.true( instances.scopes.project.authorized_collection_actions.targets.includes( 'create' @@ -301,6 +314,9 @@ module('Acceptance | targets | create', function (hooks) { test('can cancel create new TCP target', async function (assert) { assert.expect(3); + featuresService.enable('target-worker-filters-v2'); + featuresService.enable('target-worker-filters-v2-ingress'); + const targetCount = getTargetCount(); const tcpTargetCount = getTCPTargetCount(); await visit(urls.targets); @@ -321,6 +337,10 @@ module('Acceptance | targets | create', function (hooks) { test('can cancel create new SSH target', async function (assert) { assert.expect(3); + featuresService.enable('ssh-target'); + featuresService.enable('target-worker-filters-v2'); + featuresService.enable('target-worker-filters-v2-ingress'); + const targetCount = getTargetCount(); const sshTargetCount = getSSHTargetCount(); await visit(urls.targets); @@ -398,10 +418,13 @@ module('Acceptance | targets | create', function (hooks) { test('can save address', async function (assert) { assert.expect(2); + featuresService.enable('target-network-address'); + const targetCount = getTargetCount(); await visit(urls.targets); await click(`[href="${urls.newTarget}"]`); + await fillIn('[name="name"]', 'random string'); await fillIn('[name="address"]', '0.0.0.0'); await click('[type="submit"]'); @@ -414,12 +437,10 @@ module('Acceptance | targets | create', function (hooks) { }); test('address field does not exist when target network address feature is disabled', async function (assert) { - assert.expect(1); - featuresService.disable('target-network-address'); + assert.expect(2); await visit(urls.targets); - await click(`[href="${urls.newTarget}"]`); - + assert.false(featuresService.isEnabled('target-network-address')); assert.dom('[name="address"]').doesNotExist(); }); }); diff --git a/ui/admin/tests/acceptance/targets/read-test.js b/ui/admin/tests/acceptance/targets/read-test.js index e6205e150..10d792b4b 100644 --- a/ui/admin/tests/acceptance/targets/read-test.js +++ b/ui/admin/tests/acceptance/targets/read-test.js @@ -74,6 +74,8 @@ module('Acceptance | targets | read', function (hooks) { test('visiting ssh target', async function (assert) { assert.expect(2); + featuresService.enable('ssh-target'); + await visit(urls.targets); await a11yAudit(); assert.strictEqual(currentURL(), urls.targets); @@ -132,23 +134,22 @@ module('Acceptance | targets | read', function (hooks) { }); test('visiting an ssh target does not show the worker_filter deprecation message when "target-worker-filters-v2" is disabled', async function (assert) { - featuresService.disable('target-worker-filters-v2'); - assert.expect(1); + assert.expect(2); await visit(urls.sshTarget); - + assert.false(featuresService.isEnabled('target-worker-filters-v2')); assert.dom('.hds-alert').doesNotExist(); }); test('visiting a tcp target does not show the worker_filter deprecation message when "target-worker-filters-v2" is disabled', async function (assert) { - featuresService.disable('target-worker-filters-v2'); - assert.expect(1); + assert.expect(2); await visit(urls.tcpTarget); - + assert.false(featuresService.isEnabled('target-worker-filters-v2')); assert.dom('.hds-alert').doesNotExist(); }); - test('cannot navigate to a ssh target form without proper authorization', async function (assert) { + test('cannot navigate to an ssh target form without proper authorization', async function (assert) { assert.expect(2); + featuresService.enable('ssh-target'); await visit(urls.projectScope); instances.sshTarget.authorized_actions = instances.sshTarget.authorized_actions.filter((item) => item !== 'read'); @@ -161,6 +162,7 @@ module('Acceptance | targets | read', function (hooks) { test('cannot navigate to a tcp target form without proper authorization', async function (assert) { assert.expect(2); + featuresService.enable('ssh-target'); await visit(urls.projectScope); instances.tcpTarget.authorized_actions = instances.tcpTarget.authorized_actions.filter((item) => item !== 'read'); diff --git a/ui/admin/tests/acceptance/targets/update-test.js b/ui/admin/tests/acceptance/targets/update-test.js index ac7455b66..02f0b597e 100644 --- a/ui/admin/tests/acceptance/targets/update-test.js +++ b/ui/admin/tests/acceptance/targets/update-test.js @@ -39,7 +39,6 @@ module('Acceptance | targets | update', function (hooks) { hooks.beforeEach(function () { featuresService = this.owner.lookup('service:features'); - featuresService.disable('target-worker-filters-v2'); // Generate resources instances.scopes.global = this.server.create('scope', { id: 'global' }); instances.scopes.org = this.server.create('scope', { @@ -96,10 +95,9 @@ module('Acceptance | targets | update', function (hooks) { }); test('updating a target does not show the worker_filter deprecation message when "target-worker-filters-v2" is disabled', async function (assert) { - featuresService.disable('target-worker-filters-v2'); - assert.expect(1); + assert.expect(2); await visit(urls.target); - + assert.false(featuresService.isEnabled('target-worker-filters-v2')); assert.dom('.hds-alert').doesNotExist(); }); @@ -366,6 +364,8 @@ module('Acceptance | targets | update', function (hooks) { test('saving address with existing host sources brings up confirmation modal and removes host sources', async function (assert) { assert.expect(4); + featuresService.enable('ssh-target'); + featuresService.enable('target-network-address'); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; this.server.createList( @@ -410,6 +410,8 @@ module('Acceptance | targets | update', function (hooks) { test('saving address with existing host sources brings up confirmation modal and can cancel', async function (assert) { assert.expect(4); + featuresService.enable('ssh-target'); + featuresService.enable('target-network-address'); const confirmService = this.owner.lookup('service:confirm'); confirmService.enabled = true; this.server.createList( diff --git a/ui/admin/tests/acceptance/workers/create-test.js b/ui/admin/tests/acceptance/workers/create-test.js index 7d98fe115..fefe6584c 100644 --- a/ui/admin/tests/acceptance/workers/create-test.js +++ b/ui/admin/tests/acceptance/workers/create-test.js @@ -54,21 +54,21 @@ module('Acceptance | workers | create', function (hooks) { }); test('initial upstreams input field is visible for `oss` binary', async function (assert) { - assert.expect(2); + assert.expect(3); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('byow-pki-hcp-cluster-id'); await visit(newWorkerURL); const labels = findAll('label.rose-form-label'); + assert.false(featuresService.isEnabled('byow-pki-hcp-cluster-id')); assert.dom(labels[0]).doesNotIncludeText('Boundary Cluster ID'); assert.dom(labels[2]).hasText('Initial Upstreams'); }); test('download and install step shows correct oss instructions', async function (assert) { - assert.expect(2); + assert.expect(3); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('byow-pki-hcp-cluster-id'); await visit(newWorkerURL); const createSection = findAll('.worker-create-section'); + assert.false(featuresService.isEnabled('byow-pki-hcp-cluster-id')); assert.dom(createSection[1]).includesText('curl -fsSL'); assert.dom(createSection[1]).doesNotIncludeText('wget -q'); }); diff --git a/ui/admin/tests/acceptance/workers/list-test.js b/ui/admin/tests/acceptance/workers/list-test.js index 404741a53..2899b01d3 100644 --- a/ui/admin/tests/acceptance/workers/list-test.js +++ b/ui/admin/tests/acceptance/workers/list-test.js @@ -18,6 +18,8 @@ module('Acceptance | workers | list', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); + let featuresService; + const instances = { scopes: { global: null, @@ -31,14 +33,15 @@ module('Acceptance | workers | list', function (hooks) { hooks.beforeEach(function () { instances.scopes.global = this.server.create('scope', { id: 'global' }); - urls.globalScope = `/scopes/global/scopes`; urls.workers = `/scopes/global/workers`; authenticateSession({}); + featuresService = this.owner.lookup('service:features'); }); test('Users can navigate to workers with proper authorization', async function (assert) { assert.expect(2); + featuresService.enable('byow'); await visit(urls.globalScope); assert.ok( instances.scopes.global.authorized_collection_actions.workers.includes( @@ -50,6 +53,7 @@ module('Acceptance | workers | list', function (hooks) { test('Users cannot navigate to workers without list or create permissions', async function (assert) { assert.expect(1); + featuresService.enable('byow'); instances.scopes.global.authorized_collection_actions.workers = []; await visit(urls.globalScope); @@ -58,6 +62,7 @@ module('Acceptance | workers | list', function (hooks) { test('Users can navigate to workers with only create permission', async function (assert) { assert.expect(1); + featuresService.enable('byow'); instances.scopes.global.authorized_collection_actions.workers = [ 'create:worker-led', ]; @@ -68,6 +73,7 @@ module('Acceptance | workers | list', function (hooks) { test('Users can navigate to workers with only list permission', async function (assert) { assert.expect(1); + featuresService.enable('byow'); instances.scopes.global.authorized_collection_actions.workers = ['list']; await visit(urls.globalScope); diff --git a/ui/admin/tests/integration/components/worker-diagram/dual-filter/hcp/index-test.js b/ui/admin/tests/integration/components/worker-diagram/dual-filter/hcp/index-test.js index fa56d0aa1..3b7b20625 100644 --- a/ui/admin/tests/integration/components/worker-diagram/dual-filter/hcp/index-test.js +++ b/ui/admin/tests/integration/components/worker-diagram/dual-filter/hcp/index-test.js @@ -16,13 +16,11 @@ module( setupIntl(hooks); let featuresService; - const targetWorkerFilterIngress = 'target-worker-filters-v2-ingress'; const targetWorkerFilterHCP = 'target-worker-filters-v2-hcp'; hooks.beforeEach(function () { featuresService = this.owner.lookup('service:features'); featuresService.enable(targetWorkerFilterHCP); - featuresService.disable(targetWorkerFilterIngress); }); test('it renders the correct diagram when egress and ingress filter is false', async function (assert) { diff --git a/ui/admin/tests/integration/components/worker-diagram/dual-filter/index-test.js b/ui/admin/tests/integration/components/worker-diagram/dual-filter/index-test.js index 0312201ee..fa620972f 100644 --- a/ui/admin/tests/integration/components/worker-diagram/dual-filter/index-test.js +++ b/ui/admin/tests/integration/components/worker-diagram/dual-filter/index-test.js @@ -17,12 +17,10 @@ module( let featuresService; const targetWorkerFilterIngress = 'target-worker-filters-v2-ingress'; - const targetWorkerFilterHCP = 'target-worker-filters-v2-hcp'; hooks.beforeEach(function () { featuresService = this.owner.lookup('service:features'); featuresService.enable(targetWorkerFilterIngress); - featuresService.disable(targetWorkerFilterHCP); }); test('it renders the correct diagram when egress and ingress filter is false', async function (assert) { diff --git a/ui/admin/tests/integration/components/worker-diagram/index-test.js b/ui/admin/tests/integration/components/worker-diagram/index-test.js index 058c699ec..3310a90a8 100644 --- a/ui/admin/tests/integration/components/worker-diagram/index-test.js +++ b/ui/admin/tests/integration/components/worker-diagram/index-test.js @@ -19,31 +19,32 @@ module('Integration | Component | worker-diagram/index', function (hooks) { test('it renders a single filter diagram', async function (assert) { featuresService = this.owner.lookup('service:features'); - featuresService.disable(targetWorkerFilterIngress); - featuresService.disable(targetWorkerFilterHCP); - assert.expect(1); + assert.expect(3); await render(hbs``); - + assert.false(featuresService.isEnabled(targetWorkerFilterIngress)); + assert.false(featuresService.isEnabled(targetWorkerFilterHCP)); assert.dom('[data-test-single-filter-egress-off]').isVisible(); }); test('it renders a dual filter diagram when `target-worker-filters-v2-ingress` is enabled', async function (assert) { featuresService = this.owner.lookup('service:features'); featuresService.enable(targetWorkerFilterIngress); - featuresService.disable(targetWorkerFilterHCP); - assert.expect(1); + assert.expect(3); + assert.true(featuresService.isEnabled(targetWorkerFilterIngress)); + assert.false(featuresService.isEnabled(targetWorkerFilterHCP)); await render(hbs``); assert.dom('[data-test-dual-filter-egress-off-ingress-off]').isVisible(); }); - test('it renders a HCP dual filter diagram when `target-worker-filters-v2-hcp` is enabled', async function (assert) { + test('it renders an HCP dual filter diagram when `target-worker-filters-v2-hcp` is enabled', async function (assert) { featuresService = this.owner.lookup('service:features'); featuresService.enable(targetWorkerFilterIngress); featuresService.enable(targetWorkerFilterHCP); - assert.expect(1); + assert.expect(3); await render(hbs``); - + assert.true(featuresService.isEnabled(targetWorkerFilterIngress)); + assert.true(featuresService.isEnabled(targetWorkerFilterHCP)); assert .dom('[data-test-dual-filter-hcp-egress-off-ingress-off]') .isVisible(); diff --git a/ui/admin/tests/unit/abilities/credential-library-test.js b/ui/admin/tests/unit/abilities/credential-library-test.js index b1252507f..0765bb72f 100644 --- a/ui/admin/tests/unit/abilities/credential-library-test.js +++ b/ui/admin/tests/unit/abilities/credential-library-test.js @@ -17,11 +17,11 @@ module('Unit | Abilities | credential-library', function (hooks) { hooks.beforeEach(function () { features = this.owner.lookup('service:features'); - features.enable('ssh-target'); }); test('can read credential library type when authorized and feature is enabled', function (assert) { assert.expect(2); + features.enable('ssh-target'); const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const credentialLibrary = store.createRecord('credential-library', { @@ -35,6 +35,7 @@ module('Unit | Abilities | credential-library', function (hooks) { test('cannot read credential library type when unauthorized and feature is enabled', function (assert) { assert.expect(2); + features.enable('ssh-target'); const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const credentialLibrary = store.createRecord('credential-library', { @@ -47,30 +48,30 @@ module('Unit | Abilities | credential-library', function (hooks) { }); test('cannot read credential library type when unauthorized and feature is disabled', function (assert) { - assert.expect(2); + assert.expect(3); const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('ssh-target'); const credentialLibrary = store.createRecord('credential-library', { authorized_actions: [], type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, }); + assert.false(featuresService.isEnabled('ssh-target')); assert.false(canService.can('read credential-library', credentialLibrary)); credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC; assert.false(canService.can('read credential-library', credentialLibrary)); }); test('can read vault-generic but not vault-ssh-certificate when authorized and feature is disabled', function (assert) { - assert.expect(2); + assert.expect(3); const canService = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const featuresService = this.owner.lookup('service:features'); - featuresService.disable('ssh-target'); const credentialLibrary = store.createRecord('credential-library', { authorized_actions: ['read'], type: TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE, }); + assert.false(featuresService.isEnabled('ssh-target')); assert.false(canService.can('read credential-library', credentialLibrary)); credentialLibrary.type = TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC; assert.true(canService.can('read credential-library', credentialLibrary)); diff --git a/ui/admin/tests/unit/abilities/credential-test.js b/ui/admin/tests/unit/abilities/credential-test.js index 8b19f5196..b60d95a7c 100644 --- a/ui/admin/tests/unit/abilities/credential-test.js +++ b/ui/admin/tests/unit/abilities/credential-test.js @@ -9,16 +9,16 @@ import { setupTest } from 'ember-qunit'; module('Unit | Abilities | credential', function (hooks) { setupTest(hooks); - let features; + let featuresService; hooks.beforeEach(function () { // Enable json-credentials feature by default - features = this.owner.lookup('service:features'); - features.enable('json-credentials'); + featuresService = this.owner.lookup('service:features'); }); test('can read credentials, including JSON credentials, when authorized and json-credentials feature is enabled', function (assert) { assert.expect(3); + featuresService.enable('json-credentials'); const service = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const credential = store.createRecord('credential', { @@ -34,6 +34,7 @@ module('Unit | Abilities | credential', function (hooks) { test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is enabled', function (assert) { assert.expect(3); + featuresService.enable('json-credentials'); const service = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const credential = store.createRecord('credential', { @@ -48,15 +49,14 @@ module('Unit | Abilities | credential', function (hooks) { }); test('cannot read credentials, including JSON credentials, when unauthorized and json-credentials feature is disabled', function (assert) { - assert.expect(3); + assert.expect(4); const service = this.owner.lookup('service:can'); - const featuresService = this.owner.lookup('service:features'); - featuresService.disable('json-credentials'); const store = this.owner.lookup('service:store'); const credential = store.createRecord('credential', { authorized_actions: [], type: 'json', }); + assert.false(featuresService.isEnabled('json-credentials')); assert.false(service.can('read credential', credential)); credential.type = 'username_password'; assert.false(service.can('read credential', credential)); @@ -65,14 +65,14 @@ module('Unit | Abilities | credential', function (hooks) { }); test('can read credentials, excepting JSON credentials, when authorized and json-credentials feature is disabled', function (assert) { - assert.expect(3); - features.disable('json-credentials'); + assert.expect(4); const service = this.owner.lookup('service:can'); const store = this.owner.lookup('service:store'); const credential = store.createRecord('credential', { authorized_actions: ['read'], type: 'json', }); + assert.false(featuresService.isEnabled('json-credentials')); assert.false(service.can('read credential', credential)); credential.type = 'username_password'; assert.true(service.can('read credential', credential));