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  });