/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: BUSL-1.1 */ import { module, test } from 'qunit'; import { setupRenderingTest } from 'vault/tests/helpers'; import { click, find, findAll, render } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; import { setupMirage } from 'ember-cli-mirage/test-support'; import { ACTIVITY_RESPONSE_STUB } from 'vault/tests/helpers/clients/client-count-helpers'; import { CLIENT_COUNT, FILTERS } from 'vault/tests/helpers/clients/client-count-selectors'; import { GENERAL } from 'vault/tests/helpers/general-selectors'; import sinon from 'sinon'; import { ClientFilters, flattenMounts } from 'core/utils/client-count-utils'; import { parseAPITimestamp } from 'core/utils/date-formatters'; module('Integration | Component | clients/page/overview', function (hooks) { setupRenderingTest(hooks); setupMirage(hooks); hooks.beforeEach(async function () { this.server.get('sys/internal/counters/activity', () => { return { request_id: 'some-activity-id', data: ACTIVITY_RESPONSE_STUB, }; }); this.store = this.owner.lookup('service:store'); this.activity = await this.store.queryRecord('clients/activity', {}); this.mostRecentMonth = this.activity.byMonth[this.activity.byMonth.length - 1]; this.onFilterChange = sinon.spy(); this.filterQueryParams = { namespace_path: '', mount_path: '', mount_type: '', month: '' }; this.renderComponent = () => render(hbs` `); this.assertTableData = async (assert, filterKey, filterValue) => { const expectedData = flattenMounts(this.activity.byNamespace).filter( (d) => d[filterKey] === filterValue ); // Find all rendered rows and assert they satisfy the filter value and table data matches expected values const rows = findAll(GENERAL.tableRow()); rows.forEach((_, idx) => { assert.dom(GENERAL.tableData(idx, filterKey)).hasText(filterValue); // Get namespace and mount paths to find original data in expectedData const rowMountPath = find(GENERAL.tableData(idx, 'mount_path')).innerText; const rowNsPath = find(GENERAL.tableData(idx, 'namespace_path')).innerText; // find the expected clients from the response and assert the table matches const { clients: expectedClients } = expectedData.find( (d) => d.mount_path === rowMountPath && d.namespace_path === rowNsPath ); assert.dom(GENERAL.tableData(idx, 'clients')).hasText(`${expectedClients}`); }); }; }); test('it hides attribution when there is no data', async function (assert) { // Stub activity response when there's no activity data this.server.get('sys/internal/counters/activity', () => { return { request_id: 'some-activity-id', data: { by_namespace: [], end_time: '2024-08-31T23:59:59Z', months: [], start_time: '2024-01-01T00:00:00Z', total: { distinct_entities: 0, entity_clients: 0, non_entity_tokens: 0, non_entity_clients: 0, clients: 0, secret_syncs: 0, }, }, }; }); this.activity = await this.store.queryRecord('clients/activity', {}); await this.renderComponent(); assert.dom(CLIENT_COUNT.card('Client attribution')).doesNotExist('it does not render attribution card'); }); test('it initially renders attribution with by_namespace data', async function (assert) { await this.renderComponent(); const topNamespace = this.activity.byNamespace[0]; const topMount = topNamespace.mounts[0]; // Assert table renders namespace with the highest counts at the top assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(topNamespace.label); assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${topMount.clients}`); }); test('it renders dropdown lists from activity response to filter table data', async function (assert) { const expectedMonths = this.activity.byMonth .map((m) => parseAPITimestamp(m.timestamp, 'MMMM yyyy')) .reverse(); const mounts = flattenMounts(this.activity.byNamespace); const expectedNamespaces = [...new Set(mounts.map((m) => m.namespace_path))]; const expectedMountPaths = [...new Set(mounts.map((m) => m.mount_path))]; const expectedMountTypes = [...new Set(mounts.map((m) => m.mount_type))]; await this.renderComponent(); // Select each filter await click(FILTERS.dropdownToggle(ClientFilters.MONTH)); findAll(`${FILTERS.dropdown(ClientFilters.MONTH)} li button`).forEach((item, idx) => { const expected = expectedMonths[idx]; assert.dom(item).hasText(expected, `month dropdown renders: ${expected}`); }); await click(FILTERS.dropdownToggle(ClientFilters.NAMESPACE)); findAll(`${FILTERS.dropdown(ClientFilters.NAMESPACE)} li button`).forEach((item, idx) => { const expected = expectedNamespaces[idx]; assert.dom(item).hasText(expected, `namespace dropdown renders: ${expected}`); }); await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_PATH)); findAll(`${FILTERS.dropdown(ClientFilters.MOUNT_PATH)} li button`).forEach((item, idx) => { const expected = expectedMountPaths[idx]; assert.dom(item).hasText(expected, `mount_path dropdown renders: ${expected}`); }); await click(FILTERS.dropdownToggle(ClientFilters.MOUNT_TYPE)); findAll(`${FILTERS.dropdown(ClientFilters.MOUNT_TYPE)} li button`).forEach((item, idx) => { const expected = expectedMountTypes[idx]; assert.dom(item).hasText(expected, `mount_type dropdown renders: ${expected}`); }); }); // * FILTERING ASSERTIONS // Filtering tests are split between integration and acceptance tests // because changing filters updates the URL query params test('it shows correct empty state message when selected month has no data', async function (assert) { this.filterQueryParams[ClientFilters.MONTH] = '2023-06-01T00:00:00Z'; await this.renderComponent(); assert .dom(CLIENT_COUNT.card('table empty state')) .hasText('No data found Clear or change filters to view client count data. Client count documentation'); }); test('it filters data if @filterQueryParams specify a month', async function (assert) { const filterKey = 'month'; const filterValue = this.mostRecentMonth.timestamp; this.filterQueryParams[filterKey] = filterValue; await this.renderComponent(); // Drill down to new_clients then grab the first mount const sortedMounts = flattenMounts(this.mostRecentMonth.new_clients.namespaces).sort( (a, b) => b.clients - a.clients ); const topMount = sortedMounts[0]; assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(topMount.namespace_path); assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${topMount.clients}`); assert.dom(GENERAL.tableData(0, 'mount_path')).hasText(topMount.mount_path); }); test('it filters data if @filterQueryParams specify a namespace_path', async function (assert) { const filterKey = 'namespace_path'; const filterValue = 'ns1/'; this.filterQueryParams[filterKey] = filterValue; await this.renderComponent(); await this.assertTableData(assert, filterKey, filterValue); }); test('it filters data if @filterQueryParams specify a mount_path', async function (assert) { const filterKey = 'mount_path'; const filterValue = 'acme/pki/0/'; this.filterQueryParams[filterKey] = filterValue; await this.renderComponent(); await this.assertTableData(assert, filterKey, filterValue); }); test('it filters data if @filterQueryParams specify a mount_type', async function (assert) { const filterKey = 'mount_type'; const filterValue = 'kv'; this.filterQueryParams[filterKey] = filterValue; await this.renderComponent(); await this.assertTableData(assert, filterKey, filterValue); }); test('it filters data if @filterQueryParams specify a multiple filters', async function (assert) { this.filterQueryParams = { month: this.mostRecentMonth.timestamp, namespace_path: 'ns1/', mount_path: 'auth/userpass/0/', mount_type: 'userpass', }; const { namespace_path, mount_path, mount_type } = this.filterQueryParams; await this.renderComponent(); const expectedData = flattenMounts(this.mostRecentMonth.new_clients.namespaces).find( (d) => d.namespace_path === namespace_path && d.mount_path === mount_path && d.mount_type === mount_type ); assert.dom(GENERAL.tableRow()).exists({ count: 1 }); assert.dom(GENERAL.tableData(0, 'namespace_path')).hasText(expectedData.namespace_path); assert.dom(GENERAL.tableData(0, 'mount_path')).hasText(expectedData.mount_path); assert.dom(GENERAL.tableData(0, 'mount_type')).hasText(expectedData.mount_type); assert.dom(GENERAL.tableData(0, 'clients')).hasText(`${expectedData.clients}`); }); test('it renders empty state message when filter selections yield no results', async function (assert) { this.filterQueryParams = { namespace_path: 'dev/', mount_path: 'pluto/', mount_type: 'banana' }; await this.renderComponent(); assert .dom(CLIENT_COUNT.card('table empty state')) .hasText('No data found Clear or change filters to view client count data. Client count documentation'); }); });