github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/helpers/module-for-job.js (about)

     1  /* eslint-disable qunit/require-expect */
     2  /* eslint-disable qunit/no-conditional-assertions */
     3  import {
     4    click,
     5    currentRouteName,
     6    currentURL,
     7    visit,
     8  } 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 JobDetail from 'nomad-ui/tests/pages/jobs/detail';
    13  import setPolicy from 'nomad-ui/tests/utils/set-policy';
    14  
    15  // moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/
    16  // this is a misnomer in our context, because we're not using this API, however, the linter does not understand this
    17  // the linter warning will go away if we rename this factory function to generateJobDetailsTests
    18  // eslint-disable-next-line ember/no-test-module-for
    19  export default function moduleForJob(
    20    title,
    21    context,
    22    jobFactory,
    23    additionalTests
    24  ) {
    25    let job;
    26  
    27    module(title, function (hooks) {
    28      setupApplicationTest(hooks);
    29      setupMirage(hooks);
    30      hooks.before(function () {
    31        if (context !== 'allocations' && context !== 'children') {
    32          throw new Error(
    33            `Invalid context provided to moduleForJob, expected either "allocations" or "children", got ${context}`
    34          );
    35        }
    36      });
    37  
    38      hooks.beforeEach(async function () {
    39        server.create('node');
    40        job = jobFactory();
    41        if (!job.namespace || job.namespace === 'default') {
    42          await JobDetail.visit({ id: job.id });
    43        } else {
    44          await JobDetail.visit({ id: `${job.id}@${job.namespace}` });
    45        }
    46  
    47        const hasClientStatus = ['system', 'sysbatch'].includes(job.type);
    48        if (context === 'allocations' && hasClientStatus) {
    49          await click("[data-test-accordion-summary-chart='allocation-status']");
    50        }
    51      });
    52  
    53      test('visiting /jobs/:job_id', async function (assert) {
    54        const expectedURL = job.namespace
    55          ? `/jobs/${job.name}@${job.namespace}`
    56          : `/jobs/${job.name}`;
    57  
    58        assert.equal(decodeURIComponent(currentURL()), expectedURL);
    59        assert.equal(document.title, `Job ${job.name} - Nomad`);
    60      });
    61  
    62      test('the subnav links to overview', async function (assert) {
    63        await JobDetail.tabFor('overview').visit();
    64  
    65        const expectedURL = job.namespace
    66          ? `/jobs/${job.name}@${job.namespace}`
    67          : `/jobs/${job.name}`;
    68  
    69        assert.equal(decodeURIComponent(currentURL()), expectedURL);
    70      });
    71  
    72      test('the subnav links to definition', async function (assert) {
    73        await JobDetail.tabFor('definition').visit();
    74  
    75        const expectedURL = job.namespace
    76          ? `/jobs/${job.name}@${job.namespace}/definition`
    77          : `/jobs/${job.name}/definition`;
    78  
    79        assert.equal(decodeURIComponent(currentURL()), expectedURL);
    80      });
    81  
    82      test('the subnav links to versions', async function (assert) {
    83        await JobDetail.tabFor('versions').visit();
    84  
    85        const expectedURL = job.namespace
    86          ? `/jobs/${job.name}@${job.namespace}/versions`
    87          : `/jobs/${job.name}/versions`;
    88  
    89        assert.equal(decodeURIComponent(currentURL()), expectedURL);
    90      });
    91  
    92      test('the subnav links to evaluations', async function (assert) {
    93        await JobDetail.tabFor('evaluations').visit();
    94  
    95        const expectedURL = job.namespace
    96          ? `/jobs/${job.name}@${job.namespace}/evaluations`
    97          : `/jobs/${job.name}/evaluations`;
    98  
    99        assert.equal(decodeURIComponent(currentURL()), expectedURL);
   100      });
   101  
   102      test('the title buttons are dependent on job status', async function (assert) {
   103        if (job.status === 'dead') {
   104          assert.ok(JobDetail.start.isPresent);
   105          assert.ok(JobDetail.purge.isPresent);
   106          assert.notOk(JobDetail.stop.isPresent);
   107          assert.notOk(JobDetail.execButton.isPresent);
   108        } else {
   109          assert.notOk(JobDetail.start.isPresent);
   110          assert.notOk(JobDetail.purge.isPresent);
   111          assert.ok(JobDetail.stop.isPresent);
   112          assert.ok(JobDetail.execButton.isPresent);
   113        }
   114      });
   115  
   116      if (context === 'allocations') {
   117        test('allocations for the job are shown in the overview', async function (assert) {
   118          assert.ok(
   119            JobDetail.allocationsSummary.isPresent,
   120            'Allocations are shown in the summary section'
   121          );
   122          assert.ok(
   123            JobDetail.childrenSummary.isHidden,
   124            'Children are not shown in the summary section'
   125          );
   126        });
   127  
   128        test('clicking in an allocation row navigates to that allocation', async function (assert) {
   129          const allocationRow = JobDetail.allocations[0];
   130          const allocationId = allocationRow.id;
   131  
   132          await allocationRow.visitRow();
   133  
   134          assert.equal(
   135            currentURL(),
   136            `/allocations/${allocationId}`,
   137            'Allocation row links to allocation detail'
   138          );
   139        });
   140  
   141        test('clicking in a task group row navigates to that task group', async function (assert) {
   142          const tgRow = JobDetail.taskGroups[0];
   143          const tgName = tgRow.name;
   144  
   145          await tgRow.visitRow();
   146  
   147          const expectedURL = job.namespace
   148            ? `/jobs/${encodeURIComponent(job.name)}@${job.namespace}/${tgName}`
   149            : `/jobs/${encodeURIComponent(job.name)}/${tgName}`;
   150  
   151          assert.equal(currentURL(), expectedURL);
   152        });
   153  
   154        test('clicking legend item navigates to a pre-filtered allocations table', async function (assert) {
   155          const legendItem =
   156            JobDetail.allocationsSummary.legend.clickableItems[1];
   157          const status = legendItem.label;
   158          await legendItem.click();
   159  
   160          const encodedStatus = encodeURIComponent(JSON.stringify([status]));
   161          const expectedURL = new URL(
   162            urlWithNamespace(
   163              `/jobs/${job.name}@default/clients?status=${encodedStatus}`,
   164              job.namespace
   165            ),
   166            window.location
   167          );
   168          const gotURL = new URL(currentURL(), window.location);
   169          assert.deepEqual(gotURL.path, expectedURL.path);
   170          assert.deepEqual(gotURL.searchParams, expectedURL.searchParams);
   171        });
   172  
   173        test('clicking in a slice takes you to a pre-filtered allocations table', async function (assert) {
   174          const slice = JobDetail.allocationsSummary.slices[1];
   175          const status = slice.label;
   176          await slice.click();
   177  
   178          const encodedStatus = encodeURIComponent(JSON.stringify([status]));
   179          const expectedURL = new URL(
   180            urlWithNamespace(
   181              `/jobs/${encodeURIComponent(
   182                job.name
   183              )}/allocations?status=${encodedStatus}`,
   184              job.namespace
   185            ),
   186            window.location
   187          );
   188          const gotURL = new URL(currentURL(), window.location);
   189          assert.deepEqual(gotURL.pathname, expectedURL.pathname);
   190  
   191          // Sort and compare URL query params.
   192          gotURL.searchParams.sort();
   193          expectedURL.searchParams.sort();
   194          assert.equal(
   195            gotURL.searchParams.toString(),
   196            expectedURL.searchParams.toString()
   197          );
   198        });
   199      }
   200  
   201      if (context === 'children') {
   202        test('children for the job are shown in the overview', async function (assert) {
   203          assert.ok(
   204            JobDetail.childrenSummary.isPresent,
   205            'Children are shown in the summary section'
   206          );
   207          assert.ok(
   208            JobDetail.allocationsSummary.isHidden,
   209            'Allocations are not shown in the summary section'
   210          );
   211        });
   212      }
   213  
   214      for (var testName in additionalTests) {
   215        test(testName, async function (assert) {
   216          await additionalTests[testName].call(this, job, assert);
   217        });
   218      }
   219    });
   220  }
   221  
   222  // moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/
   223  // this is a misnomer in our context, because we're not using this API, however, the linter does not understand this
   224  // the linter warning will go away if we rename this factory function to generateJobClientStatusTests
   225  // eslint-disable-next-line ember/no-test-module-for
   226  export function moduleForJobWithClientStatus(
   227    title,
   228    jobFactory,
   229    additionalTests
   230  ) {
   231    let job;
   232  
   233    module(title, function (hooks) {
   234      setupApplicationTest(hooks);
   235      setupMirage(hooks);
   236  
   237      hooks.beforeEach(async function () {
   238        const clients = server.createList('node', 3, {
   239          datacenter: 'dc1',
   240          status: 'ready',
   241        });
   242        job = jobFactory();
   243        clients.forEach((c) => {
   244          server.create('allocation', { jobId: job.id, nodeId: c.id });
   245        });
   246      });
   247  
   248      module('with node:read permissions', function (hooks) {
   249        hooks.beforeEach(async function () {
   250          // Displaying the job status in client requires node:read permission.
   251          setPolicy({
   252            id: 'node-read',
   253            name: 'node-read',
   254            rulesJSON: {
   255              Node: {
   256                Policy: 'read',
   257              },
   258            },
   259          });
   260  
   261          await visitJobDetailPage(job);
   262        });
   263  
   264        test('the subnav links to clients', async function (assert) {
   265          await JobDetail.tabFor('clients').visit();
   266  
   267          const expectedURL = job.namespace
   268            ? `/jobs/${job.id}@${job.namespace}/clients`
   269            : `/jobs/${job.id}/clients`;
   270  
   271          assert.equal(currentURL(), expectedURL);
   272        });
   273  
   274        test('job status summary is shown in the overview', async function (assert) {
   275          assert.ok(
   276            JobDetail.jobClientStatusSummary.statusBar.isPresent,
   277            'Summary bar is displayed in the Job Status in Client summary section'
   278          );
   279        });
   280  
   281        test('clicking legend item navigates to a pre-filtered clients table', async function (assert) {
   282          const legendItem =
   283            JobDetail.jobClientStatusSummary.statusBar.legend.clickableItems[0];
   284          const status = legendItem.label;
   285          await legendItem.click();
   286  
   287          const encodedStatus = encodeURIComponent(JSON.stringify([status]));
   288          const expectedURL = new URL(
   289            urlWithNamespace(
   290              `/jobs/${job.name}/clients?status=${encodedStatus}`,
   291              job.namespace
   292            ),
   293            window.location
   294          );
   295          const gotURL = new URL(currentURL(), window.location);
   296          assert.deepEqual(gotURL.path, expectedURL.path);
   297          assert.deepEqual(gotURL.searchParams, expectedURL.searchParams);
   298        });
   299  
   300        test('clicking in a slice takes you to a pre-filtered clients table', async function (assert) {
   301          const slice = JobDetail.jobClientStatusSummary.statusBar.slices[0];
   302          const status = slice.label;
   303          await slice.click();
   304  
   305          const encodedStatus = encodeURIComponent(JSON.stringify([status]));
   306  
   307          const expectedURL = job.namespace
   308            ? `/jobs/${job.name}@${job.namespace}/clients?status=${encodedStatus}`
   309            : `/jobs/${job.name}/clients?status=${encodedStatus}`;
   310  
   311          assert.deepEqual(currentURL(), expectedURL, 'url is correct');
   312        });
   313  
   314        for (var testName in additionalTests) {
   315          test(testName, async function (assert) {
   316            await additionalTests[testName].call(this, job, assert);
   317          });
   318        }
   319      });
   320  
   321      module('without node:read permissions', function (hooks) {
   322        hooks.beforeEach(async function () {
   323          // Test blank Node policy to mock lack of permission.
   324          setPolicy({
   325            id: 'node',
   326            name: 'node',
   327            rulesJSON: {},
   328          });
   329  
   330          await visitJobDetailPage(job);
   331        });
   332  
   333        test('the page handles presentations concerns regarding the user not having node:read permissions', async function (assert) {
   334          assert
   335            .dom("[data-test-tab='clients']")
   336            .doesNotExist(
   337              'Job Detail Sub Navigation should not render Clients tab'
   338            );
   339  
   340          assert
   341            .dom('[data-test-nodes-not-authorized]')
   342            .exists('Renders Not Authorized message');
   343        });
   344  
   345        test('/jobs/job/clients route is protected with authorization logic', async function (assert) {
   346          await visit(`/jobs/${job.id}/clients`);
   347  
   348          assert.equal(
   349            currentRouteName(),
   350            'jobs.job.index',
   351            'The clients route cannot be visited unless you have node:read permissions'
   352          );
   353        });
   354      });
   355    });
   356  }
   357  
   358  function urlWithNamespace(url, namespace) {
   359    if (!namespace || namespace === 'default') {
   360      return url;
   361    }
   362  
   363    const parts = url.split('?');
   364    const params = new URLSearchParams(parts[1]);
   365    params.set('namespace', namespace);
   366  
   367    return `${parts[0]}?${params.toString()}`;
   368  }
   369  
   370  async function visitJobDetailPage({ id, namespace }) {
   371    if (!namespace || namespace === 'default') {
   372      await JobDetail.visit({ id });
   373    } else {
   374      await JobDetail.visit({ id: `${id}@${namespace}` });
   375    }
   376  }