github.com/hernad/nomad@v1.6.112/ui/tests/acceptance/token-test.js (about) 1 /** 2 * Copyright (c) HashiCorp, Inc. 3 * SPDX-License-Identifier: MPL-2.0 4 */ 5 6 /* eslint-disable qunit/require-expect */ 7 import { currentURL, find, findAll, visit, click } from '@ember/test-helpers'; 8 import { module, skip, test } from 'qunit'; 9 import { setupApplicationTest } from 'ember-qunit'; 10 import { setupMirage } from 'ember-cli-mirage/test-support'; 11 import a11yAudit from 'nomad-ui/tests/helpers/a11y-audit'; 12 import Tokens from 'nomad-ui/tests/pages/settings/tokens'; 13 import Jobs from 'nomad-ui/tests/pages/jobs/list'; 14 import JobDetail from 'nomad-ui/tests/pages/jobs/detail'; 15 import ClientDetail from 'nomad-ui/tests/pages/clients/detail'; 16 import Layout from 'nomad-ui/tests/pages/layout'; 17 import percySnapshot from '@percy/ember'; 18 import faker from 'nomad-ui/mirage/faker'; 19 import moment from 'moment'; 20 import { run } from '@ember/runloop'; 21 import { allScenarios } from '../../mirage/scenarios/default'; 22 import { 23 selectChoose, 24 clickTrigger, 25 } from 'ember-power-select/test-support/helpers'; 26 27 let job; 28 let node; 29 let managementToken; 30 let clientToken; 31 module('Acceptance | tokens', function (hooks) { 32 setupApplicationTest(hooks); 33 setupMirage(hooks); 34 35 hooks.beforeEach(function () { 36 window.localStorage.clear(); 37 window.sessionStorage.clear(); 38 faker.seed(1); 39 40 server.create('agent'); 41 server.create('node-pool'); 42 node = server.create('node'); 43 job = server.create('job'); 44 managementToken = server.create('token'); 45 clientToken = server.create('token'); 46 }); 47 48 test('it passes an accessibility audit', async function (assert) { 49 assert.expect(1); 50 51 await Tokens.visit(); 52 await a11yAudit(assert); 53 }); 54 55 test('the token form sets the token in local storage', async function (assert) { 56 const { secretId } = managementToken; 57 58 await Tokens.visit(); 59 assert.equal( 60 window.localStorage.nomadTokenSecret, 61 null, 62 'No token secret set' 63 ); 64 assert.ok(document.title.includes('Authorization')); 65 66 await Tokens.secret(secretId).submit(); 67 assert.equal( 68 window.localStorage.nomadTokenSecret, 69 secretId, 70 'Token secret was set' 71 ); 72 }); 73 74 // TODO: unskip once store.unloadAll reliably waits for in-flight requests to settle 75 skip('the x-nomad-token header gets sent with requests once it is set', async function (assert) { 76 const { secretId } = managementToken; 77 78 await JobDetail.visit({ id: job.id }); 79 await ClientDetail.visit({ id: node.id }); 80 81 assert.ok( 82 server.pretender.handledRequests.length > 1, 83 'Requests have been made' 84 ); 85 86 server.pretender.handledRequests.forEach((req) => { 87 assert.notOk(getHeader(req, 'x-nomad-token'), `No token for ${req.url}`); 88 }); 89 90 const requestPosition = server.pretender.handledRequests.length; 91 92 await Tokens.visit(); 93 await Tokens.secret(secretId).submit(); 94 95 await JobDetail.visit({ id: job.id }); 96 await ClientDetail.visit({ id: node.id }); 97 98 const newRequests = server.pretender.handledRequests.slice(requestPosition); 99 assert.ok(newRequests.length > 1, 'New requests have been made'); 100 101 // Cross-origin requests can't have a token 102 newRequests.forEach((req) => { 103 assert.equal( 104 getHeader(req, 'x-nomad-token'), 105 secretId, 106 `Token set for ${req.url}` 107 ); 108 }); 109 }); 110 111 test('an error message is shown when authenticating a token fails', async function (assert) { 112 const { secretId } = managementToken; 113 const bogusSecret = 'this-is-not-the-secret'; 114 assert.notEqual( 115 secretId, 116 bogusSecret, 117 'bogus secret is not somehow coincidentally equal to the real secret' 118 ); 119 120 await Tokens.visit(); 121 await Tokens.secret(bogusSecret).submit(); 122 123 assert.equal( 124 window.localStorage.nomadTokenSecret, 125 null, 126 'Token secret is discarded on failure' 127 ); 128 assert.ok(Tokens.errorMessage, 'Token error message is shown'); 129 assert.notOk(Tokens.successMessage, 'Token success message is not shown'); 130 assert.equal(Tokens.policies.length, 0, 'No token policies are shown'); 131 }); 132 133 test('a success message and a special management token message are shown when authenticating succeeds', async function (assert) { 134 const { secretId } = managementToken; 135 136 await Tokens.visit(); 137 await Tokens.secret(secretId).submit(); 138 139 await percySnapshot(assert); 140 141 assert.ok(Tokens.successMessage, 'Token success message is shown'); 142 assert.notOk(Tokens.errorMessage, 'Token error message is not shown'); 143 assert.ok(Tokens.managementMessage, 'Token management message is shown'); 144 assert.equal(Tokens.policies.length, 0, 'No token policies are shown'); 145 }); 146 147 test('a success message and associated policies are shown when authenticating succeeds', async function (assert) { 148 const { secretId } = clientToken; 149 const policy = clientToken.policies.models[0]; 150 policy.update('description', 'Make sure there is a description'); 151 152 await Tokens.visit(); 153 await Tokens.secret(secretId).submit(); 154 155 assert.ok(Tokens.successMessage, 'Token success message is shown'); 156 assert.notOk(Tokens.errorMessage, 'Token error message is not shown'); 157 assert.notOk( 158 Tokens.managementMessage, 159 'Token management message is not shown' 160 ); 161 assert.equal( 162 Tokens.policies.length, 163 clientToken.policies.length, 164 'Each policy associated with the token is listed' 165 ); 166 167 const policyElement = Tokens.policies.objectAt(0); 168 169 assert.equal(policyElement.name, policy.name, 'Policy Name'); 170 assert.equal( 171 policyElement.description, 172 policy.description, 173 'Policy Description' 174 ); 175 assert.equal(policyElement.rules, policy.rules, 'Policy Rules'); 176 }); 177 178 test('setting a token clears the store', async function (assert) { 179 const { secretId } = clientToken; 180 181 await Jobs.visit(); 182 assert.ok(find('.job-row'), 'Jobs found'); 183 184 await Tokens.visit(); 185 await Tokens.secret(secretId).submit(); 186 187 server.pretender.get('/v1/jobs', function () { 188 return [200, {}, '[]']; 189 }); 190 191 await Jobs.visit(); 192 193 // If jobs are lingering in the store, they would show up 194 assert.notOk(find('[data-test-job-row]'), 'No jobs found'); 195 }); 196 197 test('it handles expiring tokens', async function (assert) { 198 // Soon-expiring token 199 const expiringToken = server.create('token', { 200 name: "Time's a-tickin", 201 expirationTime: moment().add(1, 'm').toDate(), 202 }); 203 204 await Tokens.visit(); 205 206 // Token with no TTL 207 await Tokens.secret(clientToken.secretId).submit(); 208 assert 209 .dom('[data-test-token-expiry]') 210 .doesNotExist('No expiry shown for regular token'); 211 212 await Tokens.clear(); 213 214 // https://ember-concurrency.com/docs/testing-debugging/ 215 setTimeout(() => run.cancelTimers(), 500); 216 217 // Token with TTL 218 await Tokens.secret(expiringToken.secretId).submit(); 219 assert 220 .dom('[data-test-token-expiry]') 221 .exists('Expiry shown for TTL-having token'); 222 223 // TTL Action 224 await Jobs.visit(); 225 assert 226 .dom('.flash-message.alert-warning button') 227 .exists('A global alert exists and has a clickable button'); 228 229 await click('.flash-message.alert-warning button'); 230 assert.equal( 231 currentURL(), 232 '/settings/tokens', 233 'Redirected to tokens page on notification action' 234 ); 235 }); 236 237 test('it handles expired tokens', async function (assert) { 238 const expiredToken = server.create('token', { 239 name: 'Well past due', 240 expirationTime: moment().add(-5, 'm').toDate(), 241 }); 242 243 // GC'd or non-existent token, from localStorage or otherwise 244 window.localStorage.nomadTokenSecret = expiredToken.secretId; 245 await Tokens.visit(); 246 assert 247 .dom('[data-test-token-expired]') 248 .exists('Warning banner shown for expired token'); 249 }); 250 251 test('it forces redirect on an expired token', async function (assert) { 252 const expiredToken = server.create('token', { 253 name: 'Well past due', 254 expirationTime: moment().add(-5, 'm').toDate(), 255 }); 256 257 window.localStorage.nomadTokenSecret = expiredToken.secretId; 258 const expiredServerError = { 259 errors: [ 260 { 261 detail: 'ACL token expired', 262 }, 263 ], 264 }; 265 server.pretender.get('/v1/jobs', function () { 266 return [500, {}, JSON.stringify(expiredServerError)]; 267 }); 268 269 await Jobs.visit(); 270 assert.equal( 271 currentURL(), 272 '/settings/tokens', 273 'Redirected to tokens page due to an expired token' 274 ); 275 }); 276 277 test('it forces redirect on a not-found token', async function (assert) { 278 const longDeadToken = server.create('token', { 279 name: 'dead and gone', 280 expirationTime: moment().add(-5, 'h').toDate(), 281 }); 282 283 window.localStorage.nomadTokenSecret = longDeadToken.secretId; 284 const notFoundServerError = { 285 errors: [ 286 { 287 detail: 'ACL token not found', 288 }, 289 ], 290 }; 291 server.pretender.get('/v1/jobs', function () { 292 return [500, {}, JSON.stringify(notFoundServerError)]; 293 }); 294 295 await Jobs.visit(); 296 assert.equal( 297 currentURL(), 298 '/settings/tokens', 299 'Redirected to tokens page due to a token not being found' 300 ); 301 }); 302 303 test('it notifies you when your token has 10 minutes remaining', async function (assert) { 304 let notificationRendered = assert.async(); 305 let notificationNotRendered = assert.async(); 306 window.localStorage.clear(); 307 assert.equal( 308 window.localStorage.nomadTokenSecret, 309 null, 310 'No token secret set' 311 ); 312 assert.timeout(6000); 313 const nearlyExpiringToken = server.create('token', { 314 name: 'Not quite dead yet', 315 expirationTime: moment().add(10, 'm').add(5, 's').toDate(), 316 }); 317 318 await Tokens.visit(); 319 320 // Ember Concurrency makes testing iterations convoluted: https://ember-concurrency.com/docs/testing-debugging/ 321 // Waiting for half a second to validate that there's no warning; 322 // then a further 5 seconds to validate that there is a warning, and to explicitly cancelAllTimers(), 323 // short-circuiting our Ember Concurrency loop. 324 setTimeout(() => { 325 assert 326 .dom('.flash-message.alert-warning') 327 .doesNotExist('No notification yet for a token with 10m5s left'); 328 notificationNotRendered(); 329 setTimeout(async () => { 330 await percySnapshot(assert, { 331 percyCSS: '[data-test-expiration-timestamp] { display: none; }', 332 }); 333 334 assert 335 .dom('.flash-message.alert-warning') 336 .exists('Notification is rendered at the 10m mark'); 337 notificationRendered(); 338 run.cancelTimers(); 339 }, 5000); 340 }, 500); 341 await Tokens.secret(nearlyExpiringToken.secretId).submit(); 342 }); 343 344 test('when the ott query parameter is present upon application load it’s exchanged for a token', async function (assert) { 345 const { oneTimeSecret, secretId } = managementToken; 346 347 await JobDetail.visit({ id: job.id, ott: oneTimeSecret }); 348 349 assert.notOk( 350 currentURL().includes(oneTimeSecret), 351 'OTT is cleared from the URL after loading' 352 ); 353 354 await Tokens.visit(); 355 356 assert.equal( 357 window.localStorage.nomadTokenSecret, 358 secretId, 359 'Token secret was set' 360 ); 361 }); 362 363 test('SSO Sign-in flow: Manager', async function (assert) { 364 server.create('auth-method', { name: 'vault' }); 365 server.create('auth-method', { name: 'cognito' }); 366 server.create('token', { name: 'Thelonious' }); 367 368 await Tokens.visit(); 369 assert.dom('[data-test-auth-method]').exists({ count: 2 }); 370 await click('button[data-test-auth-method]'); 371 assert.ok(currentURL().startsWith('/oidc-mock')); 372 let managerButton = [...findAll('button')].filter((btn) => 373 btn.textContent.includes('Sign In as Manager') 374 )[0]; 375 376 assert.dom(managerButton).exists(); 377 await click(managerButton); 378 379 await percySnapshot(assert); 380 381 assert.ok(currentURL().startsWith('/settings/tokens')); 382 assert.dom('[data-test-token-name]').includesText('Token: Manager'); 383 }); 384 385 test('SSO Sign-in flow: Regular User', async function (assert) { 386 server.create('auth-method', { name: 'vault' }); 387 server.create('token', { name: 'Thelonious' }); 388 389 await Tokens.visit(); 390 assert.dom('[data-test-auth-method]').exists({ count: 1 }); 391 await click('button[data-test-auth-method]'); 392 assert.ok(currentURL().startsWith('/oidc-mock')); 393 let newTokenButton = [...findAll('button')].filter((btn) => 394 btn.textContent.includes('Sign In as Thelonious') 395 )[0]; 396 assert.dom(newTokenButton).exists(); 397 await click(newTokenButton); 398 399 assert.ok(currentURL().startsWith('/settings/tokens')); 400 assert.dom('[data-test-token-name]').includesText('Token: Thelonious'); 401 }); 402 403 test('It shows an error on failed SSO', async function (assert) { 404 server.create('auth-method', { name: 'vault' }); 405 await visit('/settings/tokens?state=failure'); 406 assert.ok(Tokens.ssoErrorMessage); 407 await Tokens.clearSSOError(); 408 assert.equal(currentURL(), '/settings/tokens', 'State query param cleared'); 409 assert.notOk(Tokens.ssoErrorMessage); 410 411 await click('button[data-test-auth-method]'); 412 assert.ok(currentURL().startsWith('/oidc-mock')); 413 414 let failureButton = find('.button.error'); 415 assert.dom(failureButton).exists(); 416 await click(failureButton); 417 assert.equal( 418 currentURL(), 419 '/settings/tokens?state=failure', 420 'Redirected with failure state' 421 ); 422 423 await percySnapshot(assert); 424 assert.ok(Tokens.ssoErrorMessage); 425 }); 426 427 test('JWT Sign-in flow: OIDC methods only', async function (assert) { 428 server.create('auth-method', { name: 'Vault', type: 'OIDC' }); 429 server.create('auth-method', { name: 'Auth0', type: 'OIDC' }); 430 await Tokens.visit(); 431 assert 432 .dom('[data-test-auth-method]') 433 .exists({ count: 2 }, 'Both OIDC methods shown'); 434 assert 435 .dom('label[for="token-input"]') 436 .hasText( 437 'Secret ID', 438 'Secret ID input shown without JWT info when no such method exists' 439 ); 440 }); 441 442 test('JWT Sign-in flow: JWT method', async function (assert) { 443 server.create('auth-method', { name: 'Vault', type: 'OIDC' }); 444 server.create('auth-method', { name: 'Auth0', type: 'OIDC' }); 445 server.create('auth-method', { name: 'JWT-Local', type: 'JWT' }); 446 await Tokens.visit(); 447 assert 448 .dom('[data-test-auth-method]') 449 .exists( 450 { count: 2 }, 451 'The newly added JWT method does not add a 3rd Auth Method button' 452 ); 453 assert 454 .dom('label[for="token-input"]') 455 .hasText('Secret ID or JWT', 'Secret ID input now shows JWT info'); 456 457 // Expect to be signed in as a manager 458 await Tokens.secret( 459 'aaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.management' 460 ).submit(); 461 assert.ok(currentURL().startsWith('/settings/tokens')); 462 assert.dom('[data-test-token-name]').includesText('Token: Manager'); 463 await Tokens.clear(); 464 465 // Expect to be signed in as a client 466 await Tokens.secret( 467 'aaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.whateverlol' 468 ).submit(); 469 assert.ok(currentURL().startsWith('/settings/tokens')); 470 assert.dom('[data-test-token-name]').includesText( 471 `Token: ${ 472 server.db.tokens.filter((token) => { 473 return token.type === 'client'; 474 })[0].name 475 }` 476 ); 477 await Tokens.clear(); 478 479 // Expect to an error on bad JWT 480 await Tokens.secret( 481 'aaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bad' 482 ).submit(); 483 assert.ok(currentURL().startsWith('/settings/tokens')); 484 assert.dom('[data-test-token-error]').exists(); 485 }); 486 487 test('JWT Sign-in flow: JWT Method Selector, Single JWT', async function (assert) { 488 server.create('auth-method', { name: 'Vault', type: 'OIDC' }); 489 server.create('auth-method', { name: 'Auth0', type: 'OIDC' }); 490 server.create('auth-method', { name: 'JWT-Local', type: 'JWT' }); 491 await Tokens.visit(); 492 assert 493 .dom('[data-test-token-submit]') 494 .exists( 495 { count: 1 }, 496 'Submit token/JWT button exists with only a single JWT ' 497 ); 498 assert 499 .dom('[data-test-token-submit]') 500 .hasText( 501 'Sign in with secret', 502 'Submit token/JWT button has correct text with only a single JWT ' 503 ); 504 await Tokens.secret('very-short-secret'); 505 assert 506 .dom('[data-test-token-submit]') 507 .hasText( 508 'Sign in with secret', 509 'A short secret still shows the "secret" verbiage on the button' 510 ); 511 await Tokens.secret( 512 'aaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.whateverlol' 513 ); 514 assert 515 .dom('[data-test-token-submit]') 516 .hasText( 517 'Sign in with JWT', 518 'A JWT-shaped secret will change button text to reflect JWT sign-in' 519 ); 520 521 assert 522 .dom('[data-test-select-jwt]') 523 .doesNotExist('No JWT selector shown with only a single method'); 524 }); 525 526 test('JWT Sign-in flow: JWT Method Selector, Multiple JWT', async function (assert) { 527 server.create('auth-method', { name: 'Vault', type: 'OIDC' }); 528 server.create('auth-method', { name: 'Auth0', type: 'OIDC' }); 529 server.create('auth-method', { 530 name: 'JWT-Local', 531 type: 'JWT', 532 default: false, 533 }); 534 server.create('auth-method', { 535 name: 'JWT-Regional', 536 type: 'JWT', 537 default: false, 538 }); 539 server.create('auth-method', { 540 name: 'JWT-Global', 541 type: 'JWT', 542 default: true, 543 }); 544 await Tokens.visit(); 545 assert 546 .dom('[data-test-token-submit]') 547 .exists( 548 { count: 1 }, 549 'Submit token/JWT button exists with only a single JWT ' 550 ); 551 assert 552 .dom('[data-test-select-jwt]') 553 .doesNotExist('No JWT selector shown with an empty token/secret'); 554 await Tokens.secret( 555 'aaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.whateverlol' 556 ); 557 assert 558 .dom('[data-test-select-jwt]') 559 .exists({ count: 1 }, 'JWT selector shown with multiple JWT methods'); 560 561 assert.equal( 562 currentURL(), 563 '/settings/tokens?jwtAuthMethod=JWT-Global', 564 'Default JWT method is selected' 565 ); 566 await clickTrigger('[data-test-select-jwt]'); 567 assert.dom('.dropdown-options').exists('Dropdown options are shown'); 568 569 await selectChoose('[data-test-select-jwt]', 'JWT-Regional'); 570 console.log(currentURL()); 571 assert.equal( 572 currentURL(), 573 '/settings/tokens?jwtAuthMethod=JWT-Regional', 574 'Selected JWT method is shown' 575 ); 576 }); 577 578 test('when the ott exchange fails an error is shown', async function (assert) { 579 await visit('/?ott=fake'); 580 581 assert.ok(Layout.error.isPresent); 582 assert.equal(Layout.error.title, 'Token Exchange Error'); 583 assert.equal( 584 Layout.error.message, 585 'Failed to exchange the one-time token.' 586 ); 587 }); 588 589 test('Tokens are shown on the policies index page', async function (assert) { 590 allScenarios.policiesTestCluster(server); 591 // Create an expired token 592 server.create('token', { 593 name: 'Expired Token', 594 id: 'just-expired', 595 policyIds: [server.db.policies[0].name], 596 expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago 597 }); 598 599 window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; 600 await visit('/policies'); 601 assert.dom('[data-test-policy-token-count]').exists(); 602 const expectedFirstPolicyTokens = server.db.tokens.filter((token) => { 603 return token.policyIds.includes(server.db.policies[0].name); 604 }); 605 assert 606 .dom('[data-test-policy-total-tokens]') 607 .hasText(expectedFirstPolicyTokens.length.toString()); 608 assert.dom('[data-test-policy-expired-tokens]').hasText('(1 expired)'); 609 window.localStorage.nomadTokenSecret = null; 610 }); 611 612 test('Tokens are shown on a policy page', async function (assert) { 613 allScenarios.policiesTestCluster(server); 614 // Create an expired token 615 server.create('token', { 616 name: 'Expired Token', 617 id: 'just-expired', 618 policyIds: [server.db.policies[0].name], 619 expirationTime: new Date(new Date().getTime() - 10 * 60 * 1000), // 10 minutes ago 620 }); 621 622 window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; 623 await visit('/policies'); 624 625 await click('[data-test-policy-row]:first-child'); 626 assert.equal(currentURL(), `/policies/${server.db.policies[0].name}`); 627 628 const expectedFirstPolicyTokens = server.db.tokens.filter((token) => { 629 return token.policyIds.includes(server.db.policies[0].name); 630 }); 631 632 assert 633 .dom('[data-test-policy-token-row]') 634 .exists( 635 { count: expectedFirstPolicyTokens.length }, 636 'Expected number of tokens are shown' 637 ); 638 assert.dom('[data-test-token-expiration-time]').hasText('10 minutes ago'); 639 640 window.localStorage.nomadTokenSecret = null; 641 }); 642 643 test('Tokens Deletion', async function (assert) { 644 allScenarios.policiesTestCluster(server); 645 const testPolicy = server.db.policies[0]; 646 const existingTokens = server.db.tokens.filter((t) => 647 t.policyIds.includes(testPolicy.name) 648 ); 649 // Create an expired token 650 server.create('token', { 651 name: 'Doomed Token', 652 id: 'enjoying-my-day-here', 653 policyIds: [testPolicy.name], 654 }); 655 656 window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; 657 await visit('/policies'); 658 659 await click('[data-test-policy-row]:first-child'); 660 assert.equal(currentURL(), `/policies/${testPolicy.name}`); 661 assert 662 .dom('[data-test-policy-token-row]') 663 .exists( 664 { count: existingTokens.length + 1 }, 665 'Expected number of tokens are shown' 666 ); 667 668 const doomedTokenRow = [...findAll('[data-test-policy-token-row]')].find( 669 (a) => a.textContent.includes('Doomed Token') 670 ); 671 672 assert.dom(doomedTokenRow).exists(); 673 674 await click(doomedTokenRow.querySelector('button')); 675 assert 676 .dom(doomedTokenRow.querySelector('[data-test-confirm-button]')) 677 .exists(); 678 await click(doomedTokenRow.querySelector('[data-test-confirm-button]')); 679 assert.dom('.flash-message.alert-success').exists(); 680 assert 681 .dom('[data-test-policy-token-row]') 682 .exists( 683 { count: existingTokens.length }, 684 'One fewer token after deletion' 685 ); 686 await percySnapshot(assert); 687 window.localStorage.nomadTokenSecret = null; 688 }); 689 690 test('Test Token Creation', async function (assert) { 691 allScenarios.policiesTestCluster(server); 692 const testPolicy = server.db.policies[0]; 693 const existingTokens = server.db.tokens.filter((t) => 694 t.policyIds.includes(testPolicy.name) 695 ); 696 697 window.localStorage.nomadTokenSecret = server.db.tokens[0].secretId; 698 await visit('/policies'); 699 700 await click('[data-test-policy-row]:first-child'); 701 assert.equal(currentURL(), `/policies/${testPolicy.name}`); 702 703 assert 704 .dom('[data-test-policy-token-row]') 705 .exists( 706 { count: existingTokens.length }, 707 'Expected number of tokens are shown' 708 ); 709 710 await click('[data-test-create-test-token]'); 711 assert.dom('.flash-message.alert-success').exists(); 712 assert 713 .dom('[data-test-policy-token-row]') 714 .exists( 715 { count: existingTokens.length + 1 }, 716 'One more token after test token creation' 717 ); 718 assert 719 .dom('[data-test-policy-token-row]:last-child [data-test-token-name]') 720 .hasText(`Example Token for ${testPolicy.name}`); 721 await percySnapshot(assert); 722 window.localStorage.nomadTokenSecret = null; 723 }); 724 725 function getHeader({ requestHeaders }, name) { 726 // Headers are case-insensitive, but object property look up is not 727 return ( 728 requestHeaders[name] || 729 requestHeaders[name.toLowerCase()] || 730 requestHeaders[name.toUpperCase()] 731 ); 732 } 733 });