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

     1  import { next } from '@ember/runloop';
     2  import { assign } from '@ember/polyfills';
     3  import { settled } from '@ember/test-helpers';
     4  import { setupTest } from 'ember-qunit';
     5  import { module, test } from 'qunit';
     6  import { startMirage } from 'nomad-ui/initializers/ember-cli-mirage';
     7  import { AbortController } from 'fetch';
     8  import { TextEncoderLite } from 'text-encoder-lite';
     9  import base64js from 'base64-js';
    10  
    11  module('Unit | Adapter | Job', function (hooks) {
    12    setupTest(hooks);
    13  
    14    hooks.beforeEach(async function () {
    15      this.store = this.owner.lookup('service:store');
    16      this.subject = () => this.store.adapterFor('job');
    17  
    18      window.sessionStorage.clear();
    19      window.localStorage.clear();
    20  
    21      this.server = startMirage();
    22  
    23      this.initializeUI = async ({ region, namespace } = {}) => {
    24        if (namespace) window.localStorage.nomadActiveNamespace = namespace;
    25        if (region) window.localStorage.nomadActiveRegion = region;
    26  
    27        this.server.create('namespace');
    28        this.server.create('namespace', { id: 'some-namespace' });
    29        this.server.create('node');
    30        this.server.create('job', { id: 'job-1', namespaceId: 'default' });
    31        this.server.create('job', { id: 'job-2', namespaceId: 'some-namespace' });
    32  
    33        this.server.create('region', { id: 'region-1' });
    34        this.server.create('region', { id: 'region-2' });
    35  
    36        this.system = this.owner.lookup('service:system');
    37  
    38        // Namespace, default region, and all regions are requests that all
    39        // job requests depend on. Fetching them ahead of time means testing
    40        // job adapter behavior in isolation.
    41        await this.system.get('namespaces');
    42        this.system.get('shouldIncludeRegion');
    43        await this.system.get('defaultRegion');
    44  
    45        // Reset the handledRequests array to avoid accounting for this
    46        // namespaces request everywhere.
    47        this.server.pretender.handledRequests.length = 0;
    48      };
    49  
    50      this.initializeWithJob = async (props = {}) => {
    51        await this.initializeUI(props);
    52  
    53        const job = await this.store.findRecord(
    54          'job',
    55          JSON.stringify(['job-1', props.namespace || 'default'])
    56        );
    57        this.server.pretender.handledRequests.length = 0;
    58        return job;
    59      };
    60    });
    61  
    62    hooks.afterEach(function () {
    63      this.server.shutdown();
    64    });
    65  
    66    test('The job endpoint is the only required endpoint for fetching a job', async function (assert) {
    67      await this.initializeUI();
    68  
    69      const { pretender } = this.server;
    70      const jobName = 'job-1';
    71      const jobNamespace = 'default';
    72      const jobId = JSON.stringify([jobName, jobNamespace]);
    73  
    74      this.subject().findRecord(null, { modelName: 'job' }, jobId);
    75  
    76      await settled();
    77      assert.deepEqual(
    78        pretender.handledRequests.mapBy('url'),
    79        [`/v1/job/${jobName}`],
    80        'The only request made is /job/:id'
    81      );
    82    });
    83  
    84    test('When a namespace is set in localStorage but a job in the default namespace is requested, the namespace query param is not present', async function (assert) {
    85      await this.initializeUI({ namespace: 'some-namespace' });
    86  
    87      const { pretender } = this.server;
    88      const jobName = 'job-1';
    89      const jobNamespace = 'default';
    90      const jobId = JSON.stringify([jobName, jobNamespace]);
    91  
    92      this.subject().findRecord(null, { modelName: 'job' }, jobId);
    93      await settled();
    94  
    95      assert.deepEqual(
    96        pretender.handledRequests.mapBy('url'),
    97        [`/v1/job/${jobName}`],
    98        'The only request made is /job/:id with no namespace query param'
    99      );
   100    });
   101  
   102    test('When a namespace is in localStorage and the requested job is in the default namespace, the namespace query param is left out', async function (assert) {
   103      await this.initializeUI({ namespace: 'red-herring' });
   104  
   105      const { pretender } = this.server;
   106      const jobName = 'job-1';
   107      const jobNamespace = 'default';
   108      const jobId = JSON.stringify([jobName, jobNamespace]);
   109  
   110      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   111      await settled();
   112  
   113      assert.deepEqual(
   114        pretender.handledRequests.mapBy('url'),
   115        [`/v1/job/${jobName}`],
   116        'The request made is /job/:id with no namespace query param'
   117      );
   118    });
   119  
   120    test('When the job has a namespace other than default, it is in the URL', async function (assert) {
   121      await this.initializeUI();
   122  
   123      const { pretender } = this.server;
   124      const jobName = 'job-2';
   125      const jobNamespace = 'some-namespace';
   126      const jobId = JSON.stringify([jobName, jobNamespace]);
   127  
   128      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   129      await settled();
   130  
   131      assert.deepEqual(
   132        pretender.handledRequests.mapBy('url'),
   133        [`/v1/job/${jobName}?namespace=${jobNamespace}`],
   134        'The only request made is /job/:id?namespace=:namespace'
   135      );
   136    });
   137  
   138    test('When there is no token set in the token service, no X-Nomad-Token header is set', async function (assert) {
   139      await this.initializeUI();
   140  
   141      const { pretender } = this.server;
   142      const jobId = JSON.stringify(['job-1', 'default']);
   143  
   144      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   145      await settled();
   146  
   147      assert.notOk(
   148        pretender.handledRequests
   149          .mapBy('requestHeaders')
   150          .some((headers) => headers['X-Nomad-Token']),
   151        'No token header present on either job request'
   152      );
   153    });
   154  
   155    test('When a token is set in the token service, then X-Nomad-Token header is set', async function (assert) {
   156      await this.initializeUI();
   157  
   158      const { pretender } = this.server;
   159      const jobId = JSON.stringify(['job-1', 'default']);
   160      const secret = 'here is the secret';
   161  
   162      this.subject().set('token.secret', secret);
   163      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   164      await settled();
   165  
   166      assert.ok(
   167        pretender.handledRequests
   168          .mapBy('requestHeaders')
   169          .every((headers) => headers['X-Nomad-Token'] === secret),
   170        'The token header is present on both job requests'
   171      );
   172    });
   173  
   174    test('findAll can be watched', async function (assert) {
   175      await this.initializeUI();
   176  
   177      const { pretender } = this.server;
   178  
   179      const request = () =>
   180        this.subject().findAll(null, { modelName: 'job' }, null, {
   181          reload: true,
   182          adapterOptions: { watch: true },
   183        });
   184  
   185      request();
   186      assert.equal(
   187        pretender.handledRequests[0].url,
   188        '/v1/jobs?index=1',
   189        'Second request is a blocking request for jobs'
   190      );
   191  
   192      await settled();
   193      request();
   194      assert.equal(
   195        pretender.handledRequests[1].url,
   196        '/v1/jobs?index=2',
   197        'Third request is a blocking request with an incremented index param'
   198      );
   199  
   200      await settled();
   201    });
   202  
   203    test('findRecord can be watched', async function (assert) {
   204      await this.initializeUI();
   205  
   206      const jobId = JSON.stringify(['job-1', 'default']);
   207      const { pretender } = this.server;
   208  
   209      const request = () =>
   210        this.subject().findRecord(null, { modelName: 'job' }, jobId, {
   211          reload: true,
   212          adapterOptions: { watch: true },
   213        });
   214  
   215      request();
   216      assert.equal(
   217        pretender.handledRequests[0].url,
   218        '/v1/job/job-1?index=1',
   219        'Second request is a blocking request for job-1'
   220      );
   221  
   222      await settled();
   223      request();
   224      assert.equal(
   225        pretender.handledRequests[1].url,
   226        '/v1/job/job-1?index=2',
   227        'Third request is a blocking request with an incremented index param'
   228      );
   229  
   230      await settled();
   231    });
   232  
   233    test('relationships can be reloaded', async function (assert) {
   234      await this.initializeUI();
   235  
   236      const { pretender } = this.server;
   237      const plainId = 'job-1';
   238      const mockModel = makeMockModel(plainId);
   239  
   240      this.subject().reloadRelationship(mockModel, 'summary');
   241      await settled();
   242      assert.equal(
   243        pretender.handledRequests[0].url,
   244        `/v1/job/${plainId}/summary`,
   245        'Relationship was reloaded'
   246      );
   247    });
   248  
   249    test('relationship reloads can be watched', async function (assert) {
   250      await this.initializeUI();
   251  
   252      const { pretender } = this.server;
   253      const plainId = 'job-1';
   254      const mockModel = makeMockModel(plainId);
   255  
   256      this.subject().reloadRelationship(mockModel, 'summary', { watch: true });
   257      assert.equal(
   258        pretender.handledRequests[0].url,
   259        '/v1/job/job-1/summary?index=1',
   260        'First request is a blocking request for job-1 summary relationship'
   261      );
   262  
   263      await settled();
   264      this.subject().reloadRelationship(mockModel, 'summary', { watch: true });
   265      await settled();
   266  
   267      assert.equal(
   268        pretender.handledRequests[1].url,
   269        '/v1/job/job-1/summary?index=2',
   270        'Second request is a blocking request with an incremented index param'
   271      );
   272    });
   273  
   274    test('findAll can be canceled', async function (assert) {
   275      await this.initializeUI();
   276  
   277      const { pretender } = this.server;
   278      const controller = new AbortController();
   279  
   280      pretender.get('/v1/jobs', () => [200, {}, '[]'], true);
   281  
   282      this.subject()
   283        .findAll(null, { modelName: 'job' }, null, {
   284          reload: true,
   285          adapterOptions: { watch: true, abortController: controller },
   286        })
   287        .catch(() => {});
   288  
   289      const { request: xhr } = pretender.requestReferences[0];
   290      assert.equal(xhr.status, 0, 'Request is still pending');
   291  
   292      // Schedule the cancelation before waiting
   293      next(() => {
   294        controller.abort();
   295      });
   296  
   297      await settled();
   298      assert.ok(xhr.aborted, 'Request was aborted');
   299    });
   300  
   301    test('findRecord can be canceled', async function (assert) {
   302      await this.initializeUI();
   303  
   304      const { pretender } = this.server;
   305      const jobId = JSON.stringify(['job-1', 'default']);
   306      const controller = new AbortController();
   307  
   308      pretender.get('/v1/job/:id', () => [200, {}, '{}'], true);
   309  
   310      this.subject().findRecord(null, { modelName: 'job' }, jobId, {
   311        reload: true,
   312        adapterOptions: { watch: true, abortController: controller },
   313      });
   314  
   315      const { request: xhr } = pretender.requestReferences[0];
   316      assert.equal(xhr.status, 0, 'Request is still pending');
   317  
   318      // Schedule the cancelation before waiting
   319      next(() => {
   320        controller.abort();
   321      });
   322  
   323      await settled();
   324      assert.ok(xhr.aborted, 'Request was aborted');
   325    });
   326  
   327    test('relationship reloads can be canceled', async function (assert) {
   328      await this.initializeUI();
   329  
   330      const { pretender } = this.server;
   331      const plainId = 'job-1';
   332      const controller = new AbortController();
   333      const mockModel = makeMockModel(plainId);
   334      pretender.get('/v1/job/:id/summary', () => [200, {}, '{}'], true);
   335  
   336      this.subject().reloadRelationship(mockModel, 'summary', {
   337        watch: true,
   338        abortController: controller,
   339      });
   340  
   341      const { request: xhr } = pretender.requestReferences[0];
   342      assert.equal(xhr.status, 0, 'Request is still pending');
   343  
   344      // Schedule the cancelation before waiting
   345      next(() => {
   346        controller.abort();
   347      });
   348  
   349      await settled();
   350      assert.ok(xhr.aborted, 'Request was aborted');
   351    });
   352  
   353    test('requests can be canceled even if multiple requests for the same URL were made', async function (assert) {
   354      await this.initializeUI();
   355  
   356      const { pretender } = this.server;
   357      const jobId = JSON.stringify(['job-1', 'default']);
   358      const controller1 = new AbortController();
   359      const controller2 = new AbortController();
   360  
   361      pretender.get('/v1/job/:id', () => [200, {}, '{}'], true);
   362  
   363      this.subject().findRecord(null, { modelName: 'job' }, jobId, {
   364        reload: true,
   365        adapterOptions: { watch: true, abortController: controller1 },
   366      });
   367  
   368      this.subject().findRecord(null, { modelName: 'job' }, jobId, {
   369        reload: true,
   370        adapterOptions: { watch: true, abortController: controller2 },
   371      });
   372  
   373      const { request: xhr } = pretender.requestReferences[0];
   374      const { request: xhr2 } = pretender.requestReferences[1];
   375      assert.equal(xhr.status, 0, 'Request is still pending');
   376      assert.equal(
   377        pretender.requestReferences.length,
   378        2,
   379        'Two findRecord requests were made'
   380      );
   381      assert.equal(
   382        pretender.requestReferences.mapBy('url').uniq().length,
   383        1,
   384        'The two requests have the same URL'
   385      );
   386  
   387      // Schedule the cancelation and resolution before waiting
   388      next(() => {
   389        controller1.abort();
   390        pretender.resolve(xhr2);
   391      });
   392  
   393      await settled();
   394      assert.ok(xhr.aborted, 'Request one was aborted');
   395      assert.notOk(xhr2.aborted, 'Request two was not aborted');
   396    });
   397  
   398    test('dispatch job encodes payload as base64', async function (assert) {
   399      const job = await this.initializeWithJob();
   400      job.set('parameterized', true);
   401  
   402      const payload = "I'm a payload 🙂";
   403  
   404      // Base64 encode payload.
   405      const Encoder = new TextEncoderLite('utf-8');
   406      const encodedPayload = base64js.fromByteArray(Encoder.encode(payload));
   407  
   408      await this.subject().dispatch(job, {}, payload);
   409  
   410      const request = this.server.pretender.handledRequests[0];
   411      assert.equal(request.url, `/v1/job/${job.plainId}/dispatch`);
   412      assert.equal(request.method, 'POST');
   413      assert.deepEqual(JSON.parse(request.requestBody), {
   414        Payload: encodedPayload,
   415        Meta: {},
   416      });
   417    });
   418  
   419    test('when there is no region set, requests are made without the region query param', async function (assert) {
   420      await this.initializeUI();
   421  
   422      const { pretender } = this.server;
   423      const jobName = 'job-1';
   424      const jobNamespace = 'default';
   425      const jobId = JSON.stringify([jobName, jobNamespace]);
   426  
   427      await settled();
   428      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   429      this.subject().findAll(null, { modelName: 'job' }, null);
   430      await settled();
   431  
   432      assert.deepEqual(
   433        pretender.handledRequests.mapBy('url'),
   434        [`/v1/job/${jobName}`, '/v1/jobs'],
   435        'No requests include the region query param'
   436      );
   437    });
   438  
   439    test('when there is a region set, requests are made with the region query param', async function (assert) {
   440      const region = 'region-2';
   441  
   442      await this.initializeUI({ region });
   443  
   444      const { pretender } = this.server;
   445      const jobName = 'job-1';
   446      const jobNamespace = 'default';
   447      const jobId = JSON.stringify([jobName, jobNamespace]);
   448  
   449      await settled();
   450      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   451      this.subject().findAll(null, { modelName: 'job' }, null);
   452      await settled();
   453  
   454      assert.deepEqual(
   455        pretender.handledRequests.mapBy('url'),
   456        [`/v1/job/${jobName}?region=${region}`, `/v1/jobs?region=${region}`],
   457        'Requests include the region query param'
   458      );
   459    });
   460  
   461    test('when the region is set to the default region, requests are made without the region query param', async function (assert) {
   462      await this.initializeUI({ region: 'region-1' });
   463  
   464      const { pretender } = this.server;
   465      const jobName = 'job-1';
   466      const jobNamespace = 'default';
   467      const jobId = JSON.stringify([jobName, jobNamespace]);
   468  
   469      await settled();
   470      this.subject().findRecord(null, { modelName: 'job' }, jobId);
   471      this.subject().findAll(null, { modelName: 'job' }, null);
   472      await settled();
   473  
   474      assert.deepEqual(
   475        pretender.handledRequests.mapBy('url'),
   476        [`/v1/job/${jobName}`, '/v1/jobs'],
   477        'No requests include the region query param'
   478      );
   479    });
   480  
   481    test('fetchRawDefinition requests include the activeRegion', async function (assert) {
   482      const region = 'region-2';
   483      const job = await this.initializeWithJob({ region });
   484  
   485      await this.subject().fetchRawDefinition(job);
   486  
   487      const request = this.server.pretender.handledRequests[0];
   488      assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
   489      assert.equal(request.method, 'GET');
   490    });
   491  
   492    test('forcePeriodic requests include the activeRegion', async function (assert) {
   493      const region = 'region-2';
   494      const job = await this.initializeWithJob({ region });
   495      job.set('periodic', true);
   496  
   497      await this.subject().forcePeriodic(job);
   498  
   499      const request = this.server.pretender.handledRequests[0];
   500      assert.equal(
   501        request.url,
   502        `/v1/job/${job.plainId}/periodic/force?region=${region}`
   503      );
   504      assert.equal(request.method, 'POST');
   505    });
   506  
   507    test('stop requests include the activeRegion', async function (assert) {
   508      const region = 'region-2';
   509      const job = await this.initializeWithJob({ region });
   510  
   511      await this.subject().stop(job);
   512  
   513      const request = this.server.pretender.handledRequests[0];
   514      assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
   515      assert.equal(request.method, 'DELETE');
   516    });
   517  
   518    test('purge requests include the activeRegion', async function (assert) {
   519      const region = 'region-2';
   520      const job = await this.initializeWithJob({ region });
   521  
   522      await this.subject().purge(job);
   523  
   524      const request = this.server.pretender.handledRequests[0];
   525      assert.equal(
   526        request.url,
   527        `/v1/job/${job.plainId}?purge=true&region=${region}`
   528      );
   529      assert.equal(request.method, 'DELETE');
   530    });
   531  
   532    test('parse requests include the activeRegion', async function (assert) {
   533      const region = 'region-2';
   534      await this.initializeUI({ region });
   535  
   536      await this.subject().parse('job "name-goes-here" {');
   537  
   538      const request = this.server.pretender.handledRequests[0];
   539      assert.equal(request.url, `/v1/jobs/parse?namespace=*&region=${region}`);
   540      assert.equal(request.method, 'POST');
   541      assert.deepEqual(JSON.parse(request.requestBody), {
   542        JobHCL: 'job "name-goes-here" {',
   543        Canonicalize: true,
   544      });
   545    });
   546  
   547    test('plan requests include the activeRegion', async function (assert) {
   548      const region = 'region-2';
   549      const job = await this.initializeWithJob({ region });
   550      job.set('_newDefinitionJSON', {});
   551  
   552      await this.subject().plan(job);
   553  
   554      const request = this.server.pretender.handledRequests[0];
   555      assert.equal(request.url, `/v1/job/${job.plainId}/plan?region=${region}`);
   556      assert.equal(request.method, 'POST');
   557    });
   558  
   559    test('run requests include the activeRegion', async function (assert) {
   560      const region = 'region-2';
   561      const job = await this.initializeWithJob({ region });
   562      job.set('_newDefinitionJSON', {});
   563  
   564      await this.subject().run(job);
   565  
   566      const request = this.server.pretender.handledRequests[0];
   567      assert.equal(request.url, `/v1/jobs?region=${region}`);
   568      assert.equal(request.method, 'POST');
   569    });
   570  
   571    test('update requests include the activeRegion', async function (assert) {
   572      const region = 'region-2';
   573      const job = await this.initializeWithJob({ region });
   574      job.set('_newDefinitionJSON', {});
   575  
   576      await this.subject().update(job);
   577  
   578      const request = this.server.pretender.handledRequests[0];
   579      assert.equal(request.url, `/v1/job/${job.plainId}?region=${region}`);
   580      assert.equal(request.method, 'POST');
   581    });
   582  
   583    test('scale requests include the activeRegion', async function (assert) {
   584      const region = 'region-2';
   585      const job = await this.initializeWithJob({ region });
   586  
   587      await this.subject().scale(job, 'group-1', 5, 'Reason: a test');
   588  
   589      const request = this.server.pretender.handledRequests[0];
   590      assert.equal(request.url, `/v1/job/${job.plainId}/scale?region=${region}`);
   591      assert.equal(request.method, 'POST');
   592    });
   593  
   594    test('dispatch requests include the activeRegion', async function (assert) {
   595      const region = 'region-2';
   596      const job = await this.initializeWithJob({ region });
   597      job.set('parameterized', true);
   598  
   599      await this.subject().dispatch(job, {}, '');
   600  
   601      const request = this.server.pretender.handledRequests[0];
   602      assert.equal(
   603        request.url,
   604        `/v1/job/${job.plainId}/dispatch?region=${region}`
   605      );
   606      assert.equal(request.method, 'POST');
   607    });
   608  });
   609  
   610  function makeMockModel(id, options) {
   611    return assign(
   612      {
   613        relationshipFor(name) {
   614          return {
   615            kind: 'belongsTo',
   616            type: 'job-summary',
   617            key: name,
   618          };
   619        },
   620        belongsTo(name) {
   621          return {
   622            link() {
   623              return `/v1/job/${id}/${name}`;
   624            },
   625          };
   626        },
   627      },
   628      options
   629    );
   630  }