import { module, test } from 'qunit'; import { visit, currentURL, click, fillIn, findAll, currentRouteName } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import { setupMirage } from 'ember-cli-mirage/test-support'; import ENV from 'vault/config/environment'; import authPage from 'vault/tests/pages/auth'; import logout from 'vault/tests/pages/logout'; import { create } from 'ember-cli-page-object'; import { clickTrigger, selectChoose } from 'ember-power-select/test-support/helpers'; import ss from 'vault/tests/pages/components/search-select'; import fm from 'vault/tests/pages/components/flash-message'; import { OIDC_BASE_URL, SELECTORS, CLIENT_LIST_RESPONSE, SCOPE_LIST_RESPONSE, SCOPE_DATA_RESPONSE, PROVIDER_LIST_RESPONSE, PROVIDER_DATA_RESPONSE, clearRecord, overrideCapabilities, overrideMirageResponse, } from 'vault/tests/helpers/oidc-config'; const searchSelect = create(ss); const flashMessage = create(fm); // OIDC_BASE_URL = '/vault/access/oidc' module('Acceptance | oidc-config providers and scopes', function (hooks) { setupApplicationTest(hooks); setupMirage(hooks); hooks.before(function () { ENV['ember-cli-mirage'].handler = 'oidcConfig'; }); hooks.beforeEach(async function () { this.store = await this.owner.lookup('service:store'); // mock client list so OIDC BASE URL does not redirect to landing call-to-action image this.server.get('/identity/oidc/client', () => overrideMirageResponse(null, CLIENT_LIST_RESPONSE)); return authPage.login(); }); hooks.afterEach(function () { return logout.visit(); }); hooks.after(function () { ENV['ember-cli-mirage'].handler = null; }); // LIST SCOPES EMPTY test('it navigates to scopes list view and renders empty state when no scopes are configured', async function (assert) { assert.expect(4); this.server.get('/identity/oidc/scope', () => overrideMirageResponse(404)); await visit(OIDC_BASE_URL); await click('[data-test-tab="scopes"]'); assert.strictEqual(currentURL(), '/vault/access/oidc/scopes'); assert.dom('[data-test-tab="scopes"]').hasClass('active', 'scopes tab is active'); assert .dom(SELECTORS.scopeEmptyState) .hasText( `No scopes yet Use scope to define identity information about the authenticated user. Learn more. Create scope`, 'renders empty state no scopes are configured' ); assert .dom(SELECTORS.scopeCreateButtonEmptyState) .hasAttribute('href', '/ui/vault/access/oidc/scopes/create', 'empty state renders create scope link'); }); // LIST SCOPE EXIST test('it renders scope list when scopes exist', async function (assert) { assert.expect(11); this.server.get('/identity/oidc/scope', () => overrideMirageResponse(null, SCOPE_LIST_RESPONSE)); this.server.get('/identity/oidc/scope/test-scope', () => overrideMirageResponse(null, SCOPE_DATA_RESPONSE) ); await visit(OIDC_BASE_URL + '/scopes'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.index', 'redirects to scopes index route when scopes exist' ); assert .dom('[data-test-oidc-scope-linked-block="test-scope"]') .exists('displays linked block for test scope'); // navigates to/from create, edit, detail views from list view await click(SELECTORS.scopeCreateButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.create', 'scope index toolbar navigates to create form' ); await click(SELECTORS.scopeCancelButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.index', 'create form navigates back to index on cancel' ); await click('[data-test-popup-menu-trigger]'); await click('[data-test-oidc-scope-menu-link="edit"]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.edit', 'linked block popup menu navigates to edit' ); await click(SELECTORS.scopeCancelButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.details', 'scope edit form navigates back to details on cancel' ); // navigate to details from index page await click('[data-test-breadcrumb-link="oidc-scopes"]'); await click('[data-test-popup-menu-trigger]'); await click('[data-test-oidc-scope-menu-link="details"]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.details', 'popup menu navigates to details' ); // check that details tab has all the information assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'details tab is active'); assert.dom(SELECTORS.scopeDeleteButton).exists('toolbar renders delete option'); assert.dom(SELECTORS.scopeEditButton).exists('toolbar renders edit button'); assert.strictEqual(findAll('[data-test-component="info-table-row"]').length, 2, 'renders all info rows'); }); // ERROR DELETING SCOPE test('it throws error when trying to delete when scope is currently being associated with any provider', async function (assert) { assert.expect(3); this.server.get('/identity/oidc/scope', () => overrideMirageResponse(null, SCOPE_LIST_RESPONSE)); this.server.get('/identity/oidc/scope/test-scope', () => overrideMirageResponse(null, SCOPE_DATA_RESPONSE) ); this.server.get('/identity/oidc/provider', () => overrideMirageResponse(null, PROVIDER_LIST_RESPONSE)); this.server.get('/identity/oidc/provider/test-provider', () => { overrideMirageResponse(null, PROVIDER_DATA_RESPONSE); }); // throw error when trying to delete test-scope since it is associated to test-provider this.server.delete( '/identity/oidc/scope/test-scope', () => ({ errors: [ 'unable to delete scope "test-scope" because it is currently referenced by these providers: test-provider', ], }), 400 ); await visit(OIDC_BASE_URL + '/scopes'); await click('[data-test-oidc-scope-linked-block="test-scope"]'); assert.dom('[data-test-oidc-scope-header]').hasText('test-scope', 'renders scope name'); assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'details tab is active'); // try to delete scope await click(SELECTORS.scopeDeleteButton); await click(SELECTORS.confirmActionButton); assert.strictEqual( flashMessage.latestMessage, 'unable to delete scope "test-scope" because it is currently referenced by these providers: test-provider', 'renders error flash upon scope deletion' ); }); // CRUD SCOPE + CRUD PROVIDER test('it creates a scope, and creates a provider with that scope', async function (assert) { assert.expect(28); //* clear out test state await clearRecord(this.store, 'oidc/scope', 'test-scope'); await clearRecord(this.store, 'oidc/provider', 'test-provider'); // create a new scope await visit(OIDC_BASE_URL + '/scopes/create'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.create', 'navigates to create form' ); await fillIn('[data-test-input="name"]', 'test-scope'); await fillIn('[data-test-input="description"]', 'this is a test'); await fillIn('[data-test-component="code-mirror-modifier"] textarea', SCOPE_DATA_RESPONSE.template); await click(SELECTORS.scopeSaveButton); assert.strictEqual( flashMessage.latestMessage, 'Successfully created the scope test-scope.', 'renders success flash upon scope creation' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.details', 'navigates to scope detail view after save' ); assert.dom(SELECTORS.scopeDetailsTab).hasClass('active', 'scope details tab is active'); assert.dom('[data-test-value-div="Name"]').hasText('test-scope', 'has correct created name'); assert .dom('[data-test-value-div="Description"]') .hasText('this is a test', 'has correct created description'); // edit scope await click(SELECTORS.scopeEditButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.edit', 'navigates to edit page from details' ); await fillIn('[data-test-input="description"]', 'this is an edit test'); await click(SELECTORS.scopeSaveButton); assert.strictEqual( flashMessage.latestMessage, 'Successfully updated the scope test-scope.', 'renders success flash upon scope updating' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.scope.details', 'navigates back to scope details on update' ); assert .dom('[data-test-value-div="Description"]') .hasText('this is an edit test', 'has correct edited description'); // create a provider using test-scope await click('[data-test-breadcrumb-link="oidc-scopes"]'); await click('[data-test-tab="providers"]'); assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); await click('[data-test-oidc-provider-create]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.create', 'navigates to provider create form' ); await fillIn('[data-test-input="name"]', 'test-provider'); await clickTrigger('#scopesSupported'); await selectChoose('#scopesSupported', 'test-scope'); await click(SELECTORS.providerSaveButton); assert.strictEqual( flashMessage.latestMessage, 'Successfully created the OIDC provider test-provider.', 'renders success flash upon provider creation' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.provider.details', 'navigates to provider detail view after save' ); // assert default values in details view are correct assert.dom('[data-test-value-div="Issuer URL"]').hasTextContaining('http://', 'issuer includes scheme'); assert .dom('[data-test-value-div="Issuer URL"]') .hasTextContaining('identity/oidc/provider/test', 'issuer path populates correctly'); assert .dom('[data-test-value-div="Scopes"] a') .hasAttribute('href', '/ui/vault/access/oidc/scopes/test-scope/details', 'lists scopes as links'); // check provider's application list view await click(SELECTORS.providerClientsTab); assert.strictEqual( findAll('[data-test-oidc-client-linked-block]').length, 2, 'all applications appear in provider applications tab' ); // edit and limit applications await click(SELECTORS.providerDetailsTab); await click(SELECTORS.providerEditButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.provider.edit', 'navigates to provider edit page from details' ); await click('[data-test-oidc-radio="limited"]'); await click('[data-test-component="search-select"]#allowedClientIds .ember-basic-dropdown-trigger'); await fillIn('.ember-power-select-search input', 'test-app'); await searchSelect.options.objectAt(0).click(); await click(SELECTORS.providerSaveButton); assert.strictEqual( flashMessage.latestMessage, 'Successfully updated the OIDC provider test-provider.', 'renders success flash upon provider updating' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.provider.details', 'navigates back to provider details after updating' ); const providerModel = this.store.peekRecord('oidc/provider', 'test-provider'); assert.propEqual( providerModel.allowedClientIds, ['whaT7KB0C3iBH1l3rXhd5HPf0n6vXU0s'], 'provider saves client_id (not id or name) in allowed_client_ids param' ); await click(SELECTORS.providerClientsTab); assert .dom('[data-test-oidc-client-linked-block]') .hasTextContaining('test-app', 'list of applications is just test-app'); // edit back to allow all await click(SELECTORS.providerDetailsTab); await click(SELECTORS.providerEditButton); await click('[data-test-oidc-radio="allow-all"]'); await click(SELECTORS.providerSaveButton); await click(SELECTORS.providerClientsTab); assert.strictEqual( findAll('[data-test-oidc-client-linked-block]').length, 2, 'all applications appear in provider applications tab' ); // delete await click(SELECTORS.providerDetailsTab); await click(SELECTORS.providerDeleteButton); await click(SELECTORS.confirmActionButton); assert.strictEqual( flashMessage.latestMessage, 'Provider deleted successfully', 'success flash message renders after deleting provider' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.index', 'navigates back to list view after delete' ); // delete scope await visit(OIDC_BASE_URL + '/scopes/test-scope/details'); await click(SELECTORS.scopeDeleteButton); await click(SELECTORS.confirmActionButton); assert.strictEqual( flashMessage.latestMessage, 'Scope deleted successfully', 'renders success flash upon deleting scope' ); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.scopes.index', 'navigates back to list view after delete' ); }); // LIST PROVIDERS test('it lists default provider and navigates to details', async function (assert) { assert.expect(7); await visit(OIDC_BASE_URL); await click('[data-test-tab="providers"]'); assert.dom('[data-test-tab="providers"]').hasClass('active', 'providers tab is active'); assert.strictEqual(currentURL(), '/vault/access/oidc/providers'); assert .dom('[data-test-oidc-provider-linked-block="default"]') .exists('index page lists default provider'); await click('[data-test-popup-menu-trigger]'); await click('[data-test-oidc-provider-menu-link="edit"]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.provider.edit', 'provider linked block popup menu navigates to edit' ); await click(SELECTORS.providerCancelButton); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.provider.details', 'provider edit form navigates back to details on cancel' ); // navigate to details from index page await click('[data-test-breadcrumb-link="oidc-providers"]'); assert.strictEqual( currentRouteName(), 'vault.cluster.access.oidc.providers.index', 'providers breadcrumb navigates back to list view' ); await click('[data-test-oidc-provider-linked-block="default"] [data-test-popup-menu-trigger]'); await click('[data-test-oidc-provider-menu-link="details"]'); assert.dom(SELECTORS.providerDeleteButton).isDisabled('delete button is disabled for default provider'); }); // PROVIDER DELETE + EDIT PERMISSIONS test('it hides delete and edit for a provider when no permission', async function (assert) { assert.expect(3); this.server.get('/identity/oidc/providers', () => overrideMirageResponse(null, { providers: ['test-provider'] }) ); this.server.get('/identity/oidc/provider/test-provider', () => overrideMirageResponse(null, { allowed_client_ids: ['*'], issuer: 'http://127.0.0.1:8200/v1/identity/oidc/provider/test-provider', scopes_supported: ['test-scope'], }) ); this.server.post('/sys/capabilities-self', () => overrideCapabilities(OIDC_BASE_URL + '/provider/test-provider', ['read']) ); await visit(OIDC_BASE_URL + '/providers'); await click('[data-test-oidc-provider-linked-block]'); assert.dom(SELECTORS.providerDetailsTab).hasClass('active', 'details tab is active'); assert.dom(SELECTORS.providerDeleteButton).doesNotExist('delete option is hidden'); assert.dom(SELECTORS.providerEditButton).doesNotExist('edit button is hidden'); }); });