github.com/anth0d/nomad@v0.0.0-20221214183521-ae3a0a2cad06/ui/tests/acceptance/token-test.js (about)

     1  /* eslint-disable qunit/require-expect */
     2  import { currentURL, find, findAll, visit, click } from '@ember/test-helpers';
     3  import { module, skip, 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 Tokens from 'nomad-ui/tests/pages/settings/tokens';
     8  import Jobs from 'nomad-ui/tests/pages/jobs/list';
     9  import JobDetail from 'nomad-ui/tests/pages/jobs/detail';
    10  import ClientDetail from 'nomad-ui/tests/pages/clients/detail';
    11  import Layout from 'nomad-ui/tests/pages/layout';
    12  import percySnapshot from '@percy/ember';
    13  import faker from 'nomad-ui/mirage/faker';
    14  import moment from 'moment';
    15  import { run } from '@ember/runloop';
    16  
    17  let job;
    18  let node;
    19  let managementToken;
    20  let clientToken;
    21  module('Acceptance | tokens', function (hooks) {
    22    setupApplicationTest(hooks);
    23    setupMirage(hooks);
    24  
    25    hooks.beforeEach(function () {
    26      window.localStorage.clear();
    27      window.sessionStorage.clear();
    28      faker.seed(1);
    29  
    30      server.create('agent');
    31      node = server.create('node');
    32      job = server.create('job');
    33      managementToken = server.create('token');
    34      clientToken = server.create('token');
    35    });
    36  
    37    test('it passes an accessibility audit', async function (assert) {
    38      assert.expect(1);
    39  
    40      await Tokens.visit();
    41      await a11yAudit(assert);
    42    });
    43  
    44    test('the token form sets the token in local storage', async function (assert) {
    45      const { secretId } = managementToken;
    46  
    47      await Tokens.visit();
    48      assert.equal(
    49        window.localStorage.nomadTokenSecret,
    50        null,
    51        'No token secret set'
    52      );
    53      assert.equal(document.title, 'Authorization - Nomad');
    54  
    55      await Tokens.secret(secretId).submit();
    56      assert.equal(
    57        window.localStorage.nomadTokenSecret,
    58        secretId,
    59        'Token secret was set'
    60      );
    61    });
    62  
    63    // TODO: unskip once store.unloadAll reliably waits for in-flight requests to settle
    64    skip('the x-nomad-token header gets sent with requests once it is set', async function (assert) {
    65      const { secretId } = managementToken;
    66  
    67      await JobDetail.visit({ id: job.id });
    68      await ClientDetail.visit({ id: node.id });
    69  
    70      assert.ok(
    71        server.pretender.handledRequests.length > 1,
    72        'Requests have been made'
    73      );
    74  
    75      server.pretender.handledRequests.forEach((req) => {
    76        assert.notOk(getHeader(req, 'x-nomad-token'), `No token for ${req.url}`);
    77      });
    78  
    79      const requestPosition = server.pretender.handledRequests.length;
    80  
    81      await Tokens.visit();
    82      await Tokens.secret(secretId).submit();
    83  
    84      await JobDetail.visit({ id: job.id });
    85      await ClientDetail.visit({ id: node.id });
    86  
    87      const newRequests = server.pretender.handledRequests.slice(requestPosition);
    88      assert.ok(newRequests.length > 1, 'New requests have been made');
    89  
    90      // Cross-origin requests can't have a token
    91      newRequests.forEach((req) => {
    92        assert.equal(
    93          getHeader(req, 'x-nomad-token'),
    94          secretId,
    95          `Token set for ${req.url}`
    96        );
    97      });
    98    });
    99  
   100    test('an error message is shown when authenticating a token fails', async function (assert) {
   101      const { secretId } = managementToken;
   102      const bogusSecret = 'this-is-not-the-secret';
   103      assert.notEqual(
   104        secretId,
   105        bogusSecret,
   106        'bogus secret is not somehow coincidentally equal to the real secret'
   107      );
   108  
   109      await Tokens.visit();
   110      await Tokens.secret(bogusSecret).submit();
   111  
   112      assert.equal(
   113        window.localStorage.nomadTokenSecret,
   114        null,
   115        'Token secret is discarded on failure'
   116      );
   117      assert.ok(Tokens.errorMessage, 'Token error message is shown');
   118      assert.notOk(Tokens.successMessage, 'Token success message is not shown');
   119      assert.equal(Tokens.policies.length, 0, 'No token policies are shown');
   120    });
   121  
   122    test('a success message and a special management token message are shown when authenticating succeeds', async function (assert) {
   123      const { secretId } = managementToken;
   124  
   125      await Tokens.visit();
   126      await Tokens.secret(secretId).submit();
   127  
   128      await percySnapshot(assert);
   129  
   130      assert.ok(Tokens.successMessage, 'Token success message is shown');
   131      assert.notOk(Tokens.errorMessage, 'Token error message is not shown');
   132      assert.ok(Tokens.managementMessage, 'Token management message is shown');
   133      assert.equal(Tokens.policies.length, 0, 'No token policies are shown');
   134    });
   135  
   136    test('a success message and associated policies are shown when authenticating succeeds', async function (assert) {
   137      const { secretId } = clientToken;
   138      const policy = clientToken.policies.models[0];
   139      policy.update('description', 'Make sure there is a description');
   140  
   141      await Tokens.visit();
   142      await Tokens.secret(secretId).submit();
   143  
   144      assert.ok(Tokens.successMessage, 'Token success message is shown');
   145      assert.notOk(Tokens.errorMessage, 'Token error message is not shown');
   146      assert.notOk(
   147        Tokens.managementMessage,
   148        'Token management message is not shown'
   149      );
   150      assert.equal(
   151        Tokens.policies.length,
   152        clientToken.policies.length,
   153        'Each policy associated with the token is listed'
   154      );
   155  
   156      const policyElement = Tokens.policies.objectAt(0);
   157  
   158      assert.equal(policyElement.name, policy.name, 'Policy Name');
   159      assert.equal(
   160        policyElement.description,
   161        policy.description,
   162        'Policy Description'
   163      );
   164      assert.equal(policyElement.rules, policy.rules, 'Policy Rules');
   165    });
   166  
   167    test('setting a token clears the store', async function (assert) {
   168      const { secretId } = clientToken;
   169  
   170      await Jobs.visit();
   171      assert.ok(find('.job-row'), 'Jobs found');
   172  
   173      await Tokens.visit();
   174      await Tokens.secret(secretId).submit();
   175  
   176      server.pretender.get('/v1/jobs', function () {
   177        return [200, {}, '[]'];
   178      });
   179  
   180      await Jobs.visit();
   181  
   182      // If jobs are lingering in the store, they would show up
   183      assert.notOk(find('[data-test-job-row]'), 'No jobs found');
   184    });
   185  
   186    test('it handles expiring tokens', async function (assert) {
   187      // Soon-expiring token
   188      const expiringToken = server.create('token', {
   189        name: "Time's a-tickin",
   190        expirationTime: moment().add(1, 'm').toDate(),
   191      });
   192  
   193      await Tokens.visit();
   194  
   195      // Token with no TTL
   196      await Tokens.secret(clientToken.secretId).submit();
   197      assert
   198        .dom('[data-test-token-expiry]')
   199        .doesNotExist('No expiry shown for regular token');
   200  
   201      await Tokens.clear();
   202  
   203      // https://ember-concurrency.com/docs/testing-debugging/
   204      setTimeout(() => run.cancelTimers(), 500);
   205  
   206      // Token with TTL
   207      await Tokens.secret(expiringToken.secretId).submit();
   208      assert
   209        .dom('[data-test-token-expiry]')
   210        .exists('Expiry shown for TTL-having token');
   211  
   212      // TTL Action
   213      await Jobs.visit();
   214      assert
   215        .dom('.flash-message.alert-error button')
   216        .exists('A global alert exists and has a clickable button');
   217  
   218      await click('.flash-message.alert-error button');
   219      assert.equal(
   220        currentURL(),
   221        '/settings/tokens',
   222        'Redirected to tokens page on notification action'
   223      );
   224    });
   225  
   226    test('it handles expired tokens', async function (assert) {
   227      const expiredToken = server.create('token', {
   228        name: 'Well past due',
   229        expirationTime: moment().add(-5, 'm').toDate(),
   230      });
   231  
   232      // GC'd or non-existent token, from localStorage or otherwise
   233      window.localStorage.nomadTokenSecret = expiredToken.secretId;
   234      await Tokens.visit();
   235      assert
   236        .dom('[data-test-token-expired]')
   237        .exists('Warning banner shown for expired token');
   238    });
   239  
   240    test('it forces redirect on an expired token', async function (assert) {
   241      const expiredToken = server.create('token', {
   242        name: 'Well past due',
   243        expirationTime: moment().add(-5, 'm').toDate(),
   244      });
   245  
   246      window.localStorage.nomadTokenSecret = expiredToken.secretId;
   247      const expiredServerError = {
   248        errors: [
   249          {
   250            detail: 'ACL token expired',
   251          },
   252        ],
   253      };
   254      server.pretender.get('/v1/jobs', function () {
   255        return [500, {}, JSON.stringify(expiredServerError)];
   256      });
   257  
   258      await Jobs.visit();
   259      assert.equal(
   260        currentURL(),
   261        '/settings/tokens',
   262        'Redirected to tokens page due to an expired token'
   263      );
   264    });
   265  
   266    test('it forces redirect on a not-found token', async function (assert) {
   267      const longDeadToken = server.create('token', {
   268        name: 'dead and gone',
   269        expirationTime: moment().add(-5, 'h').toDate(),
   270      });
   271  
   272      window.localStorage.nomadTokenSecret = longDeadToken.secretId;
   273      const notFoundServerError = {
   274        errors: [
   275          {
   276            detail: 'ACL token not found',
   277          },
   278        ],
   279      };
   280      server.pretender.get('/v1/jobs', function () {
   281        return [500, {}, JSON.stringify(notFoundServerError)];
   282      });
   283  
   284      await Jobs.visit();
   285      assert.equal(
   286        currentURL(),
   287        '/settings/tokens',
   288        'Redirected to tokens page due to a token not being found'
   289      );
   290    });
   291  
   292    test('it notifies you when your token has 10 minutes remaining', async function (assert) {
   293      let notificationRendered = assert.async();
   294      let notificationNotRendered = assert.async();
   295      window.localStorage.clear();
   296      assert.equal(
   297        window.localStorage.nomadTokenSecret,
   298        null,
   299        'No token secret set'
   300      );
   301      assert.timeout(6000);
   302      const nearlyExpiringToken = server.create('token', {
   303        name: 'Not quite dead yet',
   304        expirationTime: moment().add(10, 'm').add(5, 's').toDate(),
   305      });
   306  
   307      await Tokens.visit();
   308  
   309      // Ember Concurrency makes testing iterations convoluted: https://ember-concurrency.com/docs/testing-debugging/
   310      // Waiting for half a second to validate that there's no warning;
   311      // then a further 5 seconds to validate that there is a warning, and to explicitly cancelAllTimers(),
   312      // short-circuiting our Ember Concurrency loop.
   313      setTimeout(() => {
   314        assert
   315          .dom('.flash-message.alert-error')
   316          .doesNotExist('No notification yet for a token with 10m5s left');
   317        notificationNotRendered();
   318        setTimeout(async () => {
   319          await percySnapshot(assert, {
   320            percyCSS: '[data-test-expiration-timestamp] { display: none; }',
   321          });
   322  
   323          assert
   324            .dom('.flash-message.alert-error')
   325            .exists('Notification is rendered at the 10m mark');
   326          notificationRendered();
   327          run.cancelTimers();
   328        }, 5000);
   329      }, 500);
   330      await Tokens.secret(nearlyExpiringToken.secretId).submit();
   331    });
   332  
   333    test('when the ott query parameter is present upon application load it’s exchanged for a token', async function (assert) {
   334      const { oneTimeSecret, secretId } = managementToken;
   335  
   336      await JobDetail.visit({ id: job.id, ott: oneTimeSecret });
   337  
   338      assert.notOk(
   339        currentURL().includes(oneTimeSecret),
   340        'OTT is cleared from the URL after loading'
   341      );
   342  
   343      await Tokens.visit();
   344  
   345      assert.equal(
   346        window.localStorage.nomadTokenSecret,
   347        secretId,
   348        'Token secret was set'
   349      );
   350    });
   351  
   352    test('SSO Sign-in flow: Manager', async function (assert) {
   353      server.create('auth-method', { name: 'vault' });
   354      server.create('auth-method', { name: 'cognito' });
   355      server.create('token', { name: 'Thelonious' });
   356  
   357      await Tokens.visit();
   358      assert.dom('[data-test-auth-method]').exists({ count: 2 });
   359      await click('button[data-test-auth-method]');
   360      assert.ok(currentURL().startsWith('/oidc-mock'));
   361      let managerButton = [...findAll('button')].filter((btn) =>
   362        btn.textContent.includes('Sign In as Manager')
   363      )[0];
   364  
   365      assert.dom(managerButton).exists();
   366      await click(managerButton);
   367  
   368      await percySnapshot(assert);
   369  
   370      assert.ok(currentURL().startsWith('/settings/tokens'));
   371      assert.dom('[data-test-token-name]').includesText('Token: Manager');
   372    });
   373  
   374    test('SSO Sign-in flow: Regular User', async function (assert) {
   375      server.create('auth-method', { name: 'vault' });
   376      server.create('token', { name: 'Thelonious' });
   377  
   378      await Tokens.visit();
   379      assert.dom('[data-test-auth-method]').exists({ count: 1 });
   380      await click('button[data-test-auth-method]');
   381      assert.ok(currentURL().startsWith('/oidc-mock'));
   382      let newTokenButton = [...findAll('button')].filter((btn) =>
   383        btn.textContent.includes('Sign In as Thelonious')
   384      )[0];
   385      assert.dom(newTokenButton).exists();
   386      await click(newTokenButton);
   387  
   388      assert.ok(currentURL().startsWith('/settings/tokens'));
   389      assert.dom('[data-test-token-name]').includesText('Token: Thelonious');
   390    });
   391  
   392    test('It shows an error on failed SSO', async function (assert) {
   393      server.create('auth-method', { name: 'vault' });
   394      await visit('/settings/tokens?state=failure');
   395      assert.ok(Tokens.ssoErrorMessage);
   396      await Tokens.clearSSOError();
   397      assert.equal(currentURL(), '/settings/tokens', 'State query param cleared');
   398      assert.notOk(Tokens.ssoErrorMessage);
   399  
   400      await click('button[data-test-auth-method]');
   401      assert.ok(currentURL().startsWith('/oidc-mock'));
   402  
   403      let failureButton = find('.button.error');
   404      assert.dom(failureButton).exists();
   405      await click(failureButton);
   406      assert.equal(
   407        currentURL(),
   408        '/settings/tokens?state=failure',
   409        'Redirected with failure state'
   410      );
   411  
   412      await percySnapshot(assert);
   413      assert.ok(Tokens.ssoErrorMessage);
   414    });
   415  
   416    test('when the ott exchange fails an error is shown', async function (assert) {
   417      await visit('/?ott=fake');
   418  
   419      assert.ok(Layout.error.isPresent);
   420      assert.equal(Layout.error.title, 'Token Exchange Error');
   421      assert.equal(
   422        Layout.error.message,
   423        'Failed to exchange the one-time token.'
   424      );
   425    });
   426  
   427    function getHeader({ requestHeaders }, name) {
   428      // Headers are case-insensitive, but object property look up is not
   429      return (
   430        requestHeaders[name] ||
   431        requestHeaders[name.toLowerCase()] ||
   432        requestHeaders[name.toUpperCase()]
   433      );
   434    }
   435  });