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

     1  /**
     2   * Copyright (c) HashiCorp, Inc.
     3   * SPDX-License-Identifier: MPL-2.0
     4   */
     5  
     6  /* eslint-disable ember/no-test-module-for */
     7  /* eslint-disable qunit/require-expect */
     8  import { currentURL, settled } from '@ember/test-helpers';
     9  import { module, test } from 'qunit';
    10  import { setupApplicationTest } from 'ember-qunit';
    11  import { setupMirage } from 'ember-cli-mirage/test-support';
    12  import moment from 'moment';
    13  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
    14  import moduleForJob, {
    15    moduleForJobWithClientStatus,
    16  } from 'nomad-ui/tests/helpers/module-for-job';
    17  import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
    18  import percySnapshot from '@percy/ember';
    19  
    20  moduleForJob('Acceptance | job detail (batch)', 'allocations', () =>
    21    server.create('job', {
    22      type: 'batch',
    23      shallow: true,
    24      noActiveDeployment: true,
    25      createAllocations: true,
    26      allocStatusDistribution: {
    27        running: 1,
    28      },
    29    })
    30  );
    31  
    32  moduleForJob('Acceptance | job detail (system)', 'allocations', () =>
    33    server.create('job', {
    34      type: 'system',
    35      shallow: true,
    36      noActiveDeployment: true,
    37      createAllocations: true,
    38      allocStatusDistribution: {
    39        running: 1,
    40      },
    41    })
    42  );
    43  
    44  moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () =>
    45    server.create('job', {
    46      type: 'sysbatch',
    47      shallow: true,
    48      noActiveDeployment: true,
    49      createAllocations: true,
    50      allocStatusDistribution: {
    51        running: 1,
    52        failed: 1,
    53      },
    54    })
    55  );
    56  
    57  moduleForJobWithClientStatus(
    58    'Acceptance | job detail with client status (sysbatch)',
    59    () =>
    60      server.create('job', {
    61        status: 'running',
    62        datacenters: ['dc1'],
    63        type: 'sysbatch',
    64        createAllocations: false,
    65        noActiveDeployment: true,
    66      })
    67  );
    68  
    69  moduleForJobWithClientStatus(
    70    'Acceptance | job detail with client status (sysbatch with namespace)',
    71    () => {
    72      const namespace = server.create('namespace', { id: 'test' });
    73      return server.create('job', {
    74        status: 'running',
    75        datacenters: ['dc1'],
    76        type: 'sysbatch',
    77        namespaceId: namespace.name,
    78        createAllocations: false,
    79        noActiveDeployment: true,
    80      });
    81    }
    82  );
    83  
    84  moduleForJobWithClientStatus(
    85    'Acceptance | job detail with client status (sysbatch with namespace and wildcard dc)',
    86    () => {
    87      const namespace = server.create('namespace', { id: 'test' });
    88      return server.create('job', {
    89        status: 'running',
    90        datacenters: ['*'],
    91        type: 'sysbatch',
    92        namespaceId: namespace.name,
    93        createAllocations: false,
    94        noActiveDeployment: true,
    95      });
    96    }
    97  );
    98  
    99  moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => {
   100    const parent = server.create('job', 'periodicSysbatch', {
   101      childrenCount: 1,
   102      shallow: true,
   103      datacenters: ['dc1'],
   104      createAllocations: true,
   105      allocStatusDistribution: {
   106        running: 1,
   107      },
   108      noActiveDeployment: true,
   109    });
   110    return server.db.jobs.where({ parentId: parent.id })[0];
   111  });
   112  
   113  moduleForJobWithClientStatus(
   114    'Acceptance | job detail with client status (sysbatch child)',
   115    () => {
   116      const parent = server.create('job', 'periodicSysbatch', {
   117        childrenCount: 1,
   118        shallow: true,
   119        datacenters: ['dc1'],
   120        noActiveDeployment: true,
   121      });
   122      return server.db.jobs.where({ parentId: parent.id })[0];
   123    }
   124  );
   125  
   126  moduleForJobWithClientStatus(
   127    'Acceptance | job detail with client status (sysbatch child with namespace)',
   128    () => {
   129      const namespace = server.create('namespace', { id: 'test' });
   130      const parent = server.create('job', 'periodicSysbatch', {
   131        childrenCount: 1,
   132        shallow: true,
   133        namespaceId: namespace.name,
   134        datacenters: ['dc1'],
   135        noActiveDeployment: true,
   136      });
   137      return server.db.jobs.where({ parentId: parent.id })[0];
   138    }
   139  );
   140  
   141  moduleForJobWithClientStatus(
   142    'Acceptance | job detail with client status (sysbatch child with namespace and wildcard dc)',
   143    () => {
   144      const namespace = server.create('namespace', { id: 'test' });
   145      const parent = server.create('job', 'periodicSysbatch', {
   146        childrenCount: 1,
   147        shallow: true,
   148        namespaceId: namespace.name,
   149        datacenters: ['*'],
   150        noActiveDeployment: true,
   151      });
   152      return server.db.jobs.where({ parentId: parent.id })[0];
   153    }
   154  );
   155  
   156  moduleForJob(
   157    'Acceptance | job detail (periodic)',
   158    'children',
   159    () => server.create('job', 'periodic', { shallow: true }),
   160    {
   161      'the default sort is submitTime descending': async function (job, assert) {
   162        const mostRecentLaunch = server.db.jobs
   163          .where({ parentId: job.id })
   164          .sortBy('submitTime')
   165          .reverse()[0];
   166  
   167        assert.ok(JobDetail.jobsHeader.hasSubmitTime);
   168        assert.equal(
   169          JobDetail.jobs[0].submitTime,
   170          moment(mostRecentLaunch.submitTime / 1000000).format(
   171            'MMM DD HH:mm:ss ZZ'
   172          )
   173        );
   174      },
   175      "don't display redundant information in children table": async function (
   176        job,
   177        assert
   178      ) {
   179        assert.notOk(JobDetail.jobsHeader.hasNodePool);
   180        assert.notOk(JobDetail.jobsHeader.hasPriority);
   181        assert.notOk(JobDetail.jobsHeader.hasType);
   182      },
   183    }
   184  );
   185  
   186  moduleForJob(
   187    'Acceptance | job detail (periodic in namespace)',
   188    'children',
   189    () => {
   190      const namespace = server.create('namespace', { id: 'test' });
   191      const parent = server.create('job', 'periodic', {
   192        shallow: true,
   193        namespaceId: namespace.name,
   194      });
   195      return parent;
   196    },
   197    {
   198      "don't display namespace in children table": async function (job, assert) {
   199        assert.notOk(JobDetail.jobsHeader.hasNamespace);
   200      },
   201    }
   202  );
   203  
   204  moduleForJob(
   205    'Acceptance | job detail (parameterized)',
   206    'children',
   207    () =>
   208      server.create('job', 'parameterized', {
   209        shallow: true,
   210        noActiveDeployment: true,
   211      }),
   212    {
   213      'the default sort is submitTime descending': async (job, assert) => {
   214        const mostRecentLaunch = server.db.jobs
   215          .where({ parentId: job.id })
   216          .sortBy('submitTime')
   217          .reverse()[0];
   218  
   219        assert.ok(JobDetail.jobsHeader.hasSubmitTime);
   220        assert.equal(
   221          JobDetail.jobs[0].submitTime,
   222          moment(mostRecentLaunch.submitTime / 1000000).format(
   223            'MMM DD HH:mm:ss ZZ'
   224          )
   225        );
   226      },
   227      "don't display redundant information in children table": async function (
   228        job,
   229        assert
   230      ) {
   231        assert.notOk(JobDetail.jobsHeader.hasNodePool);
   232        assert.notOk(JobDetail.jobsHeader.hasPriority);
   233        assert.notOk(JobDetail.jobsHeader.hasType);
   234      },
   235    }
   236  );
   237  
   238  moduleForJob(
   239    'Acceptance | job detail (parameterized in namespace)',
   240    'children',
   241    () => {
   242      const namespace = server.create('namespace', { id: 'test' });
   243      const parent = server.create('job', 'parameterized', {
   244        shallow: true,
   245        namespaceId: namespace.name,
   246      });
   247      return parent;
   248    },
   249    {
   250      "don't display namespace in children table": async function (job, assert) {
   251        assert.notOk(JobDetail.jobsHeader.hasNamespace);
   252      },
   253    }
   254  );
   255  
   256  moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => {
   257    const parent = server.create('job', 'periodic', {
   258      childrenCount: 1,
   259      shallow: true,
   260      createAllocations: true,
   261      allocStatusDistribution: {
   262        running: 1,
   263      },
   264      noActiveDeployment: true,
   265    });
   266    return server.db.jobs.where({ parentId: parent.id })[0];
   267  });
   268  
   269  moduleForJob(
   270    'Acceptance | job detail (parameterized child)',
   271    'allocations',
   272    () => {
   273      const parent = server.create('job', 'parameterized', {
   274        childrenCount: 1,
   275        shallow: true,
   276        noActiveDeployment: true,
   277        createAllocations: true,
   278        allocStatusDistribution: {
   279          running: 1,
   280        },
   281      });
   282      return server.db.jobs.where({ parentId: parent.id })[0];
   283    }
   284  );
   285  
   286  moduleForJob(
   287    'Acceptance | job detail (service)',
   288    'allocations',
   289    () => server.create('job', { type: 'service', noActiveDeployment: true }),
   290    {
   291      'the subnav links to deployment': async (job, assert) => {
   292        await JobDetail.tabFor('deployments').visit();
   293        assert.equal(currentURL(), `/jobs/${job.id}/deployments`);
   294      },
   295      'when the job is not found, an error message is shown, but the URL persists':
   296        async (job, assert) => {
   297          await JobDetail.visit({ id: 'not-a-real-job' });
   298  
   299          assert.equal(
   300            server.pretender.handledRequests
   301              .filter((request) => !request.url.includes('policy'))
   302              .findBy('status', 404).url,
   303            '/v1/job/not-a-real-job',
   304            'A request to the nonexistent job is made'
   305          );
   306          assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
   307          assert.ok(JobDetail.error.isPresent, 'Error message is shown');
   308          assert.equal(
   309            JobDetail.error.title,
   310            'Not Found',
   311            'Error message is for 404'
   312          );
   313        },
   314    }
   315  );
   316  
   317  module('Acceptance | job detail (with namespaces)', function (hooks) {
   318    setupApplicationTest(hooks);
   319    setupMirage(hooks);
   320  
   321    let job, managementToken, clientToken;
   322  
   323    hooks.beforeEach(function () {
   324      server.createList('namespace', 2);
   325      server.create('node-pool');
   326      server.create('node');
   327      job = server.create('job', {
   328        type: 'service',
   329        status: 'running',
   330        namespaceId: server.db.namespaces[1].name,
   331        noActiveDeployment: true,
   332      });
   333      server.createList('job', 3, {
   334        namespaceId: server.db.namespaces[0].name,
   335      });
   336  
   337      managementToken = server.create('token');
   338      clientToken = server.create('token');
   339    });
   340  
   341    test('it passes an accessibility audit', async function (assert) {
   342      const namespace = server.db.namespaces.find(job.namespaceId);
   343      await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
   344      await a11yAudit(assert);
   345    });
   346  
   347    test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) {
   348      const namespace = server.db.namespaces.find(job.namespaceId);
   349  
   350      await JobDetail.visit({
   351        id: `${job.id}@${namespace.name}`,
   352      });
   353  
   354      assert.ok(
   355        JobDetail.statFor('namespace').text,
   356        'Namespace included in stats'
   357      );
   358    });
   359  
   360    test('the exec button state can change between namespaces', async function (assert) {
   361      const job1 = server.create('job', {
   362        status: 'running',
   363        namespaceId: server.db.namespaces[0].id,
   364      });
   365      const job2 = server.create('job', {
   366        status: 'running',
   367        namespaceId: server.db.namespaces[1].id,
   368      });
   369  
   370      window.localStorage.nomadTokenSecret = clientToken.secretId;
   371  
   372      const policy = server.create('policy', {
   373        id: 'something',
   374        name: 'something',
   375        rulesJSON: {
   376          Namespaces: [
   377            {
   378              Name: job1.namespaceId,
   379              Capabilities: ['list-jobs', 'alloc-exec'],
   380            },
   381            {
   382              Name: job2.namespaceId,
   383              Capabilities: ['list-jobs'],
   384            },
   385          ],
   386        },
   387      });
   388  
   389      clientToken.policyIds = [policy.id];
   390      clientToken.save();
   391  
   392      await JobDetail.visit({ id: job1.id });
   393      assert.notOk(JobDetail.execButton.isDisabled);
   394  
   395      const secondNamespace = server.db.namespaces[1];
   396      await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
   397  
   398      assert.ok(JobDetail.execButton.isDisabled);
   399    });
   400  
   401    test('the anonymous policy is fetched to check whether to show the exec button', async function (assert) {
   402      window.localStorage.removeItem('nomadTokenSecret');
   403  
   404      server.create('policy', {
   405        id: 'anonymous',
   406        name: 'anonymous',
   407        rulesJSON: {
   408          Namespaces: [
   409            {
   410              Name: 'default',
   411              Capabilities: ['list-jobs', 'alloc-exec'],
   412            },
   413          ],
   414        },
   415      });
   416  
   417      await JobDetail.visit({
   418        id: `${job.id}@${server.db.namespaces[1].name}`,
   419      });
   420  
   421      assert.notOk(JobDetail.execButton.isDisabled);
   422    });
   423  
   424    test('meta table is displayed if job has meta attributes', async function (assert) {
   425      const jobWithMeta = server.create('job', {
   426        status: 'running',
   427        namespaceId: server.db.namespaces[1].id,
   428        meta: {
   429          'a.b': 'c',
   430        },
   431      });
   432  
   433      await JobDetail.visit({
   434        id: `${job.id}@${server.db.namespaces[1].name}`,
   435      });
   436  
   437      assert.notOk(JobDetail.metaTable, 'Meta table not present');
   438  
   439      await JobDetail.visit({
   440        id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`,
   441      });
   442      assert.ok(JobDetail.metaTable, 'Meta table is present');
   443    });
   444  
   445    test('pack details are displayed', async function (assert) {
   446      const namespace = server.db.namespaces[1].id;
   447      const jobFromPack = server.create('job', {
   448        status: 'running',
   449        namespaceId: namespace,
   450        meta: {
   451          'pack.name': 'my-pack',
   452          'pack.version': '1.0.0',
   453        },
   454      });
   455  
   456      await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` });
   457  
   458      assert.ok(JobDetail.packTag, 'Pack tag is present');
   459      assert.equal(
   460        JobDetail.packStatFor('name').text,
   461        `Name ${jobFromPack.meta['pack.name']}`,
   462        `Pack name is ${jobFromPack.meta['pack.name']}`
   463      );
   464      assert.equal(
   465        JobDetail.packStatFor('version').text,
   466        `Version ${jobFromPack.meta['pack.version']}`,
   467        `Pack version is ${jobFromPack.meta['pack.version']}`
   468      );
   469    });
   470  
   471    test('resource recommendations show when they exist and can be expanded, collapsed, and processed', async function (assert) {
   472      server.create('feature', { name: 'Dynamic Application Sizing' });
   473  
   474      job = server.create('job', {
   475        type: 'service',
   476        status: 'running',
   477        namespaceId: server.db.namespaces[1].name,
   478        groupsCount: 3,
   479        createRecommendations: true,
   480        noActiveDeployment: true,
   481      });
   482  
   483      window.localStorage.nomadTokenSecret = managementToken.secretId;
   484      await JobDetail.visit({
   485        id: `${job.id}@${server.db.namespaces[1].name}`,
   486      });
   487  
   488      const groupsWithRecommendations = job.taskGroups.filter((group) =>
   489        group.tasks.models.any((task) => task.recommendations.models.length)
   490      );
   491      const jobRecommendationCount = groupsWithRecommendations.length;
   492  
   493      const firstRecommendationGroup = groupsWithRecommendations.models[0];
   494  
   495      assert.equal(JobDetail.recommendations.length, jobRecommendationCount);
   496  
   497      const recommendation = JobDetail.recommendations[0];
   498  
   499      assert.equal(recommendation.group, firstRecommendationGroup.name);
   500      assert.ok(recommendation.card.isHidden);
   501  
   502      const toggle = recommendation.toggleButton;
   503  
   504      assert.equal(toggle.text, 'Show');
   505  
   506      await toggle.click();
   507  
   508      assert.ok(recommendation.card.isPresent);
   509      assert.equal(toggle.text, 'Collapse');
   510  
   511      await toggle.click();
   512  
   513      assert.ok(recommendation.card.isHidden);
   514  
   515      await toggle.click();
   516  
   517      assert.equal(
   518        recommendation.card.slug.groupName,
   519        firstRecommendationGroup.name
   520      );
   521  
   522      await recommendation.card.acceptButton.click();
   523  
   524      assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);
   525  
   526      await JobDetail.tabFor('definition').visit();
   527      await JobDetail.tabFor('overview').visit();
   528  
   529      assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);
   530    });
   531  
   532    test('resource recommendations are not fetched when the feature doesn’t exist', async function (assert) {
   533      window.localStorage.nomadTokenSecret = managementToken.secretId;
   534      await JobDetail.visit({
   535        id: `${job.id}@${server.db.namespaces[1].name}`,
   536      });
   537  
   538      assert.equal(JobDetail.recommendations.length, 0);
   539  
   540      assert.equal(
   541        server.pretender.handledRequests.filter((request) =>
   542          request.url.includes('recommendations')
   543        ).length,
   544        0
   545      );
   546    });
   547  
   548    test('when the dynamic autoscaler is applied, you can scale a task within the job detail page', async function (assert) {
   549      const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
   550      const READ_ONLY_NAMESPACE = 'read-only-namespace';
   551      const clientToken = server.create('token');
   552  
   553      const namespace = server.create('namespace', {
   554        id: SCALE_AND_WRITE_NAMESPACE,
   555      });
   556      const secondNamespace = server.create('namespace', {
   557        id: READ_ONLY_NAMESPACE,
   558      });
   559  
   560      job = server.create('job', {
   561        groupCount: 0,
   562        createAllocations: false,
   563        shallow: true,
   564        noActiveDeployment: true,
   565        namespaceId: SCALE_AND_WRITE_NAMESPACE,
   566      });
   567  
   568      const job2 = server.create('job', {
   569        groupCount: 0,
   570        createAllocations: false,
   571        shallow: true,
   572        noActiveDeployment: true,
   573        namespaceId: READ_ONLY_NAMESPACE,
   574      });
   575      const scalingGroup2 = server.create('task-group', {
   576        job: job2,
   577        name: 'scaling',
   578        count: 1,
   579        shallow: true,
   580        withScaling: true,
   581      });
   582      job2.update({ taskGroupIds: [scalingGroup2.id] });
   583  
   584      const policy = server.create('policy', {
   585        id: 'something',
   586        name: 'something',
   587        rulesJSON: {
   588          Namespaces: [
   589            {
   590              Name: SCALE_AND_WRITE_NAMESPACE,
   591              Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
   592            },
   593            {
   594              Name: READ_ONLY_NAMESPACE,
   595              Capabilities: ['list-jobs', 'read-job'],
   596            },
   597          ],
   598        },
   599      });
   600      const scalingGroup = server.create('task-group', {
   601        job,
   602        name: 'scaling',
   603        count: 1,
   604        shallow: true,
   605        withScaling: true,
   606      });
   607      job.update({ taskGroupIds: [scalingGroup.id] });
   608  
   609      clientToken.policyIds = [policy.id];
   610      clientToken.save();
   611      window.localStorage.nomadTokenSecret = clientToken.secretId;
   612  
   613      await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
   614      assert.notOk(JobDetail.incrementButton.isDisabled);
   615  
   616      await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
   617      assert.ok(JobDetail.incrementButton.isDisabled);
   618    });
   619  
   620    test('handles when a job is remotely purged', async function (assert) {
   621      const namespace = server.create('namespace');
   622      const job = server.create('job', {
   623        namespaceId: namespace.id,
   624        status: 'running',
   625        type: 'service',
   626        shallow: true,
   627        noActiveDeployment: true,
   628        createAllocations: true,
   629        groupsCount: 1,
   630        groupTaskCount: 1,
   631        allocStatusDistribution: {
   632          running: 1,
   633        },
   634      });
   635  
   636      await JobDetail.visit({ id: `${job.id}@${namespace.id}` });
   637  
   638      assert.equal(currentURL(), `/jobs/${job.id}%40${namespace.id}`);
   639  
   640      // Simulate a 404 error on the job watcher
   641      const controller = this.owner.lookup('controller:jobs.job');
   642      let jobWatcher = controller.watchers.job;
   643      jobWatcher.isError = true;
   644      jobWatcher.error = { errors: [{ status: '404' }] };
   645      await settled();
   646  
   647      // User should be booted off the page
   648      assert.equal(currentURL(), '/jobs?namespace=*');
   649  
   650      // A notification should be present
   651      assert
   652        .dom('.flash-message.alert-critical')
   653        .exists('A toast error message pops up.');
   654  
   655      await percySnapshot(assert);
   656    });
   657  
   658    test('handles when a job is remotely purged, from a job subnav page', async function (assert) {
   659      const namespace = server.create('namespace');
   660      const job = server.create('job', {
   661        namespaceId: namespace.id,
   662        status: 'running',
   663        type: 'service',
   664        shallow: true,
   665        noActiveDeployment: true,
   666        createAllocations: true,
   667        groupsCount: 1,
   668        groupTaskCount: 1,
   669        allocStatusDistribution: {
   670          running: 1,
   671        },
   672      });
   673  
   674      await JobDetail.visit({ id: `${job.id}@${namespace.id}` });
   675      await JobDetail.tabFor('allocations').visit();
   676  
   677      assert.equal(currentURL(), `/jobs/${job.id}@${namespace.id}/allocations`);
   678  
   679      // Simulate a 404 error on the job watcher
   680      const controller = this.owner.lookup('controller:jobs.job');
   681      let jobWatcher = controller.watchers.job;
   682      jobWatcher.isError = true;
   683      jobWatcher.error = { errors: [{ status: '404' }] };
   684      await settled();
   685  
   686      // User should be booted off the page
   687      assert.equal(currentURL(), '/jobs?namespace=*');
   688  
   689      // A notification should be present
   690      assert
   691        .dom('.flash-message.alert-critical')
   692        .exists('A toast error message pops up.');
   693    });
   694  });