feat: 🎸 add support for upd credential
This commit is contained in:
1 parent
25f015b179
commit
e1696e9a4d
34 files changed
+1328
-153
No files matched your search
@@ -51,17 +51,17 @@ export default class GeneratedCredentialModel extends BaseModel {
|
||||
})
|
||||
version;
|
||||
|
||||
// =attributes (username_password, ssh_private_key)
|
||||
// =attributes (username_password, ssh_private_key, username_password_domain)
|
||||
@attr('string', {
|
||||
for: ['username_password', 'ssh_private_key'],
|
||||
for: ['username_password', 'ssh_private_key', 'username_password_domain'],
|
||||
isNestedAttribute: true,
|
||||
description: 'The username for credential.',
|
||||
})
|
||||
username;
|
||||
|
||||
// =attributes (username_password)
|
||||
// =attributes (username_password, username_password_domain)
|
||||
@attr('string', {
|
||||
for: 'username_password',
|
||||
for: ['username_password', 'username_password_domain'],
|
||||
isNestedAttribute: true,
|
||||
isSecret: true,
|
||||
description: 'The password for credential.',
|
||||
@@ -92,4 +92,12 @@ export default class GeneratedCredentialModel extends BaseModel {
|
||||
description: 'The secret associated with credential.',
|
||||
})
|
||||
json_object;
|
||||
|
||||
// =attributes (username_password_domain)
|
||||
@attr('string', {
|
||||
for: 'username_password_domain',
|
||||
isNestedAttribute: true,
|
||||
description: 'The domain for credential.',
|
||||
})
|
||||
domain;
|
||||
}
|
||||
@@ -4,14 +4,22 @@
|
||||
*/
|
||||
|
||||
import GeneratedCredentialLibraryModel from '../generated/models/credential-library';
|
||||
|
||||
import {
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
} from 'api/models/credential';
|
||||
/**
|
||||
* Enum options for credential library.
|
||||
*/
|
||||
export const options = {
|
||||
http_method: ['GET', 'POST'],
|
||||
key_type: ['ed25519', 'ecdsa', 'rsa'],
|
||||
credential_types: ['ssh_private_key', 'username_password'],
|
||||
credential_types: [
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
],
|
||||
mapping_overrides: {
|
||||
username_password: ['username_attribute', 'password_attribute'],
|
||||
ssh_private_key: [
|
||||
@@ -19,6 +27,11 @@ export const options = {
|
||||
'private_key_passphrase_attribute',
|
||||
'username_attribute',
|
||||
],
|
||||
username_password_domain: [
|
||||
'username_attribute',
|
||||
'password_attribute',
|
||||
'domain_attribute',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -12,10 +12,12 @@ import GeneratedCredentialModel from '../generated/models/credential';
|
||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD = 'username_password';
|
||||
export const TYPE_CREDENTIAL_SSH_PRIVATE_KEY = 'ssh_private_key';
|
||||
export const TYPE_CREDENTIAL_JSON = 'json';
|
||||
export const TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN = 'username_password_domain';
|
||||
|
||||
export const TYPES_CREDENTIAL = Object.freeze([
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
TYPE_CREDENTIAL_JSON,
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,8 +7,14 @@ import factory from '../generated/factories/credential';
|
||||
import { faker } from '@faker-js/faker';
|
||||
import permissions from '../helpers/permissions';
|
||||
import generatedId from '../helpers/id';
|
||||
import {
|
||||
TYPES_CREDENTIAL,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
} from 'api/models/credential';
|
||||
|
||||
const types = ['username_password', 'ssh_private_key', 'json'];
|
||||
const types = [...TYPES_CREDENTIAL];
|
||||
|
||||
export default factory.extend({
|
||||
type: (i) => types[i % types.length],
|
||||
@@ -20,6 +26,8 @@ export default factory.extend({
|
||||
return generatedId('credup_');
|
||||
case 'json':
|
||||
return generatedId('credjson_');
|
||||
case 'username_password_domain':
|
||||
return generatedId('credupd_');
|
||||
}
|
||||
},
|
||||
authorized_actions: () =>
|
||||
@@ -30,8 +38,21 @@ export default factory.extend({
|
||||
'delete',
|
||||
],
|
||||
attributes() {
|
||||
return {
|
||||
username: faker.internet.userName(),
|
||||
};
|
||||
switch (this.type) {
|
||||
case TYPE_CREDENTIAL_USERNAME_PASSWORD:
|
||||
return { username: faker.internet.userName() };
|
||||
|
||||
case TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN:
|
||||
return {
|
||||
username: faker.internet.userName(),
|
||||
domain: faker.internet.domainName(),
|
||||
};
|
||||
|
||||
case TYPE_CREDENTIAL_SSH_PRIVATE_KEY:
|
||||
return { username: faker.internet.userName() };
|
||||
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import {
|
||||
TYPE_CREDENTIAL_JSON,
|
||||
TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN,
|
||||
TYPE_CREDENTIAL_USERNAME_PASSWORD
|
||||
} from 'api/models/credential';
|
||||
|
||||
|
||||
module('Unit | Model | credential', function (hooks) {
|
||||
setupTest(hooks);
|
||||
@@ -28,12 +35,12 @@ module('Unit | Model | credential', function (hooks) {
|
||||
|
||||
test('it has isJSON property and returns the expected values', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('credential', { type: 'json' });
|
||||
const modelA = store.createRecord('credential', { type: TYPE_CREDENTIAL_JSON });
|
||||
const modelB = store.createRecord('credential', {
|
||||
type: 'username_password',
|
||||
type: TYPE_CREDENTIAL_USERNAME_PASSWORD,
|
||||
});
|
||||
const modelC = store.createRecord('credential', {
|
||||
type: 'ssh_private_key',
|
||||
type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY,
|
||||
});
|
||||
|
||||
assert.true(modelA.isJSON);
|
||||
@@ -44,15 +51,17 @@ module('Unit | Model | credential', function (hooks) {
|
||||
test('it has isUnknown property and returns the expected values', async function (assert) {
|
||||
const store = this.owner.lookup('service:store');
|
||||
const modelA = store.createRecord('credential', {
|
||||
type: 'username_password',
|
||||
type: TYPE_CREDENTIAL_USERNAME_PASSWORD
|
||||
});
|
||||
const modelB = store.createRecord('credential', {
|
||||
type: 'ssh_private_key',
|
||||
type: TYPE_CREDENTIAL_SSH_PRIVATE_KEY
|
||||
});
|
||||
const modelC = store.createRecord('credential', { type: 'unknown' });
|
||||
const modelD = store.createRecord('credential', { type: TYPE_CREDENTIAL_USERNAME_PASSWORD_DOMAIN });
|
||||
|
||||
assert.false(modelA.isUnknown);
|
||||
assert.false(modelB.isUnknown);
|
||||
assert.true(modelC.isUnknown);
|
||||
assert.false(modelD.isUnknown);
|
||||
});
|
||||
});
|
||||
@@ -904,10 +904,12 @@ credential-library:
|
||||
new: New Credential Library
|
||||
ssh_private_key: SSH Private Key
|
||||
username_password: Username & Password
|
||||
username_password_domain: Username, Password & Domain
|
||||
username_attribute: Username
|
||||
password_attribute: Password
|
||||
private_key_passphrase_attribute: Private Key Passphrase
|
||||
private_key_attribute: Private Key
|
||||
domain_attribute: Domain
|
||||
actions:
|
||||
create: New Credential Library
|
||||
delete: Delete Credential Library
|
||||
@@ -922,6 +924,7 @@ credential-library:
|
||||
credential_type:
|
||||
label: Credential Type
|
||||
help: Specify the type of credential this library will issue. This must be set to use credential injection. After this data is saved, it cannot be changed later.
|
||||
description: The type of credential the library uses for credential injection.
|
||||
credential_mapping_overrides:
|
||||
label: Credential mapping overrides
|
||||
help: Specify the credential fields to use instead of the defaults
|
||||
@@ -974,12 +977,14 @@ credential:
|
||||
types:
|
||||
username_password: Username & Password
|
||||
ssh_private_key: Username & Key Pair
|
||||
username_password_domain: Username, Password & Domain
|
||||
json: JSON
|
||||
unknown: Unknown
|
||||
help:
|
||||
username_password: Connect using the provided username and password.
|
||||
ssh_private_key: Connect using a username, public key, and private key.
|
||||
json: Connect using a JSON blob containing the credentials.
|
||||
username_password_domain: Includes a domain field for Active Directory
|
||||
form:
|
||||
username:
|
||||
label: Username
|
||||
@@ -987,6 +992,9 @@ credential:
|
||||
password:
|
||||
label: Password
|
||||
help: The password to use when signing in with this credential store.
|
||||
domain:
|
||||
label: Domain
|
||||
help: The Active Directory domain or FQDN (e.g., contoso.local)
|
||||
private_key:
|
||||
label: SSH Private Key
|
||||
help: The private key to use when connecting with this credential store.
|
||||
|
||||
@@ -224,6 +224,71 @@ export class CredentialStoresPage extends BaseResourcePage {
|
||||
return credentialName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a static username, password & Domain credential.
|
||||
* Assumes you have selected the desired project.
|
||||
* @param {string} username username for the credential
|
||||
* @param {string} password password for the credential
|
||||
* @param {string} domain domain for the credential
|
||||
* @returns Name of the credential
|
||||
*/
|
||||
async createStaticCredentialUsernamePasswordDomain(
|
||||
username,
|
||||
password,
|
||||
domain,
|
||||
) {
|
||||
const credentialsBreadcrumbIsVisible = await this.page
|
||||
.getByRole('breadcrumbs', { name: 'Credentials' })
|
||||
.isVisible();
|
||||
if (credentialsBreadcrumbIsVisible) {
|
||||
await this.page.getByRole('breadcrumbs', { name: 'Credentials' }).click();
|
||||
} else {
|
||||
await this.page
|
||||
.getByRole('link', { name: 'Credentials', exact: true })
|
||||
.click();
|
||||
}
|
||||
|
||||
await expect(
|
||||
this.page
|
||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
||||
.getByText('Credentials'),
|
||||
).toBeVisible();
|
||||
|
||||
const newButtonIsVisible = await this.page
|
||||
.getByRole('link', { name: 'New', exact: true })
|
||||
.isVisible();
|
||||
if (newButtonIsVisible) {
|
||||
await this.page.getByRole('link', { name: 'New', exact: true }).click();
|
||||
} else {
|
||||
await this.page.getByText('Manage').click();
|
||||
await this.page.getByRole('link', { name: 'New Credential' }).click();
|
||||
}
|
||||
|
||||
const credentialName = 'Credential ' + nanoid();
|
||||
await this.page.getByLabel('Name (Optional)').fill(credentialName);
|
||||
await this.page.getByLabel('Description').fill('This is an automated test');
|
||||
await this.page
|
||||
.getByRole('group', { name: 'Type' })
|
||||
.getByLabel('Username, Password & Domain')
|
||||
.click();
|
||||
await this.page
|
||||
.getByLabel('Username Required', { exact: true })
|
||||
.fill(username);
|
||||
await this.page
|
||||
.getByLabel('Password Required', { exact: true })
|
||||
.fill(password);
|
||||
await this.page.getByLabel('Domain Required', { exact: true }).fill(domain);
|
||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||
await this.dismissSuccessAlert();
|
||||
await expect(
|
||||
this.page
|
||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
||||
.getByText(credentialName),
|
||||
).toBeVisible();
|
||||
|
||||
return credentialName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a vault-generic credential library. Assumes you have selected
|
||||
* the desired credential store.
|
||||
|
||||
@@ -46,7 +46,9 @@ test(
|
||||
targetPort,
|
||||
);
|
||||
const credentialStoresPage = new CredentialStoresPage(page);
|
||||
await credentialStoresPage.createStaticCredentialStore();
|
||||
const credentialStoreName =
|
||||
await credentialStoresPage.createStaticCredentialStore();
|
||||
|
||||
const credentialName =
|
||||
await credentialStoresPage.createStaticCredentialKeyPair(
|
||||
sshUser,
|
||||
@@ -58,6 +60,32 @@ test(
|
||||
'testPassword',
|
||||
);
|
||||
|
||||
const credentialName3 =
|
||||
await credentialStoresPage.createStaticCredentialUsernamePasswordDomain(
|
||||
sshUser,
|
||||
'testPassword',
|
||||
'testDomain',
|
||||
);
|
||||
|
||||
// TODO: we can test the target association after RDP target is implemented, for now we test if the credential is visible in the UI
|
||||
await page
|
||||
.getByRole('navigation', { name: 'Application local navigation' })
|
||||
.getByRole('link', { name: 'Credential Stores' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('link', { name: credentialStoreName }),
|
||||
).toBeVisible();
|
||||
|
||||
// Navigate to the Credential Store
|
||||
await page.getByRole('link', { name: credentialStoreName }).click();
|
||||
|
||||
// click on the Credentials tab
|
||||
await page.getByRole('link', { name: 'Credentials' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link').getByText(credentialName3),
|
||||
).toBeVisible();
|
||||
|
||||
await targetsPage.addBrokeredCredentialsToTarget(
|
||||
targetName,
|
||||
credentialName,
|
||||
|
||||
@@ -22,6 +22,7 @@ test.beforeAll(async () => {
|
||||
let orgName;
|
||||
let projectName;
|
||||
let targetName;
|
||||
let credentialStoreName;
|
||||
|
||||
test.beforeEach(async ({ page, targetAddress, targetPort }) => {
|
||||
await page.goto('/');
|
||||
@@ -36,7 +37,8 @@ test.beforeEach(async ({ page, targetAddress, targetPort }) => {
|
||||
targetPort,
|
||||
);
|
||||
const credentialStoresPage = new CredentialStoresPage(page);
|
||||
await credentialStoresPage.createStaticCredentialStore();
|
||||
credentialStoreName =
|
||||
await credentialStoresPage.createStaticCredentialStore();
|
||||
});
|
||||
|
||||
test(
|
||||
@@ -280,6 +282,7 @@ test(
|
||||
}) => {
|
||||
try {
|
||||
const credentialStoresPage = new CredentialStoresPage(page);
|
||||
|
||||
const credentialName =
|
||||
await credentialStoresPage.createStaticCredentialKeyPair(
|
||||
sshUser,
|
||||
@@ -291,6 +294,33 @@ test(
|
||||
'testPassword',
|
||||
);
|
||||
|
||||
const credentialName3 =
|
||||
await credentialStoresPage.createStaticCredentialUsernamePasswordDomain(
|
||||
sshUser,
|
||||
'testPassword',
|
||||
'testDomain',
|
||||
);
|
||||
|
||||
// TODO: Handle target association later when we work on RDP targets and modify the existing target association
|
||||
// for now we simply test if the credential is visible in the UI
|
||||
await page
|
||||
.getByRole('navigation', { name: 'Application local navigation' })
|
||||
.getByRole('link', { name: 'Credential Stores' })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByRole('link', { name: credentialStoreName }),
|
||||
).toBeVisible();
|
||||
|
||||
// Navigate to the Credential Store
|
||||
await page.getByRole('link', { name: credentialStoreName }).click();
|
||||
|
||||
// click on the Credentials tab
|
||||
await page.getByRole('link', { name: 'Credentials' }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('link').getByText(credentialName3),
|
||||
).toBeVisible();
|
||||
|
||||
const targetsPage = new TargetsPage(page);
|
||||
await targetsPage.addBrokeredCredentialsToTarget(
|
||||
targetName,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* SPDX-License-Identifier: BUSL-1.1
|
||||
*/
|
||||
|
||||
import { expect } from '@playwright/test';
|
||||
import { test } from '../../global-setup.js';
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
@@ -53,31 +54,14 @@ test(
|
||||
let orgId;
|
||||
let connect;
|
||||
try {
|
||||
execSync(
|
||||
`vault policy write ${boundaryPolicyName} ./admin/tests/fixtures/boundary-controller-policy.hcl`,
|
||||
const clientToken = await vaultCli.setupVaultCredentialStore(
|
||||
boundaryPolicyName,
|
||||
secretPolicyName,
|
||||
secretsPath,
|
||||
secretName,
|
||||
sshUser,
|
||||
sshKeyPath,
|
||||
);
|
||||
execSync(`vault secrets enable -path=${secretsPath} kv-v2`);
|
||||
execSync(
|
||||
`vault kv put -mount ${secretsPath} ${secretName} ` +
|
||||
` username=${sshUser}` +
|
||||
` private_key=@${sshKeyPath}`,
|
||||
);
|
||||
execSync(
|
||||
`vault policy write ${secretPolicyName} ./admin/tests/fixtures/kv-policy.hcl`,
|
||||
);
|
||||
const vaultToken = JSON.parse(
|
||||
execSync(
|
||||
`vault token create` +
|
||||
` -no-default-policy=true` +
|
||||
` -policy=${boundaryPolicyName}` +
|
||||
` -policy=${secretPolicyName}` +
|
||||
` -orphan=true` +
|
||||
` -period=20m` +
|
||||
` -renewable=true` +
|
||||
` -format=json`,
|
||||
),
|
||||
);
|
||||
const clientToken = vaultToken.auth.client_token;
|
||||
|
||||
const orgsPage = new OrgsPage(page);
|
||||
const orgName = await orgsPage.createOrg();
|
||||
@@ -134,3 +118,49 @@ test(
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'Vault Credential Store - Username, Password & Domain credential type',
|
||||
{ tag: ['@ent', '@aws', '@docker'] },
|
||||
async ({ page, sshUser, sshKeyPath, vaultAddrPrivate, workerTagEgress }) => {
|
||||
let orgId;
|
||||
try {
|
||||
// Set up Vault credential store
|
||||
const clientToken = await vaultCli.setupVaultCredentialStore(
|
||||
boundaryPolicyName,
|
||||
secretPolicyName,
|
||||
secretsPath,
|
||||
secretName,
|
||||
sshUser,
|
||||
sshKeyPath,
|
||||
);
|
||||
const orgsPage = new OrgsPage(page);
|
||||
await orgsPage.createOrg();
|
||||
const projectsPage = new ProjectsPage(page);
|
||||
await projectsPage.createProject();
|
||||
|
||||
const credentialStoresPage = new CredentialStoresPage(page);
|
||||
await credentialStoresPage.createVaultCredentialStoreWithWorkerFilter(
|
||||
vaultAddrPrivate,
|
||||
clientToken,
|
||||
`"${workerTagEgress}" in "/tags/type"`,
|
||||
);
|
||||
|
||||
const credentialLibraryName =
|
||||
await credentialStoresPage.createVaultGenericCredentialLibraryEnt(
|
||||
`${secretsPath}/data/${secretName}`,
|
||||
'Username, Password & Domain',
|
||||
);
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByRole('navigation', { name: 'breadcrumbs' })
|
||||
.getByText(credentialLibraryName),
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
if (orgId) {
|
||||
await boundaryCli.deleteScope(orgId);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -163,3 +163,59 @@ export async function setupVaultOidc(
|
||||
authPolicyName: authPolicyName,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a Vault credential store by configuring policies, enabling secrets,
|
||||
* and creating a token with specific permissions.
|
||||
*
|
||||
* @async
|
||||
* @function setupVaultCredentialStore
|
||||
* @param {string} boundaryPolicyName - The name of the boundary policy to be created.
|
||||
* @param {string} secretPolicyName - The name of the secret policy to be created.
|
||||
* @param {string} secretsPath - The path where the secrets engine will be enabled.
|
||||
* @param {string} secretName - The name of the secret to be stored in the secrets engine.
|
||||
* @param {string} sshUser - The SSH username to be stored in the secret.
|
||||
* @param {string} sshKeyPath - The file path to the SSH private key to be stored in the secret.
|
||||
* @returns {Promise<string>} Vault Token
|
||||
*/
|
||||
export async function setupVaultCredentialStore(
|
||||
boundaryPolicyName,
|
||||
secretPolicyName,
|
||||
secretsPath,
|
||||
secretName,
|
||||
sshUser,
|
||||
sshKeyPath,
|
||||
) {
|
||||
let clientToken;
|
||||
try {
|
||||
execSync(
|
||||
`vault policy write ${boundaryPolicyName} ./admin/tests/fixtures/boundary-controller-policy.hcl`,
|
||||
);
|
||||
execSync(`vault secrets enable -path=${secretsPath} kv-v2`);
|
||||
execSync(
|
||||
`vault kv put -mount ${secretsPath} ${secretName} ` +
|
||||
` username=${sshUser}` +
|
||||
` private_key=@${sshKeyPath}`,
|
||||
);
|
||||
execSync(
|
||||
`vault policy write ${secretPolicyName} ./admin/tests/fixtures/kv-policy.hcl`,
|
||||
);
|
||||
const vaultToken = JSON.parse(
|
||||
execSync(
|
||||
`vault token create \
|
||||
-no-default-policy=true \
|
||||
-policy=${boundaryPolicyName} \
|
||||
-policy=${secretPolicyName} \
|
||||
-orphan=true \
|
||||
-period=20m \
|
||||
-renewable=true \
|
||||
-format=json`,
|
||||
),
|
||||
);
|
||||
clientToken = vaultToken.auth.client_token;
|
||||
} catch (e) {
|
||||
console.log(`${e.stderr}`);
|
||||
}
|
||||
|
||||
return clientToken;
|
||||
}
|
||||
@@ -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|
|
||||
|
||||
+1
-1
@@ -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',
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
+232
@@ -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');
|
||||
});
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user