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®ion=${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=*®ion=${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 }