github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/job-run-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 import AdapterError from '@ember-data/adapter/error'; 7 import { 8 click, 9 currentRouteName, 10 currentURL, 11 fillIn, 12 visit, 13 settled, 14 } from '@ember/test-helpers'; 15 import { assign } from '@ember/polyfills'; 16 import { module, test } from 'qunit'; 17 import { 18 selectChoose, 19 clickTrigger, 20 } from 'ember-power-select/test-support/helpers'; 21 import { setupApplicationTest } from 'ember-qunit'; 22 import { setupMirage } from 'ember-cli-mirage/test-support'; 23 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 24 import setupCodeMirror from 'nomad-ui/tests/helpers/codemirror'; 25 import JobRun from 'nomad-ui/tests/pages/jobs/run'; 26 import percySnapshot from '@percy/ember'; 27 28 const newJobName = 'new-job'; 29 const newJobTaskGroupName = 'redis'; 30 const newJobNamespace = 'default'; 31 32 let managementToken, clientToken; 33 34 const jsonJob = (overrides) => { 35 return JSON.stringify( 36 assign( 37 {}, 38 { 39 Name: newJobName, 40 Namespace: newJobNamespace, 41 Datacenters: ['dc1'], 42 Priority: 50, 43 TaskGroups: [ 44 { 45 Name: newJobTaskGroupName, 46 Tasks: [ 47 { 48 Name: 'redis', 49 Driver: 'docker', 50 }, 51 ], 52 }, 53 ], 54 }, 55 overrides 56 ), 57 null, 58 2 59 ); 60 }; 61 62 module('Acceptance | job run', function (hooks) { 63 setupApplicationTest(hooks); 64 setupMirage(hooks); 65 setupCodeMirror(hooks); 66 67 hooks.beforeEach(function () { 68 // Required for placing allocations (a result of creating jobs) 69 server.create('node-pool'); 70 server.create('node'); 71 72 managementToken = server.create('token'); 73 clientToken = server.create('token'); 74 75 window.localStorage.nomadTokenSecret = managementToken.secretId; 76 }); 77 78 test('it passes an accessibility audit', async function (assert) { 79 assert.expect(1); 80 81 await JobRun.visit(); 82 await a11yAudit(assert); 83 }); 84 85 test('visiting /jobs/run', async function (assert) { 86 await JobRun.visit(); 87 88 assert.equal(currentURL(), '/jobs/run'); 89 assert.equal(document.title, 'Run a job - Nomad'); 90 }); 91 92 test('when submitting a job, the site redirects to the new job overview page', async function (assert) { 93 const spec = jsonJob(); 94 95 await JobRun.visit(); 96 97 await JobRun.editor.editor.fillIn(spec); 98 await JobRun.editor.plan(); 99 await JobRun.editor.run(); 100 assert.equal( 101 currentURL(), 102 `/jobs/${newJobName}@${newJobNamespace}`, 103 `Redirected to the job overview page for ${newJobName}` 104 ); 105 }); 106 107 test('when submitting a job to a different namespace, the redirect to the job overview page takes namespace into account', async function (assert) { 108 const newNamespace = 'second-namespace'; 109 110 server.create('namespace', { id: newNamespace }); 111 const spec = jsonJob({ Namespace: newNamespace }); 112 113 await JobRun.visit(); 114 115 await JobRun.editor.editor.fillIn(spec); 116 await JobRun.editor.plan(); 117 await JobRun.editor.run(); 118 assert.equal( 119 currentURL(), 120 `/jobs/${newJobName}@${newNamespace}`, 121 `Redirected to the job overview page for ${newJobName} and switched the namespace to ${newNamespace}` 122 ); 123 }); 124 125 test('when the user doesn’t have permission to run a job, redirects to the job overview page', async function (assert) { 126 window.localStorage.nomadTokenSecret = clientToken.secretId; 127 128 await JobRun.visit(); 129 assert.equal(currentURL(), '/jobs'); 130 }); 131 132 test('when using client token user can still go to job page if they have correct permissions', async function (assert) { 133 const clientTokenWithPolicy = server.create('token'); 134 const newNamespace = 'second-namespace'; 135 136 server.create('namespace', { id: newNamespace }); 137 server.create('job', { 138 groupCount: 0, 139 createAllocations: false, 140 shallow: true, 141 noActiveDeployment: true, 142 namespaceId: newNamespace, 143 }); 144 145 const policy = server.create('policy', { 146 id: 'something', 147 name: 'something', 148 rulesJSON: { 149 Namespaces: [ 150 { 151 Name: newNamespace, 152 Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'], 153 }, 154 ], 155 }, 156 }); 157 158 clientTokenWithPolicy.policyIds = [policy.id]; 159 clientTokenWithPolicy.save(); 160 window.localStorage.nomadTokenSecret = clientTokenWithPolicy.secretId; 161 162 await JobRun.visit({ namespace: newNamespace }); 163 assert.equal(currentURL(), `/jobs/run?namespace=${newNamespace}`); 164 }); 165 166 module('job template flow', function () { 167 test('allows user with the correct permissions to fill in the editor using a job template', async function (assert) { 168 assert.expect(10); 169 // Arrange 170 await JobRun.visit(); 171 assert 172 .dom('[data-test-choose-template]') 173 .exists('A button allowing a user to select a template appears.'); 174 175 server.get('/vars', function (_server, fakeRequest) { 176 assert.deepEqual( 177 fakeRequest.queryParams, 178 { 179 prefix: 'nomad/job-templates', 180 namespace: '*', 181 }, 182 'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.' 183 ); 184 return [ 185 { 186 ID: 'nomad/job-templates/foo', 187 Namespace: 'default', 188 Path: 'nomad/job-templates/foo', 189 }, 190 ]; 191 }); 192 193 server.get( 194 '/var/nomad%2Fjob-templates%2Ffoo', 195 function (_server, fakeRequest) { 196 assert.deepEqual( 197 fakeRequest.queryParams, 198 { 199 namespace: 'default', 200 }, 201 'Dispatches O(n+1) query to retrive items.' 202 ); 203 return { 204 ID: 'nomad/job-templates/foo', 205 Namespace: 'default', 206 Path: 'nomad/job-templates/foo', 207 Items: { 208 template: 'Hello World!', 209 label: 'foo', 210 }, 211 }; 212 } 213 ); 214 // Act 215 await click('[data-test-choose-template]'); 216 assert.equal(currentRouteName(), 'jobs.run.templates.index'); 217 218 // Assert 219 assert 220 .dom('[data-test-template-list]') 221 .exists('A list of available job templates is rendered.'); 222 assert 223 .dom('[data-test-apply]') 224 .exists('A button to apply the selected templated is displayed.'); 225 assert 226 .dom('[data-test-cancel]') 227 .exists('A button to cancel the template selection is displayed.'); 228 229 await click('[data-test-template-card=Foo]'); 230 await click('[data-test-apply]'); 231 232 assert.equal( 233 currentURL(), 234 '/jobs/run?template=nomad%2Fjob-templates%2Ffoo%40default' 235 ); 236 assert.dom('[data-test-editor]').containsText('Hello World!'); 237 }); 238 239 test('a user can create their own job template', async function (assert) { 240 assert.expect(7); 241 // Arrange 242 await JobRun.visit(); 243 await click('[data-test-choose-template]'); 244 245 // Assert 246 assert 247 .dom('[data-test-template-card]') 248 .exists({ count: 4 }, 'A list of default job templates is rendered.'); 249 250 await click('[data-test-create-new-button]'); 251 assert.equal(currentRouteName(), 'jobs.run.templates.new'); 252 253 await fillIn('[data-test-template-name]', 'foo'); 254 await fillIn('[data-test-template-description]', 'foo-bar-baz'); 255 const codeMirror = getCodeMirrorInstance('[data-test-template-json]'); 256 codeMirror.setValue(jsonJob()); 257 258 server.put('/var/:varId', function (_server, fakeRequest) { 259 assert.deepEqual( 260 JSON.parse(fakeRequest.requestBody), 261 { 262 Path: 'nomad/job-templates/foo', 263 CreateIndex: null, 264 ModifyIndex: null, 265 Namespace: 'default', 266 ID: 'nomad/job-templates/foo', 267 Items: { description: 'foo-bar-baz', template: jsonJob() }, 268 }, 269 'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.' 270 ); 271 return { 272 Items: { description: 'foo-bar-baz', template: jsonJob() }, 273 Namespace: 'default', 274 Path: 'nomad/job-templates/foo', 275 }; 276 }); 277 278 server.get('/vars', function (_server, fakeRequest) { 279 assert.deepEqual( 280 fakeRequest.queryParams, 281 { 282 prefix: 'nomad/job-templates', 283 namespace: '*', 284 }, 285 'It makes a request to the /vars endpoint with the appropriate query parameters for job templates.' 286 ); 287 return [ 288 { 289 ID: 'nomad/job-templates/foo', 290 Namespace: 'default', 291 Path: 'nomad/job-templates/foo', 292 }, 293 ]; 294 }); 295 296 server.get( 297 '/var/nomad%2Fjob-templates%2Ffoo', 298 function (_server, fakeRequest) { 299 assert.deepEqual( 300 fakeRequest.queryParams, 301 { 302 namespace: 'default', 303 }, 304 'Dispatches O(n+1) query to retrive items.' 305 ); 306 return { 307 ID: 'nomad/job-templates/foo', 308 Namespace: 'default', 309 Path: 'nomad/job-templates/foo', 310 Items: { 311 template: 'qud', 312 label: 'foo', 313 }, 314 }; 315 } 316 ); 317 318 await click('[data-test-save-template]'); 319 assert.equal(currentRouteName(), 'jobs.run.templates.index'); 320 assert 321 .dom('[data-test-template-card=Foo]') 322 .exists('The newly created template appears in the list.'); 323 }); 324 325 test('a toast notification alerts the user if there is an error saving the newly created job template', async function (assert) { 326 assert.expect(5); 327 // Arrange 328 await JobRun.visit(); 329 await click('[data-test-choose-template]'); 330 331 // Assert 332 assert 333 .dom('[data-test-template-card]') 334 .exists({ count: 4 }, 'A list of default job templates is rendered.'); 335 336 await click('[data-test-create-new-button]'); 337 assert.equal(currentRouteName(), 'jobs.run.templates.new'); 338 assert 339 .dom('[data-test-save-template]') 340 .isDisabled('the save button should be disabled if no path is set'); 341 342 await fillIn('[data-test-template-name]', 'try@'); 343 await fillIn('[data-test-template-description]', 'foo-bar-baz'); 344 const codeMirror = getCodeMirrorInstance('[data-test-template-json]'); 345 codeMirror.setValue(jsonJob()); 346 347 server.put('/var/:varId?cas=0', function () { 348 return new AdapterError({ 349 detail: `invalid path "nomad/job-templates/try@"`, 350 status: 500, 351 }); 352 }); 353 354 await click('[data-test-save-template]'); 355 assert.equal( 356 currentRouteName(), 357 'jobs.run.templates.new', 358 'We do not navigate away from the page if an error is returned by the API.' 359 ); 360 assert 361 .dom('.flash-message.alert-critical') 362 .exists('A toast error message pops up.'); 363 }); 364 365 test('a user cannot create a job template if one with the same name and namespace already exists', async function (assert) { 366 assert.expect(4); 367 // Arrange 368 await JobRun.visit(); 369 await click('[data-test-choose-template]'); 370 server.create('variable', { 371 path: 'nomad/job-templates/foo', 372 namespace: 'default', 373 id: 'nomad/job-templates/foo', 374 }); 375 server.create('namespace', { id: 'test' }); 376 377 this.system = this.owner.lookup('service:system'); 378 this.system.shouldShowNamespaces = true; 379 380 // Assert 381 assert 382 .dom('[data-test-template-card]') 383 .exists({ count: 4 }, 'A list of default job templates is rendered.'); 384 385 await click('[data-test-create-new-button]'); 386 assert.equal(currentRouteName(), 'jobs.run.templates.new'); 387 388 await fillIn('[data-test-template-name]', 'foo'); 389 assert 390 .dom('[data-test-duplicate-error]') 391 .exists('an error message alerts the user'); 392 393 await clickTrigger('[data-test-namespace-facet]'); 394 await selectChoose('[data-test-namespace-facet]', 'test'); 395 396 assert 397 .dom('[data-test-duplicate-error]') 398 .doesNotExist( 399 'an error disappears when name or namespace combination is unique' 400 ); 401 402 // Clean-up 403 this.system.shouldShowNamespaces = false; 404 }); 405 406 test('a user can save code from the editor as a template', async function (assert) { 407 assert.expect(4); 408 // Arrange 409 await JobRun.visit(); 410 await JobRun.editor.editor.fillIn(jsonJob()); 411 412 await click('[data-test-save-as-template]'); 413 assert.equal( 414 currentRouteName(), 415 'jobs.run.templates.new', 416 'We navigate template creation page.' 417 ); 418 419 // Assert 420 assert 421 .dom('[data-test-template-name]') 422 .hasNoText('No template name is prefilled.'); 423 assert 424 .dom('[data-test-template-description]') 425 .hasNoText('No template description is prefilled.'); 426 427 const codeMirror = getCodeMirrorInstance('[data-test-template-json]'); 428 const json = codeMirror.getValue(); 429 430 assert.equal( 431 json, 432 jsonJob(), 433 'Template is filled out with text from the editor.' 434 ); 435 }); 436 437 test('a user can edit a template', async function (assert) { 438 assert.expect(5); 439 440 // Arrange 441 server.create('variable', { 442 path: 'nomad/job-templates/foo', 443 namespace: 'default', 444 id: 'nomad/job-templates/foo', 445 Items: {}, 446 }); 447 448 await visit('/jobs/run/templates/manage'); 449 450 assert.equal(currentRouteName(), 'jobs.run.templates.manage'); 451 assert 452 .dom('[data-test-template-list]') 453 .exists('A list of templates is visible'); 454 await percySnapshot(assert); 455 await click('[data-test-edit-template="nomad/job-templates/foo"]'); 456 assert.equal( 457 currentRouteName(), 458 'jobs.run.templates.template', 459 'Navigates to edit template view' 460 ); 461 462 server.put('/var/:varId', function (_server, fakeRequest) { 463 assert.deepEqual( 464 JSON.parse(fakeRequest.requestBody), 465 { 466 Path: 'nomad/job-templates/foo', 467 CreateIndex: null, 468 ModifyIndex: null, 469 Namespace: 'default', 470 ID: 'nomad/job-templates/foo', 471 Items: { description: 'baz qud thud' }, 472 }, 473 'It makes a PUT request to the /vars/:varId endpoint with the appropriate request body for job templates.' 474 ); 475 476 return { 477 Items: { description: 'baz qud thud' }, 478 Namespace: 'default', 479 Path: 'nomad/job-templates/foo', 480 }; 481 }); 482 483 await fillIn('[data-test-template-description]', 'baz qud thud'); 484 await click('[data-test-edit-template]'); 485 486 assert.equal( 487 currentRouteName(), 488 'jobs.run.templates.index', 489 'We navigate back to the templates view.' 490 ); 491 }); 492 493 test('a user can delete a template', async function (assert) { 494 assert.expect(5); 495 496 // Arrange 497 server.create('variable', { 498 path: 'nomad/job-templates/foo', 499 namespace: 'default', 500 id: 'nomad/job-templates/foo', 501 Items: {}, 502 }); 503 504 server.create('variable', { 505 path: 'nomad/job-templates/bar', 506 namespace: 'default', 507 id: 'nomad/job-templates/bar', 508 Items: {}, 509 }); 510 511 server.create('variable', { 512 path: 'nomad/job-templates/baz', 513 namespace: 'default', 514 id: 'nomad/job-templates/baz', 515 Items: {}, 516 }); 517 518 await visit('/jobs/run/templates/manage'); 519 520 assert.equal(currentRouteName(), 'jobs.run.templates.manage'); 521 assert 522 .dom('[data-test-template-list]') 523 .exists('A list of templates is visible'); 524 525 await click('[data-test-idle-button]'); 526 await click('[data-test-confirm-button]'); 527 assert 528 .dom('[data-test-edit-template="nomad/job-templates/foo"]') 529 .doesNotExist('The template is removed from the list.'); 530 531 await click('[data-test-edit-template="nomad/job-templates/bar"]'); 532 await click('[data-test-idle-button]'); 533 await click('[data-test-confirm-button]'); 534 535 assert.equal( 536 currentRouteName(), 537 'jobs.run.templates.manage', 538 'We navigate back to the templates manager view.' 539 ); 540 541 assert 542 .dom('[data-test-edit-template="nomad/job-templates/bar"]') 543 .doesNotExist('The template is removed from the list.'); 544 }); 545 546 test('a user sees accurate template information', async function (assert) { 547 assert.expect(3); 548 549 // Arrange 550 server.create('variable', { 551 path: 'nomad/job-templates/foo', 552 namespace: 'default', 553 id: 'nomad/job-templates/foo', 554 Items: { 555 template: 'qud', 556 label: 'foo', 557 description: 'bar baz', 558 }, 559 }); 560 561 await visit('/jobs/run/templates'); 562 563 assert.equal(currentRouteName(), 'jobs.run.templates.index'); 564 assert.dom('[data-test-template-card="Foo"]').exists(); 565 566 this.store = this.owner.lookup('service:store'); 567 this.store.unloadAll(); 568 await settled(); 569 570 assert 571 .dom('[data-test-template-card="Foo"]') 572 .doesNotExist( 573 'The template reactively updates to changes in the Ember Data Store.' 574 ); 575 }); 576 577 test('default templates', async function (assert) { 578 assert.expect(4); 579 const NUMBER_OF_DEFAULT_TEMPLATES = 4; 580 581 await visit('/jobs/run/templates'); 582 583 assert.equal(currentRouteName(), 'jobs.run.templates.index'); 584 assert 585 .dom('[data-test-template-card]') 586 .exists({ count: NUMBER_OF_DEFAULT_TEMPLATES }); 587 588 await percySnapshot(assert); 589 590 await click('[data-test-template-card="Hello world"]'); 591 await click('[data-test-apply]'); 592 593 assert.equal( 594 currentURL(), 595 '/jobs/run?template=nomad%2Fjob-templates%2Fdefault%2Fhello-world' 596 ); 597 assert.dom('[data-test-editor]').includesText('job "hello-world"'); 598 }); 599 }); 600 });