github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/helpers/module-for-job.js (about) 1 /* eslint-disable qunit/require-expect */ 2 /* eslint-disable qunit/no-conditional-assertions */ 3 import { 4 click, 5 currentRouteName, 6 currentURL, 7 visit, 8 } from '@ember/test-helpers'; 9 import { module, test } from 'qunit'; 10 import { setupApplicationTest } from 'ember-qunit'; 11 import { setupMirage } from 'ember-cli-mirage/test-support'; 12 import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; 13 import setPolicy from 'nomad-ui/tests/utils/set-policy'; 14 15 // moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/ 16 // this is a misnomer in our context, because we're not using this API, however, the linter does not understand this 17 // the linter warning will go away if we rename this factory function to generateJobDetailsTests 18 // eslint-disable-next-line ember/no-test-module-for 19 export default function moduleForJob( 20 title, 21 context, 22 jobFactory, 23 additionalTests 24 ) { 25 let job; 26 27 module(title, function (hooks) { 28 setupApplicationTest(hooks); 29 setupMirage(hooks); 30 hooks.before(function () { 31 if (context !== 'allocations' && context !== 'children') { 32 throw new Error( 33 `Invalid context provided to moduleForJob, expected either "allocations" or "children", got ${context}` 34 ); 35 } 36 }); 37 38 hooks.beforeEach(async function () { 39 server.create('node'); 40 job = jobFactory(); 41 if (!job.namespace || job.namespace === 'default') { 42 await JobDetail.visit({ id: job.id }); 43 } else { 44 await JobDetail.visit({ id: `${job.id}@${job.namespace}` }); 45 } 46 47 const hasClientStatus = ['system', 'sysbatch'].includes(job.type); 48 if (context === 'allocations' && hasClientStatus) { 49 await click("[data-test-accordion-summary-chart='allocation-status']"); 50 } 51 }); 52 53 test('visiting /jobs/:job_id', async function (assert) { 54 const expectedURL = job.namespace 55 ? `/jobs/${job.name}@${job.namespace}` 56 : `/jobs/${job.name}`; 57 58 assert.equal(decodeURIComponent(currentURL()), expectedURL); 59 assert.equal(document.title, `Job ${job.name} - Nomad`); 60 }); 61 62 test('the subnav links to overview', async function (assert) { 63 await JobDetail.tabFor('overview').visit(); 64 65 const expectedURL = job.namespace 66 ? `/jobs/${job.name}@${job.namespace}` 67 : `/jobs/${job.name}`; 68 69 assert.equal(decodeURIComponent(currentURL()), expectedURL); 70 }); 71 72 test('the subnav links to definition', async function (assert) { 73 await JobDetail.tabFor('definition').visit(); 74 75 const expectedURL = job.namespace 76 ? `/jobs/${job.name}@${job.namespace}/definition` 77 : `/jobs/${job.name}/definition`; 78 79 assert.equal(decodeURIComponent(currentURL()), expectedURL); 80 }); 81 82 test('the subnav links to versions', async function (assert) { 83 await JobDetail.tabFor('versions').visit(); 84 85 const expectedURL = job.namespace 86 ? `/jobs/${job.name}@${job.namespace}/versions` 87 : `/jobs/${job.name}/versions`; 88 89 assert.equal(decodeURIComponent(currentURL()), expectedURL); 90 }); 91 92 test('the subnav links to evaluations', async function (assert) { 93 await JobDetail.tabFor('evaluations').visit(); 94 95 const expectedURL = job.namespace 96 ? `/jobs/${job.name}@${job.namespace}/evaluations` 97 : `/jobs/${job.name}/evaluations`; 98 99 assert.equal(decodeURIComponent(currentURL()), expectedURL); 100 }); 101 102 test('the title buttons are dependent on job status', async function (assert) { 103 if (job.status === 'dead') { 104 assert.ok(JobDetail.start.isPresent); 105 assert.ok(JobDetail.purge.isPresent); 106 assert.notOk(JobDetail.stop.isPresent); 107 assert.notOk(JobDetail.execButton.isPresent); 108 } else { 109 assert.notOk(JobDetail.start.isPresent); 110 assert.notOk(JobDetail.purge.isPresent); 111 assert.ok(JobDetail.stop.isPresent); 112 assert.ok(JobDetail.execButton.isPresent); 113 } 114 }); 115 116 if (context === 'allocations') { 117 test('allocations for the job are shown in the overview', async function (assert) { 118 assert.ok( 119 JobDetail.allocationsSummary.isPresent, 120 'Allocations are shown in the summary section' 121 ); 122 assert.ok( 123 JobDetail.childrenSummary.isHidden, 124 'Children are not shown in the summary section' 125 ); 126 }); 127 128 test('clicking in an allocation row navigates to that allocation', async function (assert) { 129 const allocationRow = JobDetail.allocations[0]; 130 const allocationId = allocationRow.id; 131 132 await allocationRow.visitRow(); 133 134 assert.equal( 135 currentURL(), 136 `/allocations/${allocationId}`, 137 'Allocation row links to allocation detail' 138 ); 139 }); 140 141 test('clicking in a task group row navigates to that task group', async function (assert) { 142 const tgRow = JobDetail.taskGroups[0]; 143 const tgName = tgRow.name; 144 145 await tgRow.visitRow(); 146 147 const expectedURL = job.namespace 148 ? `/jobs/${encodeURIComponent(job.name)}@${job.namespace}/${tgName}` 149 : `/jobs/${encodeURIComponent(job.name)}/${tgName}`; 150 151 assert.equal(currentURL(), expectedURL); 152 }); 153 154 test('clicking legend item navigates to a pre-filtered allocations table', async function (assert) { 155 const legendItem = 156 JobDetail.allocationsSummary.legend.clickableItems[1]; 157 const status = legendItem.label; 158 await legendItem.click(); 159 160 const encodedStatus = encodeURIComponent(JSON.stringify([status])); 161 const expectedURL = new URL( 162 urlWithNamespace( 163 `/jobs/${job.name}@default/clients?status=${encodedStatus}`, 164 job.namespace 165 ), 166 window.location 167 ); 168 const gotURL = new URL(currentURL(), window.location); 169 assert.deepEqual(gotURL.path, expectedURL.path); 170 assert.deepEqual(gotURL.searchParams, expectedURL.searchParams); 171 }); 172 173 test('clicking in a slice takes you to a pre-filtered allocations table', async function (assert) { 174 const slice = JobDetail.allocationsSummary.slices[1]; 175 const status = slice.label; 176 await slice.click(); 177 178 const encodedStatus = encodeURIComponent(JSON.stringify([status])); 179 const expectedURL = new URL( 180 urlWithNamespace( 181 `/jobs/${encodeURIComponent( 182 job.name 183 )}/allocations?status=${encodedStatus}`, 184 job.namespace 185 ), 186 window.location 187 ); 188 const gotURL = new URL(currentURL(), window.location); 189 assert.deepEqual(gotURL.pathname, expectedURL.pathname); 190 191 // Sort and compare URL query params. 192 gotURL.searchParams.sort(); 193 expectedURL.searchParams.sort(); 194 assert.equal( 195 gotURL.searchParams.toString(), 196 expectedURL.searchParams.toString() 197 ); 198 }); 199 } 200 201 if (context === 'children') { 202 test('children for the job are shown in the overview', async function (assert) { 203 assert.ok( 204 JobDetail.childrenSummary.isPresent, 205 'Children are shown in the summary section' 206 ); 207 assert.ok( 208 JobDetail.allocationsSummary.isHidden, 209 'Allocations are not shown in the summary section' 210 ); 211 }); 212 } 213 214 for (var testName in additionalTests) { 215 test(testName, async function (assert) { 216 await additionalTests[testName].call(this, job, assert); 217 }); 218 } 219 }); 220 } 221 222 // moduleFor is an old Ember-QUnit API that is deprected https://guides.emberjs.com/v1.10.0/testing/unit-test-helpers/ 223 // this is a misnomer in our context, because we're not using this API, however, the linter does not understand this 224 // the linter warning will go away if we rename this factory function to generateJobClientStatusTests 225 // eslint-disable-next-line ember/no-test-module-for 226 export function moduleForJobWithClientStatus( 227 title, 228 jobFactory, 229 additionalTests 230 ) { 231 let job; 232 233 module(title, function (hooks) { 234 setupApplicationTest(hooks); 235 setupMirage(hooks); 236 237 hooks.beforeEach(async function () { 238 const clients = server.createList('node', 3, { 239 datacenter: 'dc1', 240 status: 'ready', 241 }); 242 job = jobFactory(); 243 clients.forEach((c) => { 244 server.create('allocation', { jobId: job.id, nodeId: c.id }); 245 }); 246 }); 247 248 module('with node:read permissions', function (hooks) { 249 hooks.beforeEach(async function () { 250 // Displaying the job status in client requires node:read permission. 251 setPolicy({ 252 id: 'node-read', 253 name: 'node-read', 254 rulesJSON: { 255 Node: { 256 Policy: 'read', 257 }, 258 }, 259 }); 260 261 await visitJobDetailPage(job); 262 }); 263 264 test('the subnav links to clients', async function (assert) { 265 await JobDetail.tabFor('clients').visit(); 266 267 const expectedURL = job.namespace 268 ? `/jobs/${job.id}@${job.namespace}/clients` 269 : `/jobs/${job.id}/clients`; 270 271 assert.equal(currentURL(), expectedURL); 272 }); 273 274 test('job status summary is shown in the overview', async function (assert) { 275 assert.ok( 276 JobDetail.jobClientStatusSummary.statusBar.isPresent, 277 'Summary bar is displayed in the Job Status in Client summary section' 278 ); 279 }); 280 281 test('clicking legend item navigates to a pre-filtered clients table', async function (assert) { 282 const legendItem = 283 JobDetail.jobClientStatusSummary.statusBar.legend.clickableItems[0]; 284 const status = legendItem.label; 285 await legendItem.click(); 286 287 const encodedStatus = encodeURIComponent(JSON.stringify([status])); 288 const expectedURL = new URL( 289 urlWithNamespace( 290 `/jobs/${job.name}/clients?status=${encodedStatus}`, 291 job.namespace 292 ), 293 window.location 294 ); 295 const gotURL = new URL(currentURL(), window.location); 296 assert.deepEqual(gotURL.path, expectedURL.path); 297 assert.deepEqual(gotURL.searchParams, expectedURL.searchParams); 298 }); 299 300 test('clicking in a slice takes you to a pre-filtered clients table', async function (assert) { 301 const slice = JobDetail.jobClientStatusSummary.statusBar.slices[0]; 302 const status = slice.label; 303 await slice.click(); 304 305 const encodedStatus = encodeURIComponent(JSON.stringify([status])); 306 307 const expectedURL = job.namespace 308 ? `/jobs/${job.name}@${job.namespace}/clients?status=${encodedStatus}` 309 : `/jobs/${job.name}/clients?status=${encodedStatus}`; 310 311 assert.deepEqual(currentURL(), expectedURL, 'url is correct'); 312 }); 313 314 for (var testName in additionalTests) { 315 test(testName, async function (assert) { 316 await additionalTests[testName].call(this, job, assert); 317 }); 318 } 319 }); 320 321 module('without node:read permissions', function (hooks) { 322 hooks.beforeEach(async function () { 323 // Test blank Node policy to mock lack of permission. 324 setPolicy({ 325 id: 'node', 326 name: 'node', 327 rulesJSON: {}, 328 }); 329 330 await visitJobDetailPage(job); 331 }); 332 333 test('the page handles presentations concerns regarding the user not having node:read permissions', async function (assert) { 334 assert 335 .dom("[data-test-tab='clients']") 336 .doesNotExist( 337 'Job Detail Sub Navigation should not render Clients tab' 338 ); 339 340 assert 341 .dom('[data-test-nodes-not-authorized]') 342 .exists('Renders Not Authorized message'); 343 }); 344 345 test('/jobs/job/clients route is protected with authorization logic', async function (assert) { 346 await visit(`/jobs/${job.id}/clients`); 347 348 assert.equal( 349 currentRouteName(), 350 'jobs.job.index', 351 'The clients route cannot be visited unless you have node:read permissions' 352 ); 353 }); 354 }); 355 }); 356 } 357 358 function urlWithNamespace(url, namespace) { 359 if (!namespace || namespace === 'default') { 360 return url; 361 } 362 363 const parts = url.split('?'); 364 const params = new URLSearchParams(parts[1]); 365 params.set('namespace', namespace); 366 367 return `${parts[0]}?${params.toString()}`; 368 } 369 370 async function visitJobDetailPage({ id, namespace }) { 371 if (!namespace || namespace === 'default') { 372 await JobDetail.visit({ id }); 373 } else { 374 await JobDetail.visit({ id: `${id}@${namespace}` }); 375 } 376 }