github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/job-detail-test.js (about) 1 /* eslint-disable ember/no-test-module-for */ 2 /* eslint-disable qunit/require-expect */ 3 import { currentURL } from '@ember/test-helpers'; 4 import { module, test } from 'qunit'; 5 import { setupApplicationTest } from 'ember-qunit'; 6 import { setupMirage } from 'ember-cli-mirage/test-support'; 7 import moment from 'moment'; 8 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 9 import moduleForJob, { 10 moduleForJobWithClientStatus, 11 } from 'nomad-ui/tests/helpers/module-for-job'; 12 import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; 13 14 moduleForJob('Acceptance | job detail (batch)', 'allocations', () => 15 server.create('job', { type: 'batch', shallow: true }) 16 ); 17 18 moduleForJob('Acceptance | job detail (system)', 'allocations', () => 19 server.create('job', { type: 'system', shallow: true }) 20 ); 21 22 moduleForJobWithClientStatus( 23 'Acceptance | job detail with client status (system)', 24 () => 25 server.create('job', { 26 status: 'running', 27 datacenters: ['dc1'], 28 type: 'system', 29 createAllocations: false, 30 }) 31 ); 32 33 moduleForJob('Acceptance | job detail (sysbatch)', 'allocations', () => 34 server.create('job', { type: 'sysbatch', shallow: true }) 35 ); 36 37 moduleForJobWithClientStatus( 38 'Acceptance | job detail with client status (sysbatch)', 39 () => 40 server.create('job', { 41 status: 'running', 42 datacenters: ['dc1'], 43 type: 'sysbatch', 44 createAllocations: false, 45 }) 46 ); 47 48 moduleForJobWithClientStatus( 49 'Acceptance | job detail with client status (sysbatch with namespace)', 50 () => { 51 const namespace = server.create('namespace', { id: 'test' }); 52 return server.create('job', { 53 status: 'running', 54 datacenters: ['dc1'], 55 type: 'sysbatch', 56 namespaceId: namespace.name, 57 createAllocations: false, 58 }); 59 } 60 ); 61 62 moduleForJob('Acceptance | job detail (sysbatch child)', 'allocations', () => { 63 const parent = server.create('job', 'periodicSysbatch', { 64 childrenCount: 1, 65 shallow: true, 66 datacenters: ['dc1'], 67 }); 68 return server.db.jobs.where({ parentId: parent.id })[0]; 69 }); 70 71 moduleForJobWithClientStatus( 72 'Acceptance | job detail with client status (sysbatch child)', 73 () => { 74 const parent = server.create('job', 'periodicSysbatch', { 75 childrenCount: 1, 76 shallow: true, 77 datacenters: ['dc1'], 78 }); 79 return server.db.jobs.where({ parentId: parent.id })[0]; 80 } 81 ); 82 83 moduleForJobWithClientStatus( 84 'Acceptance | job detail with client status (sysbatch child with namespace)', 85 () => { 86 const namespace = server.create('namespace', { id: 'test' }); 87 const parent = server.create('job', 'periodicSysbatch', { 88 childrenCount: 1, 89 shallow: true, 90 namespaceId: namespace.name, 91 datacenters: ['dc1'], 92 }); 93 return server.db.jobs.where({ parentId: parent.id })[0]; 94 } 95 ); 96 97 moduleForJob( 98 'Acceptance | job detail (periodic)', 99 'children', 100 () => server.create('job', 'periodic', { shallow: true }), 101 { 102 'the default sort is submitTime descending': async function (job, assert) { 103 const mostRecentLaunch = server.db.jobs 104 .where({ parentId: job.id }) 105 .sortBy('submitTime') 106 .reverse()[0]; 107 108 assert.ok(JobDetail.jobsHeader.hasSubmitTime); 109 assert.equal( 110 JobDetail.jobs[0].submitTime, 111 moment(mostRecentLaunch.submitTime / 1000000).format( 112 'MMM DD HH:mm:ss ZZ' 113 ) 114 ); 115 }, 116 } 117 ); 118 119 moduleForJob( 120 'Acceptance | job detail (periodic in namespace)', 121 'children', 122 () => { 123 const namespace = server.create('namespace', { id: 'test' }); 124 const parent = server.create('job', 'periodic', { 125 shallow: true, 126 namespaceId: namespace.name, 127 }); 128 return parent; 129 }, 130 { 131 'display namespace in children table': async function (job, assert) { 132 assert.ok(JobDetail.jobsHeader.hasNamespace); 133 assert.equal(JobDetail.jobs[0].namespace, job.namespace); 134 }, 135 } 136 ); 137 138 moduleForJob( 139 'Acceptance | job detail (parameterized)', 140 'children', 141 () => server.create('job', 'parameterized', { shallow: true }), 142 { 143 'the default sort is submitTime descending': async (job, assert) => { 144 const mostRecentLaunch = server.db.jobs 145 .where({ parentId: job.id }) 146 .sortBy('submitTime') 147 .reverse()[0]; 148 149 assert.ok(JobDetail.jobsHeader.hasSubmitTime); 150 assert.equal( 151 JobDetail.jobs[0].submitTime, 152 moment(mostRecentLaunch.submitTime / 1000000).format( 153 'MMM DD HH:mm:ss ZZ' 154 ) 155 ); 156 }, 157 } 158 ); 159 160 moduleForJob( 161 'Acceptance | job detail (parameterized in namespace)', 162 'children', 163 () => { 164 const namespace = server.create('namespace', { id: 'test' }); 165 const parent = server.create('job', 'parameterized', { 166 shallow: true, 167 namespaceId: namespace.name, 168 }); 169 return parent; 170 }, 171 { 172 'display namespace in children table': async function (job, assert) { 173 assert.ok(JobDetail.jobsHeader.hasNamespace); 174 assert.equal(JobDetail.jobs[0].namespace, job.namespace); 175 }, 176 } 177 ); 178 179 moduleForJob('Acceptance | job detail (periodic child)', 'allocations', () => { 180 const parent = server.create('job', 'periodic', { 181 childrenCount: 1, 182 shallow: true, 183 }); 184 return server.db.jobs.where({ parentId: parent.id })[0]; 185 }); 186 187 moduleForJob( 188 'Acceptance | job detail (parameterized child)', 189 'allocations', 190 () => { 191 const parent = server.create('job', 'parameterized', { 192 childrenCount: 1, 193 shallow: true, 194 }); 195 return server.db.jobs.where({ parentId: parent.id })[0]; 196 } 197 ); 198 199 moduleForJob( 200 'Acceptance | job detail (service)', 201 'allocations', 202 () => server.create('job', { type: 'service' }), 203 { 204 'the subnav links to deployment': async (job, assert) => { 205 await JobDetail.tabFor('deployments').visit(); 206 assert.equal(currentURL(), `/jobs/${job.id}/deployments`); 207 }, 208 'when the job is not found, an error message is shown, but the URL persists': 209 async (job, assert) => { 210 await JobDetail.visit({ id: 'not-a-real-job' }); 211 212 assert.equal( 213 server.pretender.handledRequests 214 .filter((request) => !request.url.includes('policy')) 215 .findBy('status', 404).url, 216 '/v1/job/not-a-real-job', 217 'A request to the nonexistent job is made' 218 ); 219 assert.equal(currentURL(), '/jobs/not-a-real-job', 'The URL persists'); 220 assert.ok(JobDetail.error.isPresent, 'Error message is shown'); 221 assert.equal( 222 JobDetail.error.title, 223 'Not Found', 224 'Error message is for 404' 225 ); 226 }, 227 } 228 ); 229 230 module('Acceptance | job detail (with namespaces)', function (hooks) { 231 setupApplicationTest(hooks); 232 setupMirage(hooks); 233 234 let job, managementToken, clientToken; 235 236 hooks.beforeEach(function () { 237 server.createList('namespace', 2); 238 server.create('node'); 239 job = server.create('job', { 240 type: 'service', 241 status: 'running', 242 namespaceId: server.db.namespaces[1].name, 243 }); 244 server.createList('job', 3, { 245 namespaceId: server.db.namespaces[0].name, 246 }); 247 248 managementToken = server.create('token'); 249 clientToken = server.create('token'); 250 }); 251 252 test('it passes an accessibility audit', async function (assert) { 253 const namespace = server.db.namespaces.find(job.namespaceId); 254 await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); 255 await a11yAudit(assert); 256 }); 257 258 test('when there are namespaces, the job detail page states the namespace for the job', async function (assert) { 259 const namespace = server.db.namespaces.find(job.namespaceId); 260 261 await JobDetail.visit({ 262 id: `${job.id}@${namespace.name}`, 263 }); 264 265 assert.ok( 266 JobDetail.statFor('namespace').text, 267 'Namespace included in stats' 268 ); 269 }); 270 271 test('the exec button state can change between namespaces', async function (assert) { 272 const job1 = server.create('job', { 273 status: 'running', 274 namespaceId: server.db.namespaces[0].id, 275 }); 276 const job2 = server.create('job', { 277 status: 'running', 278 namespaceId: server.db.namespaces[1].id, 279 }); 280 281 window.localStorage.nomadTokenSecret = clientToken.secretId; 282 283 const policy = server.create('policy', { 284 id: 'something', 285 name: 'something', 286 rulesJSON: { 287 Namespaces: [ 288 { 289 Name: job1.namespaceId, 290 Capabilities: ['list-jobs', 'alloc-exec'], 291 }, 292 { 293 Name: job2.namespaceId, 294 Capabilities: ['list-jobs'], 295 }, 296 ], 297 }, 298 }); 299 300 clientToken.policyIds = [policy.id]; 301 clientToken.save(); 302 303 await JobDetail.visit({ id: job1.id }); 304 assert.notOk(JobDetail.execButton.isDisabled); 305 306 const secondNamespace = server.db.namespaces[1]; 307 await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` }); 308 309 assert.ok(JobDetail.execButton.isDisabled); 310 }); 311 312 test('the anonymous policy is fetched to check whether to show the exec button', async function (assert) { 313 window.localStorage.removeItem('nomadTokenSecret'); 314 315 server.create('policy', { 316 id: 'anonymous', 317 name: 'anonymous', 318 rulesJSON: { 319 Namespaces: [ 320 { 321 Name: 'default', 322 Capabilities: ['list-jobs', 'alloc-exec'], 323 }, 324 ], 325 }, 326 }); 327 328 await JobDetail.visit({ 329 id: `${job.id}@${server.db.namespaces[1].name}`, 330 }); 331 332 assert.notOk(JobDetail.execButton.isDisabled); 333 }); 334 335 test('meta table is displayed if job has meta attributes', async function (assert) { 336 const jobWithMeta = server.create('job', { 337 status: 'running', 338 namespaceId: server.db.namespaces[1].id, 339 meta: { 340 'a.b': 'c', 341 }, 342 }); 343 344 await JobDetail.visit({ 345 id: `${job.id}@${server.db.namespaces[1].name}`, 346 }); 347 348 assert.notOk(JobDetail.metaTable, 'Meta table not present'); 349 350 await JobDetail.visit({ 351 id: `${jobWithMeta.id}@${server.db.namespaces[1].name}`, 352 }); 353 assert.ok(JobDetail.metaTable, 'Meta table is present'); 354 }); 355 356 test('pack details are displayed', async function (assert) { 357 const namespace = server.db.namespaces[1].id; 358 const jobFromPack = server.create('job', { 359 status: 'running', 360 namespaceId: namespace, 361 meta: { 362 'pack.name': 'my-pack', 363 'pack.version': '1.0.0', 364 }, 365 }); 366 367 await JobDetail.visit({ id: `${jobFromPack.id}@${namespace}` }); 368 369 assert.ok(JobDetail.packTag, 'Pack tag is present'); 370 assert.equal( 371 JobDetail.packStatFor('name').text, 372 `Name ${jobFromPack.meta['pack.name']}`, 373 `Pack name is ${jobFromPack.meta['pack.name']}` 374 ); 375 assert.equal( 376 JobDetail.packStatFor('version').text, 377 `Version ${jobFromPack.meta['pack.version']}`, 378 `Pack version is ${jobFromPack.meta['pack.version']}` 379 ); 380 }); 381 382 test('resource recommendations show when they exist and can be expanded, collapsed, and processed', async function (assert) { 383 server.create('feature', { name: 'Dynamic Application Sizing' }); 384 385 job = server.create('job', { 386 type: 'service', 387 status: 'running', 388 namespaceId: server.db.namespaces[1].name, 389 groupsCount: 3, 390 createRecommendations: true, 391 }); 392 393 window.localStorage.nomadTokenSecret = managementToken.secretId; 394 await JobDetail.visit({ 395 id: `${job.id}@${server.db.namespaces[1].name}`, 396 }); 397 398 const groupsWithRecommendations = job.taskGroups.filter((group) => 399 group.tasks.models.any((task) => task.recommendations.models.length) 400 ); 401 const jobRecommendationCount = groupsWithRecommendations.length; 402 403 const firstRecommendationGroup = groupsWithRecommendations.models[0]; 404 405 assert.equal(JobDetail.recommendations.length, jobRecommendationCount); 406 407 const recommendation = JobDetail.recommendations[0]; 408 409 assert.equal(recommendation.group, firstRecommendationGroup.name); 410 assert.ok(recommendation.card.isHidden); 411 412 const toggle = recommendation.toggleButton; 413 414 assert.equal(toggle.text, 'Show'); 415 416 await toggle.click(); 417 418 assert.ok(recommendation.card.isPresent); 419 assert.equal(toggle.text, 'Collapse'); 420 421 await toggle.click(); 422 423 assert.ok(recommendation.card.isHidden); 424 425 await toggle.click(); 426 427 assert.equal( 428 recommendation.card.slug.groupName, 429 firstRecommendationGroup.name 430 ); 431 432 await recommendation.card.acceptButton.click(); 433 434 assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1); 435 436 await JobDetail.tabFor('definition').visit(); 437 await JobDetail.tabFor('overview').visit(); 438 439 assert.equal(JobDetail.recommendations.length, jobRecommendationCount - 1); 440 }); 441 442 test('resource recommendations are not fetched when the feature doesn’t exist', async function (assert) { 443 window.localStorage.nomadTokenSecret = managementToken.secretId; 444 await JobDetail.visit({ 445 id: `${job.id}@${server.db.namespaces[1].name}`, 446 }); 447 448 assert.equal(JobDetail.recommendations.length, 0); 449 450 assert.equal( 451 server.pretender.handledRequests.filter((request) => 452 request.url.includes('recommendations') 453 ).length, 454 0 455 ); 456 }); 457 458 test('when the dynamic autoscaler is applied, you can scale a task within the job detail page', async function (assert) { 459 const SCALE_AND_WRITE_NAMESPACE = 'scale-and-write-namespace'; 460 const READ_ONLY_NAMESPACE = 'read-only-namespace'; 461 const clientToken = server.create('token'); 462 463 const namespace = server.create('namespace', { 464 id: SCALE_AND_WRITE_NAMESPACE, 465 }); 466 const secondNamespace = server.create('namespace', { 467 id: READ_ONLY_NAMESPACE, 468 }); 469 470 job = server.create('job', { 471 groupCount: 0, 472 createAllocations: false, 473 shallow: true, 474 noActiveDeployment: true, 475 namespaceId: SCALE_AND_WRITE_NAMESPACE, 476 }); 477 478 const job2 = server.create('job', { 479 groupCount: 0, 480 createAllocations: false, 481 shallow: true, 482 noActiveDeployment: true, 483 namespaceId: READ_ONLY_NAMESPACE, 484 }); 485 const scalingGroup2 = server.create('task-group', { 486 job: job2, 487 name: 'scaling', 488 count: 1, 489 shallow: true, 490 withScaling: true, 491 }); 492 job2.update({ taskGroupIds: [scalingGroup2.id] }); 493 494 const policy = server.create('policy', { 495 id: 'something', 496 name: 'something', 497 rulesJSON: { 498 Namespaces: [ 499 { 500 Name: SCALE_AND_WRITE_NAMESPACE, 501 Capabilities: ['scale-job', 'submit-job', 'read-job', 'list-jobs'], 502 }, 503 { 504 Name: READ_ONLY_NAMESPACE, 505 Capabilities: ['list-jobs', 'read-job'], 506 }, 507 ], 508 }, 509 }); 510 const scalingGroup = server.create('task-group', { 511 job, 512 name: 'scaling', 513 count: 1, 514 shallow: true, 515 withScaling: true, 516 }); 517 job.update({ taskGroupIds: [scalingGroup.id] }); 518 519 clientToken.policyIds = [policy.id]; 520 clientToken.save(); 521 window.localStorage.nomadTokenSecret = clientToken.secretId; 522 523 await JobDetail.visit({ id: `${job.id}@${namespace.name}` }); 524 assert.notOk(JobDetail.incrementButton.isDisabled); 525 526 await JobDetail.visit({ id: `${job2.id}@${secondNamespace.name}` }); 527 assert.ok(JobDetail.incrementButton.isDisabled); 528 }); 529 });