feat: 🎸 add support for upd credential

This commit is contained in:
Dharini Jeeva authored and Dharini committed 2025-09-03 15:59:29 -07:00
1 parent 25f015b179
commit e1696e9a4d
34 files changed
+1328 -153

No files matched your search

@@ -115,37 +115,53 @@
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Select::Field
@isOptional={{true}}
@width='100%'
name='credential_type'
disabled={{or form.disabled (not @model.isNew)}}
{{on 'input' this.selectCredentialType}}
as |F|
>
<F.Label>
{{t 'resources.credential-library.form.credential_type.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.credential-library.form.credential_type.help'}}
</F.HelperText>
<F.Options>
<option value='' />
{{#if @model.isNew}}
<Hds::Form::Select::Field
@isOptional={{true}}
@width='100%'
name='credential_type'
disabled={{form.disabled}}
{{on 'input' this.selectCredentialType}}
as |F|
>
<F.Label>
{{t 'resources.credential-library.form.credential_type.label'}}
</F.Label>
<F.HelperText>
{{t 'resources.credential-library.form.credential_type.help'}}
</F.HelperText>
<F.Options>
<option value='' />
{{#each this.credentialTypes as |type|}}
<option value={{type}} selected={{eq type @model.credential_type}}>
{{t (concat 'resources.credential-library.titles.' type)}}
</option>
{{/each}}
</F.Options>
{{#if @model.errors.credential_type}}
<F.Error as |E|>
{{#each @model.errors.credential_type as |error|}}
<E.Message>{{error.message}}</E.Message>
{{#each this.credentialTypes as |type|}}
<option value={{type}} selected={{eq type @model.credential_type}}>
{{t (concat 'resources.credential-library.titles.' type)}}
</option>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Select::Field>
</F.Options>
{{#if @model.errors.credential_type}}
<F.Error as |E|>
{{#each @model.errors.credential_type as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Select::Field>
{{else}}
<InfoField
@value={{t
(concat 'resources.credential-library.titles.' @model.credential_type)
}}
as |F|
>
<F.Label>{{t
'resources.credential-library.form.credential_type.label'
}}</F.Label>
<F.HelperText>
{{t 'resources.credential-library.form.credential_type.description'}}
</F.HelperText>
</InfoField>
{{/if}}
{{#if @model.credential_type}}
<Form::Field::ListWrapper
@@ -53,26 +53,11 @@
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::RadioCard::Group @name={{t 'form.type.label'}} as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each @types as |credentialType|}}
<G.RadioCard
@value={{credentialType}}
@checked={{eq credentialType @model.type}}
@maxWidth='20rem'
{{on 'input' (fn @changeType credentialType)}}
as |R|
>
<R.Label>{{t (concat 'resources.credential.types.' credentialType)}}
</R.Label>
<R.Description>{{t
(concat 'resources.credential.help.' credentialType)
}}
</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
<Form::Credential::Radio
@model={{@model}}
@types={{@types}}
@changeType={{@changeType}}
/>
<Hds::Form::Fieldset as |F|>
<F.Legend>{{t 'resources.credential.form.json.label'}}</F.Legend>
<F.HelperText>{{t 'resources.credential.form.json.help'}}</F.HelperText>
@@ -0,0 +1,27 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Hds::Form::Radio::Group
class='credential-selection'
@name={{t 'form.type.label'}}
as |G|
>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each @types as |credentialType|}}
<G.RadioField
@value={{credentialType}}
checked={{eq credentialType @model.type}}
{{on 'input' (fn @changeType credentialType)}}
as |F|
>
<F.Label>{{t
(concat 'resources.credential.types.' credentialType)
}}</F.Label>
<F.HelperText>{{t
(concat 'resources.credential.help.' credentialType)
}}</F.HelperText>
</G.RadioField>
{{/each}}
</Hds::Form::Radio::Group>
@@ -51,27 +51,13 @@
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::RadioCard::Group @name={{t 'form.type.label'}} as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each @types as |credentialType|}}
<G.RadioCard
@value={{credentialType}}
@maxWidth='20rem'
@checked={{eq credentialType @model.type}}
{{on 'input' (fn @changeType credentialType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.credential.types.' credentialType)
}}</R.Label>
<R.Description>{{t
(concat 'resources.credential.help.' credentialType)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
{{#if @model.isNew}}
<Form::Credential::Radio
@model={{@model}}
@types={{@types}}
@changeType={{@changeType}}
/>
{{else}}
<InfoField
@value={{t (concat 'resources.credential.types.' @model.type)}}
@@ -83,7 +69,6 @@
</F.HelperText>
</InfoField>
{{/if}}
<Hds::Form::TextInput::Field
name='username'
@value={{@model.username}}
@@ -53,26 +53,11 @@
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Hds::Form::RadioCard::Group @name={{t 'form.type.label'}} as |G|>
<G.Legend>{{t 'form.type.label'}}</G.Legend>
{{#each @types as |credentialType|}}
<G.RadioCard
@value={{credentialType}}
@maxWidth='20rem'
@checked={{eq credentialType @model.type}}
{{on 'input' (fn @changeType credentialType)}}
as |R|
>
<R.Label>{{t
(concat 'resources.credential.types.' credentialType)
}}</R.Label>
<R.Description>{{t
(concat 'resources.credential.help.' credentialType)
}}</R.Description>
</G.RadioCard>
{{/each}}
</Hds::Form::RadioCard::Group>
<Form::Credential::Radio
@model={{@model}}
@types={{@types}}
@changeType={{@changeType}}
/>
{{else}}
<InfoField
@value={{t (concat 'resources.credential.types.' @model.type)}}
@@ -0,0 +1,152 @@
{{!
Copyright (c) HashiCorp, Inc.
SPDX-License-Identifier: BUSL-1.1
}}
<Rose::Form
@onSubmit={{this.handleSubmit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@showEditToggle={{if @model.isNew false true}}
as |form|
>
<Hds::Form::TextInput::Field
name='name'
@value={{@model.name}}
@isInvalid={{@model.errors.name}}
@isOptional={{true}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'name')}}
as |F|
>
<F.Label>{{t 'form.name.label'}}</F.Label>
<F.HelperText>{{t 'form.name.help'}}</F.HelperText>
{{#if @model.errors.name}}
<F.Error as |E|>
{{#each @model.errors.name as |error|}}
<E.Message data-test-error-message-name>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
<Hds::Form::Textarea::Field
name='description'
@value={{@model.description}}
@isInvalid={{@model.errors.description}}
@isOptional={{true}}
disabled={{form.disabled}}
as |F|
>
<F.Label>{{t 'form.description.label'}}</F.Label>
<F.HelperText>{{t 'form.description.help'}}</F.HelperText>
{{#if @model.errors.description}}
<F.Error as |E|>
{{#each @model.errors.description as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::Textarea::Field>
{{#if @model.isNew}}
<Form::Credential::Radio
@model={{@model}}
@types={{@types}}
@changeType={{@changeType}}
/>
{{else}}
<InfoField
@value={{t (concat 'resources.credential.types.' @model.type)}}
readonly={{true}}
as |F|
>
<F.Label>{{t 'form.type.label'}}</F.Label>
<F.HelperText>{{t (concat 'resources.credential.help.' @model.type)}}
</F.HelperText>
</InfoField>
{{/if}}
<Hds::Form::TextInput::Field
name='username'
@isRequired={{true}}
@value={{@model.username}}
@isInvalid={{@model.errors.username}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'username')}}
{{on 'blur' this.handleUsername}}
as |F|
>
<F.Label>{{t 'resources.credential.form.username.label'}}</F.Label>
<F.HelperText>{{t 'resources.credential.form.username.help'}}</F.HelperText>
{{#if @model.errors.username}}
<F.Error as |E|>
{{#each @model.errors.username as |error|}}
<E.Message>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#if (or @model.isNew form.isEditable)}}
<Hds::Form::TextInput::Field
name='password'
@isRequired={{true}}
@type='password'
@value={{@model.password}}
@isInvalid={{@model.errors.password}}
disabled={{form.disabled}}
autocomplete='new-password'
{{on 'input' (set-from-event @model 'password')}}
as |F|
>
<F.Label>{{t 'resources.credential.form.password.label'}}</F.Label>
<F.HelperText>{{t
'resources.credential.form.password.help'
}}</F.HelperText>
{{#if @model.errors.password}}
<F.Error as |E|>
{{#each @model.errors.password as |error|}}
<E.Message data-test-error-message-password>
{{error.message}}
</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{/if}}
<Hds::Form::TextInput::Field
name='domain'
@isRequired={{true}}
@value={{@model.domain}}
@isInvalid={{@model.errors.domain}}
disabled={{form.disabled}}
{{on 'input' (set-from-event @model 'domain')}}
as |F|
>
<F.Label>{{t 'resources.credential.form.domain.label'}}</F.Label>
<F.HelperText>{{t 'resources.credential.form.domain.help'}}</F.HelperText>
{{#if @model.errors.domain}}
<F.Error as |E|>
{{#each @model.errors.domain as |error|}}
<E.Message
data-test-error-message-domain
>{{error.message}}</E.Message>
{{/each}}
</F.Error>
{{/if}}
</Hds::Form::TextInput::Field>
{{#if (can 'save model' @model)}}
<form.actions
@enableEditText={{t 'actions.edit-form'}}
@submitText={{t 'actions.save'}}
@cancelText={{t 'actions.cancel'}}
/>
{{/if}}
</Rose::Form>
@@ -0,0 +1,52 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import Component from '@glimmer/component';
import { action } from '@ember/object';
export default class FormCredentialUsernamePasswordDomainComponent extends Component {
// =actions
/**
* Pass the username input value for further processing.
*/
@action
handleUsername(event) {
const { value } = event.target;
this.processUsername(value);
}
/**
* Process the username input and call the submit action
*/
@action
handleSubmit() {
this.processUsername(this.args.model.username);
this.args.submit();
}
/**
* Process the username input by splitting it into username and domain
* and updating the model accordingly.
* @param {string} value - The input value from the username field
*/
processUsername(value) {
const arr = value?.split(/[@\\]/) ?? [];
// Check if the length of the arr is 2
// This is to avoid cases where the username might be hello@world@again
// We let the server handle the validation in that case
// and only update the model if we have exactly one username and one domain after splitting
if (arr.length === 2) {
const [username, domain] = arr;
// Update the model with the values only if both username and domain are present
if (username && domain) {
this.args.model.username = username;
this.args.model.domain = domain;
}
}
}
}
@@ -149,7 +149,7 @@
</Hds::Form::TextInput::Field>
<Hds::Form::Radio::Group
class='dynamic-credential-selection'
class='credential-selection'
@layout='vertical'
@name='credential_type'
as |G|
@@ -68,7 +68,7 @@ export default class ScopesScopeCredentialStoresCredentialStoreCredentialsIndexC
*/
@action
@loading
@notifyError(({ message }) => message)
@notifyError(({ message }) => message, { catch: true })
@notifySuccess(({ isNew }) =>
isNew ? 'notifications.create-success' : 'notifications.save-success',
)
+1 -1
View File
@@ -967,7 +967,7 @@
}
}
.dynamic-credential-selection {
.credential-selection {
.hds-form-group__control-field {
margin-bottom: 0;
}
@@ -14,6 +14,7 @@ import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/creden
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN } from 'api/models/credential';
module('Acceptance | credential-libraries | create', function (hooks) {
setupApplicationTest(hooks);
@@ -22,6 +23,7 @@ module('Acceptance | credential-libraries | create', function (hooks) {
let featuresService;
let getCredentialLibraryCount;
let getUsernamePasswordDomainCredentialLibraryCount;
const instances = {
scopes: {
@@ -73,6 +75,11 @@ module('Acceptance | credential-libraries | create', function (hooks) {
// Generate resource counter
getCredentialLibraryCount = () =>
this.server.schema.credentialLibraries.all().models.length;
getUsernamePasswordDomainCredentialLibraryCount = () => {
return this.server.schema.credentialLibraries.where({
credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
}).length;
};
await authenticateSession({ username: 'admin' });
featuresService = this.owner.lookup('service:features');
});
@@ -110,10 +117,13 @@ module('Acceptance | credential-libraries | create', function (hooks) {
await visit(urls.newCredentialLibrary);
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await select(selectors.FIELD_CRED_TYPE, selectors.FIELD_CRED_TYPE_VALUE);
await select(
selectors.FIELD_CRED_TYPE,
selectors.FIELD_CRED_TYPE_SSH_VALUE,
);
await select(
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_VALUE,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE,
);
await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'key');
await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN);
@@ -129,13 +139,58 @@ module('Acceptance | credential-libraries | create', function (hooks) {
);
assert.strictEqual(
credentialLibrary.credentialType,
selectors.FIELD_CRED_TYPE_VALUE,
selectors.FIELD_CRED_TYPE_SSH_VALUE,
);
assert.deepEqual(credentialLibrary.credentialMappingOverrides, {
private_key_attribute: 'key',
});
});
test('can create a new credential library with username, password and domain type for vault generic', async function (assert) {
const credentialLibraryCount = getCredentialLibraryCount();
const usernamePasswordDomainCredentialLibraryCount =
getUsernamePasswordDomainCredentialLibraryCount();
await visit(urls.newCredentialLibrary);
await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE);
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await select(
selectors.FIELD_CRED_TYPE,
selectors.FIELD_CRED_TYPE_UPD_VALUE,
);
await select(
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE,
);
await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'domain');
await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN);
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(getCredentialLibraryCount(), credentialLibraryCount + 1);
assert.strictEqual(
getUsernamePasswordDomainCredentialLibraryCount(),
usernamePasswordDomainCredentialLibraryCount + 1,
);
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
});
assert.strictEqual(
credentialLibrary.name,
commonSelectors.FIELD_NAME_VALUE,
);
assert.strictEqual(
credentialLibrary.credentialType,
selectors.FIELD_CRED_TYPE_UPD_VALUE,
);
assert.deepEqual(credentialLibrary.credentialMappingOverrides, {
domain_attribute: 'domain',
});
});
test('can create a new credential library of type vault ssh cert', async function (assert) {
setRunOptions({
rules: {
@@ -15,6 +15,8 @@ import sinon from 'sinon';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC } from 'api/models/credential-library';
import { TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN } from 'api/models/credential';
module('Acceptance | credential-libraries | delete', function (hooks) {
setupApplicationTest(hooks);
@@ -22,6 +24,7 @@ module('Acceptance | credential-libraries | delete', function (hooks) {
setupSqlite(hooks);
let getCredentialLibraryCount;
let getUsernamePasswordDomainCredentialLibraryCount;
const instances = {
scopes: {
@@ -38,6 +41,7 @@ module('Acceptance | credential-libraries | delete', function (hooks) {
credentialLibraries: null,
newCredentialLibrary: null,
unknownCredentialLibrary: null,
usernamePasswordDomainCredentialLibrary: null,
};
hooks.beforeEach(async function () {
@@ -58,6 +62,15 @@ module('Acceptance | credential-libraries | delete', function (hooks) {
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
});
instances.usernamePasswordDomainCredentialLibrary = this.server.create(
'credential-library',
{
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
// Generate route URLs for resources
urls.credentialStores = `/scopes/${instances.scopes.project.id}/credential-stores`;
urls.credentialStore = `${urls.credentialStores}/${instances.credentialStore.id}`;
@@ -65,9 +78,15 @@ module('Acceptance | credential-libraries | delete', function (hooks) {
urls.credentialLibrary = `${urls.credentialLibraries}/${instances.credentialLibrary.id}`;
urls.newCredentialLibrary = `${urls.credentialLibraries}/new`;
urls.unknownCredentialLibrary = `${urls.credentialLibraries}/foo`;
urls.usernamePasswordDomainCredentialLibrary = `${urls.credentialLibraries}/${instances.usernamePasswordDomainCredentialLibrary.id}`;
// Generate resource counter
getCredentialLibraryCount = () =>
this.server.schema.credentialLibraries.all().models.length;
getUsernamePasswordDomainCredentialLibraryCount = () => {
return this.server.schema.credentialLibraries.where({
credential_type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
}).length;
};
await authenticateSession({});
});
@@ -175,4 +194,18 @@ module('Acceptance | credential-libraries | delete', function (hooks) {
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText('Oops.');
});
test('can delete username password and domain credential library type', async function (assert) {
const usernamePasswordDomainCredentialLibraryCount =
getUsernamePasswordDomainCredentialLibraryCount();
await visit(urls.usernamePasswordDomainCredentialLibrary);
await click(selectors.MANAGE_DROPDOWN_CRED_LIB);
await click(selectors.MANAGE_DROPDOWN_CRED_LIB_DELETE);
assert.strictEqual(
getUsernamePasswordDomainCredentialLibraryCount(),
usernamePasswordDomainCredentialLibraryCount - 1,
);
});
});
@@ -9,9 +9,13 @@ import { setupApplicationTest } from 'admin/tests/helpers';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/credential-library';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
} from 'api/models/credential-library';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN } from 'api/models/credential';
module('Acceptance | credential-libraries | read', function (hooks) {
setupApplicationTest(hooks);
@@ -169,4 +173,21 @@ module('Acceptance | credential-libraries | read', function (hooks) {
assert.notEqual(currentURL(), incorrectUrl);
assert.strictEqual(currentURL(), correctUrl);
});
test('visiting vault credential library of type username password and domain', async function (assert) {
const usernamePasswordDomainCredentialLibrary = this.server.create(
'credential-library',
{
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
const url = `${urls.credentialLibraries}/${usernamePasswordDomainCredentialLibrary.id}`;
await visit(url);
assert.strictEqual(currentURL(), url);
});
});
@@ -4,7 +4,8 @@
*/
export const FIELD_CRED_TYPE = '[name=credential_type]';
export const FIELD_CRED_TYPE_VALUE = 'ssh_private_key';
export const FIELD_CRED_TYPE_SSH_VALUE = 'ssh_private_key';
export const FIELD_CRED_TYPE_UPD_VALUE = 'username_password_domain';
export const FIELD_VAULT_PATH = '[name=vault_path]';
export const FIELD_VAULT_PATH_VALUE = 'path';
@@ -27,7 +28,9 @@ export const FIELD_KEY_ID_VALUE = 'key_id';
export const FIELD_CRED_MAP_OVERRIDES_SELECT =
'[name="credential_mapping_overrides"] select';
export const FIELD_CRED_MAP_OVERRIDES_SELECT_VALUE = 'private_key_attribute';
export const FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE =
'private_key_attribute';
export const FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE = 'domain_attribute';
export const FIELD_CRED_MAP_OVERRIDES_INPUT =
'[name="credential_mapping_overrides"] input';
export const FIELD_CRED_MAP_OVERRIDES_BTN =
@@ -10,10 +10,14 @@ import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import { setupSqlite } from 'api/test-support/helpers/sqlite';
import { authenticateSession } from 'ember-simple-auth/test-support';
import { Response } from 'miragejs';
import { TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE } from 'api/models/credential-library';
import {
TYPE_CREDENTIAL_LIBRARY_VAULT_SSH_CERTIFICATE,
TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
} from 'api/models/credential-library';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import { TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN } from 'api/models/credential';
module('Acceptance | credential-libraries | update', function (hooks) {
setupApplicationTest(hooks);
@@ -38,6 +42,7 @@ module('Acceptance | credential-libraries | update', function (hooks) {
credentialLibraries: null,
newCredentialLibrary: null,
unknownCredentialLibrary: null,
usernamePasswordDomainCredentialLibrary: null,
};
hooks.beforeEach(async function () {
@@ -58,6 +63,15 @@ module('Acceptance | credential-libraries | update', function (hooks) {
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
});
instances.usernamePasswordDomainCredentialLibrary = this.server.create(
'credential-library',
{
scope: instances.scopes.project,
credentialStore: instances.credentialStore,
type: TYPE_CREDENTIAL_LIBRARY_VAULT_GENERIC,
credential_type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
// Generate route URLs for resources
urls.globalScope = `/scopes/global/scopes`;
urls.orgScope = `/scopes/${instances.scopes.org.id}/scopes`;
@@ -68,6 +82,7 @@ module('Acceptance | credential-libraries | update', function (hooks) {
urls.credentialLibrary = `${urls.credentialLibraries}/${instances.credentialLibrary.id}`;
urls.newCredentialLibrary = `${urls.credentialLibraries}/new`;
urls.unknownCredentialLibrary = `${urls.credentialLibraries}/foo`;
urls.usernamePasswordDomainCredentialLibrary = `${urls.credentialLibraries}/${instances.usernamePasswordDomainCredentialLibrary.id}`;
await authenticateSession({});
});
@@ -127,7 +142,7 @@ module('Acceptance | credential-libraries | update', function (hooks) {
await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE);
await select(
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_VALUE,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_SSH_VALUE,
);
await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'key');
await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN);
@@ -153,6 +168,45 @@ module('Acceptance | credential-libraries | update', function (hooks) {
});
});
test('can update a vault generic credential library of username, password and domain type and save changes', async function (assert) {
await visit(urls.usernamePasswordDomainCredentialLibrary);
await click(commonSelectors.EDIT_BTN);
await fillIn(commonSelectors.FIELD_NAME, commonSelectors.FIELD_NAME_VALUE);
await fillIn(
commonSelectors.FIELD_DESCRIPTION,
commonSelectors.FIELD_DESCRIPTION_VALUE,
);
await fillIn(selectors.FIELD_VAULT_PATH, selectors.FIELD_VAULT_PATH_VALUE);
await select(
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT,
selectors.FIELD_CRED_MAP_OVERRIDES_SELECT_DOMAIN_VALUE,
);
await fillIn(selectors.FIELD_CRED_MAP_OVERRIDES_INPUT, 'domain');
await click(selectors.FIELD_CRED_MAP_OVERRIDES_BTN);
await click(commonSelectors.SAVE_BTN);
const credentialLibrary = this.server.schema.credentialLibraries.findBy({
credentialType: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
});
assert.strictEqual(
credentialLibrary.name,
commonSelectors.FIELD_NAME_VALUE,
);
assert.strictEqual(
credentialLibrary.description,
commonSelectors.FIELD_DESCRIPTION_VALUE,
);
assert.strictEqual(
credentialLibrary.attributes.path,
selectors.FIELD_VAULT_PATH_VALUE,
);
assert.deepEqual(credentialLibrary.credentialMappingOverrides, {
domain_attribute: 'domain',
});
});
test('saving an existing credential library with invalid fields displays error messages', async function (assert) {
setRunOptions({
rules: {
@@ -288,7 +342,7 @@ module('Acceptance | credential-libraries | update', function (hooks) {
await visit(urls.credentialLibrary);
await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
assert.dom(selectors.FIELD_CRED_TYPE).isDisabled();
assert.dom(selectors.FIELD_CRED_TYPE).doesNotExist();
});
test('can update a vault ssh cert credential library and save changes', async function (assert) {
@@ -24,6 +24,7 @@ module(
let getCredentialsCount;
let getUsernamePasswordCredentialCount;
let getUsernameKeyPairCredentialCount;
let getUsernamePasswordDomainCredentialCount;
let getJsonCredentialCount;
let featuresService;
@@ -80,6 +81,11 @@ module(
getJsonCredentialCount = () => {
return this.server.schema.credentials.where({ type: 'json' }).length;
};
getUsernamePasswordDomainCredentialCount = () => {
return this.server.schema.credentials.where({
type: 'username_password_domain'
}).length;
};
await authenticateSession({});
});
@@ -112,6 +118,61 @@ module(
);
});
test('users can create a new username, password & domain credential', async function (assert) {
const credentialsCount = getCredentialsCount();
const usernamePasswordDomainCredentialCount =
getUsernamePasswordDomainCredentialCount();
await visit(urls.credentials);
await click(commonSelectors.HREF(urls.newCredential));
await fillIn(
commonSelectors.FIELD_NAME,
commonSelectors.FIELD_NAME_VALUE,
);
await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN);
await fillIn(selectors.FIELD_USERNAME, selectors.FIELD_USERNAME_VALUE);
await fillIn(selectors.FIELD_PASSWORD, selectors.FIELD_PASSWORD_VALUE);
await fillIn(selectors.FIELD_DOMAIN, selectors.FIELD_DOMAIN_VALUE);
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
assert.strictEqual(
getUsernamePasswordDomainCredentialCount(),
usernamePasswordDomainCredentialCount + 1,
);
});
test('users can create a new username & password credential with domain in username field', async function (assert) {
const credentialsCount = getCredentialsCount();
const usernamePasswordDomainCredentialCount =
getUsernamePasswordDomainCredentialCount();
await visit(urls.credentials);
await click(commonSelectors.HREF(urls.newCredential));
await fillIn(
commonSelectors.FIELD_NAME,
commonSelectors.FIELD_NAME_VALUE,
);
await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN);
await fillIn(selectors.FIELD_USERNAME, selectors.FIELD_USERNAME_WITH_DOMAIN_VALUE);
await fillIn(selectors.FIELD_PASSWORD, selectors.FIELD_PASSWORD_VALUE);
// check that the domain field is filled in
assert.dom(selectors.FIELD_DOMAIN).hasValue(selectors.FIELD_DOMAIN_VALUE);
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(getCredentialsCount(), credentialsCount + 1);
assert.strictEqual(
getUsernamePasswordDomainCredentialCount(),
usernamePasswordDomainCredentialCount + 1,
);
});
test('users can create a new username & key pair credential', async function (assert) {
setRunOptions({
rules: {
@@ -254,6 +315,22 @@ module(
assert.strictEqual(getCredentialsCount(), credentialsCount);
});
test('users can cancel creation of new username, password & domain credential', async function (assert) {
const credentialsCount = getCredentialsCount();
await visit(urls.credentials);
await click(commonSelectors.HREF(urls.newCredential));
await fillIn(
commonSelectors.FIELD_NAME,
commonSelectors.FIELD_NAME_VALUE,
);
await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN);
await click(commonSelectors.CANCEL_BTN);
assert.strictEqual(currentURL(), urls.credentials);
assert.strictEqual(getCredentialsCount(), credentialsCount);
});
test('users can switch away from JSON type credentials and the json_object value will be cleared', async function (assert) {
setRunOptions({
rules: {
@@ -397,6 +474,42 @@ module(
.hasText(errorDescription);
});
test('saving a new username, password & domain credential with invalid fields displays error messages', async function (assert) {
const errorMessage = 'Error in provided request.';
const errorDescription =
'Field required for creating a username-password-domain credential.';
await visit(urls.credentials);
this.server.post('/credentials', () => {
return new Response(
400,
{},
{
status: 400,
code: 'invalid_argument',
message: errorMessage,
details: {
request_fields: [
{
name: 'attributes.domain',
description: errorDescription,
},
],
},
},
);
}
);
await click(commonSelectors.HREF(urls.newCredential));
await click(selectors.FIELD_TYPE_USERNAME_PASSWORD_DOMAIN);
await click(commonSelectors.SAVE_BTN);
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(errorMessage);
assert
.dom(selectors.FIELD_DOMAIN_ERROR)
.hasText('Field required for creating a username-password-domain credential.');
});
test('saving a new json credential with invalid fields displays error messages', async function (assert) {
setRunOptions({
rules: {
@@ -13,6 +13,11 @@ import { Response } from 'miragejs';
import * as selectors from './selectors';
import * as commonSelectors from 'admin/tests/helpers/selectors';
import { setRunOptions } from 'ember-a11y-testing/test-support';
import {
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
TYPE_CREDENTIAL_USERNAME_PASSWORD,
} from 'api/models/credential';
module(
'Acceptance | credential-stores | credentials | delete',
@@ -24,6 +29,7 @@ module(
let getUsernamePasswordCredentialCount;
let getUsernameKeyPairCredentialCount;
let getJSONCredentialCount;
let getUsernamePasswordDomainCredentialCount;
const mockResponseMessage = 'Oops.';
const mockResponse = () => {
@@ -53,6 +59,7 @@ module(
usernamePasswordCredential: null,
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
};
hooks.beforeEach(async function () {
@@ -72,13 +79,21 @@ module(
instances.usernamePasswordCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
type: 'username_password',
type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
});
instances.usernameKeyPairCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
type: 'ssh_private_key',
type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
});
instances.usernamePasswordDomainCredential = this.server.create(
'credential',
{
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
},
);
instances.jsonCredential = this.server.create('credential', {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
@@ -91,20 +106,27 @@ module(
urls.credentials = `${urls.staticCredentialStore}/credentials`;
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
// Generate resource counter
getUsernamePasswordCredentialCount = () => {
return this.server.schema.credentials.where({
type: 'username_password',
type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
}).length;
};
getUsernameKeyPairCredentialCount = () => {
return this.server.schema.credentials.where({ type: 'ssh_private_key' })
.length;
return this.server.schema.credentials.where({
type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
}).length;
};
getJSONCredentialCount = () => {
return this.server.schema.credentials.where({ type: 'json' }).length;
};
getUsernamePasswordDomainCredentialCount = () => {
return this.server.schema.credentials.where({
type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
}).length;
};
await authenticateSession({});
});
@@ -176,6 +198,22 @@ module(
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount - 1);
});
test('can delete username, password & domain credential', async function (assert) {
const usernamePasswordDomainCredentialCount =
getUsernamePasswordDomainCredentialCount();
await visit(urls.usernamePasswordDomainCredential);
await click(selectors.MANAGE_DROPDOWN);
await click(selectors.MANAGE_DROPDOWN_DELETE);
assert.strictEqual(currentURL(), urls.credentials);
assert.strictEqual(
getUsernamePasswordDomainCredentialCount(),
usernamePasswordDomainCredentialCount - 1,
);
});
test('cannot delete a username & password credential without proper authorization', async function (assert) {
const usernamePasswordCredentialCount =
getUsernamePasswordCredentialCount();
@@ -223,6 +261,23 @@ module(
assert.strictEqual(getJSONCredentialCount(), jsonCredentialCount);
});
test('cannot delete a username, password & domain credential without proper authorization', async function (assert) {
const usernamePasswordDomainCredentialCount =
getUsernamePasswordDomainCredentialCount();
instances.usernamePasswordDomainCredential.authorized_actions =
instances.usernamePasswordDomainCredential.authorized_actions.filter(
(item) => item !== 'delete',
);
await visit(urls.usernamePasswordDomainCredential);
assert.strictEqual(currentURL(), urls.usernamePasswordDomainCredential);
assert.dom(selectors.MANAGE_DROPDOWN).doesNotExist();
assert.strictEqual(
getUsernamePasswordDomainCredentialCount(),
usernamePasswordDomainCredentialCount,
);
});
test('can accept delete username & password credential via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -353,6 +408,24 @@ module(
);
});
test('can cancel delete username, password & domain credential via dialog', async function (assert) {
const confirmService = this.owner.lookup('service:confirm');
confirmService.enabled = true;
const usernamePasswordDomainCredentialCount =
getUsernamePasswordDomainCredentialCount();
await visit(urls.usernamePasswordDomainCredential);
await click(selectors.MANAGE_DROPDOWN);
await click(selectors.MANAGE_DROPDOWN_DELETE);
await click(commonSelectors.MODAL_WARNING_CANCEL_BTN);
assert.strictEqual(currentURL(), urls.usernamePasswordDomainCredential);
assert.strictEqual(
getUsernamePasswordDomainCredentialCount(),
usernamePasswordDomainCredentialCount,
);
});
test('can cancel delete JSON credential via dialog', async function (assert) {
setRunOptions({
rules: {
@@ -432,5 +505,25 @@ module(
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText(mockResponseMessage);
});
test('deleting a username, password & domain credential which errors displays error message', async function (assert) {
this.server.del('/credentials/:id', () => {
return new Response(
490,
{},
{
status: 490,
code: 'error',
message: 'Oops.',
},
);
});
await visit(urls.usernamePasswordDomainCredential);
await click(selectors.MANAGE_DROPDOWN);
await click(selectors.MANAGE_DROPDOWN_DELETE);
assert.dom(commonSelectors.ALERT_TOAST_BODY).hasText('Oops.');
});
},
);
@@ -28,6 +28,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
usernamePasswordCredential: null,
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
};
const urls = {
@@ -39,6 +40,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
usernameKeyPairCredential: null,
jsonCredential: null,
unknownCredential: null,
usernamePasswordDomainCredential: null,
};
hooks.beforeEach(async function () {
@@ -70,6 +72,15 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
credentialStore: instances.staticCredentialStore,
type: 'json',
});
instances.usernamePasswordDomainCredential = this.server.create(
'credential',
{
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
type: 'username_password_domain',
},
);
// Generate route URLs for resources
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.credentialStores = `${urls.projectScope}/credential-stores`;
@@ -78,6 +89,7 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
urls.unknownCredential = `${urls.credentials}/foo`;
await authenticateSession({ username: 'admin' });
featuresService = this.owner.lookup('service:features');
@@ -144,6 +156,20 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
assert.strictEqual(currentURL(), urls.jsonCredential);
});
test('visiting username, password & domain credential', async function (assert) {
await visit(urls.staticCredentialStore);
await click(commonSelectors.HREF(urls.credentials));
await a11yAudit();
assert.strictEqual(currentURL(), urls.credentials);
await click(commonSelectors.HREF(urls.usernamePasswordDomainCredential));
await a11yAudit();
assert.strictEqual(currentURL(), urls.usernamePasswordDomainCredential);
});
test('cannot navigate to a username & password credential form without proper authorization', async function (assert) {
setRunOptions({
rules: {
@@ -216,6 +242,22 @@ module('Acceptance | credential-stores | credentials | read', function (hooks) {
.doesNotExist();
});
test('cannot navigate to a username, password & domain credential form without proper authorization', async function (assert) {
instances.usernamePasswordDomainCredential.authorized_actions =
instances.usernamePasswordDomainCredential.authorized_actions.filter(
(item) => item != 'read',
);
await visit(urls.credentials);
assert
.dom(
commonSelectors.TABLE_RESOURCE_LINK(
urls.usernamePasswordDomainCredential,
),
)
.doesNotExist();
});
test('cannot navigate to a JSON credential form when feature not enabled', async function (assert) {
setRunOptions({
rules: {
@@ -5,11 +5,23 @@
export const FIELD_TYPE_SSH = '[value=ssh_private_key]';
export const FIELD_TYPE_USERNAME_PASSWORD = '[value=username_password]';
export const FIELD_TYPE_USERNAME_PASSWORD_DOMAIN =
'[value=username_password_domain]';
export const FIELD_DOMAIN =
'[name=domain]';
export const FIELD_PASSWORD = '[name=password]';
export const FIELD_USERNAME = '[name=username]';
export const FIELD_TYPE_JSON = '[value=json]';
export const FIELD_DOMAIN_VALUE = 'g.com';
export const FIELD_USERNAME_VALUE = 'username123';
export const FIELD_PASSWORD_VALUE = 'password123';
export const FIELD_USERNAME_WITH_DOMAIN_VALUE = 'username@g.com';
export const FIELD_EDITOR = '[data-test-code-editor-field-editor] textarea';
export const FIELD_EDITOR_VALUE = '{"test": "value"}';
export const FIELD_PASSWORD_ERROR = '[data-test-error-message-password]';
export const FIELD_SSH_PRIVATE_KEY = '[name=private_key]';
export const FIELD_DOMAIN_ERROR = '[data-test-error-message-domain]';
export const FIELD_SSH_PRIVATE_KEY_ERROR =
'[data-test-error-message-private-key]';
export const FIELD_SSH_PRIVATE_KEY_PASSPHRASE = '[name=private_key_passphrase]';
@@ -40,6 +40,7 @@ module(
usernamePasswordCredential: null,
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
};
const urls = {
@@ -50,6 +51,7 @@ module(
usernamePasswordCredential: null,
usernameKeyPairCredential: null,
jsonCredential: null,
usernamePasswordDomainCredential: null,
};
const mockResponseMessage = 'Error in provided request.';
@@ -104,6 +106,15 @@ module(
credentialStore: instances.staticCredentialStore,
type: 'json',
});
instances.usernamePasswordDomainCredential = this.server.create(
'credential',
{
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
type: 'username_password_domain',
},
);
// Generate route URLs for resources
urls.projectScope = `/scopes/${instances.scopes.project.id}`;
urls.credentialStores = `${urls.projectScope}/credential-stores`;
@@ -112,6 +123,7 @@ module(
urls.usernamePasswordCredential = `${urls.credentials}/${instances.usernamePasswordCredential.id}`;
urls.usernameKeyPairCredential = `${urls.credentials}/${instances.usernameKeyPairCredential.id}`;
urls.jsonCredential = `${urls.credentials}/${instances.jsonCredential.id}`;
urls.usernamePasswordDomainCredential = `${urls.credentials}/${instances.usernamePasswordDomainCredential.id}`;
featuresService = this.owner.lookup('service:features');
await authenticateSession({});
@@ -192,6 +204,26 @@ module(
);
});
test('can save changes to existing username, password & domain credential', async function (assert) {
const mockInput = 'random string';
await visit(urls.usernamePasswordDomainCredential);
await click('form [type="button"]', 'Activate edit mode');
await fillIn(selectors.FIELD_USERNAME, mockInput);
await fillIn(selectors.FIELD_DOMAIN, 'g.com');
await click(commonSelectors.SAVE_BTN);
assert.strictEqual(currentURL(), urls.usernamePasswordDomainCredential);
const credential = this.server.schema.credentials.where({
type: 'username_password_domain',
}).models[0];
assert.strictEqual(credential.attributes.username, mockInput);
assert.strictEqual(credential.attributes.domain, 'g.com');
});
test('cannot make changes to an existing username & password credential without proper authorization', async function (assert) {
instances.usernamePasswordCredential.authorized_actions =
instances.usernamePasswordCredential.authorized_actions.filter(
@@ -558,6 +590,19 @@ module(
}
});
test('can cancel unsaved username, password & domain credential changes via dialog', async function (assert) {
const name = instances.usernamePasswordDomainCredential.name;
await visit(urls.usernamePasswordDomainCredential);
await click(commonSelectors.EDIT_BTN, 'Activate edit mode');
await fillIn('[name="username"]', 'random string');
// cancel changes
await click(commonSelectors.CANCEL_BTN);
assert.strictEqual(instances.usernamePasswordDomainCredential.name, name);
});
test('can cancel discard unsaved JSON credential changes via dialog', async function (assert) {
assert.expect(6);
const confirmService = this.owner.lookup('service:confirm');
@@ -30,7 +30,7 @@ export const FIELD_ROLE_ARN = '[name="role_arn"]';
export const FIELD_ROLE_ARN_VALUE = 'arn:aws:iam';
export const FIELD_CREDENTIAL_TYPE =
'.dynamic-credential-selection input:checked';
'.credential-selection input:checked';
export const FIELD_DYNAMIC_CREDENTIAL = '[name="dynamic-credential"]';
export const FIELD_DYNAMIC_CREDENTIAL_VALUE = 'dynamic-credentials';
export const FIELD_STATIC_CREDENTIAL = '[name="static-credential"]';
@@ -75,7 +75,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
type: 'static',
scope: instances.scopes.project,
});
instances.credentials = this.server.createList('credential', 3, {
instances.credentials = this.server.createList('credential', 4, {
scope: instances.scopes.project,
credentialStore: instances.staticCredentialStore,
});
@@ -115,7 +115,7 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
urls.brokeredCredentialSources = `${urls.target}/brokered-credential-sources`;
urls.credentialLibrary = `${urls.projectScope}/credential-stores/${instances.credentialLibrary.credentialStoreId}/credential-libraries/${instances.credentialLibrary.id}`;
urls.credential = `${urls.projectScope}/credential-stores/${instances.credential.credentialStoreId}/credentials/${instances.credential.id}`;
urls.jsonCredential = `${urls.projectScope}/credential-stores/${instances.credentials[2].credentialStoreId}/credentials/${instances.credentials[2].id}`;
urls.jsonCredential = `${urls.projectScope}/credential-stores/${instances.credentials[3].credentialStoreId}/credentials/${instances.credentials[3].id}`;
urls.addBrokeredCredentialSources = `${urls.target}/add-brokered-credential-sources`;
getCredentialLibraryCount = () =>
this.server.schema.credentialLibraries.all().models.length;
@@ -191,14 +191,14 @@ module('Acceptance | targets | brokered credential sources', function (hooks) {
},
});
const jsonCredential = instances.credentials[2];
const jsonCredential = instances.credentials[3];
instances.target.update({
brokeredCredentialSourceIds: [...randomlySelectedCredentials],
});
await visit(urls.brokeredCredentialSources);
assert.false(featuresService.isEnabled('json-credentials'));
assert.dom(commonSelectors.TABLE_ROW(3)).includesText(jsonCredential.name);
assert.dom(commonSelectors.TABLE_ROW(4)).includesText(jsonCredential.name);
assert.dom(commonSelectors.HREF(urls.jsonCredential)).doesNotExist();
});
@@ -0,0 +1,232 @@
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'admin/tests/helpers';
import { click, fillIn, render, blur } from '@ember/test-helpers';
import { hbs } from 'ember-cli-htmlbars';
import { setupIntl } from 'ember-intl/test-support';
import sinon from 'sinon';
module(
'Integration | Component | form/credential/username-password-domain',
function (hooks) {
setupRenderingTest(hooks);
setupIntl(hooks, 'en-us');
test('it renders', async function (assert) {
this.model = {};
this.submit = () => { };
this.cancel = () => { };
await render(
hbs`<Form::Credential::UsernamePasswordDomain @model={{this.model}} @submit={{this.submit}} @cancel={{this.cancel}} />`,
);
assert.dom('[name=domain]').exists();
assert.dom('[name=username]').exists();
assert.dom('[name=password]').doesNotExist();
assert.dom('[name=type]').doesNotExist();
assert.dom('.info-field').exists();
assert.dom('[name=name]').exists();
assert.dom('[name=description]').exists();
});
test('it splits the username and domain when the value contains delimiters such as `@` or `\\` on `onBlur`', async function (assert) {
this.model = { username: '', domain: '', authorized_actions: ['update'] };
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'username@domain.com');
await blur('[name=username]');
assert.strictEqual(this.model.username, 'username');
assert.strictEqual(this.model.domain, 'domain.com');
// Test with a different delimiter `\`
await fillIn('[name=username]', `username\\domain.com`);
await blur('[name=username]');
assert.strictEqual(this.model.username, 'username');
assert.strictEqual(this.model.domain, 'domain.com');
});
test('it splits the username and domain when the value contains delimiters such as `@` or `\\` when submit is clicked', async function (assert) {
this.model = { username: '', domain: '', authorized_actions: ['update'] };
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'username@domain.com');
await click('button');
assert.strictEqual(this.model.username, 'username');
assert.strictEqual(this.model.domain, 'domain.com');
});
test('it does not split the username and domain when the value does not contain a delimiter', async function (assert) {
this.model = { username: '', domain: '', authorized_actions: ['update'] };
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'username');
await blur('[name=username]');
assert.strictEqual(this.model.username, 'username');
assert.strictEqual(this.model.domain, '');
});
test('it does not split if the username or domain is empty', async function (assert) {
this.model = { username: '', domain: '', authorized_actions: ['update'] };
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
// Test with username ending with a delimiter
await fillIn('[name=username]', 'username@');
await blur('[name=username]');
assert.strictEqual(this.model.username, 'username@');
assert.strictEqual(this.model.domain, '');
// Check for empty username with domain
await fillIn('[name=username]', '@domain.com');
await blur('[name=username]');
assert.strictEqual(this.model.username, '@domain.com');
assert.strictEqual(this.model.domain, '');
});
test('it does not split when the username is empty or contains multiple delimiters', async function (assert) {
this.model = { username: '', domain: '', authorized_actions: ['update'] };
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
// Check for an empty username
await fillIn('[name=username]', '');
await blur('[name=username]');
assert.strictEqual(this.model.username, '');
assert.strictEqual(this.model.domain, '');
// Check for a username containing multiple delimiters
await fillIn('[name=username]', 'hello@world@again');
await blur('[name=username]');
assert.strictEqual(this.model.username, 'hello@world@again');
assert.strictEqual(this.model.domain, '');
});
test('it does not update the model when the username remains unchanged', async function (assert) {
this.model = {
username: 'username',
domain: 'domain.com',
authorized_actions: ['update'],
};
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'username@domain.com');
await blur('[name=username]');
assert.strictEqual(this.model.username, 'username');
assert.strictEqual(this.model.domain, 'domain.com');
});
test('it allows the username and domain to be updated', async function (assert) {
this.model = {
username: 'username',
domain: 'domain.com',
authorized_actions: ['update'],
};
this.submit = () => { };
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'newname');
await blur('[name=username]');
await fillIn('[name=domain]', 'newdomain.com');
assert.strictEqual(this.model.username, 'newname');
assert.strictEqual(this.model.domain, 'newdomain.com');
});
test('it updates the username and domain when the submit action is triggered', async function (assert) {
this.model = {
username: 'username',
domain: 'domain.com',
authorized_actions: ['update'],
};
const submitSpy = sinon.spy();
this.submit = submitSpy;
this.cancel = () => { };
await render(hbs`
<Form::Credential::UsernamePasswordDomain
@model={{this.model}}
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
await click('button');
await fillIn('[name=username]', 'username2@domain2.com');
await click('[type=submit]');
assert.ok(submitSpy.calledOnce);
assert.deepEqual(submitSpy.firstCall.args, []);
assert.strictEqual(this.model.username, 'username2');
assert.strictEqual(this.model.domain, 'domain2.com');
});
},
);