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

     1  /* eslint-disable qunit/require-expect */
     2  import { currentURL, waitFor } from '@ember/test-helpers';
     3  import { module, test } from 'qunit';
     4  import { setupApplicationTest } from 'ember-qunit';
     5  import { setupMirage } from 'ember-cli-mirage/test-support';
     6  import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit';
     7  import Task from 'nomad-ui/tests/pages/allocations/task/detail';
     8  import Layout from 'nomad-ui/tests/pages/layout';
     9  import moment from 'moment';
    10  
    11  let allocation;
    12  let task;
    13  
    14  module('Acceptance | task detail', function (hooks) {
    15    setupApplicationTest(hooks);
    16    setupMirage(hooks);
    17  
    18    hooks.beforeEach(async function () {
    19      server.create('agent');
    20      server.create('node');
    21      server.create('job', { createAllocations: false });
    22      allocation = server.create('allocation', 'withTaskWithPorts', {
    23        clientStatus: 'running',
    24      });
    25      server.db.taskStates.update(
    26        { allocationId: allocation.id },
    27        { state: 'running' }
    28      );
    29      task = server.db.taskStates.where({ allocationId: allocation.id })[0];
    30  
    31      await Task.visit({ id: allocation.id, name: task.name });
    32    });
    33  
    34    test('it passes an accessibility audit', async function (assert) {
    35      assert.expect(1);
    36  
    37      await a11yAudit(assert);
    38    });
    39  
    40    test('/allocation/:id/:task_name should name the task and list high-level task information', async function (assert) {
    41      assert.ok(Task.title.text.includes(task.name), 'Task name');
    42      assert.ok(Task.state.includes(task.state), 'Task state');
    43  
    44      assert.ok(
    45        Task.startedAt.includes(
    46          moment(task.startedAt).format("MMM DD, 'YY HH:mm:ss ZZ")
    47        ),
    48        'Task started at'
    49      );
    50  
    51      const lifecycle = server.db.tasks.where({ name: task.name })[0].Lifecycle;
    52  
    53      let lifecycleName = 'main';
    54      if (
    55        lifecycle &&
    56        (lifecycle.Hook === 'prestart' || lifecycle.Hook === 'poststart')
    57      ) {
    58        lifecycleName = `${lifecycle.Hook}-${
    59          lifecycle.Sidecar ? 'sidecar' : 'ephemeral'
    60        }`;
    61      }
    62      if (lifecycle && lifecycle.Hook === 'poststop') {
    63        lifecycleName = 'poststop';
    64      }
    65  
    66      assert.equal(Task.lifecycle, lifecycleName);
    67  
    68      assert.equal(document.title, `Task ${task.name} - Nomad`);
    69    });
    70  
    71    test('breadcrumbs match jobs / job / task group / allocation / task', async function (assert) {
    72      const { jobId, taskGroup } = allocation;
    73      const job = server.db.jobs.find(jobId);
    74  
    75      const shortId = allocation.id.split('-')[0];
    76      assert.equal(
    77        Layout.breadcrumbFor('jobs.index').text,
    78        'Jobs',
    79        'Jobs is the first breadcrumb'
    80      );
    81  
    82      await waitFor('[data-test-job-breadcrumb]');
    83      assert.equal(
    84        Layout.breadcrumbFor('jobs.job.index').text,
    85        `Job ${job.name}`,
    86        'Job is the second breadcrumb'
    87      );
    88      assert.equal(
    89        Layout.breadcrumbFor('jobs.job.task-group').text,
    90        `Task Group ${taskGroup}`,
    91        'Task Group is the third breadcrumb'
    92      );
    93      assert.equal(
    94        Layout.breadcrumbFor('allocations.allocation').text,
    95        `Allocation ${shortId}`,
    96        'Allocation short id is the fourth breadcrumb'
    97      );
    98      assert.equal(
    99        Layout.breadcrumbFor('allocations.allocation.task').text,
   100        `Task ${task.name}`,
   101        'Task name is the fifth breadcrumb'
   102      );
   103  
   104      await Layout.breadcrumbFor('jobs.index').visit();
   105      assert.equal(currentURL(), '/jobs', 'Jobs breadcrumb links correctly');
   106  
   107      await Task.visit({ id: allocation.id, name: task.name });
   108      await Layout.breadcrumbFor('jobs.job.index').visit();
   109      assert.equal(
   110        currentURL(),
   111        `/jobs/${job.id}@default`,
   112        'Job breadcrumb links correctly'
   113      );
   114  
   115      await Task.visit({ id: allocation.id, name: task.name });
   116      await Layout.breadcrumbFor('jobs.job.task-group').visit();
   117      assert.equal(
   118        currentURL(),
   119        `/jobs/${job.id}@default/${taskGroup}`,
   120        'Task Group breadcrumb links correctly'
   121      );
   122  
   123      await Task.visit({ id: allocation.id, name: task.name });
   124      await Layout.breadcrumbFor('allocations.allocation').visit();
   125      assert.equal(
   126        currentURL(),
   127        `/allocations/${allocation.id}`,
   128        'Allocations breadcrumb links correctly'
   129      );
   130    });
   131  
   132    test('/allocation/:id/:task_name should include resource utilization graphs', async function (assert) {
   133      assert.equal(
   134        Task.resourceCharts.length,
   135        2,
   136        'Two resource utilization graphs'
   137      );
   138      assert.equal(
   139        Task.resourceCharts.objectAt(0).name,
   140        'CPU',
   141        'First chart is CPU'
   142      );
   143      assert.equal(
   144        Task.resourceCharts.objectAt(1).name,
   145        'Memory',
   146        'Second chart is Memory'
   147      );
   148    });
   149  
   150    test('the events table lists all recent events', async function (assert) {
   151      const events = server.db.taskEvents.where({ taskStateId: task.id });
   152  
   153      assert.equal(
   154        Task.events.length,
   155        events.length,
   156        `Lists ${events.length} events`
   157      );
   158    });
   159  
   160    test('when a task has volumes, the volumes table is shown', async function (assert) {
   161      const taskGroup = server.schema.taskGroups.where({
   162        jobId: allocation.jobId,
   163        name: allocation.taskGroup,
   164      }).models[0];
   165  
   166      const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name);
   167  
   168      assert.ok(Task.hasVolumes);
   169      assert.equal(Task.volumes.length, jobTask.volumeMounts.length);
   170    });
   171  
   172    test('when a task does not have volumes, the volumes table is not shown', async function (assert) {
   173      const job = server.create('job', {
   174        createAllocations: false,
   175        noHostVolumes: true,
   176      });
   177      allocation = server.create('allocation', {
   178        jobId: job.id,
   179        clientStatus: 'running',
   180      });
   181      task = server.db.taskStates.where({ allocationId: allocation.id })[0];
   182  
   183      await Task.visit({ id: allocation.id, name: task.name });
   184      assert.notOk(Task.hasVolumes);
   185    });
   186  
   187    test('each volume in the volumes table shows information about the volume', async function (assert) {
   188      const taskGroup = server.schema.taskGroups.where({
   189        jobId: allocation.jobId,
   190        name: allocation.taskGroup,
   191      }).models[0];
   192  
   193      const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name);
   194      const volume = jobTask.volumeMounts[0];
   195  
   196      Task.volumes[0].as((volumeRow) => {
   197        assert.equal(volumeRow.name, volume.Volume);
   198        assert.equal(volumeRow.destination, volume.Destination);
   199        assert.equal(
   200          volumeRow.permissions,
   201          volume.ReadOnly ? 'Read' : 'Read/Write'
   202        );
   203        assert.equal(
   204          volumeRow.clientSource,
   205          taskGroup.volumes[volume.Volume].Source
   206        );
   207      });
   208    });
   209  
   210    test('each recent event should list the time, type, and description of the event', async function (assert) {
   211      const event = server.db.taskEvents.where({ taskStateId: task.id })[0];
   212      const recentEvent = Task.events.objectAt(Task.events.length - 1);
   213  
   214      assert.equal(
   215        recentEvent.time,
   216        moment(event.time / 1000000).format("MMM DD, 'YY HH:mm:ss ZZ"),
   217        'Event timestamp'
   218      );
   219      assert.equal(recentEvent.type, event.type, 'Event type');
   220      assert.equal(recentEvent.message, event.displayMessage, 'Event message');
   221    });
   222  
   223    test('when the allocation is not found, the application errors', async function (assert) {
   224      await Task.visit({ id: 'not-a-real-allocation', name: task.name });
   225  
   226      assert.equal(
   227        server.pretender.handledRequests
   228          .filter((request) => !request.url.includes('policy'))
   229          .findBy('status', 404).url,
   230        '/v1/allocation/not-a-real-allocation',
   231        'A request to the nonexistent allocation is made'
   232      );
   233      assert.equal(
   234        currentURL(),
   235        `/allocations/not-a-real-allocation/${task.name}`,
   236        'The URL persists'
   237      );
   238      assert.ok(Task.error.isPresent, 'Error message is shown');
   239      assert.equal(Task.error.title, 'Not Found', 'Error message is for 404');
   240    });
   241  
   242    test('when the allocation is found but the task is not, the application errors', async function (assert) {
   243      await Task.visit({ id: allocation.id, name: 'not-a-real-task-name' });
   244  
   245      assert.ok(
   246        server.pretender.handledRequests
   247          .filterBy('status', 200)
   248          .mapBy('url')
   249          .includes(`/v1/allocation/${allocation.id}`),
   250        'A request to the allocation is made successfully'
   251      );
   252      assert.equal(
   253        currentURL(),
   254        `/allocations/${allocation.id}/not-a-real-task-name`,
   255        'The URL persists'
   256      );
   257      assert.ok(Task.error.isPresent, 'Error message is shown');
   258      assert.equal(Task.error.title, 'Not Found', 'Error message is for 404');
   259    });
   260  
   261    test('task can be restarted', async function (assert) {
   262      await Task.restart.idle();
   263      await Task.restart.confirm();
   264  
   265      const request = server.pretender.handledRequests.findBy('method', 'PUT');
   266      assert.equal(
   267        request.url,
   268        `/v1/client/allocation/${allocation.id}/restart`,
   269        'Restart request is made for the allocation'
   270      );
   271  
   272      assert.deepEqual(
   273        JSON.parse(request.requestBody),
   274        { TaskName: task.name },
   275        'Restart request is made for the correct task'
   276      );
   277    });
   278  
   279    test('when task restart fails (403), an ACL permissions error message is shown', async function (assert) {
   280      server.pretender.put('/v1/client/allocation/:id/restart', () => [
   281        403,
   282        {},
   283        '',
   284      ]);
   285  
   286      await Task.restart.idle();
   287      await Task.restart.confirm();
   288  
   289      assert.ok(Task.inlineError.isShown, 'Inline error is shown');
   290      assert.ok(
   291        Task.inlineError.title.includes('Could Not Restart Task'),
   292        'Title is descriptive'
   293      );
   294      assert.ok(
   295        /ACL token.+?allocation lifecycle/.test(Task.inlineError.message),
   296        'Message mentions ACLs and the appropriate permission'
   297      );
   298  
   299      await Task.inlineError.dismiss();
   300  
   301      assert.notOk(Task.inlineError.isShown, 'Inline error is no longer shown');
   302    });
   303  
   304    test('when task restart fails (500), the error message from the API is piped through to the alert', async function (assert) {
   305      const message = 'A plaintext error message';
   306      server.pretender.put('/v1/client/allocation/:id/restart', () => [
   307        500,
   308        {},
   309        message,
   310      ]);
   311  
   312      await Task.restart.idle();
   313      await Task.restart.confirm();
   314  
   315      assert.ok(Task.inlineError.isShown);
   316      assert.ok(Task.inlineError.title.includes('Could Not Restart Task'));
   317      assert.equal(Task.inlineError.message, message);
   318  
   319      await Task.inlineError.dismiss();
   320  
   321      assert.notOk(Task.inlineError.isShown);
   322    });
   323  
   324    test('exec button is present', async function (assert) {
   325      assert.ok(Task.execButton.isPresent);
   326    });
   327  });
   328  
   329  module('Acceptance | task detail (no addresses)', function (hooks) {
   330    setupApplicationTest(hooks);
   331    setupMirage(hooks);
   332  
   333    hooks.beforeEach(async function () {
   334      server.create('agent');
   335      server.create('node');
   336      server.create('job');
   337      allocation = server.create('allocation', 'withoutTaskWithPorts', {
   338        clientStatus: 'running',
   339      });
   340      task = server.db.taskStates.where({ allocationId: allocation.id })[0];
   341  
   342      await Task.visit({ id: allocation.id, name: task.name });
   343    });
   344  });
   345  
   346  module('Acceptance | task detail (different namespace)', function (hooks) {
   347    setupApplicationTest(hooks);
   348    setupMirage(hooks);
   349  
   350    hooks.beforeEach(async function () {
   351      server.create('agent');
   352      server.create('node');
   353      server.create('namespace');
   354      server.create('namespace', { id: 'other-namespace' });
   355      server.create('job', {
   356        createAllocations: false,
   357        namespaceId: 'other-namespace',
   358      });
   359      allocation = server.create('allocation', 'withTaskWithPorts', {
   360        clientStatus: 'running',
   361      });
   362      task = server.db.taskStates.where({ allocationId: allocation.id })[0];
   363  
   364      await Task.visit({ id: allocation.id, name: task.name });
   365    });
   366  
   367    test('breadcrumbs match jobs / job / task group / allocation / task', async function (assert) {
   368      const { jobId, taskGroup } = allocation;
   369      const job = server.db.jobs.find(jobId);
   370  
   371      await Layout.breadcrumbFor('jobs.index').visit();
   372      assert.equal(
   373        currentURL(),
   374        '/jobs?namespace=*',
   375        'Jobs breadcrumb links correctly'
   376      );
   377  
   378      await Task.visit({ id: allocation.id, name: task.name });
   379      await Layout.breadcrumbFor('jobs.job.index').visit();
   380      assert.equal(
   381        currentURL(),
   382        `/jobs/${job.id}@other-namespace`,
   383        'Job breadcrumb links correctly'
   384      );
   385  
   386      await Task.visit({ id: allocation.id, name: task.name });
   387      await Layout.breadcrumbFor('jobs.job.task-group').visit();
   388      assert.equal(
   389        currentURL(),
   390        `/jobs/${job.id}@other-namespace/${taskGroup}`,
   391        'Task Group breadcrumb links correctly'
   392      );
   393  
   394      await Task.visit({ id: allocation.id, name: task.name });
   395      await Layout.breadcrumbFor('allocations.allocation').visit();
   396      assert.equal(
   397        currentURL(),
   398        `/allocations/${allocation.id}`,
   399        'Allocations breadcrumb links correctly'
   400      );
   401    });
   402  });
   403  
   404  module('Acceptance | task detail (not running)', function (hooks) {
   405    setupApplicationTest(hooks);
   406    setupMirage(hooks);
   407  
   408    hooks.beforeEach(async function () {
   409      server.create('agent');
   410      server.create('node');
   411      server.create('namespace');
   412      server.create('namespace', { id: 'other-namespace' });
   413      server.create('job', {
   414        createAllocations: false,
   415        namespaceId: 'other-namespace',
   416      });
   417      allocation = server.create('allocation', 'withTaskWithPorts', {
   418        clientStatus: 'complete',
   419      });
   420      task = server.db.taskStates.where({ allocationId: allocation.id })[0];
   421  
   422      await Task.visit({ id: allocation.id, name: task.name });
   423    });
   424  
   425    test('when the allocation for a task is not running, the resource utilization graphs are replaced by an empty message', async function (assert) {
   426      assert.equal(Task.resourceCharts.length, 0, 'No resource charts');
   427      assert.equal(
   428        Task.resourceEmptyMessage,
   429        "Task isn't running",
   430        'Empty message is appropriate'
   431      );
   432    });
   433  
   434    test('exec button is absent', async function (assert) {
   435      assert.notOk(Task.execButton.isPresent);
   436    });
   437  });
   438  
   439  module('Acceptance | proxy task detail', function (hooks) {
   440    setupApplicationTest(hooks);
   441    setupMirage(hooks);
   442  
   443    hooks.beforeEach(async function () {
   444      server.create('agent');
   445      server.create('node');
   446      server.create('job', { createAllocations: false });
   447      allocation = server.create('allocation', 'withTaskWithPorts', {
   448        clientStatus: 'running',
   449      });
   450  
   451      const taskState = allocation.taskStates.models[0];
   452      const task = server.schema.tasks.findBy({ name: taskState.name });
   453      task.update('kind', 'connect-proxy:task');
   454      task.save();
   455  
   456      await Task.visit({ id: allocation.id, name: taskState.name });
   457    });
   458  
   459    test('a proxy tag is shown', async function (assert) {
   460      assert.ok(Task.title.proxyTag.isPresent);
   461    });
   462  });