github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/job-detail-test.js (about)

     1  /* eslint-disable ember/no-test-module-for */
     2  /* eslint-disable qunit/require-expect */
     3  import { currentURL } from '@ember/test-helpers';
     4  import { module, test } from 'qunit';
     5  import { setupApplicationTest } from 'ember-qunit';
     6  import { setupMirage } from 'ember-cli-mirage/test-support';
     7  import moment from 'moment';
     8  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
     9  import moduleForJob, {
    10    moduleForJobWithClientStatus,
    11  } from 'nomad-ui/tests/helpers/module-for-job';
    12  import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
    13  
    14  moduleForJob('Acceptance | job detail (batch)', 'allocations', () =>
    15    server.create('job', { type: 'batch', shallow: true })
    16  );
    17  
    18  moduleForJob('Acceptance | job detail (system)', 'allocations', () =>
    19    server.create('job', { type: 'system', shallow: true })
    20  );
    21  
    22  moduleForJobWithClientStatus(
    23    'Acceptance | job detail with client status (system)',
    24    () =>
    25      server.create('job', {
    26        status: 'running',
    27        datacenters: ['dc1'],
    28        type: 'system',
    29        createAllocations: false,
    30      })
    31  );
    32  
    33  moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () =>
    34    server.create('job', { type: 'sysbatch', shallow: true })
    35  );
    36  
    37  moduleForJobWithClientStatus(
    38    'Acceptance | job detail with client status (sysbatch)',
    39    () =>
    40      server.create('job', {
    41        status: 'running',
    42        datacenters: ['dc1'],
    43        type: 'sysbatch',
    44        createAllocations: false,
    45      })
    46  );
    47  
    48  moduleForJobWithClientStatus(
    49    'Acceptance | job detail with client status (sysbatch with namespace)',
    50    () => {
    51      const namespace = server.create('namespace', { id: 'test' });
    52      return server.create('job', {
    53        status: 'running',
    54        datacenters: ['dc1'],
    55        type: 'sysbatch',
    56        namespaceId: namespace.name,
    57        createAllocations: false,
    58      });
    59    }
    60  );
    61  
    62  moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => {
    63    const parent = server.create('job', 'periodicSysbatch', {
    64      childrenCount: 1,
    65      shallow: true,
    66      datacenters: ['dc1'],
    67    });
    68    return server.db.jobs.where({ parentId: parent.id })[0];
    69  });
    70  
    71  moduleForJobWithClientStatus(
    72    'Acceptance | job detail with client status (sysbatch child)',
    73    () => {
    74      const parent = server.create('job', 'periodicSysbatch', {
    75        childrenCount: 1,
    76        shallow: true,
    77        datacenters: ['dc1'],
    78      });
    79      return server.db.jobs.where({ parentId: parent.id })[0];
    80    }
    81  );
    82  
    83  moduleForJobWithClientStatus(
    84    'Acceptance | job detail with client status (sysbatch child with namespace)',
    85    () => {
    86      const namespace = server.create('namespace', { id: 'test' });
    87      const parent = server.create('job', 'periodicSysbatch', {
    88        childrenCount: 1,
    89        shallow: true,
    90        namespaceId: namespace.name,
    91        datacenters: ['dc1'],
    92      });
    93      return server.db.jobs.where({ parentId: parent.id })[0];
    94    }
    95  );
    96  
    97  moduleForJob(
    98    'Acceptance | job detail (periodic)',
    99    'children',
   100    () => server.create('job', 'periodic', { shallow: true }),
   101    {
   102      'the default sort is submitTime descending': async function (job, assert) {
   103        const mostRecentLaunch = server.db.jobs
   104          .where({ parentId: job.id })
   105          .sortBy('submitTime')
   106          .reverse()[0];
   107  
   108        assert.ok(JobDetail.jobsHeader.hasSubmitTime);
   109        assert.equal(
   110          JobDetail.jobs[0].submitTime,
   111          moment(mostRecentLaunch.submitTime / 1000000).format(
   112            'MMM DD HH:mm:ss ZZ'
   113          )
   114        );
   115      },
   116    }
   117  );
   118  
   119  moduleForJob(
   120    'Acceptance | job detail (periodic in namespace)',
   121    'children',
   122    () => {
   123      const namespace = server.create('namespace', { id: 'test' });
   124      const parent = server.create('job', 'periodic', {
   125        shallow: true,
   126        namespaceId: namespace.name,
   127      });
   128      return parent;
   129    },
   130    {
   131      'display namespace in children table': async function (job, assert) {
   132        assert.ok(JobDetail.jobsHeader.hasNamespace);
   133        assert.equal(JobDetail.jobs[0].namespace, job.namespace);
   134      },
   135    }
   136  );
   137  
   138  moduleForJob(
   139    'Acceptance | job detail (parameterized)',
   140    'children',
   141    () => server.create('job', 'parameterized', { shallow: true }),
   142    {
   143      'the default sort is submitTime descending': async (job, assert) => {
   144        const mostRecentLaunch = server.db.jobs
   145          .where({ parentId: job.id })
   146          .sortBy('submitTime')
   147          .reverse()[0];
   148  
   149        assert.ok(JobDetail.jobsHeader.hasSubmitTime);
   150        assert.equal(
   151          JobDetail.jobs[0].submitTime,
   152          moment(mostRecentLaunch.submitTime / 1000000).format(
   153            'MMM DD HH:mm:ss ZZ'
   154          )
   155        );
   156      },
   157    }
   158  );
   159  
   160  moduleForJob(
   161    'Acceptance | job detail (parameterized in namespace)',
   162    'children',
   163    () => {
   164      const namespace = server.create('namespace', { id: 'test' });
   165      const parent = server.create('job', 'parameterized', {
   166        shallow: true,
   167        namespaceId: namespace.name,
   168      });
   169      return parent;
   170    },
   171    {
   172      'display namespace in children table': async function (job, assert) {
   173        assert.ok(JobDetail.jobsHeader.hasNamespace);
   174        assert.equal(JobDetail.jobs[0].namespace, job.namespace);
   175      },
   176    }
   177  );
   178  
   179  moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => {
   180    const parent = server.create('job', 'periodic', {
   181      childrenCount: 1,
   182      shallow: true,
   183    });
   184    return server.db.jobs.where({ parentId: parent.id })[0];
   185  });
   186  
   187  moduleForJob(
   188    'Acceptance | job detail (parameterized child)',
   189    'allocations',
   190    () => {
   191      const parent = server.create('job', 'parameterized', {
   192        childrenCount: 1,
   193        shallow: true,
   194      });
   195      return server.db.jobs.where({ parentId: parent.id })[0];
   196    }
   197  );
   198  
   199  moduleForJob(
   200    'Acceptance | job detail (service)',
   201    'allocations',
   202    () => server.create('job', { type: 'service' }),
   203    {
   204      'the subnav links to deployment': async (job, assert) => {
   205        await JobDetail.tabFor('deployments').visit();
   206        assert.equal(currentURL(), `/jobs/${job.id}/deployments`);
   207      },
   208      'when the job is not found, an error message is shown, but the URL persists':
   209        async (job, assert) => {
   210          await JobDetail.visit({ id: 'not-a-real-job' });
   211  
   212          assert.equal(
   213            server.pretender.handledRequests
   214              .filter((request) => !request.url.includes('policy'))
   215              .findBy('status', 404).url,
   216            '/v1/job/not-a-real-job',
   217            'A request to the nonexistent job is made'
   218          );
   219          assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists');
   220          assert.ok(JobDetail.error.isPresent, 'Error message is shown');
   221          assert.equal(
   222            JobDetail.error.title,
   223            'Not Found',
   224            'Error message is for 404'
   225          );
   226        },
   227    }
   228  );
   229  
   230  module('Acceptance | job detail (with namespaces)', function (hooks) {
   231    setupApplicationTest(hooks);
   232    setupMirage(hooks);
   233  
   234    let job, managementToken, clientToken;
   235  
   236    hooks.beforeEach(function () {
   237      server.createList('namespace', 2);
   238      server.create('node');
   239      job = server.create('job', {
   240        type: 'service',
   241        status: 'running',
   242        namespaceId: server.db.namespaces[1].name,
   243      });
   244      server.createList('job', 3, {
   245        namespaceId: server.db.namespaces[0].name,
   246      });
   247  
   248      managementToken = server.create('token');
   249      clientToken = server.create('token');
   250    });
   251  
   252    test('it passes an accessibility audit', async function (assert) {
   253      const namespace = server.db.namespaces.find(job.namespaceId);
   254      await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
   255      await a11yAudit(assert);
   256    });
   257  
   258    test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) {
   259      const namespace = server.db.namespaces.find(job.namespaceId);
   260  
   261      await JobDetail.visit({
   262        id: `${job.id}@${namespace.name}`,
   263      });
   264  
   265      assert.ok(
   266        JobDetail.statFor('namespace').text,
   267        'Namespace included in stats'
   268      );
   269    });
   270  
   271    test('the exec button state can change between namespaces', async function (assert) {
   272      const job1 = server.create('job', {
   273        status: 'running',
   274        namespaceId: server.db.namespaces[0].id,
   275      });
   276      const job2 = server.create('job', {
   277        status: 'running',
   278        namespaceId: server.db.namespaces[1].id,
   279      });
   280  
   281      window.localStorage.nomadTokenSecret = clientToken.secretId;
   282  
   283      const policy = server.create('policy', {
   284        id: 'something',
   285        name: 'something',
   286        rulesJSON: {
   287          Namespaces: [
   288            {
   289              Name: job1.namespaceId,
   290              Capabilities: ['list-jobs', 'alloc-exec'],
   291            },
   292            {
   293              Name: job2.namespaceId,
   294              Capabilities: ['list-jobs'],
   295            },
   296          ],
   297        },
   298      });
   299  
   300      clientToken.policyIds = [policy.id];
   301      clientToken.save();
   302  
   303      await JobDetail.visit({ id: job1.id });
   304      assert.notOk(JobDetail.execButton.isDisabled);
   305  
   306      const secondNamespace = server.db.namespaces[1];
   307      await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
   308  
   309      assert.ok(JobDetail.execButton.isDisabled);
   310    });
   311  
   312    test('the anonymous policy is fetched to check whether to show the exec button', async function (assert) {
   313      window.localStorage.removeItem('nomadTokenSecret');
   314  
   315      server.create('policy', {
   316        id: 'anonymous',
   317        name: 'anonymous',
   318        rulesJSON: {
   319          Namespaces: [
   320            {
   321              Name: 'default',
   322              Capabilities: ['list-jobs', 'alloc-exec'],
   323            },
   324          ],
   325        },
   326      });
   327  
   328      await JobDetail.visit({
   329        id: `${job.id}@${server.db.namespaces[1].name}`,
   330      });
   331  
   332      assert.notOk(JobDetail.execButton.isDisabled);
   333    });
   334  
   335    test('meta table is displayed if job has meta attributes', async function (assert) {
   336      const jobWithMeta = server.create('job', {
   337        status: 'running',
   338        namespaceId: server.db.namespaces[1].id,
   339        meta: {
   340          'a.b': 'c',
   341        },
   342      });
   343  
   344      await JobDetail.visit({
   345        id: `${job.id}@${server.db.namespaces[1].name}`,
   346      });
   347  
   348      assert.notOk(JobDetail.metaTable, 'Meta table not present');
   349  
   350      await JobDetail.visit({
   351        id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`,
   352      });
   353      assert.ok(JobDetail.metaTable, 'Meta table is present');
   354    });
   355  
   356    test('pack details are displayed', async function (assert) {
   357      const namespace = server.db.namespaces[1].id;
   358      const jobFromPack = server.create('job', {
   359        status: 'running',
   360        namespaceId: namespace,
   361        meta: {
   362          'pack.name': 'my-pack',
   363          'pack.version': '1.0.0',
   364        },
   365      });
   366  
   367      await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` });
   368  
   369      assert.ok(JobDetail.packTag, 'Pack tag is present');
   370      assert.equal(
   371        JobDetail.packStatFor('name').text,
   372        `Name ${jobFromPack.meta['pack.name']}`,
   373        `Pack name is ${jobFromPack.meta['pack.name']}`
   374      );
   375      assert.equal(
   376        JobDetail.packStatFor('version').text,
   377        `Version ${jobFromPack.meta['pack.version']}`,
   378        `Pack version is ${jobFromPack.meta['pack.version']}`
   379      );
   380    });
   381  
   382    test('resource recommendations show when they exist and can be expanded, collapsed, and processed', async function (assert) {
   383      server.create('feature', { name: 'Dynamic Application Sizing' });
   384  
   385      job = server.create('job', {
   386        type: 'service',
   387        status: 'running',
   388        namespaceId: server.db.namespaces[1].name,
   389        groupsCount: 3,
   390        createRecommendations: true,
   391      });
   392  
   393      window.localStorage.nomadTokenSecret = managementToken.secretId;
   394      await JobDetail.visit({
   395        id: `${job.id}@${server.db.namespaces[1].name}`,
   396      });
   397  
   398      const groupsWithRecommendations = job.taskGroups.filter((group) =>
   399        group.tasks.models.any((task) => task.recommendations.models.length)
   400      );
   401      const jobRecommendationCount = groupsWithRecommendations.length;
   402  
   403      const firstRecommendationGroup = groupsWithRecommendations.models[0];
   404  
   405      assert.equal(JobDetail.recommendations.length, jobRecommendationCount);
   406  
   407      const recommendation = JobDetail.recommendations[0];
   408  
   409      assert.equal(recommendation.group, firstRecommendationGroup.name);
   410      assert.ok(recommendation.card.isHidden);
   411  
   412      const toggle = recommendation.toggleButton;
   413  
   414      assert.equal(toggle.text, 'Show');
   415  
   416      await toggle.click();
   417  
   418      assert.ok(recommendation.card.isPresent);
   419      assert.equal(toggle.text, 'Collapse');
   420  
   421      await toggle.click();
   422  
   423      assert.ok(recommendation.card.isHidden);
   424  
   425      await toggle.click();
   426  
   427      assert.equal(
   428        recommendation.card.slug.groupName,
   429        firstRecommendationGroup.name
   430      );
   431  
   432      await recommendation.card.acceptButton.click();
   433  
   434      assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);
   435  
   436      await JobDetail.tabFor('definition').visit();
   437      await JobDetail.tabFor('overview').visit();
   438  
   439      assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1);
   440    });
   441  
   442    test('resource recommendations are not fetched when the feature doesn’t exist', async function (assert) {
   443      window.localStorage.nomadTokenSecret = managementToken.secretId;
   444      await JobDetail.visit({
   445        id: `${job.id}@${server.db.namespaces[1].name}`,
   446      });
   447  
   448      assert.equal(JobDetail.recommendations.length, 0);
   449  
   450      assert.equal(
   451        server.pretender.handledRequests.filter((request) =>
   452          request.url.includes('recommendations')
   453        ).length,
   454        0
   455      );
   456    });
   457  
   458    test('when the dynamic autoscaler is applied, you can scale a task within the job detail page', async function (assert) {
   459      const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace';
   460      const READ_ONLY_NAMESPACE = 'read-only-namespace';
   461      const clientToken = server.create('token');
   462  
   463      const namespace = server.create('namespace', {
   464        id: SCALE_AND_WRITE_NAMESPACE,
   465      });
   466      const secondNamespace = server.create('namespace', {
   467        id: READ_ONLY_NAMESPACE,
   468      });
   469  
   470      job = server.create('job', {
   471        groupCount: 0,
   472        createAllocations: false,
   473        shallow: true,
   474        noActiveDeployment: true,
   475        namespaceId: SCALE_AND_WRITE_NAMESPACE,
   476      });
   477  
   478      const job2 = server.create('job', {
   479        groupCount: 0,
   480        createAllocations: false,
   481        shallow: true,
   482        noActiveDeployment: true,
   483        namespaceId: READ_ONLY_NAMESPACE,
   484      });
   485      const scalingGroup2 = server.create('task-group', {
   486        job: job2,
   487        name: 'scaling',
   488        count: 1,
   489        shallow: true,
   490        withScaling: true,
   491      });
   492      job2.update({ taskGroupIds: [scalingGroup2.id] });
   493  
   494      const policy = server.create('policy', {
   495        id: 'something',
   496        name: 'something',
   497        rulesJSON: {
   498          Namespaces: [
   499            {
   500              Name: SCALE_AND_WRITE_NAMESPACE,
   501              Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'],
   502            },
   503            {
   504              Name: READ_ONLY_NAMESPACE,
   505              Capabilities: ['list-jobs', 'read-job'],
   506            },
   507          ],
   508        },
   509      });
   510      const scalingGroup = server.create('task-group', {
   511        job,
   512        name: 'scaling',
   513        count: 1,
   514        shallow: true,
   515        withScaling: true,
   516      });
   517      job.update({ taskGroupIds: [scalingGroup.id] });
   518  
   519      clientToken.policyIds = [policy.id];
   520      clientToken.save();
   521      window.localStorage.nomadTokenSecret = clientToken.secretId;
   522  
   523      await JobDetail.visit({ id: `${job.id}@${namespace.name}` });
   524      assert.notOk(JobDetail.incrementButton.isDisabled);
   525  
   526      await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` });
   527      assert.ok(JobDetail.incrementButton.isDisabled);
   528    });
   529  });