open-nomad/ui/tests/acceptance/job-run-test.js
2023-04-10 15:36:59 +00:00

600 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. 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: MPL-2.0
*/
import AdapterError from '@ember-data/adapter/error';
import {
click,
currentRouteName,
currentURL,
fillIn,
visit,
settled,
} from '@ember/test-helpers';
import { assign } from '@ember/polyfills';
import { module, test } from 'qunit';
import {
selectChoose,
clickTrigger,
} from 'ember-power-select/test-support/helpers';
import { setupApplicationTest } from 'ember-qunit';
import { setupMirage } from 'ember-cli-mirage/test-support';
import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
import JobRun from 'nomad-ui/tests/pages/jobs/run';
import percySnapshot from '@percy/ember';
const newJobName = 'new-job';
const newJobTaskGroupName = 'redis';
const newJobNamespace = 'default';
let managementToken, clientToken;
const jsonJob = (overrides) => {
return JSON.stringify(
assign(
{},
{
Name: newJobName,
Namespace: newJobNamespace,
Datacenters: ['dc1'],
Priority: 50,
TaskGroups: [
{
Name: newJobTaskGroupName,
Tasks: [
{
Name: 'redis',
Driver: 'docker',
},
],
},
],
},
overrides
),
null,
2
);
};
module('Acceptance | job run', function (hooks) {
setupApplicationTest(hooks);
setupMirage(hooks);
setupCodeMirror(hooks);
hooks.beforeEach(function () {
// Required for placing allocations (a result of creating jobs)
server.create('node');
managementToken = server.create('token');
clientToken = server.create('token');
window.localStorage.nomadTokenSecret = managementToken.secretId;
});
test('it passes an accessibility audit', async function (assert) {
assert.expect(1);
await JobRun.visit();
await a11yAudit(assert);
});
test('visiting /jobs/run', async function (assert) {
await JobRun.visit();
assert.equal(currentURL(), '/jobs/run');
assert.equal(document.title, 'Run a job - Nomad');
});
test('when submitting a job, the site redirects to the new job overview page', async function (assert) {
const spec = jsonJob();
await JobRun.visit();
await JobRun.editor.editor.fillIn(spec);
await JobRun.editor.plan();
await JobRun.editor.run();
assert.equal(
currentURL(),
`/jobs/${newJobName}@${newJobNamespace}`,
`Redirected to the job overview page for ${newJobName}`
);
});
test('when submitting a job to a different namespace, the redirect to the job overview page takes namespace into account', async function (assert) {
const newNamespace = 'second-namespace';
server.create('namespace', { id: newNamespace });
const spec = jsonJob({ Namespace: newNamespace });
await JobRun.visit();
await JobRun.editor.editor.fillIn(spec);
await JobRun.editor.plan();
await JobRun.editor.run();
assert.equal(
currentURL(),
`/jobs/${newJobName}@${newNamespace}`,
`Redirected to the job overview page for ${newJobName} and switched the namespace to ${newNamespace}`
);
});
test('when the user doesnt have permission to run a job, redirects to the job overview page', async function (assert) {
window.localStorage.nomadTokenSecret = clientToken.secretId;
await JobRun.visit();
assert.equal(currentURL(), '/jobs');
});
test('when using client token user can still go to job page if they have correct permissions', async function (assert) {
const clientTokenWithPolicy = server.create('token');
const newNamespace = 'second-namespace';
server.create('namespace', { id: newNamespace });
server.create('job', {
groupCount: 0,
createAllocations: false,
shallow: true,
noActiveDeployment: true,
namespaceId: newNamespace,
});
const policy = server.create('policy', {
id: 'something',
name: 'something',
rulesJSON: {
Namespaces: [
{
Name: newNamespace,
Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
},
],
},
});
clientTokenWithPolicy.policyIds = [policy.id];
clientTokenWithPolicy.save();
window.localStorage.nomadTokenSecret = clientTokenWithPolicy.secretId;
await JobRun.visit({ namespace: newNamespace });
assert.equal(currentURL(), `/jobs/run?namespace=${newNamespace}`);
});
module('job template flow', function () {
test('allows user with the correct permissions to fill in the editor using a job template', async function (assert) {
assert.expect(10);
// Arrange
await JobRun.visit();
assert
.dom('[data-test-choose-template]')
.exists('A button allowing a user to select a template appears.');
server.get('/vars', function (_server, fakeRequest) {
assert.deepEqual(
fakeRequest.queryParams,
{
prefix: 'nomad/job-templates',
namespace: '*',
},
'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.'
);
return [
{
ID: 'nomad/job-templates/foo',
Namespace: 'default',
Path: 'nomad/job-templates/foo',
},
];
});
server.get(
'/var/nomad%2Fjob-templates%2Ffoo',
function (_server, fakeRequest) {
assert.deepEqual(
fakeRequest.queryParams,
{
namespace: 'default',
},
'Dispatches O(n+1) query to retrive items.'
);
return {
ID: 'nomad/job-templates/foo',
Namespace: 'default',
Path: 'nomad/job-templates/foo',
Items: {
template: 'Hello World!',
label: 'foo',
},
};
}
);
// Act
await click('[data-test-choose-template]');
assert.equal(currentRouteName(), 'jobs.run.templates.index');
// Assert
assert
.dom('[data-test-template-list]')
.exists('A list of available job templates is rendered.');
assert
.dom('[data-test-apply]')
.exists('A button to apply the selected templated is displayed.');
assert
.dom('[data-test-cancel]')
.exists('A button to cancel the template selection is displayed.');
await click('[data-test-template-card=Foo]');
await click('[data-test-apply]');
assert.equal(
currentURL(),
'/jobs/run?template=nomad%2Fjob-templates%2Ffoo%40default'
);
assert.dom('[data-test-editor]').containsText('Hello World!');
});
test('a user can create their own job template', async function (assert) {
assert.expect(7);
// Arrange
await JobRun.visit();
await click('[data-test-choose-template]');
// Assert
assert
.dom('[data-test-template-card]')
.exists({ count: 4 }, 'A list of default job templates is rendered.');
await click('[data-test-create-new-button]');
assert.equal(currentRouteName(), 'jobs.run.templates.new');
await fillIn('[data-test-template-name]', 'foo');
await fillIn('[data-test-template-description]', 'foo-bar-baz');
const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
codeMirror.setValue(jsonJob());
server.put('/var/:varId', function (_server, fakeRequest) {
assert.deepEqual(
JSON.parse(fakeRequest.requestBody),
{
Path: 'nomad/job-templates/foo',
CreateIndex: null,
ModifyIndex: null,
Namespace: 'default',
ID: 'nomad/job-templates/foo',
Items: { description: 'foo-bar-baz', template: jsonJob() },
},
'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.'
);
return {
Items: { description: 'foo-bar-baz', template: jsonJob() },
Namespace: 'default',
Path: 'nomad/job-templates/foo',
};
});
server.get('/vars', function (_server, fakeRequest) {
assert.deepEqual(
fakeRequest.queryParams,
{
prefix: 'nomad/job-templates',
namespace: '*',
},
'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.'
);
return [
{
ID: 'nomad/job-templates/foo',
Namespace: 'default',
Path: 'nomad/job-templates/foo',
},
];
});
server.get(
'/var/nomad%2Fjob-templates%2Ffoo',
function (_server, fakeRequest) {
assert.deepEqual(
fakeRequest.queryParams,
{
namespace: 'default',
},
'Dispatches O(n+1) query to retrive items.'
);
return {
ID: 'nomad/job-templates/foo',
Namespace: 'default',
Path: 'nomad/job-templates/foo',
Items: {
template: 'qud',
label: 'foo',
},
};
}
);
await click('[data-test-save-template]');
assert.equal(currentRouteName(), 'jobs.run.templates.index');
assert
.dom('[data-test-template-card=Foo]')
.exists('The newly created template appears in the list.');
});
test('a toast notification alerts the user if there is an error saving the newly created job template', async function (assert) {
assert.expect(5);
// Arrange
await JobRun.visit();
await click('[data-test-choose-template]');
// Assert
assert
.dom('[data-test-template-card]')
.exists({ count: 4 }, 'A list of default job templates is rendered.');
await click('[data-test-create-new-button]');
assert.equal(currentRouteName(), 'jobs.run.templates.new');
assert
.dom('[data-test-save-template]')
.isDisabled('the save button should be disabled if no path is set');
await fillIn('[data-test-template-name]', 'try@');
await fillIn('[data-test-template-description]', 'foo-bar-baz');
const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
codeMirror.setValue(jsonJob());
server.put('/var/:varId?cas=0', function () {
return new AdapterError({
detail: `invalid path "nomad/job-templates/try@"`,
status: 500,
});
});
await click('[data-test-save-template]');
assert.equal(
currentRouteName(),
'jobs.run.templates.new',
'We do not navigate away from the page if an error is returned by the API.'
);
assert
.dom('.flash-message.alert-critical')
.exists('A toast error message pops up.');
});
test('a user cannot create a job template if one with the same name and namespace already exists', async function (assert) {
assert.expect(4);
// Arrange
await JobRun.visit();
await click('[data-test-choose-template]');
server.create('variable', {
path: 'nomad/job-templates/foo',
namespace: 'default',
id: 'nomad/job-templates/foo',
});
server.create('namespace', { id: 'test' });
this.system = this.owner.lookup('service:system');
this.system.shouldShowNamespaces = true;
// Assert
assert
.dom('[data-test-template-card]')
.exists({ count: 4 }, 'A list of default job templates is rendered.');
await click('[data-test-create-new-button]');
assert.equal(currentRouteName(), 'jobs.run.templates.new');
await fillIn('[data-test-template-name]', 'foo');
assert
.dom('[data-test-duplicate-error]')
.exists('an error message alerts the user');
await clickTrigger('[data-test-namespace-facet]');
await selectChoose('[data-test-namespace-facet]', 'test');
assert
.dom('[data-test-duplicate-error]')
.doesNotExist(
'an error disappears when name or namespace combination is unique'
);
// Clean-up
this.system.shouldShowNamespaces = false;
});
test('a user can save code from the editor as a template', async function (assert) {
assert.expect(4);
// Arrange
await JobRun.visit();
await JobRun.editor.editor.fillIn(jsonJob());
await click('[data-test-save-as-template]');
assert.equal(
currentRouteName(),
'jobs.run.templates.new',
'We navigate template creation page.'
);
// Assert
assert
.dom('[data-test-template-name]')
.hasNoText('No template name is prefilled.');
assert
.dom('[data-test-template-description]')
.hasNoText('No template description is prefilled.');
const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
const json = codeMirror.getValue();
assert.equal(
json,
jsonJob(),
'Template is filled out with text from the editor.'
);
});
test('a user can edit a template', async function (assert) {
assert.expect(5);
// Arrange
server.create('variable', {
path: 'nomad/job-templates/foo',
namespace: 'default',
id: 'nomad/job-templates/foo',
Items: {},
});
await visit('/jobs/run/templates/manage');
assert.equal(currentRouteName(), 'jobs.run.templates.manage');
assert
.dom('[data-test-template-list]')
.exists('A list of templates is visible');
await percySnapshot(assert);
await click('[data-test-edit-template="nomad/job-templates/foo"]');
assert.equal(
currentRouteName(),
'jobs.run.templates.template',
'Navigates to edit template view'
);
server.put('/var/:varId', function (_server, fakeRequest) {
assert.deepEqual(
JSON.parse(fakeRequest.requestBody),
{
Path: 'nomad/job-templates/foo',
CreateIndex: null,
ModifyIndex: null,
Namespace: 'default',
ID: 'nomad/job-templates/foo',
Items: { description: 'baz qud thud' },
},
'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.'
);
return {
Items: { description: 'baz qud thud' },
Namespace: 'default',
Path: 'nomad/job-templates/foo',
};
});
await fillIn('[data-test-template-description]', 'baz qud thud');
await click('[data-test-edit-template]');
assert.equal(
currentRouteName(),
'jobs.run.templates.index',
'We navigate back to the templates view.'
);
});
test('a user can delete a template', async function (assert) {
assert.expect(5);
// Arrange
server.create('variable', {
path: 'nomad/job-templates/foo',
namespace: 'default',
id: 'nomad/job-templates/foo',
Items: {},
});
server.create('variable', {
path: 'nomad/job-templates/bar',
namespace: 'default',
id: 'nomad/job-templates/bar',
Items: {},
});
server.create('variable', {
path: 'nomad/job-templates/baz',
namespace: 'default',
id: 'nomad/job-templates/baz',
Items: {},
});
await visit('/jobs/run/templates/manage');
assert.equal(currentRouteName(), 'jobs.run.templates.manage');
assert
.dom('[data-test-template-list]')
.exists('A list of templates is visible');
await click('[data-test-idle-button]');
await click('[data-test-confirm-button]');
assert
.dom('[data-test-edit-template="nomad/job-templates/foo"]')
.doesNotExist('The template is removed from the list.');
await click('[data-test-edit-template="nomad/job-templates/bar"]');
await click('[data-test-idle-button]');
await click('[data-test-confirm-button]');
assert.equal(
currentRouteName(),
'jobs.run.templates.manage',
'We navigate back to the templates manager view.'
);
assert
.dom('[data-test-edit-template="nomad/job-templates/bar"]')
.doesNotExist('The template is removed from the list.');
});
test('a user sees accurate template information', async function (assert) {
assert.expect(3);
// Arrange
server.create('variable', {
path: 'nomad/job-templates/foo',
namespace: 'default',
id: 'nomad/job-templates/foo',
Items: {
template: 'qud',
label: 'foo',
description: 'bar baz',
},
});
await visit('/jobs/run/templates');
assert.equal(currentRouteName(), 'jobs.run.templates.index');
assert.dom('[data-test-template-card="Foo"]').exists();
this.store = this.owner.lookup('service:store');
this.store.unloadAll();
await settled();
assert
.dom('[data-test-template-card="Foo"]')
.doesNotExist(
'The template reactively updates to changes in the Ember Data Store.'
);
});
test('default templates', async function (assert) {
assert.expect(4);
const NUMBER_OF_DEFAULT_TEMPLATES = 4;
await visit('/jobs/run/templates');
assert.equal(currentRouteName(), 'jobs.run.templates.index');
assert
.dom('[data-test-template-card]')
.exists({ count: NUMBER_OF_DEFAULT_TEMPLATES });
await percySnapshot(assert);
await click('[data-test-template-card="Hello world"]');
await click('[data-test-apply]');
assert.equal(
currentURL(),
'/jobs/run?template=nomad%2Fjob-templates%2Fdefault%2Fhello-world'
);
assert.dom('[data-test-editor]').includesText('job "hello-world"');
});
});
});