fix: 🐛 add storage bucket within target (#324)

* fix: 🐛 add storage bucket within target
This commit is contained in:
Dharini Jeeva authored and GitHub committed 2023-06-07 15:45:46 -07:00
1 parent c0b590dad2
commit 0cd4439b71
16 files changed
+564 -12

No files matched your search

@@ -56,7 +56,7 @@
@color='primary'
@icon='plus'
@text={{t 'resources.storage-bucket.messages.none.link'}}
@route='scopes.scope.storage-buckets.new'
@route='scopes.scope.targets.target.enable-session-recording.create-storage-bucket'
/>
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@@ -0,0 +1,5 @@
import Controller from '@ember/controller';
export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorageBucketController extends Controller {
// =services
}
+3 -1
View File
@@ -85,7 +85,9 @@ Router.map(function () {
this.route('injected-application-credential-sources');
this.route('add-brokered-credential-sources');
this.route('add-injected-application-credential-sources');
this.route('enable-session-recording');
this.route('enable-session-recording', function () {
this.route('create-storage-bucket');
});
});
this.route('new');
});
@@ -0,0 +1,87 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import {
TYPE_STORAGE_BUCKET_PLUGIN,
TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
} from 'api/models/storage-bucket';
import { action } from '@ember/object';
import { notifySuccess, notifyError } from 'core/decorators/notify';
import { loading } from 'ember-loading';
export default class ScopesScopeTargetsTargetEnableSessionRecordingCreateStorageBucketRoute extends Route {
// =services
@service store;
@service router;
// =methods
/**
* Creates a new unsaved storage bucket.
* @return {StorageBucket}
*/
model() {
const scopeModel = this.store.peekRecord('scope', 'global');
const record = this.store.createRecord('storage-bucket', {
type: TYPE_STORAGE_BUCKET_PLUGIN,
compositeType: TYPE_STORAGE_BUCKET_PLUGIN_AWS_S3,
});
record.scopeModel = scopeModel;
return record;
}
async afterModel() {
let scopes;
const orgScopes = (
await this.store.query('scope', { scope_id: 'global' })
).map((scope) => ({ model: scope }));
scopes = [
{ model: this.store.peekRecord('scope', 'global') },
...orgScopes,
];
this.scopes = scopes;
}
/**
* Adds available global and org scopes to the context.
* @param {Controller} controller
*/
setupController(controller) {
super.setupController(...arguments);
controller.set('scopes', this.scopes);
}
// =actions
/**
* Handle save
* @param {StorageBucketModel} storageBucket
*/
@action
@loading
@notifyError(({ message }) => message)
@notifySuccess('notifications.save-success')
async save(storageBucket) {
await storageBucket.save();
await this.router.transitionTo(
'scopes.scope.targets.target.enable-session-recording'
);
this.refresh();
}
/**
* Rollback changes on storage buckets.
* @param {StorageBucketModel} storageBucket
*/
@action
cancel(storageBucket) {
storageBucket.rollbackAttributes();
this.router.transitionTo(
'scopes.scope.targets.target.enable-session-recording'
);
}
}
@@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class ScopesScopeTargetsTargetEnableSessionRecordingRoute extends Route {
export default class ScopesScopeTargetsTargetEnableSessionRecordingIndexRoute extends Route {
// =services
@service store;
@service router;
@@ -19,7 +19,7 @@ export default class ScopesScopeTargetsTargetEnableSessionRecordingRoute extends
}
/**
* Load storage buckets from target's project scope and global scope
* Load storage buckets from target's parent's scope and global scope
* @param {Model} model
*/
async afterModel() {
@@ -30,15 +30,15 @@ export default class ScopesScopeTargetsTargetEnableSessionRecordingRoute extends
scope_id: 'global',
});
//fetch storage buckets from target's org scope
//fetch storage buckets from target's parent's scope
const orgScopeStorageBuckets = await this.store.query('storage-bucket', {
scope_id,
});
const storageBucketList = [
...globalScopeStorageBuckets.toArray(),
...orgScopeStorageBuckets.toArray(),
];
this.storageBucketList = storageBucketList;
}
// =actions
@@ -11,7 +11,6 @@
</page.breadcrumbs>
<page.header>
{{! TODO: Add valid doc link and description}}
<h2>
{{t 'resources.storage-bucket.titles.new'}}
<DocLink @doc='storage-bucket' @iconSize='large' />
@@ -0,0 +1,25 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: MPL-2.0
}}
{{page-title (t 'titles.new')}}
<Rose::Layout::Page as |page|>
<page.header>
<h2>
{{t 'resources.storage-bucket.titles.new'}}
<DocLink @doc='storage-bucket' @iconSize='large' />
</h2>
</page.header>
<page.body>
<Form::StorageBucket
@model={{@model}}
@submit={{route-action 'save' @model}}
@cancel={{route-action 'cancel' @model}}
@scopes={{this.scopes}}
/>
</page.body>
</Rose::Layout::Page>
@@ -85,7 +85,7 @@ module('Acceptance | targets | enable session recording', function (hooks) {
urls.target = `${urls.targets}/${instances.target.id}`;
urls.enableSessionRecording = `${urls.target}/enable-session-recording`;
urls.storageBuckets = `${urls.projectScope}/storage-buckets`;
urls.newStorageBucket = `${urls.storageBuckets}/new`;
urls.newStorageBucket = `${urls.enableSessionRecording}/create-storage-bucket`;
urls.storageBucket = `${urls.storageBuckets}/${storageBucketOne.id}`;
authenticateSession({});
@@ -0,0 +1,166 @@
import { module, test } from 'qunit';
import { visit, currentURL, click, fillIn } from '@ember/test-helpers';
import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { Response } from 'miragejs';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_TARGET_SSH } from 'api/models/target';
module(
'Acceptance | targets | enable session recording | create storage bucket',
function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let features;
let getStorageBucketCount;
const SAVE_BTN_SELECTOR = '[type="submit"]';
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
const NAME_FIELD_SELECTOR = '[name="name"]';
const ALERT_TEXT_SELECTOR = '[role="alert"] div';
const FIELD_ERROR_TEXT_SELECTOR = '.hds-form-error__message';
const NAME_FIELD_TEXT = 'random string';
const BUCKET_NAME_FIELD_SELECTOR = '[name="bucket_name"]';
const BUCKET_PREFIX_FIELD_SELECTOR = '[name="bucket_prefix"]';
const instances = {
scopes: {
global: null,
org: null,
},
};
const urls = {
globalScope: null,
projectScope: null,
targets: null,
target: null,
enableSessionRecording: null,
newStorageBucket: null,
};
hooks.beforeEach(function () {
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.target = this.server.create('target', {
scope: instances.scopes.project,
type: TYPE_TARGET_SSH,
});
urls.globalScope = `/scopes/global`;
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.targets = `${urls.projectScope}/targets`;
urls.target = `${urls.targets}/${instances.target.id}`;
urls.enableSessionRecording = `${urls.target}/enable-session-recording`;
urls.newStorageBucket = `${urls.enableSessionRecording}/create-storage-bucket`;
getStorageBucketCount = () =>
this.server.schema.storageBuckets.all().models.length;
features = this.owner.lookup('service:features');
features.enable('ssh-session-recording');
authenticateSession({});
});
test('users can create a new storage bucket with global scope', async function (assert) {
assert.expect(7);
const storageBucketCount = getStorageBucketCount();
await visit(urls.enableSessionRecording);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click('[value="global"]');
assert.dom(BUCKET_NAME_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_NAME_FIELD_SELECTOR).doesNotHaveAttribute('readOnly');
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).doesNotHaveAttribute('readOnly');
await click(SAVE_BTN_SELECTOR);
const storageBucket = this.server.schema.storageBuckets.findBy({
name: NAME_FIELD_TEXT,
});
assert.strictEqual(storageBucket.name, NAME_FIELD_TEXT);
assert.strictEqual(storageBucket.scopeId, 'global');
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('users can create a new storage bucket with org scope', async function (assert) {
assert.expect(7);
const storageBucketCount = getStorageBucketCount();
await visit(urls.enableSessionRecording);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click(`[value="${instances.scopes.org.scope.id}"]`);
assert.dom(BUCKET_NAME_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).isNotDisabled();
assert.dom(BUCKET_NAME_FIELD_SELECTOR).doesNotHaveAttribute('readOnly');
assert.dom(BUCKET_PREFIX_FIELD_SELECTOR).doesNotHaveAttribute('readOnly');
await click(SAVE_BTN_SELECTOR);
const storageBucket = this.server.schema.storageBuckets.findBy({
name: NAME_FIELD_TEXT,
});
assert.strictEqual(storageBucket.name, NAME_FIELD_TEXT);
assert.strictEqual(storageBucket.scopeId, instances.scopes.org.scope.id);
assert.strictEqual(getStorageBucketCount(), storageBucketCount + 1);
});
test('user can cancel new storage bucket creation', async function (assert) {
assert.expect(2);
const storageBucketCount = getStorageBucketCount();
await visit(urls.enableSessionRecording);
await click(`[href="${urls.newStorageBucket}"]`);
await fillIn(NAME_FIELD_SELECTOR, NAME_FIELD_TEXT);
await click(CANCEL_BTN_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
assert.strictEqual(getStorageBucketCount(), storageBucketCount);
});
test('saving a new storage bucket with invalid fields displays error messages', async function (assert) {
assert.expect(2);
this.server.post('/storage-buckets', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: 'The request was invalid.',
details: {
request_fields: [
{
name: 'name',
description: 'Name is required.',
},
],
},
}
);
});
await visit(urls.enableSessionRecording);
await click(`[href="${urls.newStorageBucket}"]`);
await click(SAVE_BTN_SELECTOR);
await a11yAudit();
assert.dom(ALERT_TEXT_SELECTOR).hasText('The request was invalid.');
assert.dom(FIELD_ERROR_TEXT_SELECTOR).hasText('Name is required.');
});
}
);
@@ -0,0 +1,235 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import { module, test } from 'qunit';
import { visit, find, click, currentURL } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_TARGET_SSH } from 'api/models/target';
import select from '@ember/test-helpers/dom/select';
module('Acceptance | targets | enable session recording', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
let featuresService;
let storageBucketOne;
let storageBucketTwo;
const SETTINGS_LINK_SELECTOR = '.target-sidebar .hds-link-standalone';
const ENABLE_BUTTON_SELECTOR = '.target-sidebar .hds-button';
const DROPDOWN_SELECTOR = '[name=storage_bucket_id]';
const LINK_LIST_SELECTOR = '.link-list-item > a';
const LINK_LIST_SELECTOR_ITEM_TEXT = '.link-list-item__text';
const TOGGLE_SELECTOR = '[name=target-enable-session-recording]';
const LINK_TO_NEW_STORAGE_BUCKET =
'.enable-session-recording-toggle .hds-link-standalone';
const SAVE_BTN_SELECTOR = '.rose-form-actions [type="submit"]';
const CANCEL_BTN_SELECTOR = '.rose-form-actions [type="button"]';
// Instances
const instances = {
scopes: {
global: null,
org: null,
},
sessionRecording: null,
};
//URLs
const urls = {
globalScope: null,
orgScope: null,
projectScope: null,
targets: null,
target: null,
enableSessionRecording: null,
storageBuckets: null,
newStorageBucket: null,
};
hooks.beforeEach(function () {
featuresService = this.owner.lookup('service:features');
// Generate resources
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.target = this.server.create('target', {
scope: instances.scopes.project,
type: TYPE_TARGET_SSH,
});
instances.storageBucket = this.server.createList('storage-bucket', 2, {
scope: instances.scopes.global,
});
storageBucketOne = instances.storageBucket[0];
storageBucketTwo = instances.storageBucket[1];
// Generate route URLs for resources
urls.globalScope = `/scopes/global/scopes`;
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.targets = `${urls.projectScope}/targets`;
urls.target = `${urls.targets}/${instances.target.id}`;
urls.enableSessionRecording = `${urls.target}/enable-session-recording`;
urls.storageBuckets = `${urls.projectScope}/storage-buckets`;
urls.newStorageBucket = `${urls.enableSessionRecording}/create-storage-bucket`;
urls.storageBucket = `${urls.storageBuckets}/${storageBucketOne.id}`;
authenticateSession({});
});
test('cannot enable session recording for a target without proper authorization', async function (assert) {
assert.expect(2);
assert.false(featuresService.isEnabled('ssh-session-recording'));
await visit(urls.target);
await a11yAudit();
assert.dom('.target-sidebar a').doesNotExist();
});
test('users can click on enable-recording button in target session-recording sidebar and it takes them to enable session recording', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(2);
await visit(urls.target);
assert.dom(SETTINGS_LINK_SELECTOR).doesNotExist();
await click(ENABLE_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
});
test('users can click on settings link in target session-recording sidebar and it takes them to enable session recording', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(1);
instances.target.update({
storageBucketId: storageBucketOne.id,
enableSessionRecording: true,
});
await visit(urls.target);
await click(SETTINGS_LINK_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
});
test('users can click on associated storage bucket card on an ssh target', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(1);
instances.target.update({
storageBucketId: storageBucketOne.id,
enableSessionRecording: true,
});
await visit(urls.target);
await click(LINK_LIST_SELECTOR);
assert.strictEqual(currentURL(), urls.storageBucket);
});
test('toggle should be disabled and storage buckets list should be shown when enable session recording button is clicked', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(3);
await visit(urls.target);
await click(ENABLE_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
assert.dom(TOGGLE_SELECTOR).isVisible();
assert.dom(DROPDOWN_SELECTOR).isNotVisible();
});
test('storage buckets list is hidden when toggle is disabled', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(4);
await visit(urls.target);
await click(ENABLE_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
assert.dom(TOGGLE_SELECTOR).isVisible();
assert.dom(DROPDOWN_SELECTOR).isNotVisible();
await click(TOGGLE_SELECTOR);
assert.dom(DROPDOWN_SELECTOR).isVisible();
});
test('link to add new storage bucket should be displayed and redirect to new storage buckets form', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(3);
await visit(urls.target);
await click(ENABLE_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
assert.dom(LINK_TO_NEW_STORAGE_BUCKET).isVisible();
await click(LINK_TO_NEW_STORAGE_BUCKET);
assert.strictEqual(currentURL(), urls.newStorageBucket);
});
test('retain last selected dropdown list value when the toggle is off', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(3);
instances.target.update({
storageBucketId: storageBucketOne.id,
enable_session_recording: true,
});
await visit(urls.target);
await click(SETTINGS_LINK_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
const existingStorageBucketId = find(DROPDOWN_SELECTOR).value;
//disable the toggle, dropdown should be hidden
await click(TOGGLE_SELECTOR);
assert.dom(DROPDOWN_SELECTOR).isNotVisible();
//enable the toggle now,
//it should have the previously selected storage_bucket_id
await click(TOGGLE_SELECTOR);
assert.dom(DROPDOWN_SELECTOR).hasValue(existingStorageBucketId);
});
test('can assign a storage bucket for the target', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(5);
await visit(urls.target);
await click(ENABLE_BUTTON_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
await click(TOGGLE_SELECTOR);
assert.dom(DROPDOWN_SELECTOR).hasNoValue();
await select(DROPDOWN_SELECTOR, storageBucketTwo.id);
assert.dom(DROPDOWN_SELECTOR).hasValue();
await click(SAVE_BTN_SELECTOR);
assert.strictEqual(currentURL(), urls.target);
assert.strictEqual(
find(LINK_LIST_SELECTOR_ITEM_TEXT).textContent.trim(),
storageBucketTwo.name
);
});
test('can cancel changes to an existing storage bucket selection', async function (assert) {
featuresService.enable('ssh-session-recording');
assert.expect(3);
instances.target.update({
storageBucketId: storageBucketOne.id,
enable_session_recording: true,
});
await visit(urls.target);
await click(SETTINGS_LINK_SELECTOR);
assert.strictEqual(currentURL(), urls.enableSessionRecording);
//update the storage bucket selection
await select(DROPDOWN_SELECTOR, storageBucketTwo);
await click(CANCEL_BTN_SELECTOR);
assert.strictEqual(currentURL(), urls.target);
assert.strictEqual(
find(LINK_LIST_SELECTOR_ITEM_TEXT).textContent.trim(),
storageBucketOne.name
);
});
});
@@ -0,0 +1,17 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/targets/target/enable-session-recording/create-storage-bucket',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/targets/target/enable-session-recording/create-storage-bucket'
);
assert.ok(controller);
});
}
);
@@ -2,14 +2,14 @@ import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Controller | scopes/scope/targets/target/enable-session-recording',
'Unit | Controller | scopes/scope/targets/target/enable-session-recording/index',
function (hooks) {
setupTest(hooks);
// TODO: Replace this with your real tests.
test('it exists', function (assert) {
let controller = this.owner.lookup(
'controller:scopes/scope/targets/target/enable-session-recording'
'controller:scopes/scope/targets/target/enable-session-recording/index'
);
assert.ok(controller);
});
@@ -0,0 +1,16 @@
import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Route | scopes/scope/targets/target/enable-session-recording/create-storage-bucket',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/targets/target/enable-session-recording/create-storage-bucket'
);
assert.ok(route);
});
}
);
@@ -2,13 +2,13 @@ import { module, test } from 'qunit';
import { setupTest } from 'admin/tests/helpers';
module(
'Unit | Route | scopes/scope/targets/target/enable-session-recording',
'Unit | Route | scopes/scope/targets/target/enable-session-recording/index',
function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
let route = this.owner.lookup(
'route:scopes/scope/targets/target/enable-session-recording'
'route:scopes/scope/targets/target/enable-session-recording/index'
);
assert.ok(route);
});