/** * 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 doesn’t 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"'); }); }); });