Files
nomad/ui/tests/integration/components/variable-form-test.js
Phil Renaud e8db588368 ui: Warning and better error handling for invalid-named variables and job templates (#19989)
* Warning and better error handling for invalid-named variables and job templates

* Warning and better error handling for invalid-named variables and job templates

* Tests for variable pathname warnings

* Only show the bad-name warning if the variable is being created and path is editable
2024-02-15 11:54:09 -05:00

515 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: BUSL-1.1
*/
import { module, test } from 'qunit';
import { setupRenderingTest } from 'ember-qunit';
import { hbs } from 'ember-cli-htmlbars';
import { componentA11yAudit } from 'nomad-ui/tests/helpers/a11y-audit';
import { click, typeIn, find, findAll, render } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
import { codeFillable, code } from 'nomad-ui/tests/pages/helpers/codemirror';
import percySnapshot from '@percy/ember';
import { clickToggle, clickOption } from 'nomad-ui/tests/helpers/helios';
import faker from 'nomad-ui/mirage/faker';
module('Integration | Component | variable-form', function (hooks) {
setupRenderingTest(hooks);
setupMirage(hooks);
setupCodeMirror(hooks);
test('passes an accessibility audit', async function (assert) {
assert.expect(1);
this.set(
'mockedModel',
server.create('variable', {
keyValues: [{ key: '', value: '' }],
})
);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await componentA11yAudit(this.element, assert);
});
test('shows a single row by default and modifies on "Add More" and "Delete"', async function (assert) {
this.set(
'mockedModel',
server.create('variable', {
keyValues: [{ key: '', value: '' }],
})
);
assert.expect(7);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
assert.equal(
findAll('div.key-value').length,
1,
'A single KV row exists by default'
);
assert
.dom('[data-test-add-kv]')
.isDisabled(
'The "Add More" button is disabled until key and value are filled'
);
await typeIn('[data-test-var-key]', 'foo');
assert
.dom('[data-test-add-kv]')
.isDisabled(
'The "Add More" button is still disabled with only key filled'
);
await typeIn('[data-test-var-value]', 'bar');
assert
.dom('[data-test-add-kv]')
.isNotDisabled(
'The "Add More" button is no longer disabled after key and value are filled'
);
await click('[data-test-add-kv]');
assert.equal(
findAll('div.key-value').length,
2,
'A second KV row exists after adding a new one'
);
await typeIn('.key-value:last-of-type [data-test-var-key]', 'foo');
await typeIn('.key-value:last-of-type [data-test-var-value]', 'bar');
await click('[data-test-add-kv]');
assert.equal(
findAll('div.key-value').length,
3,
'A third KV row exists after adding a new one'
);
await click('.delete-entry-button');
assert.equal(
findAll('div.key-value').length,
2,
'Back down to two rows after hitting delete'
);
});
module('editing and creating new key/value pairs', function () {
test('it should allow each key/value row to toggle password visibility', async function (assert) {
faker.seed(1);
this.set(
'mockedModel',
server.create('variable', {
keyValues: [{ key: 'foo', value: 'bar' }],
})
);
assert.expect(6);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await click('[data-test-add-kv]'); // add a second variable
findAll('.value-label').forEach((label, iter) => {
const maskedInput = label.querySelector('.hds-form-masked-input');
assert.ok(
maskedInput.classList.contains('hds-form-masked-input--is-masked'),
`Value ${iter + 1} is hidden by default`
);
});
await click('.hds-form-visibility-toggle');
const [firstRow, secondRow] = findAll('.hds-form-masked-input');
assert.ok(
firstRow.classList.contains('hds-form-masked-input--is-not-masked'),
'Only the row that is clicked on toggles visibility'
);
assert.ok(
secondRow.classList.contains('hds-form-masked-input--is-masked'),
'Rows that are not clicked remain obscured'
);
await click('.hds-form-visibility-toggle');
assert.ok(
firstRow.classList.contains('hds-form-masked-input--is-masked'),
'Only the row that is clicked on toggles visibility'
);
assert.ok(
secondRow.classList.contains('hds-form-masked-input--is-masked'),
'Rows that are not clicked remain obscured'
);
await percySnapshot(assert);
});
});
test('Existing variable shows properties by default', async function (assert) {
assert.expect(13);
const keyValues = [
{ key: 'my-completely-normal-key', value: 'never' },
{ key: 'another key, but with spaces', value: 'gonna' },
{ key: 'once/more/with/slashes', value: 'give' },
{ key: 'and_some_underscores', value: 'you' },
{ key: 'and\\now/for-something_completely@different', value: 'up' },
];
this.set(
'mockedModel',
server.create('variable', {
path: 'my/path/to',
keyValues,
})
);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
assert.equal(
findAll('div.key-value').length,
5,
'Shows 5 existing key values'
);
assert.equal(
findAll('.delete-entry-button').length,
5,
'Shows "delete" for all five rows'
);
assert.equal(
findAll('[data-test-add-kv]').length,
1,
'Shows "add more" only on the last row'
);
findAll('div.key-value').forEach((row, idx) => {
assert.equal(
row.querySelector(`[data-test-var-key]`).value,
keyValues[idx].key,
`Key ${idx + 1} is correct`
);
assert.equal(
row.querySelector(`[data-test-var-value]`).value,
keyValues[idx].value,
keyValues[idx].value
);
});
});
test('Prevent editing path input on existing variables', async function (assert) {
assert.expect(3);
const variable = await this.server.create('variable', {
name: 'foo',
namespace: 'bar',
path: '/baz/bat',
keyValues: [{ key: '', value: '' }],
});
variable.isNew = false;
this.set('variable', variable);
await render(hbs`<VariableForm @model={{this.variable}} />`);
assert.dom('[data-test-path-input]').hasValue('/baz/bat', 'Path is set');
assert
.dom('[data-test-path-input]')
.isDisabled('Existing variable is in disabled state');
variable.isNew = true;
variable.path = '';
this.set('variable', variable);
await render(hbs`<VariableForm @model={{this.variable}} />`);
assert
.dom('[data-test-path-input]')
.isNotDisabled('New variable is not in disabled state');
});
module('Validation', function () {
test('warns when you try to create a path that already exists', async function (assert) {
this.server.createList('namespace', 3);
this.set(
'mockedModel',
server.create('variable', {
path: '',
keyValues: [{ key: '', value: '' }],
})
);
server.create('variable', {
path: 'baz/bat',
});
server.create('variable', {
path: 'baz/bat/qux',
namespace: server.db.namespaces[2].id,
});
this.set('existingVariables', server.db.variables.toArray());
await render(
hbs`<VariableForm @model={{this.mockedModel}} @existingVariables={{this.existingVariables}} />`
);
await typeIn('[data-test-path-input]', 'foo/bar');
assert.dom('[data-test-duplicate-variable-error]').doesNotExist();
assert
.dom('[data-test-path-input]')
.doesNotHaveClass('hds-form-text-input--is-invalid');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', 'baz/bat');
assert.dom('[data-test-duplicate-variable-error]').exists();
assert
.dom('[data-test-path-input]')
.hasClass('hds-form-text-input--is-invalid');
await clickToggle('[data-test-variable-namespace-filter]');
await clickOption(
'[data-test-variable-namespace-filter]',
server.db.namespaces[2].id
);
assert.dom('[data-test-duplicate-variable-error]').doesNotExist();
assert
.dom('[data-test-path-input]')
.doesNotHaveClass('hds-form-text-input--is-invalid');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', 'baz/bat/qux');
assert.dom('[data-test-duplicate-variable-error]').exists();
assert
.dom('[data-test-path-input]')
.hasClass('hds-form-text-input--is-invalid');
});
test('warns when you try to create a path with invalid characters', async function (assert) {
this.server.createList('namespace', 3);
this.set(
'mockedModel',
server.create('variable', {
path: '',
keyValues: [{ key: '', value: '' }],
})
);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await typeIn('[data-test-path-input]', 'foo-bar');
assert.dom('[data-test-invalid-path-error]').doesNotExist();
assert
.dom('[data-test-path-input]')
.doesNotHaveClass('hds-form-text-input--is-invalid');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', 'foo bar');
assert
.dom('[data-test-invalid-path-error]')
.exists('Space makes path invalid');
assert
.dom('[data-test-path-input]')
.hasClass('hds-form-text-input--is-invalid');
document.querySelector('[data-test-path-input]').value = ''; // clear current input
await typeIn('[data-test-path-input]', '_');
assert.dom('[data-test-invalid-path-error]').doesNotExist();
// Try 129 characters
let longString = 'a'.repeat(129);
await typeIn('[data-test-path-input]', longString);
assert
.dom('[data-test-invalid-path-error]')
.exists('Long name makes path invalid');
});
test('warns you when you set a key with . in it', async function (assert) {
this.set(
'mockedModel',
server.create('variable', {
keyValues: [{ key: '', value: '' }],
})
);
const testCases = [
{
name: 'valid key',
key: 'superSecret2',
warn: false,
},
{
name: 'invalid key with dot',
key: 'super.secret',
warn: true,
},
{
name: 'invalid key with slash',
key: 'super/secret',
warn: true,
},
{
name: 'invalid key with emoji',
key: 'supersecretspy🕵',
warn: true,
},
{
name: 'unicode letters',
key: '世界',
warn: false,
},
{
name: 'unicode numbers',
key: '٣٢١',
warn: false,
},
{
name: 'unicode letters and numbers',
key: '世٢界١',
warn: false,
},
];
for (const tc of testCases) {
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await typeIn('[data-test-var-key]', tc.key);
if (tc.warn) {
assert.dom('.key-value-error').exists(tc.name);
} else {
assert.dom('.key-value-error').doesNotExist(tc.name);
}
}
});
test('warns you when you create a duplicate key', async function (assert) {
this.set(
'mockedModel',
server.create('variable', {
keyValues: [{ key: 'myKey', value: 'myVal' }],
})
);
await render(hbs`<VariableForm @model={{this.mockedModel}} />`);
await click('[data-test-add-kv]');
const secondKey = document.querySelectorAll('[data-test-var-key]')[1];
await typeIn(secondKey, 'myWonderfulKey');
assert.dom('.key-value-error').doesNotExist();
secondKey.value = '';
await typeIn(secondKey, 'myKey');
assert.dom('.key-value-error').exists();
});
});
module('Views', function () {
test('Allows you to swap between JSON and Key/Value Views', async function (assert) {
this.set(
'mockedModel',
server.create('variable', {
path: '',
keyValues: [{ key: '', value: '' }],
})
);
this.set(
'existingVariables',
server.createList('variable', 1, {
path: 'baz/bat',
})
);
this.set('view', 'table');
await render(
hbs`<VariableForm @model={{this.mockedModel}} @existingVariables={{this.existingVariables}} @view={{this.view}} />`
);
assert.dom('.key-value').exists();
assert.dom('.CodeMirror').doesNotExist();
this.set('view', 'json');
assert.dom('.key-value').doesNotExist();
assert.dom('.CodeMirror').exists();
});
test('Persists Key/Values table data to JSON', async function (assert) {
faker.seed(1);
assert.expect(2);
const keyValues = [
{ key: 'foo', value: '123' },
{ key: 'bar', value: '456' },
];
this.set(
'mockedModel',
server.create('variable', {
path: '',
keyValues,
})
);
this.set('view', 'json');
await render(
hbs`<VariableForm @model={{this.mockedModel}} @view={{this.view}} />`
);
await percySnapshot(assert);
const keyValuesAsJSON = keyValues.reduce((acc, { key, value }) => {
acc[key] = value;
return acc;
}, {});
assert.equal(
code('.editor-wrapper').get(),
JSON.stringify(keyValuesAsJSON, null, 2),
'JSON editor contains the key values, stringified, by default'
);
this.set('view', 'table');
await click('[data-test-add-kv]');
await typeIn('.key-value:last-of-type [data-test-var-key]', 'howdy');
await typeIn('.key-value:last-of-type [data-test-var-value]', 'partner');
this.set('view', 'json');
assert.ok(
code('[data-test-json-editor]').get().includes('"howdy": "partner"'),
'JSON editor contains the new key value'
);
});
test('Persists JSON data to Key/Values table', async function (assert) {
const keyValues = [{ key: '', value: '' }];
this.set(
'mockedModel',
server.create('variable', {
path: '',
keyValues,
})
);
this.set('view', 'json');
await render(
hbs`<VariableForm @model={{this.mockedModel}} @view={{this.view}} />`
);
codeFillable('[data-test-json-editor]').get()(
JSON.stringify({ golden: 'gate' }, null, 2)
);
this.set('view', 'table');
assert.equal(
find(`.key-value:last-of-type [data-test-var-key]`).value,
'golden',
'Key persists from JSON to Table'
);
assert.equal(
find(`.key-value:last-of-type [data-test-var-value]`).value,
'gate',
'Value persists from JSON to Table'
);
});
});
});