github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/task-detail-test.js (about) 1 /* eslint-disable qunit/require-expect */ 2 import { currentURL, waitFor } from '@ember/test-helpers'; 3 import { module, test } from 'qunit'; 4 import { setupApplicationTest } from 'ember-qunit'; 5 import { setupMirage } from 'ember-cli-mirage/test-support'; 6 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 7 import Task from 'nomad-ui/tests/pages/allocations/task/detail'; 8 import Layout from 'nomad-ui/tests/pages/layout'; 9 import moment from 'moment'; 10 11 let allocation; 12 let task; 13 14 module('Acceptance | task detail', function (hooks) { 15 setupApplicationTest(hooks); 16 setupMirage(hooks); 17 18 hooks.beforeEach(async function () { 19 server.create('agent'); 20 server.create('node'); 21 server.create('job', { createAllocations: false }); 22 allocation = server.create('allocation', 'withTaskWithPorts', { 23 clientStatus: 'running', 24 }); 25 server.db.taskStates.update( 26 { allocationId: allocation.id }, 27 { state: 'running' } 28 ); 29 task = server.db.taskStates.where({ allocationId: allocation.id })[0]; 30 31 await Task.visit({ id: allocation.id, name: task.name }); 32 }); 33 34 test('it passes an accessibility audit', async function (assert) { 35 assert.expect(1); 36 37 await a11yAudit(assert); 38 }); 39 40 test('/allocation/:id/:task_name should name the task and list high-level task information', async function (assert) { 41 assert.ok(Task.title.text.includes(task.name), 'Task name'); 42 assert.ok(Task.state.includes(task.state), 'Task state'); 43 44 assert.ok( 45 Task.startedAt.includes( 46 moment(task.startedAt).format("MMM DD, 'YY HH:mm:ss ZZ") 47 ), 48 'Task started at' 49 ); 50 51 const lifecycle = server.db.tasks.where({ name: task.name })[0].Lifecycle; 52 53 let lifecycleName = 'main'; 54 if ( 55 lifecycle && 56 (lifecycle.Hook === 'prestart' || lifecycle.Hook === 'poststart') 57 ) { 58 lifecycleName = `${lifecycle.Hook}-${ 59 lifecycle.Sidecar ? 'sidecar' : 'ephemeral' 60 }`; 61 } 62 if (lifecycle && lifecycle.Hook === 'poststop') { 63 lifecycleName = 'poststop'; 64 } 65 66 assert.equal(Task.lifecycle, lifecycleName); 67 68 assert.equal(document.title, `Task ${task.name} - Nomad`); 69 }); 70 71 test('breadcrumbs match jobs / job / task group / allocation / task', async function (assert) { 72 const { jobId, taskGroup } = allocation; 73 const job = server.db.jobs.find(jobId); 74 75 const shortId = allocation.id.split('-')[0]; 76 assert.equal( 77 Layout.breadcrumbFor('jobs.index').text, 78 'Jobs', 79 'Jobs is the first breadcrumb' 80 ); 81 82 await waitFor('[data-test-job-breadcrumb]'); 83 assert.equal( 84 Layout.breadcrumbFor('jobs.job.index').text, 85 `Job ${job.name}`, 86 'Job is the second breadcrumb' 87 ); 88 assert.equal( 89 Layout.breadcrumbFor('jobs.job.task-group').text, 90 `Task Group ${taskGroup}`, 91 'Task Group is the third breadcrumb' 92 ); 93 assert.equal( 94 Layout.breadcrumbFor('allocations.allocation').text, 95 `Allocation ${shortId}`, 96 'Allocation short id is the fourth breadcrumb' 97 ); 98 assert.equal( 99 Layout.breadcrumbFor('allocations.allocation.task').text, 100 `Task ${task.name}`, 101 'Task name is the fifth breadcrumb' 102 ); 103 104 await Layout.breadcrumbFor('jobs.index').visit(); 105 assert.equal(currentURL(), '/jobs', 'Jobs breadcrumb links correctly'); 106 107 await Task.visit({ id: allocation.id, name: task.name }); 108 await Layout.breadcrumbFor('jobs.job.index').visit(); 109 assert.equal( 110 currentURL(), 111 `/jobs/${job.id}@default`, 112 'Job breadcrumb links correctly' 113 ); 114 115 await Task.visit({ id: allocation.id, name: task.name }); 116 await Layout.breadcrumbFor('jobs.job.task-group').visit(); 117 assert.equal( 118 currentURL(), 119 `/jobs/${job.id}@default/${taskGroup}`, 120 'Task Group breadcrumb links correctly' 121 ); 122 123 await Task.visit({ id: allocation.id, name: task.name }); 124 await Layout.breadcrumbFor('allocations.allocation').visit(); 125 assert.equal( 126 currentURL(), 127 `/allocations/${allocation.id}`, 128 'Allocations breadcrumb links correctly' 129 ); 130 }); 131 132 test('/allocation/:id/:task_name should include resource utilization graphs', async function (assert) { 133 assert.equal( 134 Task.resourceCharts.length, 135 2, 136 'Two resource utilization graphs' 137 ); 138 assert.equal( 139 Task.resourceCharts.objectAt(0).name, 140 'CPU', 141 'First chart is CPU' 142 ); 143 assert.equal( 144 Task.resourceCharts.objectAt(1).name, 145 'Memory', 146 'Second chart is Memory' 147 ); 148 }); 149 150 test('the events table lists all recent events', async function (assert) { 151 const events = server.db.taskEvents.where({ taskStateId: task.id }); 152 153 assert.equal( 154 Task.events.length, 155 events.length, 156 `Lists ${events.length} events` 157 ); 158 }); 159 160 test('when a task has volumes, the volumes table is shown', async function (assert) { 161 const taskGroup = server.schema.taskGroups.where({ 162 jobId: allocation.jobId, 163 name: allocation.taskGroup, 164 }).models[0]; 165 166 const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name); 167 168 assert.ok(Task.hasVolumes); 169 assert.equal(Task.volumes.length, jobTask.volumeMounts.length); 170 }); 171 172 test('when a task does not have volumes, the volumes table is not shown', async function (assert) { 173 const job = server.create('job', { 174 createAllocations: false, 175 noHostVolumes: true, 176 }); 177 allocation = server.create('allocation', { 178 jobId: job.id, 179 clientStatus: 'running', 180 }); 181 task = server.db.taskStates.where({ allocationId: allocation.id })[0]; 182 183 await Task.visit({ id: allocation.id, name: task.name }); 184 assert.notOk(Task.hasVolumes); 185 }); 186 187 test('each volume in the volumes table shows information about the volume', async function (assert) { 188 const taskGroup = server.schema.taskGroups.where({ 189 jobId: allocation.jobId, 190 name: allocation.taskGroup, 191 }).models[0]; 192 193 const jobTask = taskGroup.tasks.models.find((m) => m.name === task.name); 194 const volume = jobTask.volumeMounts[0]; 195 196 Task.volumes[0].as((volumeRow) => { 197 assert.equal(volumeRow.name, volume.Volume); 198 assert.equal(volumeRow.destination, volume.Destination); 199 assert.equal( 200 volumeRow.permissions, 201 volume.ReadOnly ? 'Read' : 'Read/Write' 202 ); 203 assert.equal( 204 volumeRow.clientSource, 205 taskGroup.volumes[volume.Volume].Source 206 ); 207 }); 208 }); 209 210 test('each recent event should list the time, type, and description of the event', async function (assert) { 211 const event = server.db.taskEvents.where({ taskStateId: task.id })[0]; 212 const recentEvent = Task.events.objectAt(Task.events.length - 1); 213 214 assert.equal( 215 recentEvent.time, 216 moment(event.time / 1000000).format("MMM DD, 'YY HH:mm:ss ZZ"), 217 'Event timestamp' 218 ); 219 assert.equal(recentEvent.type, event.type, 'Event type'); 220 assert.equal(recentEvent.message, event.displayMessage, 'Event message'); 221 }); 222 223 test('when the allocation is not found, the application errors', async function (assert) { 224 await Task.visit({ id: 'not-a-real-allocation', name: task.name }); 225 226 assert.equal( 227 server.pretender.handledRequests 228 .filter((request) => !request.url.includes('policy')) 229 .findBy('status', 404).url, 230 '/v1/allocation/not-a-real-allocation', 231 'A request to the nonexistent allocation is made' 232 ); 233 assert.equal( 234 currentURL(), 235 `/allocations/not-a-real-allocation/${task.name}`, 236 'The URL persists' 237 ); 238 assert.ok(Task.error.isPresent, 'Error message is shown'); 239 assert.equal(Task.error.title, 'Not Found', 'Error message is for 404'); 240 }); 241 242 test('when the allocation is found but the task is not, the application errors', async function (assert) { 243 await Task.visit({ id: allocation.id, name: 'not-a-real-task-name' }); 244 245 assert.ok( 246 server.pretender.handledRequests 247 .filterBy('status', 200) 248 .mapBy('url') 249 .includes(`/v1/allocation/${allocation.id}`), 250 'A request to the allocation is made successfully' 251 ); 252 assert.equal( 253 currentURL(), 254 `/allocations/${allocation.id}/not-a-real-task-name`, 255 'The URL persists' 256 ); 257 assert.ok(Task.error.isPresent, 'Error message is shown'); 258 assert.equal(Task.error.title, 'Not Found', 'Error message is for 404'); 259 }); 260 261 test('task can be restarted', async function (assert) { 262 await Task.restart.idle(); 263 await Task.restart.confirm(); 264 265 const request = server.pretender.handledRequests.findBy('method', 'PUT'); 266 assert.equal( 267 request.url, 268 `/v1/client/allocation/${allocation.id}/restart`, 269 'Restart request is made for the allocation' 270 ); 271 272 assert.deepEqual( 273 JSON.parse(request.requestBody), 274 { TaskName: task.name }, 275 'Restart request is made for the correct task' 276 ); 277 }); 278 279 test('when task restart fails (403), an ACL permissions error message is shown', async function (assert) { 280 server.pretender.put('/v1/client/allocation/:id/restart', () => [ 281 403, 282 {}, 283 '', 284 ]); 285 286 await Task.restart.idle(); 287 await Task.restart.confirm(); 288 289 assert.ok(Task.inlineError.isShown, 'Inline error is shown'); 290 assert.ok( 291 Task.inlineError.title.includes('Could Not Restart Task'), 292 'Title is descriptive' 293 ); 294 assert.ok( 295 /ACL token.+?allocation lifecycle/.test(Task.inlineError.message), 296 'Message mentions ACLs and the appropriate permission' 297 ); 298 299 await Task.inlineError.dismiss(); 300 301 assert.notOk(Task.inlineError.isShown, 'Inline error is no longer shown'); 302 }); 303 304 test('when task restart fails (500), the error message from the API is piped through to the alert', async function (assert) { 305 const message = 'A plaintext error message'; 306 server.pretender.put('/v1/client/allocation/:id/restart', () => [ 307 500, 308 {}, 309 message, 310 ]); 311 312 await Task.restart.idle(); 313 await Task.restart.confirm(); 314 315 assert.ok(Task.inlineError.isShown); 316 assert.ok(Task.inlineError.title.includes('Could Not Restart Task')); 317 assert.equal(Task.inlineError.message, message); 318 319 await Task.inlineError.dismiss(); 320 321 assert.notOk(Task.inlineError.isShown); 322 }); 323 324 test('exec button is present', async function (assert) { 325 assert.ok(Task.execButton.isPresent); 326 }); 327 }); 328 329 module('Acceptance | task detail (no addresses)', function (hooks) { 330 setupApplicationTest(hooks); 331 setupMirage(hooks); 332 333 hooks.beforeEach(async function () { 334 server.create('agent'); 335 server.create('node'); 336 server.create('job'); 337 allocation = server.create('allocation', 'withoutTaskWithPorts', { 338 clientStatus: 'running', 339 }); 340 task = server.db.taskStates.where({ allocationId: allocation.id })[0]; 341 342 await Task.visit({ id: allocation.id, name: task.name }); 343 }); 344 }); 345 346 module('Acceptance | task detail (different namespace)', function (hooks) { 347 setupApplicationTest(hooks); 348 setupMirage(hooks); 349 350 hooks.beforeEach(async function () { 351 server.create('agent'); 352 server.create('node'); 353 server.create('namespace'); 354 server.create('namespace', { id: 'other-namespace' }); 355 server.create('job', { 356 createAllocations: false, 357 namespaceId: 'other-namespace', 358 }); 359 allocation = server.create('allocation', 'withTaskWithPorts', { 360 clientStatus: 'running', 361 }); 362 task = server.db.taskStates.where({ allocationId: allocation.id })[0]; 363 364 await Task.visit({ id: allocation.id, name: task.name }); 365 }); 366 367 test('breadcrumbs match jobs / job / task group / allocation / task', async function (assert) { 368 const { jobId, taskGroup } = allocation; 369 const job = server.db.jobs.find(jobId); 370 371 await Layout.breadcrumbFor('jobs.index').visit(); 372 assert.equal( 373 currentURL(), 374 '/jobs?namespace=*', 375 'Jobs breadcrumb links correctly' 376 ); 377 378 await Task.visit({ id: allocation.id, name: task.name }); 379 await Layout.breadcrumbFor('jobs.job.index').visit(); 380 assert.equal( 381 currentURL(), 382 `/jobs/${job.id}@other-namespace`, 383 'Job breadcrumb links correctly' 384 ); 385 386 await Task.visit({ id: allocation.id, name: task.name }); 387 await Layout.breadcrumbFor('jobs.job.task-group').visit(); 388 assert.equal( 389 currentURL(), 390 `/jobs/${job.id}@other-namespace/${taskGroup}`, 391 'Task Group breadcrumb links correctly' 392 ); 393 394 await Task.visit({ id: allocation.id, name: task.name }); 395 await Layout.breadcrumbFor('allocations.allocation').visit(); 396 assert.equal( 397 currentURL(), 398 `/allocations/${allocation.id}`, 399 'Allocations breadcrumb links correctly' 400 ); 401 }); 402 }); 403 404 module('Acceptance | task detail (not running)', function (hooks) { 405 setupApplicationTest(hooks); 406 setupMirage(hooks); 407 408 hooks.beforeEach(async function () { 409 server.create('agent'); 410 server.create('node'); 411 server.create('namespace'); 412 server.create('namespace', { id: 'other-namespace' }); 413 server.create('job', { 414 createAllocations: false, 415 namespaceId: 'other-namespace', 416 }); 417 allocation = server.create('allocation', 'withTaskWithPorts', { 418 clientStatus: 'complete', 419 }); 420 task = server.db.taskStates.where({ allocationId: allocation.id })[0]; 421 422 await Task.visit({ id: allocation.id, name: task.name }); 423 }); 424 425 test('when the allocation for a task is not running, the resource utilization graphs are replaced by an empty message', async function (assert) { 426 assert.equal(Task.resourceCharts.length, 0, 'No resource charts'); 427 assert.equal( 428 Task.resourceEmptyMessage, 429 "Task isn't running", 430 'Empty message is appropriate' 431 ); 432 }); 433 434 test('exec button is absent', async function (assert) { 435 assert.notOk(Task.execButton.isPresent); 436 }); 437 }); 438 439 module('Acceptance | proxy task detail', function (hooks) { 440 setupApplicationTest(hooks); 441 setupMirage(hooks); 442 443 hooks.beforeEach(async function () { 444 server.create('agent'); 445 server.create('node'); 446 server.create('job', { createAllocations: false }); 447 allocation = server.create('allocation', 'withTaskWithPorts', { 448 clientStatus: 'running', 449 }); 450 451 const taskState = allocation.taskStates.models[0]; 452 const task = server.schema.tasks.findBy({ name: taskState.name }); 453 task.update('kind', 'connect-proxy:task'); 454 task.save(); 455 456 await Task.visit({ id: allocation.id, name: taskState.name }); 457 }); 458 459 test('a proxy tag is shown', async function (assert) { 460 assert.ok(Task.title.proxyTag.isPresent); 461 }); 462 });