github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/job-run-test.js (about)

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  import AdapterError from '@ember-data/adapter/error';
     7  import {
     8    click,
     9    currentRouteName,
    10    currentURL,
    11    fillIn,
    12    visit,
    13    settled,
    14  } from '@ember/test-helpers';
    15  import { assign } from '@ember/polyfills';
    16  import { module, test } from 'qunit';
    17  import {
    18    selectChoose,
    19    clickTrigger,
    20  } from 'ember-power-select/test-support/helpers';
    21  import { setupApplicationTest } from 'ember-qunit';
    22  import { setupMirage } from 'ember-cli-mirage/test-support';
    23  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
    24  import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror';
    25  import JobRun from 'nomad-ui/tests/pages/jobs/run';
    26  import percySnapshot from '@percy/ember';
    27  
    28  const newJobName = 'new-job';
    29  const newJobTaskGroupName = 'redis';
    30  const newJobNamespace = 'default';
    31  
    32  let managementToken, clientToken;
    33  
    34  const jsonJob = (overrides) => {
    35    return JSON.stringify(
    36      assign(
    37        {},
    38        {
    39          Name: newJobName,
    40          Namespace: newJobNamespace,
    41          Datacenters: ['dc1'],
    42          Priority: 50,
    43          TaskGroups: [
    44            {
    45              Name: newJobTaskGroupName,
    46              Tasks: [
    47                {
    48                  Name: 'redis',
    49                  Driver: 'docker',
    50                },
    51              ],
    52            },
    53          ],
    54        },
    55        overrides
    56      ),
    57      null,
    58      2
    59    );
    60  };
    61  
    62  module('Acceptance | job run', function (hooks) {
    63    setupApplicationTest(hooks);
    64    setupMirage(hooks);
    65    setupCodeMirror(hooks);
    66  
    67    hooks.beforeEach(function () {
    68      // Required for placing allocations (a result of creating jobs)
    69      server.create('node-pool');
    70      server.create('node');
    71  
    72      managementToken = server.create('token');
    73      clientToken = server.create('token');
    74  
    75      window.localStorage.nomadTokenSecret = managementToken.secretId;
    76    });
    77  
    78    test('it passes an accessibility audit', async function (assert) {
    79      assert.expect(1);
    80  
    81      await JobRun.visit();
    82      await a11yAudit(assert);
    83    });
    84  
    85    test('visiting /jobs/run', async function (assert) {
    86      await JobRun.visit();
    87  
    88      assert.equal(currentURL(), '/jobs/run');
    89      assert.equal(document.title, 'Run a job - Nomad');
    90    });
    91  
    92    test('when submitting a job, the site redirects to the new job overview page', async function (assert) {
    93      const spec = jsonJob();
    94  
    95      await JobRun.visit();
    96  
    97      await JobRun.editor.editor.fillIn(spec);
    98      await JobRun.editor.plan();
    99      await JobRun.editor.run();
   100      assert.equal(
   101        currentURL(),
   102        `/jobs/${newJobName}@${newJobNamespace}`,
   103        `Redirected to the job overview page for ${newJobName}`
   104      );
   105    });
   106  
   107    test('when submitting a job to a different namespace, the redirect to the job overview page takes namespace into account', async function (assert) {
   108      const newNamespace = 'second-namespace';
   109  
   110      server.create('namespace', { id: newNamespace });
   111      const spec = jsonJob({ Namespace: newNamespace });
   112  
   113      await JobRun.visit();
   114  
   115      await JobRun.editor.editor.fillIn(spec);
   116      await JobRun.editor.plan();
   117      await JobRun.editor.run();
   118      assert.equal(
   119        currentURL(),
   120        `/jobs/${newJobName}@${newNamespace}`,
   121        `Redirected to the job overview page for ${newJobName} and switched the namespace to ${newNamespace}`
   122      );
   123    });
   124  
   125    test('when the user doesn’t have permission to run a job, redirects to the job overview page', async function (assert) {
   126      window.localStorage.nomadTokenSecret = clientToken.secretId;
   127  
   128      await JobRun.visit();
   129      assert.equal(currentURL(), '/jobs');
   130    });
   131  
   132    test('when using client token user can still go to job page if they have correct permissions', async function (assert) {
   133      const clientTokenWithPolicy = server.create('token');
   134      const newNamespace = 'second-namespace';
   135  
   136      server.create('namespace', { id: newNamespace });
   137      server.create('job', {
   138        groupCount: 0,
   139        createAllocations: false,
   140        shallow: true,
   141        noActiveDeployment: true,
   142        namespaceId: newNamespace,
   143      });
   144  
   145      const policy = server.create('policy', {
   146        id: 'something',
   147        name: 'something',
   148        rulesJSON: {
   149          Namespaces: [
   150            {
   151              Name: newNamespace,
   152              Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
   153            },
   154          ],
   155        },
   156      });
   157  
   158      clientTokenWithPolicy.policyIds = [policy.id];
   159      clientTokenWithPolicy.save();
   160      window.localStorage.nomadTokenSecret = clientTokenWithPolicy.secretId;
   161  
   162      await JobRun.visit({ namespace: newNamespace });
   163      assert.equal(currentURL(), `/jobs/run?namespace=${newNamespace}`);
   164    });
   165  
   166    module('job template flow', function () {
   167      test('allows user with the correct permissions to fill in the editor using a job template', async function (assert) {
   168        assert.expect(10);
   169        // Arrange
   170        await JobRun.visit();
   171        assert
   172          .dom('[data-test-choose-template]')
   173          .exists('A button allowing a user to select a template appears.');
   174  
   175        server.get('/vars', function (_server, fakeRequest) {
   176          assert.deepEqual(
   177            fakeRequest.queryParams,
   178            {
   179              prefix: 'nomad/job-templates',
   180              namespace: '*',
   181            },
   182            'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.'
   183          );
   184          return [
   185            {
   186              ID: 'nomad/job-templates/foo',
   187              Namespace: 'default',
   188              Path: 'nomad/job-templates/foo',
   189            },
   190          ];
   191        });
   192  
   193        server.get(
   194          '/var/nomad%2Fjob-templates%2Ffoo',
   195          function (_server, fakeRequest) {
   196            assert.deepEqual(
   197              fakeRequest.queryParams,
   198              {
   199                namespace: 'default',
   200              },
   201              'Dispatches O(n+1) query to retrive items.'
   202            );
   203            return {
   204              ID: 'nomad/job-templates/foo',
   205              Namespace: 'default',
   206              Path: 'nomad/job-templates/foo',
   207              Items: {
   208                template: 'Hello World!',
   209                label: 'foo',
   210              },
   211            };
   212          }
   213        );
   214        // Act
   215        await click('[data-test-choose-template]');
   216        assert.equal(currentRouteName(), 'jobs.run.templates.index');
   217  
   218        // Assert
   219        assert
   220          .dom('[data-test-template-list]')
   221          .exists('A list of available job templates is rendered.');
   222        assert
   223          .dom('[data-test-apply]')
   224          .exists('A button to apply the selected templated is displayed.');
   225        assert
   226          .dom('[data-test-cancel]')
   227          .exists('A button to cancel the template selection is displayed.');
   228  
   229        await click('[data-test-template-card=Foo]');
   230        await click('[data-test-apply]');
   231  
   232        assert.equal(
   233          currentURL(),
   234          '/jobs/run?template=nomad%2Fjob-templates%2Ffoo%40default'
   235        );
   236        assert.dom('[data-test-editor]').containsText('Hello World!');
   237      });
   238  
   239      test('a user can create their own job template', async function (assert) {
   240        assert.expect(7);
   241        // Arrange
   242        await JobRun.visit();
   243        await click('[data-test-choose-template]');
   244  
   245        // Assert
   246        assert
   247          .dom('[data-test-template-card]')
   248          .exists({ count: 4 }, 'A list of default job templates is rendered.');
   249  
   250        await click('[data-test-create-new-button]');
   251        assert.equal(currentRouteName(), 'jobs.run.templates.new');
   252  
   253        await fillIn('[data-test-template-name]', 'foo');
   254        await fillIn('[data-test-template-description]', 'foo-bar-baz');
   255        const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
   256        codeMirror.setValue(jsonJob());
   257  
   258        server.put('/var/:varId', function (_server, fakeRequest) {
   259          assert.deepEqual(
   260            JSON.parse(fakeRequest.requestBody),
   261            {
   262              Path: 'nomad/job-templates/foo',
   263              CreateIndex: null,
   264              ModifyIndex: null,
   265              Namespace: 'default',
   266              ID: 'nomad/job-templates/foo',
   267              Items: { description: 'foo-bar-baz', template: jsonJob() },
   268            },
   269            'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.'
   270          );
   271          return {
   272            Items: { description: 'foo-bar-baz', template: jsonJob() },
   273            Namespace: 'default',
   274            Path: 'nomad/job-templates/foo',
   275          };
   276        });
   277  
   278        server.get('/vars', function (_server, fakeRequest) {
   279          assert.deepEqual(
   280            fakeRequest.queryParams,
   281            {
   282              prefix: 'nomad/job-templates',
   283              namespace: '*',
   284            },
   285            'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.'
   286          );
   287          return [
   288            {
   289              ID: 'nomad/job-templates/foo',
   290              Namespace: 'default',
   291              Path: 'nomad/job-templates/foo',
   292            },
   293          ];
   294        });
   295  
   296        server.get(
   297          '/var/nomad%2Fjob-templates%2Ffoo',
   298          function (_server, fakeRequest) {
   299            assert.deepEqual(
   300              fakeRequest.queryParams,
   301              {
   302                namespace: 'default',
   303              },
   304              'Dispatches O(n+1) query to retrive items.'
   305            );
   306            return {
   307              ID: 'nomad/job-templates/foo',
   308              Namespace: 'default',
   309              Path: 'nomad/job-templates/foo',
   310              Items: {
   311                template: 'qud',
   312                label: 'foo',
   313              },
   314            };
   315          }
   316        );
   317  
   318        await click('[data-test-save-template]');
   319        assert.equal(currentRouteName(), 'jobs.run.templates.index');
   320        assert
   321          .dom('[data-test-template-card=Foo]')
   322          .exists('The newly created template appears in the list.');
   323      });
   324  
   325      test('a toast notification alerts the user if there is an error saving the newly created job template', async function (assert) {
   326        assert.expect(5);
   327        // Arrange
   328        await JobRun.visit();
   329        await click('[data-test-choose-template]');
   330  
   331        // Assert
   332        assert
   333          .dom('[data-test-template-card]')
   334          .exists({ count: 4 }, 'A list of default job templates is rendered.');
   335  
   336        await click('[data-test-create-new-button]');
   337        assert.equal(currentRouteName(), 'jobs.run.templates.new');
   338        assert
   339          .dom('[data-test-save-template]')
   340          .isDisabled('the save button should be disabled if no path is set');
   341  
   342        await fillIn('[data-test-template-name]', 'try@');
   343        await fillIn('[data-test-template-description]', 'foo-bar-baz');
   344        const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
   345        codeMirror.setValue(jsonJob());
   346  
   347        server.put('/var/:varId?cas=0', function () {
   348          return new AdapterError({
   349            detail: `invalid path "nomad/job-templates/try@"`,
   350            status: 500,
   351          });
   352        });
   353  
   354        await click('[data-test-save-template]');
   355        assert.equal(
   356          currentRouteName(),
   357          'jobs.run.templates.new',
   358          'We do not navigate away from the page if an error is returned by the API.'
   359        );
   360        assert
   361          .dom('.flash-message.alert-critical')
   362          .exists('A toast error message pops up.');
   363      });
   364  
   365      test('a user cannot create a job template if one with the same name and namespace already exists', async function (assert) {
   366        assert.expect(4);
   367        // Arrange
   368        await JobRun.visit();
   369        await click('[data-test-choose-template]');
   370        server.create('variable', {
   371          path: 'nomad/job-templates/foo',
   372          namespace: 'default',
   373          id: 'nomad/job-templates/foo',
   374        });
   375        server.create('namespace', { id: 'test' });
   376  
   377        this.system = this.owner.lookup('service:system');
   378        this.system.shouldShowNamespaces = true;
   379  
   380        // Assert
   381        assert
   382          .dom('[data-test-template-card]')
   383          .exists({ count: 4 }, 'A list of default job templates is rendered.');
   384  
   385        await click('[data-test-create-new-button]');
   386        assert.equal(currentRouteName(), 'jobs.run.templates.new');
   387  
   388        await fillIn('[data-test-template-name]', 'foo');
   389        assert
   390          .dom('[data-test-duplicate-error]')
   391          .exists('an error message alerts the user');
   392  
   393        await clickTrigger('[data-test-namespace-facet]');
   394        await selectChoose('[data-test-namespace-facet]', 'test');
   395  
   396        assert
   397          .dom('[data-test-duplicate-error]')
   398          .doesNotExist(
   399            'an error disappears when name or namespace combination is unique'
   400          );
   401  
   402        // Clean-up
   403        this.system.shouldShowNamespaces = false;
   404      });
   405  
   406      test('a user can save code from the editor as a template', async function (assert) {
   407        assert.expect(4);
   408        // Arrange
   409        await JobRun.visit();
   410        await JobRun.editor.editor.fillIn(jsonJob());
   411  
   412        await click('[data-test-save-as-template]');
   413        assert.equal(
   414          currentRouteName(),
   415          'jobs.run.templates.new',
   416          'We navigate template creation page.'
   417        );
   418  
   419        // Assert
   420        assert
   421          .dom('[data-test-template-name]')
   422          .hasNoText('No template name is prefilled.');
   423        assert
   424          .dom('[data-test-template-description]')
   425          .hasNoText('No template description is prefilled.');
   426  
   427        const codeMirror = getCodeMirrorInstance('[data-test-template-json]');
   428        const json = codeMirror.getValue();
   429  
   430        assert.equal(
   431          json,
   432          jsonJob(),
   433          'Template is filled out with text from the editor.'
   434        );
   435      });
   436  
   437      test('a user can edit a template', async function (assert) {
   438        assert.expect(5);
   439  
   440        // Arrange
   441        server.create('variable', {
   442          path: 'nomad/job-templates/foo',
   443          namespace: 'default',
   444          id: 'nomad/job-templates/foo',
   445          Items: {},
   446        });
   447  
   448        await visit('/jobs/run/templates/manage');
   449  
   450        assert.equal(currentRouteName(), 'jobs.run.templates.manage');
   451        assert
   452          .dom('[data-test-template-list]')
   453          .exists('A list of templates is visible');
   454        await percySnapshot(assert);
   455        await click('[data-test-edit-template="nomad/job-templates/foo"]');
   456        assert.equal(
   457          currentRouteName(),
   458          'jobs.run.templates.template',
   459          'Navigates to edit template view'
   460        );
   461  
   462        server.put('/var/:varId', function (_server, fakeRequest) {
   463          assert.deepEqual(
   464            JSON.parse(fakeRequest.requestBody),
   465            {
   466              Path: 'nomad/job-templates/foo',
   467              CreateIndex: null,
   468              ModifyIndex: null,
   469              Namespace: 'default',
   470              ID: 'nomad/job-templates/foo',
   471              Items: { description: 'baz qud thud' },
   472            },
   473            'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.'
   474          );
   475  
   476          return {
   477            Items: { description: 'baz qud thud' },
   478            Namespace: 'default',
   479            Path: 'nomad/job-templates/foo',
   480          };
   481        });
   482  
   483        await fillIn('[data-test-template-description]', 'baz qud thud');
   484        await click('[data-test-edit-template]');
   485  
   486        assert.equal(
   487          currentRouteName(),
   488          'jobs.run.templates.index',
   489          'We navigate back to the templates view.'
   490        );
   491      });
   492  
   493      test('a user can delete a template', async function (assert) {
   494        assert.expect(5);
   495  
   496        // Arrange
   497        server.create('variable', {
   498          path: 'nomad/job-templates/foo',
   499          namespace: 'default',
   500          id: 'nomad/job-templates/foo',
   501          Items: {},
   502        });
   503  
   504        server.create('variable', {
   505          path: 'nomad/job-templates/bar',
   506          namespace: 'default',
   507          id: 'nomad/job-templates/bar',
   508          Items: {},
   509        });
   510  
   511        server.create('variable', {
   512          path: 'nomad/job-templates/baz',
   513          namespace: 'default',
   514          id: 'nomad/job-templates/baz',
   515          Items: {},
   516        });
   517  
   518        await visit('/jobs/run/templates/manage');
   519  
   520        assert.equal(currentRouteName(), 'jobs.run.templates.manage');
   521        assert
   522          .dom('[data-test-template-list]')
   523          .exists('A list of templates is visible');
   524  
   525        await click('[data-test-idle-button]');
   526        await click('[data-test-confirm-button]');
   527        assert
   528          .dom('[data-test-edit-template="nomad/job-templates/foo"]')
   529          .doesNotExist('The template is removed from the list.');
   530  
   531        await click('[data-test-edit-template="nomad/job-templates/bar"]');
   532        await click('[data-test-idle-button]');
   533        await click('[data-test-confirm-button]');
   534  
   535        assert.equal(
   536          currentRouteName(),
   537          'jobs.run.templates.manage',
   538          'We navigate back to the templates manager view.'
   539        );
   540  
   541        assert
   542          .dom('[data-test-edit-template="nomad/job-templates/bar"]')
   543          .doesNotExist('The template is removed from the list.');
   544      });
   545  
   546      test('a user sees accurate template information', async function (assert) {
   547        assert.expect(3);
   548  
   549        // Arrange
   550        server.create('variable', {
   551          path: 'nomad/job-templates/foo',
   552          namespace: 'default',
   553          id: 'nomad/job-templates/foo',
   554          Items: {
   555            template: 'qud',
   556            label: 'foo',
   557            description: 'bar baz',
   558          },
   559        });
   560  
   561        await visit('/jobs/run/templates');
   562  
   563        assert.equal(currentRouteName(), 'jobs.run.templates.index');
   564        assert.dom('[data-test-template-card="Foo"]').exists();
   565  
   566        this.store = this.owner.lookup('service:store');
   567        this.store.unloadAll();
   568        await settled();
   569  
   570        assert
   571          .dom('[data-test-template-card="Foo"]')
   572          .doesNotExist(
   573            'The template reactively updates to changes in the Ember Data Store.'
   574          );
   575      });
   576  
   577      test('default templates', async function (assert) {
   578        assert.expect(4);
   579        const NUMBER_OF_DEFAULT_TEMPLATES = 4;
   580  
   581        await visit('/jobs/run/templates');
   582  
   583        assert.equal(currentRouteName(), 'jobs.run.templates.index');
   584        assert
   585          .dom('[data-test-template-card]')
   586          .exists({ count: NUMBER_OF_DEFAULT_TEMPLATES });
   587  
   588        await percySnapshot(assert);
   589  
   590        await click('[data-test-template-card="Hello world"]');
   591        await click('[data-test-apply]');
   592  
   593        assert.equal(
   594          currentURL(),
   595          '/jobs/run?template=nomad%2Fjob-templates%2Fdefault%2Fhello-world'
   596        );
   597        assert.dom('[data-test-editor]').includesText('job "hello-world"');
   598      });
   599    });
   600  });