mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-02 03:27:54 +00:00
UI: handle reduced disclosure endpoints (#24262)
* Create app-footer component with tests * glimmerize vault route + controller * Add dev mode badge to new footer * Fix version on dashboard * update app-footer tests * update version title component * Handle case for chroot namespace fail on health check * cleanup * fix ent tests * add missing headers * extra version fetch on login success, clear version on logout and seal * Add coverage for clearing version on seal * rename isOSS to isCommunity * remove is-version helper * test version in footer on unseal flow * fix enterprise test * VAULT-21399 test coverage * VAULT-21400 test coverage
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { assign } from '@ember/polyfills';
|
||||
import { hash, resolve } from 'rsvp';
|
||||
import { assert } from '@ember/debug';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
@@ -22,6 +21,7 @@ const ENDPOINTS = [
|
||||
'init',
|
||||
'capabilities-self',
|
||||
'license',
|
||||
'internal/ui/version',
|
||||
];
|
||||
|
||||
const REPLICATION_ENDPOINTS = {
|
||||
@@ -55,12 +55,12 @@ export default ApplicationAdapter.extend({
|
||||
id,
|
||||
name: snapshot.attr('name'),
|
||||
};
|
||||
ret = assign(ret, health);
|
||||
ret = Object.assign(ret, health);
|
||||
if (sealStatus instanceof AdapterError === false) {
|
||||
ret = assign(ret, { nodes: [sealStatus] });
|
||||
ret = Object.assign(ret, { nodes: [sealStatus] });
|
||||
}
|
||||
if (replicationStatus && replicationStatus instanceof AdapterError === false) {
|
||||
ret = assign(ret, replicationStatus.data);
|
||||
ret = Object.assign(ret, replicationStatus.data);
|
||||
}
|
||||
return resolve(ret);
|
||||
});
|
||||
@@ -94,6 +94,10 @@ export default ApplicationAdapter.extend({
|
||||
});
|
||||
},
|
||||
|
||||
fetchVersion() {
|
||||
return this.ajax(`${this.urlFor('internal/ui/version')}`, 'GET').catch(() => ({}));
|
||||
},
|
||||
|
||||
sealStatus() {
|
||||
return this.ajax(this.urlFor('seal-status'), 'GET', { unauthenticated: true });
|
||||
},
|
||||
|
||||
31
ui/app/components/app-footer.hbs
Normal file
31
ui/app/components/app-footer.hbs
Normal file
@@ -0,0 +1,31 @@
|
||||
{{!
|
||||
Copyright (c) HashiCorp, Inc.
|
||||
SPDX-License-Identifier: BUSL-1.1
|
||||
~}}
|
||||
|
||||
<Hds::AppFooter as |AF|>
|
||||
<AF.ExtraBefore>
|
||||
{{#if this.isDevelopment}}
|
||||
<div class="env-banner">
|
||||
<div class="level-item notification">
|
||||
<Icon @name="git-branch" /><Icon @name="pencil-tool" />
|
||||
Local development
|
||||
<Icon @name="pencil-tool" /><Icon @name="git-branch" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</AF.ExtraBefore>
|
||||
<AF.Link @href={{changelog-url-for this.version.version}} data-test-footer-version>
|
||||
Vault
|
||||
{{this.version.version}}
|
||||
</AF.Link>
|
||||
{{#if this.version.isCommunity}}
|
||||
<AF.Link @href="https://hashicorp.com/products/vault/trial?source=vaultui" data-test-footer-upgrade-link>
|
||||
Upgrade to Vault Enterprise
|
||||
</AF.Link>
|
||||
{{/if}}
|
||||
<AF.Link @href={{doc-link "/vault"}} data-test-footer-documentation-link>
|
||||
Documentation
|
||||
</AF.Link>
|
||||
<AF.LegalLinks />
|
||||
</Hds::AppFooter>
|
||||
16
ui/app/components/app-footer.js
Normal file
16
ui/app/components/app-footer.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { service } from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import ENV from 'vault/config/environment';
|
||||
|
||||
export default class AppFooterComponent extends Component {
|
||||
@service version;
|
||||
|
||||
get isDevelopment() {
|
||||
return ENV.environment === 'development';
|
||||
}
|
||||
}
|
||||
@@ -12,17 +12,10 @@ import { inject as service } from '@ember/service';
|
||||
*
|
||||
* @example
|
||||
* ```js
|
||||
* <Dashboard::VaultVersionTitle />
|
||||
* <Dashboard::VaultVersionTitle @version={{this.versionSvc}} />
|
||||
* ```
|
||||
*/
|
||||
|
||||
export default class DashboardVaultVersionTitle extends Component {
|
||||
@service version;
|
||||
@service namespace;
|
||||
|
||||
get versionHeader() {
|
||||
return this.version.isEnterprise
|
||||
? `Vault v${this.version.version.slice(0, this.version.version.indexOf('+'))}`
|
||||
: `Vault v${this.version.version}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
d="M69.7218638,30.2482468 L63.2587814,8.45301543 L58,8.45301543 L65.9885305,34.6072931 L73.4551971,34.6072931 L81.4437276,8.45301543 L76.1849462,8.45301543 L69.7218638,30.2482468 Z M97.6329749,22.0014025 C97.6329749,17.2103787 95.8265233,15.0897616 89.6845878,15.0897616 C87.5168459,15.0897616 84.8272401,15.4431978 82.9806452,15.9929874 L83.5827957,19.6451613 C85.3089606,19.2917251 87.2358423,19.056101 89.0021505,19.056101 C92.1333333,19.056101 92.7354839,19.802244 92.7354839,21.9228612 L92.7354839,23.9256662 L88.0387097,23.9256662 C84.0645161,23.9256662 82.3383513,25.4179523 82.3383513,29.3057504 C82.3383513,32.6044881 83.8637993,35 87.4365591,35 C89.4035842,35 91.4910394,34.4502104 93.2573477,33.3113604 L93.618638,34.6072931 L97.6329749,34.6072931 L97.6329749,22.0014025 Z M92.7354839,30.2089762 C91.8121864,30.7194951 90.4874552,31.1907433 89.0422939,31.1907433 C87.5168459,31.1907433 87.0752688,30.601683 87.0752688,29.2664797 C87.0752688,27.8134642 87.5168459,27.3814867 89.1225806,27.3814867 L92.7354839,27.3814867 L92.7354839,30.2089762 Z M102.421505,15.4824684 L102.421505,29.345021 C102.421505,32.7615708 103.585663,35 106.837276,35 C109.125448,35 112.216487,34.1753156 114.665233,32.997195 L115.146953,34.6072931 L118.880287,34.6072931 L118.880287,15.4824684 L113.982796,15.4824684 L113.982796,28.7559607 C112.216487,29.6591865 110.088889,30.3660589 108.884588,30.3660589 C107.760573,30.3660589 107.318996,29.85554 107.318996,28.8345021 L107.318996,15.4824684 L102.421505,15.4824684 Z M129.168459,34.6072931 L129.168459,7 L124.270968,7.66760168 L124.270968,34.6072931 L129.168459,34.6072931 Z M144.394265,30.601683 C143.551254,30.8373072 142.6681,30.9943899 141.94552,30.9943899 C140.660932,30.9943899 140.179211,30.3267882 140.179211,29.3057504 L140.179211,19.2917251 L144.875986,19.2917251 L145.197133,15.4824684 L140.179211,15.4824684 L140.179211,10.0631136 L135.28172,10.7307153 L135.28172,15.4824684 L132.351254,15.4824684 L132.351254,19.2917251 L135.28172,19.2917251 L135.28172,29.9340813 C135.28172,33.3506311 137.088172,35 140.660932,35 C141.905376,35 143.912545,34.6858345 144.956272,34.2538569 L144.394265,30.601683 Z"
|
||||
></path>
|
||||
|
||||
{{#if (is-version "Enterprise")}}
|
||||
{{#if this.version.isEnterprise}}
|
||||
<g id="vault-logo-edition-enterprise" transform="translate(65.000000, 40.000000)" fill-rule="nonzero">
|
||||
<polygon
|
||||
points="0.435816733 0.579322709 4.39438247 0.579322709 4.39438247 1.35454183 1.30175299 1.35454183 1.30175299 3.46577689 4.17171315 3.46577689 4.17171315 4.24099602 1.30175299 4.24099602 1.30175299 6.52541833 4.40262948 6.52541833 4.40262948 7.30063745 0.435816733 7.30063745"
|
||||
|
||||
@@ -3,20 +3,15 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { inject as service } from '@ember/service';
|
||||
import Controller from '@ember/controller';
|
||||
import config from '../config/environment';
|
||||
|
||||
export default Controller.extend({
|
||||
queryParams: [
|
||||
export default class VaultController extends Controller {
|
||||
queryParams = [
|
||||
{
|
||||
wrappedToken: 'wrapped_token',
|
||||
redirectTo: 'redirect_to',
|
||||
},
|
||||
],
|
||||
wrappedToken: '',
|
||||
redirectTo: '',
|
||||
env: config.environment,
|
||||
auth: service(),
|
||||
store: service(),
|
||||
});
|
||||
];
|
||||
wrappedToken = '';
|
||||
redirectTo = '';
|
||||
}
|
||||
|
||||
@@ -9,11 +9,12 @@ import { task, timeout } from 'ember-concurrency';
|
||||
import { sanitizePath } from 'core/utils/sanitize-path';
|
||||
|
||||
export default Controller.extend({
|
||||
flashMessages: service(),
|
||||
vaultController: controller('vault'),
|
||||
clusterController: controller('vault.cluster'),
|
||||
flashMessages: service(),
|
||||
namespaceService: service('namespace'),
|
||||
featureFlagService: service('featureFlag'),
|
||||
version: service(),
|
||||
auth: service(),
|
||||
router: service(),
|
||||
queryParams: [{ authMethod: 'with', oidcProvider: 'o' }],
|
||||
@@ -56,6 +57,7 @@ export default Controller.extend({
|
||||
|
||||
authSuccess({ isRoot, namespace }) {
|
||||
let transition;
|
||||
this.version.fetchVersion();
|
||||
if (this.redirectTo) {
|
||||
// here we don't need the namespace because it will be encoded in redirectTo
|
||||
transition = this.router.transitionTo(this.redirectTo);
|
||||
|
||||
@@ -8,6 +8,8 @@ import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({
|
||||
auth: service(),
|
||||
router: service(),
|
||||
version: service(),
|
||||
|
||||
actions: {
|
||||
seal() {
|
||||
@@ -17,7 +19,9 @@ export default Controller.extend({
|
||||
.then(() => {
|
||||
this.model.cluster.get('leaderNode').set('sealed', true);
|
||||
this.auth.deleteCurrentToken();
|
||||
return this.transitionToRoute('vault.cluster.unseal');
|
||||
// Reset version so it doesn't show on footer
|
||||
this.version.version = null;
|
||||
return this.router.transitionTo('vault.cluster.unseal');
|
||||
});
|
||||
},
|
||||
},
|
||||
|
||||
@@ -4,14 +4,16 @@
|
||||
*/
|
||||
|
||||
import Controller from '@ember/controller';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
export default Controller.extend({
|
||||
router: service(),
|
||||
showLicenseError: false,
|
||||
|
||||
actions: {
|
||||
transitionToCluster() {
|
||||
return this.model.reload().then(() => {
|
||||
return this.transitionToRoute('vault.cluster', this.model.name);
|
||||
return this.router.transitionTo('vault.cluster', this.model.name);
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -8,18 +8,21 @@ import { Promise } from 'rsvp';
|
||||
import { inject as service } from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
import Ember from 'ember';
|
||||
/* eslint-disable ember/no-ember-testing-in-module-scope */
|
||||
const SPLASH_DELAY = Ember.testing ? 0 : 300;
|
||||
|
||||
export default Route.extend({
|
||||
store: service(),
|
||||
version: service(),
|
||||
const SPLASH_DELAY = 300;
|
||||
|
||||
export default class VaultRoute extends Route {
|
||||
@service router;
|
||||
@service store;
|
||||
@service version;
|
||||
|
||||
beforeModel() {
|
||||
return this.version.fetchVersion();
|
||||
},
|
||||
// So we can know what type (Enterprise/Community) we're running
|
||||
return this.version.fetchType();
|
||||
}
|
||||
|
||||
model() {
|
||||
const delay = Ember.testing ? 0 : SPLASH_DELAY;
|
||||
// hardcode single cluster
|
||||
const fixture = {
|
||||
data: {
|
||||
@@ -34,13 +37,13 @@ export default Route.extend({
|
||||
return new Promise((resolve) => {
|
||||
later(() => {
|
||||
resolve(this.store.peekAll('cluster'));
|
||||
}, SPLASH_DELAY);
|
||||
}, delay);
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
redirect(model, transition) {
|
||||
if (model.get('length') === 1 && transition.targetName === 'vault.index') {
|
||||
return this.transitionTo('vault.cluster', model.get('firstObject.name'));
|
||||
return this.router.transitionTo('vault.cluster', model.get('firstObject.name'));
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
const managedRoot = this.featureFlagService.managedNamespaceRoot;
|
||||
assert(
|
||||
'Cannot use VAULT_CLOUD_ADMIN_NAMESPACE flag with non-enterprise Vault version',
|
||||
!(managedRoot && this.version.isOSS)
|
||||
!(managedRoot && this.version.isCommunity)
|
||||
);
|
||||
if (!namespace && currentTokenName && !Ember.testing) {
|
||||
// if no namespace queryParam and user authenticated,
|
||||
@@ -80,6 +80,7 @@ export default Route.extend(ModelBoundaryRoute, ClusterRoute, {
|
||||
if (id) {
|
||||
this.auth.setCluster(id);
|
||||
if (this.auth.currentToken) {
|
||||
this.version.fetchVersion();
|
||||
await this.permissions.getPaths.perform();
|
||||
}
|
||||
return this.version.fetchFeatures();
|
||||
|
||||
@@ -10,10 +10,11 @@ import { inject as service } from '@ember/service';
|
||||
export default Route.extend(ClusterRoute, {
|
||||
store: service(),
|
||||
version: service(),
|
||||
router: service(),
|
||||
|
||||
beforeModel() {
|
||||
if (this.version.isOSS) {
|
||||
this.transitionTo('vault.cluster');
|
||||
if (this.version.isCommunity) {
|
||||
this.router.transitionTo('vault.cluster');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export default Route.extend(ModelBoundaryRoute, {
|
||||
permissions: service(),
|
||||
namespaceService: service('namespace'),
|
||||
router: service(),
|
||||
version: service(),
|
||||
|
||||
modelTypes: computed(function () {
|
||||
return ['secret', 'secret-engine'];
|
||||
@@ -32,6 +33,7 @@ export default Route.extend(ModelBoundaryRoute, {
|
||||
this.console.clearLog(true);
|
||||
this.flashMessages.clearMessages();
|
||||
this.permissions.reset();
|
||||
this.version.version = null;
|
||||
|
||||
queryParams.with = authType;
|
||||
if (ns) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export default Route.extend(ClusterRoute, ListRoute, {
|
||||
version: service(),
|
||||
|
||||
shouldReturnEmptyModel(policyType, version) {
|
||||
return policyType !== 'acl' && (version.get('isOSS') || !version.get('hasSentinel'));
|
||||
return policyType !== 'acl' && (version.get('isCommunity') || !version.get('hasSentinel'));
|
||||
},
|
||||
|
||||
model(params) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export default Service.extend({
|
||||
},
|
||||
|
||||
tokenForUrl(url) {
|
||||
if (this.version.isOSS) {
|
||||
if (this.version.isCommunity) {
|
||||
return null;
|
||||
}
|
||||
let pathForUrl = parseURL(url).pathname;
|
||||
@@ -89,7 +89,7 @@ export default Service.extend({
|
||||
checkForControlGroup(callbackArgs, response, wasWrapTTLRequested) {
|
||||
const creationPath = response && response?.wrap_info?.creation_path;
|
||||
if (
|
||||
this.version.isOSS ||
|
||||
this.version.isCommunity ||
|
||||
wasWrapTTLRequested ||
|
||||
!response ||
|
||||
(creationPath && WRAPPED_RESPONSE_PATHS.includes(creationPath)) ||
|
||||
|
||||
@@ -11,7 +11,17 @@ export default class VersionService extends Service {
|
||||
@service store;
|
||||
@tracked features = [];
|
||||
@tracked version = null;
|
||||
@tracked type = null;
|
||||
|
||||
get isEnterprise() {
|
||||
return this.type === 'enterprise';
|
||||
}
|
||||
|
||||
get isCommunity() {
|
||||
return !this.isEnterprise;
|
||||
}
|
||||
|
||||
/* Features */
|
||||
get hasPerfReplication() {
|
||||
return this.features.includes('Performance Replication');
|
||||
}
|
||||
@@ -32,26 +42,35 @@ export default class VersionService extends Service {
|
||||
return this.features.includes('Control Groups');
|
||||
}
|
||||
|
||||
get isEnterprise() {
|
||||
if (!this.version) return false;
|
||||
return this.version.includes('+');
|
||||
get versionDisplay() {
|
||||
if (!this.version) {
|
||||
return '';
|
||||
}
|
||||
return this.isEnterprise ? `v${this.version.slice(0, this.version.indexOf('+'))}` : `v${this.version}`;
|
||||
}
|
||||
|
||||
get isOSS() {
|
||||
return !this.isEnterprise;
|
||||
@task({ drop: true })
|
||||
*getVersion() {
|
||||
if (this.version) return;
|
||||
const response = yield this.store.adapterFor('cluster').fetchVersion();
|
||||
this.version = response.data?.version;
|
||||
}
|
||||
|
||||
@task
|
||||
*getVersion() {
|
||||
if (this.version) return;
|
||||
const response = yield this.store.adapterFor('cluster').sealStatus();
|
||||
this.version = response.version;
|
||||
return;
|
||||
*getType() {
|
||||
if (this.type !== null) return;
|
||||
const response = yield this.store.adapterFor('cluster').health();
|
||||
if (response.has_chroot_namespace) {
|
||||
// chroot_namespace feature is only available in enterprise
|
||||
this.type = 'enterprise';
|
||||
return;
|
||||
}
|
||||
this.type = response.enterprise ? 'enterprise' : 'community';
|
||||
}
|
||||
|
||||
@keepLatestTask
|
||||
*getFeatures() {
|
||||
if (this.features?.length || this.isOSS) {
|
||||
if (this.features?.length || this.isCommunity) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -67,6 +86,10 @@ export default class VersionService extends Service {
|
||||
return this.getVersion.perform();
|
||||
}
|
||||
|
||||
fetchType() {
|
||||
return this.getType.perform();
|
||||
}
|
||||
|
||||
fetchFeatures() {
|
||||
return this.getFeatures.perform();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
.env-banner {
|
||||
align-self: center;
|
||||
font-size: 0.8rem;
|
||||
border-radius: 3rem;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
@@ -13,8 +13,6 @@
|
||||
); // only use case for purple in the app. define here instead of utils/color_var
|
||||
animation: env-banner-color-rotate 8s infinite linear alternate;
|
||||
color: $white;
|
||||
margin-top: -20px;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.hs-icon {
|
||||
margin: 0;
|
||||
@@ -22,7 +20,7 @@
|
||||
|
||||
.notification {
|
||||
background-color: transparent;
|
||||
line-height: 1.66;
|
||||
line-height: 2;
|
||||
padding: 0 $spacing-12;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<PageHeader as |p|>
|
||||
<p.levelLeft>
|
||||
<h1 class="title is-3" data-test-dashboard-card-header="Vault version">
|
||||
{{this.versionHeader}}
|
||||
Vault
|
||||
{{@version.versionDisplay}}
|
||||
{{#if @version.isEnterprise}}
|
||||
<Hds::Badge @text={{this.namespace.currentNamespace}} @icon="org" data-test-badge-namespace />
|
||||
{{/if}}
|
||||
|
||||
@@ -5,28 +5,4 @@
|
||||
|
||||
{{outlet}}
|
||||
|
||||
<Hds::AppFooter as |AF|>
|
||||
<AF.Link @href={{changelog-url-for this.auth.activeCluster.leaderNode.version}}>
|
||||
Vault
|
||||
{{this.auth.activeCluster.leaderNode.version}}
|
||||
</AF.Link>
|
||||
{{#if (is-version "OSS")}}
|
||||
<AF.Link @href="https://hashicorp.com/products/vault/trial?source=vaultui">
|
||||
Upgrade to Vault Enterprise
|
||||
</AF.Link>
|
||||
{{/if}}
|
||||
<AF.Link @href={{doc-link "/vault"}}>
|
||||
Documentation
|
||||
</AF.Link>
|
||||
<AF.LegalLinks />
|
||||
</Hds::AppFooter>
|
||||
|
||||
{{#if (eq this.env "development")}}
|
||||
<div class="env-banner level development">
|
||||
<div class="level-item notification">
|
||||
<Icon @name="git-branch" /><Icon @name="pencil-tool" />
|
||||
Local development
|
||||
<Icon @name="pencil-tool" /><Icon @name="git-branch" />
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<AppFooter />
|
||||
@@ -1,24 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
/* eslint-disable ember/no-observers */
|
||||
import { inject as service } from '@ember/service';
|
||||
import { assert } from '@ember/debug';
|
||||
import Helper from '@ember/component/helper';
|
||||
import { observer } from '@ember/object';
|
||||
|
||||
export default Helper.extend({
|
||||
version: service(),
|
||||
onFeaturesChange: observer('version.version', function () {
|
||||
this.recompute();
|
||||
}),
|
||||
compute([sku]) {
|
||||
if (sku !== 'OSS' && sku !== 'Enterprise') {
|
||||
assert(`${sku} is not one of the available values for Vault versions.`, false);
|
||||
return false;
|
||||
}
|
||||
return this.get(`version.is${sku}`);
|
||||
},
|
||||
});
|
||||
@@ -1,6 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
export { default } from 'core/helpers/is-version';
|
||||
@@ -18,6 +18,7 @@ export default function (server) {
|
||||
|
||||
server.get('/sys/health', function () {
|
||||
return {
|
||||
enterprise: true,
|
||||
initialized: true,
|
||||
sealed: false,
|
||||
standby: false,
|
||||
|
||||
@@ -26,6 +26,7 @@ const generateHealthResponse = (now, state) => {
|
||||
break;
|
||||
}
|
||||
return {
|
||||
enterprise: true,
|
||||
initialized: true,
|
||||
sealed: false,
|
||||
standby: false,
|
||||
|
||||
137
ui/tests/acceptance/enterprise-reduced-disclosure-test.js
Normal file
137
ui/tests/acceptance/enterprise-reduced-disclosure-test.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupApplicationTest } from 'ember-qunit';
|
||||
import { setupMirage } from 'ember-cli-mirage/test-support';
|
||||
import { settled, visit } from '@ember/test-helpers';
|
||||
import authPage from 'vault/tests/pages/auth';
|
||||
import { createTokenCmd, runCmd, tokenWithPolicyCmd } from 'vault/tests/helpers/commands';
|
||||
import { pollCluster } from 'vault/tests/helpers/poll-cluster';
|
||||
import VAULT_KEYS from 'vault/tests/helpers/vault-keys';
|
||||
import ENV from 'vault/config/environment';
|
||||
|
||||
const { unsealKeys } = VAULT_KEYS;
|
||||
const SELECTORS = {
|
||||
footerVersion: `[data-test-footer-version]`,
|
||||
dashboardTitle: `[data-test-dashboard-card-header="Vault version"]`,
|
||||
};
|
||||
|
||||
module('Acceptance | Enterprise | reduced disclosure test', function (hooks) {
|
||||
setupApplicationTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
hooks.before(function () {
|
||||
ENV['ember-cli-mirage'].handler = 'mfaConfig';
|
||||
});
|
||||
hooks.beforeEach(function () {
|
||||
this.versionSvc = this.owner.lookup('service:version');
|
||||
return authPage.logout();
|
||||
});
|
||||
hooks.after(function () {
|
||||
ENV['ember-cli-mirage'].handler = null;
|
||||
});
|
||||
|
||||
test('it works when reduced disclosure enabled', async function (assert) {
|
||||
const namespace = 'reduced-disclosure';
|
||||
assert.dom(SELECTORS.footerVersion).hasText(`Vault`, 'shows Vault without version when logged out');
|
||||
await authPage.login();
|
||||
|
||||
// Ensure it shows version on dashboard
|
||||
assert.dom(SELECTORS.dashboardTitle).includesText(`Vault v1.`);
|
||||
assert
|
||||
.dom(SELECTORS.footerVersion)
|
||||
.hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version after login');
|
||||
|
||||
await runCmd(`write sys/namespaces/${namespace} -f`, false);
|
||||
await authPage.loginNs(namespace);
|
||||
|
||||
assert
|
||||
.dom(SELECTORS.footerVersion)
|
||||
.hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version within namespace');
|
||||
|
||||
const token = await runCmd(createTokenCmd('default'));
|
||||
|
||||
await authPage.logout();
|
||||
assert.dom(SELECTORS.footerVersion).hasText(`Vault`, 'no vault version after logout');
|
||||
|
||||
await authPage.loginNs(namespace, token);
|
||||
assert
|
||||
.dom(SELECTORS.footerVersion)
|
||||
.hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version for default policy in namespace');
|
||||
});
|
||||
|
||||
test('it works for user accessing child namespace', async function (assert) {
|
||||
const namespace = 'reduced-disclosure';
|
||||
await authPage.login();
|
||||
|
||||
await runCmd(`write sys/namespaces/${namespace} -f`, false);
|
||||
const token = await runCmd(
|
||||
tokenWithPolicyCmd(
|
||||
'child-ns-access',
|
||||
`
|
||||
path "${namespace}/sys/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`
|
||||
)
|
||||
);
|
||||
|
||||
await authPage.logout();
|
||||
await authPage.login(token);
|
||||
assert
|
||||
.dom(SELECTORS.footerVersion)
|
||||
.hasText(`Vault ${this.versionSvc.version}`, 'shows Vault version for default policy in namespace');
|
||||
|
||||
// navigate to child namespace
|
||||
await visit(`/vault/dashboard?namespace=${namespace}`);
|
||||
assert
|
||||
.dom(SELECTORS.footerVersion)
|
||||
.hasText(
|
||||
`Vault ${this.versionSvc.version}`,
|
||||
'shows Vault version for default policy in child namespace'
|
||||
);
|
||||
assert.dom(SELECTORS.dashboardTitle).includesText('Vault v1.');
|
||||
});
|
||||
|
||||
test('shows correct version on unseal flow', async function (assert) {
|
||||
await authPage.login();
|
||||
|
||||
const versionSvc = this.owner.lookup('service:version');
|
||||
await visit('/vault/settings/seal');
|
||||
assert
|
||||
.dom('[data-test-footer-version]')
|
||||
.hasText(`Vault ${versionSvc.version}`, 'shows version on seal page');
|
||||
assert.strictEqual(currentURL(), '/vault/settings/seal');
|
||||
|
||||
// seal
|
||||
await click('[data-test-seal]');
|
||||
|
||||
await click('[data-test-confirm-button]');
|
||||
|
||||
await pollCluster(this.owner);
|
||||
await settled();
|
||||
assert.strictEqual(currentURL(), '/vault/unseal', 'vault is on the unseal page');
|
||||
assert.dom('[data-test-footer-version]').hasText(`Vault`, 'Clears version on unseal');
|
||||
|
||||
// unseal
|
||||
for (const key of unsealKeys) {
|
||||
await fillIn('[data-test-shamir-key-input]', key);
|
||||
|
||||
await click('button[type="submit"]');
|
||||
|
||||
await pollCluster(this.owner);
|
||||
await settled();
|
||||
}
|
||||
|
||||
assert.dom('[data-test-cluster-status]').doesNotExist('ui does not show sealed warning');
|
||||
assert.strictEqual(currentRouteName(), 'vault.cluster.auth', 'vault is ready to authenticate');
|
||||
assert.dom('[data-test-footer-version]').hasText(`Vault`, 'Version is still not shown before auth');
|
||||
await authPage.login();
|
||||
assert
|
||||
.dom('[data-test-footer-version]')
|
||||
.hasText(`Vault ${versionSvc.version}`, 'Version is shown after login');
|
||||
});
|
||||
});
|
||||
@@ -22,7 +22,6 @@ module('Acceptance | Enterprise | sidebar navigation', function (hooks) {
|
||||
|
||||
// common links are tested in the sidebar-nav test and will not be covered here
|
||||
test('it should render enterprise only navigation links', async function (assert) {
|
||||
assert.expect(12);
|
||||
assert.dom(panel('Cluster')).exists('Cluster nav panel renders');
|
||||
|
||||
await click(link('Replication'));
|
||||
|
||||
47
ui/tests/integration/components/app-footer-test.js
Normal file
47
ui/tests/integration/components/app-footer-test.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) HashiCorp, Inc.
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupRenderingTest } from 'vault/tests/helpers';
|
||||
import { render } from '@ember/test-helpers';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
|
||||
const selectors = {
|
||||
versionDisplay: '[data-test-footer-version]',
|
||||
upgradeLink: '[data-test-footer-upgrade-link]',
|
||||
docsLink: '[data-test-footer-documentation-link]',
|
||||
};
|
||||
|
||||
module('Integration | Component | app-footer', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
hooks.beforeEach(function () {
|
||||
this.versionSvc = this.owner.lookup('service:version');
|
||||
});
|
||||
|
||||
test('it renders a sane default', async function (assert) {
|
||||
await render(hbs`<AppFooter />`);
|
||||
assert.dom(selectors.versionDisplay).hasText('Vault', 'Vault without version by default');
|
||||
assert.dom(selectors.upgradeLink).hasText('Upgrade to Vault Enterprise', 'upgrade link shows');
|
||||
assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link');
|
||||
});
|
||||
|
||||
test('it renders for community version', async function (assert) {
|
||||
this.versionSvc.version = '1.15.1';
|
||||
this.versionSvc.type = 'community';
|
||||
await render(hbs`<AppFooter />`);
|
||||
assert.dom(selectors.versionDisplay).hasText('Vault 1.15.1', 'Vault shows version when available');
|
||||
assert.dom(selectors.upgradeLink).hasText('Upgrade to Vault Enterprise', 'upgrade link shows');
|
||||
assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link');
|
||||
});
|
||||
test('it renders for ent version', async function (assert) {
|
||||
this.versionSvc.version = '1.15.1+hsm';
|
||||
this.versionSvc.type = 'enterprise';
|
||||
await render(hbs`<AppFooter />`);
|
||||
assert.dom(selectors.versionDisplay).hasText('Vault 1.15.1+hsm', 'shows version when available');
|
||||
assert.dom(selectors.upgradeLink).doesNotExist('upgrade link not shown');
|
||||
assert.dom(selectors.docsLink).hasText('Documentation', 'displays docs link');
|
||||
});
|
||||
});
|
||||
@@ -114,6 +114,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
||||
test('it should show client count on enterprise w/ license', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.license = {
|
||||
autoloaded: {
|
||||
license_id: '7adbf1f4-56ef-35cd-3a6c-50ef2627865d',
|
||||
@@ -142,6 +143,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
||||
test('it should hide client count on enterprise w/o license ', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.isRootNamespace = true;
|
||||
|
||||
await render(
|
||||
@@ -168,6 +170,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
||||
test('it should hide replication on enterprise not on root namespace', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.isRootNamespace = false;
|
||||
this.license = {
|
||||
autoloaded: {
|
||||
@@ -217,6 +220,7 @@ module('Integration | Component | dashboard/overview', function (hooks) {
|
||||
test('shows the learn more card on enterprise', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.version.features = [
|
||||
'Performance Replication',
|
||||
'DR Replication',
|
||||
|
||||
@@ -28,6 +28,7 @@ module('Integration | Component | license-banners', function (hooks) {
|
||||
this.tomorrow = addDays(mockNow, 1);
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
hooks.after(function () {
|
||||
timestamp.now.restore();
|
||||
|
||||
@@ -24,7 +24,7 @@ module('Integration | Component | link-status', function (hooks) {
|
||||
|
||||
// this can be removed once feature is released for OSS
|
||||
hooks.beforeEach(function () {
|
||||
this.owner.lookup('service:version').set('version', '1.13.0+ent');
|
||||
this.owner.lookup('service:version').set('type', 'enterprise');
|
||||
this.statuses = statuses;
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ module('Integration | Component | link-status', function (hooks) {
|
||||
});
|
||||
|
||||
test('it does not render banner in oss version', async function (assert) {
|
||||
this.owner.lookup('service:version').set('version', '1.13.0');
|
||||
this.owner.lookup('service:version').set('type', 'community');
|
||||
|
||||
await render(hbs`
|
||||
<LinkStatus @status={{get this.statuses 0}} />
|
||||
@@ -50,7 +50,6 @@ module('Integration | Component | link-status', function (hooks) {
|
||||
await render(hbs`
|
||||
<LinkStatus @status={{get this.statuses 0}} />
|
||||
`);
|
||||
|
||||
assert.dom(SELECTORS.bannerConnected).exists('Success banner renders for connected state');
|
||||
assert
|
||||
.dom('[data-test-link-status]')
|
||||
|
||||
@@ -50,7 +50,7 @@ module('Integration | Component | mount-backend/type-form', function (hooks) {
|
||||
module('Enterprise', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.12.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
});
|
||||
|
||||
test('it renders correct items for enterprise secrets', async function (assert) {
|
||||
|
||||
@@ -151,7 +151,7 @@ module('Integration | Component | Page::PkiConfigurationDetails', function (hook
|
||||
|
||||
test('it renders enterprise params in crl section', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
await render(
|
||||
hbs`<Page::PkiConfigurationDetails @urls={{this.urls}} @crl={{this.crl}} @hasConfig={{true}} />,`,
|
||||
{ owner: this.engine }
|
||||
@@ -166,7 +166,7 @@ module('Integration | Component | Page::PkiConfigurationDetails', function (hook
|
||||
|
||||
test('it does not render enterprise params in crl section', async function (assert) {
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1';
|
||||
this.version.type = 'community';
|
||||
await render(
|
||||
hbs`<Page::PkiConfigurationDetails @urls={{this.urls}} @crl={{this.crl}} @hasConfig={{true}} />,`,
|
||||
{ owner: this.engine }
|
||||
|
||||
@@ -276,7 +276,7 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks)
|
||||
test('it renders enterprise only params', async function (assert) {
|
||||
assert.expect(6);
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.server.post(`/${this.backend}/config/acme`, () => {});
|
||||
this.server.post(`/${this.backend}/config/cluster`, () => {});
|
||||
this.server.post(`/${this.backend}/config/crl`, (schema, req) => {
|
||||
@@ -327,7 +327,7 @@ module('Integration | Component | page/pki-configuration-edit', function (hooks)
|
||||
test('it does not render enterprise only params for OSS', async function (assert) {
|
||||
assert.expect(9);
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.13.1';
|
||||
this.version.type = 'community';
|
||||
this.server.post(`/${this.backend}/config/acme`, () => {});
|
||||
this.server.post(`/${this.backend}/config/cluster`, () => {});
|
||||
this.server.post(`/${this.backend}/config/crl`, (schema, req) => {
|
||||
|
||||
@@ -19,7 +19,7 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = '1.14.1+ent';
|
||||
this.version.type = 'enterprise';
|
||||
this.server.post('/sys/capabilities-self', () => {});
|
||||
this.onSave = () => {};
|
||||
this.onCancel = () => {};
|
||||
@@ -33,7 +33,6 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
||||
|
||||
test('it hides or shows fields depending on auto-tidy toggle', async function (assert) {
|
||||
assert.expect(37);
|
||||
this.version.version = '1.14.1+ent';
|
||||
const sectionHeaders = [
|
||||
'Universal operations',
|
||||
'ACME operations',
|
||||
@@ -82,7 +81,6 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
||||
|
||||
test('it renders all attribute fields, including enterprise', async function (assert) {
|
||||
assert.expect(25);
|
||||
this.version.version = '1.14.1+ent';
|
||||
this.autoTidy.enabled = true;
|
||||
const skipFields = ['enabled', 'tidyAcme', 'intervalDuration']; // combined with duration ttl or asserted separately
|
||||
await render(
|
||||
@@ -123,7 +121,7 @@ module('Integration | Component | pki tidy form', function (hooks) {
|
||||
|
||||
test('it hides enterprise fields for OSS', async function (assert) {
|
||||
assert.expect(7);
|
||||
this.version.version = '1.14.1';
|
||||
this.version.type = 'community';
|
||||
this.autoTidy.enabled = true;
|
||||
|
||||
const enterpriseFields = [
|
||||
|
||||
@@ -27,7 +27,7 @@ module('Integration | Component | sidebar-frame', function (hooks) {
|
||||
const currentCluster = this.owner.lookup('service:currentCluster');
|
||||
currentCluster.setCluster({ hcpLinkStatus: 'connected' });
|
||||
const version = this.owner.lookup('service:version');
|
||||
version.version = '1.13.0-dev1+ent';
|
||||
version.type = 'enterprise';
|
||||
|
||||
await render(hbs`
|
||||
<Sidebar::Frame @showSidebar={{true}}>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default create({
|
||||
await this.usernameInput(username);
|
||||
return this.passwordInput(password).submit();
|
||||
},
|
||||
loginNs: async function (ns) {
|
||||
loginNs: async function (ns, token = rootToken) {
|
||||
// make sure we're always logged out and logged back in
|
||||
await this.logout();
|
||||
await settled();
|
||||
@@ -55,7 +55,7 @@ export default create({
|
||||
await settled();
|
||||
await this.namespaceInput(ns);
|
||||
await settled();
|
||||
await this.tokenInput(rootToken).submit();
|
||||
await this.tokenInput(token).submit();
|
||||
return;
|
||||
},
|
||||
clickLogout: async function (clearNamespace = false) {
|
||||
|
||||
@@ -79,7 +79,7 @@ module('Unit | Adapter | kv/data', function (hooks) {
|
||||
hooks.beforeEach(function () {
|
||||
this.store = this.owner.lookup('service:store');
|
||||
this.version = this.owner.lookup('service:version');
|
||||
this.version.version = 'example+ent'; // Required for testing control-group flow
|
||||
this.version.type = 'enterprise'; // Required for testing control-group flow
|
||||
this.secretMountPath = this.owner.lookup('service:secret-mount-path');
|
||||
this.backend = 'my/kv-back&end';
|
||||
this.secretMountPath.currentPath = this.backend;
|
||||
|
||||
@@ -51,8 +51,8 @@ module('Unit | Service | control group', function (hooks) {
|
||||
|
||||
hooks.afterEach(function () {});
|
||||
|
||||
const isOSS = (context) => set(context, 'version.isOSS', true);
|
||||
const isEnt = (context) => set(context, 'version.isOSS', false);
|
||||
const isCommunity = (context) => set(context, 'version.type', 'community');
|
||||
const isEnt = (context) => set(context, 'version.type', 'enterprise');
|
||||
const resolvesArgs = (assert, result, expectedArgs) => {
|
||||
return result.then((...args) => {
|
||||
return assert.deepEqual(args, expectedArgs, 'resolves with the passed args');
|
||||
@@ -61,31 +61,31 @@ module('Unit | Service | control group', function (hooks) {
|
||||
|
||||
[
|
||||
[
|
||||
'it resolves isOSS:true, wrapTTL: true, response: has wrap_info',
|
||||
isOSS,
|
||||
'it resolves isCommunity:true, wrapTTL: true, response: has wrap_info',
|
||||
isCommunity,
|
||||
[[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true],
|
||||
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
|
||||
],
|
||||
[
|
||||
'it resolves isOSS:true, wrapTTL: false, response: has no wrap_info',
|
||||
isOSS,
|
||||
'it resolves isCommunity:true, wrapTTL: false, response: has no wrap_info',
|
||||
isCommunity,
|
||||
[[{ one: 'two', three: 'four' }], { wrap_info: null }, false],
|
||||
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
|
||||
],
|
||||
[
|
||||
'it resolves isOSS: false and wrapTTL:true response: has wrap_info',
|
||||
'it resolves isCommunity: false and wrapTTL:true response: has wrap_info',
|
||||
isEnt,
|
||||
[[{ one: 'two', three: 'four' }], { wrap_info: { token: 'foo', accessor: 'bar' } }, true],
|
||||
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
|
||||
],
|
||||
[
|
||||
'it resolves isOSS: false and wrapTTL:false response: has no wrap_info',
|
||||
'it resolves isCommunity: false and wrapTTL:false response: has no wrap_info',
|
||||
isEnt,
|
||||
[[{ one: 'two', three: 'four' }], { wrap_info: null }, false],
|
||||
(assert, result) => resolvesArgs(assert, result, [{ one: 'two', three: 'four' }]),
|
||||
],
|
||||
[
|
||||
'it rejects isOSS: false, wrapTTL:false, response: has wrap_info',
|
||||
'it rejects isCommunity: false, wrapTTL:false, response: has wrap_info',
|
||||
isEnt,
|
||||
[
|
||||
[{ one: 'two', three: 'four' }],
|
||||
@@ -107,7 +107,8 @@ module('Unit | Service | control group', function (hooks) {
|
||||
],
|
||||
].forEach(function ([name, setup, args, expectation]) {
|
||||
test(`checkForControlGroup: ${name}`, function (assert) {
|
||||
const assertCount = name === 'it rejects isOSS: false, wrapTTL:false, response: has wrap_info' ? 2 : 1;
|
||||
const assertCount =
|
||||
name === 'it rejects isCommunity: false, wrapTTL:false, response: has wrap_info' ? 2 : 1;
|
||||
assert.expect(assertCount);
|
||||
if (setup) {
|
||||
setup(this);
|
||||
|
||||
@@ -9,24 +9,17 @@ import { setupTest } from 'ember-qunit';
|
||||
module('Unit | Service | version', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
test('setting version computes isOSS properly', function (assert) {
|
||||
test('setting type computes isCommunity properly', function (assert) {
|
||||
const service = this.owner.lookup('service:version');
|
||||
service.version = '0.9.5';
|
||||
assert.true(service.isOSS);
|
||||
service.type = 'community';
|
||||
assert.true(service.isCommunity);
|
||||
assert.false(service.isEnterprise);
|
||||
});
|
||||
|
||||
test('setting version computes isEnterprise properly', function (assert) {
|
||||
test('setting type computes isEnterprise properly', function (assert) {
|
||||
const service = this.owner.lookup('service:version');
|
||||
service.version = '0.9.5+ent';
|
||||
assert.false(service.isOSS);
|
||||
assert.true(service.isEnterprise);
|
||||
});
|
||||
|
||||
test('setting version with hsm ending computes isEnterprise properly', function (assert) {
|
||||
const service = this.owner.lookup('service:version');
|
||||
service.version = '0.9.5+ent.hsm';
|
||||
assert.false(service.isOSS);
|
||||
service.type = 'enterprise';
|
||||
assert.false(service.isCommunity);
|
||||
assert.true(service.isEnterprise);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user