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