diff --git a/changelog/27352.txt b/changelog/27352.txt new file mode 100644 index 000000000000..70a7fa366126 --- /dev/null +++ b/changelog/27352.txt @@ -0,0 +1,3 @@ +```release-note:bug +ui: fix issue where a month without new clients breaks the client count dashboard +``` diff --git a/ui/lib/core/addon/utils/client-count-utils.ts b/ui/lib/core/addon/utils/client-count-utils.ts index a41b1d263102..69091f888ea1 100644 --- a/ui/lib/core/addon/utils/client-count-utils.ts +++ b/ui/lib/core/addon/utils/client-count-utils.ts @@ -86,7 +86,9 @@ export const formatDateObject = (dateObj: { monthIdx: number; year: number }, is return getUnixTime(utc); }; -export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => { +export const formatByMonths = ( + monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[] +) => { const sortedPayload = sortMonthsByTimestamp(monthsArray); return sortedPayload?.map((m) => { const month = parseAPITimestamp(m.timestamp, 'M/yy') as string; @@ -95,23 +97,28 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity if (m.counts) { const totalClientsByNamespace = formatByNamespace(m.namespaces); const newClientsByNamespace = formatByNamespace(m.new_clients?.namespaces); + + let newClients: ByMonthNewClients = { month, timestamp, namespaces: [] }; + if (m.new_clients?.counts) { + newClients = { + month, + timestamp, + ...destructureClientCounts(m?.new_clients?.counts), + namespaces: formatByNamespace(m.new_clients?.namespaces), + }; + } return { month, timestamp, ...destructureClientCounts(m.counts), - namespaces: formatByNamespace(m.namespaces) || [], + namespaces: formatByNamespace(m.namespaces), namespaces_by_key: namespaceArrayToObject( totalClientsByNamespace, newClientsByNamespace, month, m.timestamp ), - new_clients: { - month, - timestamp, - ...destructureClientCounts(m?.new_clients?.counts), - namespaces: formatByNamespace(m.new_clients?.namespaces) || [], - }, + new_clients: newClients, }; } // empty month @@ -125,7 +132,8 @@ export const formatByMonths = (monthsArray: ActivityMonthBlock[] | EmptyActivity }); }; -export const formatByNamespace = (namespaceArray: NamespaceObject[]) => { +export const formatByNamespace = (namespaceArray: NamespaceObject[] | null): ByNamespaceClients[] => { + if (!Array.isArray(namespaceArray)) return []; return namespaceArray.map((ns) => { // i.e. 'namespace_path' is an empty string for 'root', so use namespace_id const label = ns.namespace_path === '' ? ns.namespace_id : ns.namespace_path; @@ -158,7 +166,9 @@ export const destructureClientCounts = (verboseObject: Counts | ByNamespaceClien ); }; -export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyActivityMonthBlock[]) => { +export const sortMonthsByTimestamp = ( + monthsArray: (ActivityMonthBlock | EmptyActivityMonthBlock | NoNewClientsActivityMonthBlock)[] +) => { const sortedPayload = [...monthsArray]; return sortedPayload.sort((a, b) => compareAsc(parseAPITimestamp(a.timestamp) as Date, parseAPITimestamp(b.timestamp) as Date) @@ -168,7 +178,7 @@ export const sortMonthsByTimestamp = (monthsArray: ActivityMonthBlock[] | EmptyA export const namespaceArrayToObject = ( monthTotals: ByNamespaceClients[], // technically this arg (monthNew) is the same type as above, just nested inside monthly new clients - monthNew: ByMonthClients['new_clients']['namespaces'], + monthNew: ByMonthClients['new_clients']['namespaces'] | null, month: string, timestamp: string ) => { @@ -176,36 +186,45 @@ export const namespaceArrayToObject = ( // it's an object in each month data block where the keys are namespace paths // and values include new and total client counts for that namespace in that month const namespaces_by_key = monthTotals.reduce((nsObject: { [key: string]: NamespaceByKey }, ns) => { + const keyedNs: NamespaceByKey = { + ...destructureClientCounts(ns), + timestamp, + month, + mounts_by_key: {}, + new_clients: { + month, + timestamp, + label: ns.label, + mounts: [], + }, + }; const newNsClients = monthNew?.find((n) => n.label === ns.label); + // mounts_by_key is is used to filter further in a namespace and get monthly activity by mount + // it's an object inside the namespace block where the keys are mount paths + // and the values include new and total client counts for that mount in that month + keyedNs.mounts_by_key = ns.mounts.reduce( + (mountObj: { [key: string]: MountByKey }, mount) => { + const mountNewClients = newNsClients ? newNsClients.mounts.find((m) => m.label === mount.label) : {}; + mountObj[mount.label] = { + ...mount, + timestamp, + month, + new_clients: { + timestamp, + month, + label: mount.label, + ...mountNewClients, + }, + }; + + return mountObj; + }, + {} as { [key: string]: MountByKey } + ); if (newNsClients) { - // mounts_by_key is is used to filter further in a namespace and get monthly activity by mount - // it's an object inside the namespace block where the keys are mount paths - // and the values include new and total client counts for that mount in that month - const mounts_by_key = ns.mounts.reduce( - (mountObj: { [key: string]: MountByKey }, mount) => { - const newMountClients = newNsClients.mounts.find((m) => m.label === mount.label); - - if (newMountClients) { - mountObj[mount.label] = { - ...mount, - timestamp, - month, - new_clients: { month, timestamp, ...newMountClients }, - }; - } - return mountObj; - }, - {} as { [key: string]: MountByKey } - ); - - nsObject[ns.label] = { - ...destructureClientCounts(ns), - timestamp, - month, - new_clients: { month, timestamp, ...newNsClients }, - mounts_by_key, - }; + keyedNs.new_clients = { month, timestamp, ...newNsClients }; } + nsObject[ns.label] = keyedNs; return nsObject; }, {}); @@ -239,6 +258,15 @@ export interface TotalClients { acme_clients: number; } +// extend this type when the counts are optional (eg for new clients) +interface TotalClientsSometimes { + clients?: number; + entity_clients?: number; + non_entity_clients?: number; + secret_syncs?: number; + acme_clients?: number; +} + export interface ByNamespaceClients extends TotalClients { label: string; mounts: MountClients[]; @@ -255,7 +283,9 @@ export interface ByMonthClients extends TotalClients { namespaces_by_key: { [key: string]: NamespaceByKey }; new_clients: ByMonthNewClients; } -export interface ByMonthNewClients extends TotalClients { + +// clients numbers are only returned if month is of type ActivityMonthBlock +export interface ByMonthNewClients extends TotalClientsSometimes { month: string; timestamp: string; namespaces: ByNamespaceClients[]; @@ -268,7 +298,7 @@ export interface NamespaceByKey extends TotalClients { new_clients: NamespaceNewClients; } -export interface NamespaceNewClients extends TotalClients { +export interface NamespaceNewClients extends TotalClientsSometimes { month: string; timestamp: string; label: string; @@ -282,7 +312,7 @@ export interface MountByKey extends TotalClients { new_clients: MountNewClients; } -export interface MountNewClients extends TotalClients { +export interface MountNewClients extends TotalClientsSometimes { month: string; timestamp: string; label: string; @@ -308,6 +338,16 @@ export interface ActivityMonthBlock { }; } +export interface NoNewClientsActivityMonthBlock { + timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month) + counts: Counts; + namespaces: NamespaceObject[]; + new_clients: { + counts: null; + namespaces: null; + }; +} + export interface EmptyActivityMonthBlock { timestamp: string; // YYYY-MM-01T00:00:00Z (always the first day of the month) counts: null; diff --git a/ui/tests/helpers/clients/client-count-helpers.js b/ui/tests/helpers/clients/client-count-helpers.js index 374b3669fece..d433c9f09597 100644 --- a/ui/tests/helpers/clients/client-count-helpers.js +++ b/ui/tests/helpers/clients/client-count-helpers.js @@ -40,7 +40,7 @@ export function assertBarChart(assert, chartName, byMonthData, isStacked = false } export const ACTIVITY_RESPONSE_STUB = { - start_time: '2023-08-01T00:00:00Z', + start_time: '2023-06-01T00:00:00Z', end_time: '2023-09-30T23:59:59Z', // is always the last day and hour of the month queried by_namespace: [ { @@ -148,11 +148,209 @@ export const ACTIVITY_RESPONSE_STUB = { ], months: [ { - timestamp: '2023-08-01T00:00:00Z', + timestamp: '2023-06-01T00:00:00Z', counts: null, namespaces: null, new_clients: null, }, + { + timestamp: '2023-07-01T00:00:00Z', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + new_clients: { + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + }, + }, + { + timestamp: '2023-08-01T00:00:00Z', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + namespaces: [ + { + namespace_id: 'root', + namespace_path: '', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + distinct_entities: 100, + non_entity_tokens: 100, + }, + mounts: [ + { + mount_path: 'pki-engine-0', + counts: { + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'auth/authid/0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + { + mount_path: 'kvv2-engine-0', + counts: { + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + distinct_entities: 0, + non_entity_tokens: 0, + }, + }, + ], + }, + ], + new_clients: { + counts: null, + namespaces: null, + }, + }, { timestamp: '2023-09-01T00:00:00Z', counts: { @@ -646,10 +844,323 @@ export const SERIALIZED_ACTIVITY_RESPONSE = { ], by_month: [ { - month: '8/23', - timestamp: '2023-08-01T00:00:00Z', + month: '6/23', + timestamp: '2023-06-01T00:00:00Z', namespaces: [], namespaces_by_key: {}, + new_clients: { + month: '6/23', + timestamp: '2023-06-01T00:00:00Z', + namespaces: [], + }, + }, + { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + namespaces_by_key: { + root: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + mounts_by_key: { + 'pki-engine-0': { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + }, + 'kvv2-engine-0': { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + timestamp: '2023-07-01T00:00:00Z', + month: '7/23', + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + }, + }, + }, + }, + new_clients: { + month: '7/23', + timestamp: '2023-07-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + }, + }, + { + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + namespaces: [ + { + label: 'root', + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + mounts: [ + { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + }, + { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + }, + { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + }, + ], + }, + ], + namespaces_by_key: { + root: { + acme_clients: 100, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 100, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + label: 'root', + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + mounts: [], + }, + mounts_by_key: { + 'pki-engine-0': { + label: 'pki-engine-0', + acme_clients: 100, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 0, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + label: 'pki-engine-0', + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + }, + }, + 'auth/authid/0': { + label: 'auth/authid/0', + acme_clients: 0, + clients: 100, + entity_clients: 100, + non_entity_clients: 100, + secret_syncs: 0, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + label: 'auth/authid/0', + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + }, + }, + 'kvv2-engine-0': { + label: 'kvv2-engine-0', + acme_clients: 0, + clients: 100, + entity_clients: 0, + non_entity_clients: 0, + secret_syncs: 100, + timestamp: '2023-08-01T00:00:00Z', + month: '8/23', + new_clients: { + label: 'kvv2-engine-0', + month: '8/23', + timestamp: '2023-08-01T00:00:00Z', + }, + }, + }, + }, + }, new_clients: { month: '8/23', timestamp: '2023-08-01T00:00:00Z', diff --git a/ui/tests/integration/components/dashboard/client-count-card-test.js b/ui/tests/integration/components/dashboard/client-count-card-test.js index de4a81f07d08..388de5097e20 100644 --- a/ui/tests/integration/components/dashboard/client-count-card-test.js +++ b/ui/tests/integration/components/dashboard/client-count-card-test.js @@ -43,7 +43,7 @@ module('Integration | Component | dashboard/client-count-card', function (hooks) assert .dom(CLIENT_COUNT.statText('Total')) .hasText( - `Total The number of clients in this billing period (Aug 2023 - Sep 2023). ${formatNumber([ + `Total The number of clients in this billing period (Jun 2023 - Sep 2023). ${formatNumber([ total.clients, ])}` ); diff --git a/ui/tests/integration/utils/client-count-utils-test.js b/ui/tests/integration/utils/client-count-utils-test.js index 24c9f85d339f..14444f8d9229 100644 --- a/ui/tests/integration/utils/client-count-utils-test.js +++ b/ui/tests/integration/utils/client-count-utils-test.js @@ -129,27 +129,29 @@ module('Integration | Util | client count utils', function (hooks) { }); test('formatByMonths: it formats the months array', async function (assert) { - assert.expect(5); + assert.expect(9); const original = [...RESPONSE.months]; - const [formattedNoData, formattedWithActivity] = formatByMonths(RESPONSE.months); + const [formattedNoData, formattedWithActivity, formattedNoNew] = formatByMonths(RESPONSE.months); // instead of asserting the whole expected response, broken up so tests are easier to debug // but kept whole above to copy/paste updated response expectations in the future - const [expectedNoData, expectedWithActivity] = SERIALIZED_ACTIVITY_RESPONSE.by_month; - const { namespaces, new_clients } = expectedWithActivity; + const [expectedNoData, expectedWithActivity, expectedNoNew] = SERIALIZED_ACTIVITY_RESPONSE.by_month; assert.propEqual(formattedNoData, expectedNoData, 'it formats months without data'); - assert.propEqual( - formattedWithActivity.namespaces, - namespaces, - 'it formats namespaces array for months with data' - ); - assert.propEqual( - formattedWithActivity.new_clients, - new_clients, - 'it formats new_clients block for months with data' - ); + ['namespaces', 'new_clients', 'namespaces_by_key'].forEach((key) => { + assert.propEqual( + formattedWithActivity[key], + expectedWithActivity[key], + `it formats ${key} array for months with data` + ); + assert.propEqual( + formattedNoNew[key], + expectedNoNew[key], + `it formats the ${key} array for months with no new clients` + ); + }); + assert.propEqual(RESPONSE.months, original, 'it does not modify original months array'); assert.propEqual(formatByMonths([]), [], 'it returns an empty array if the months key is empty'); }); @@ -187,7 +189,7 @@ module('Integration | Util | client count utils', function (hooks) { test('sortMonthsByTimestamp: sorts timestamps chronologically, oldest to most recent', async function (assert) { assert.expect(2); // API returns them in order so this test is extra extra - const unOrdered = [RESPONSE.months[1], RESPONSE.months[0]]; // mixup order + const unOrdered = [RESPONSE.months[1], RESPONSE.months[0], RESPONSE.months[3], RESPONSE.months[2]]; // mixup order const original = [...RESPONSE.months]; const expected = RESPONSE.months; assert.propEqual(sortMonthsByTimestamp(unOrdered), expected); @@ -195,32 +197,30 @@ module('Integration | Util | client count utils', function (hooks) { }); test('namespaceArrayToObject: it returns namespaces_by_key and mounts_by_key', async function (assert) { - assert.expect(5); - - // month at 0-index has no data so use second month in array, empty month format covered by formatByMonths test above - const original = { ...RESPONSE.months[1] }; - const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[1].namespaces_by_key; - const formattedTotal = formatByNamespace(RESPONSE.months[1].namespaces); - - const testObject = namespaceArrayToObject( - formattedTotal, - formatByNamespace(RESPONSE.months[1].new_clients.namespaces), - '9/23', - '2023-09-01T00:00:00Z' - ); + // namespaceArrayToObject only called when there are counts, so skip month 0 which has no counts + for (let i = 1; i < RESPONSE.months.length; i++) { + const original = { ...RESPONSE.months[i] }; + const expectedObject = SERIALIZED_ACTIVITY_RESPONSE.by_month[i].namespaces_by_key; + const formattedTotal = formatByNamespace(RESPONSE.months[i].namespaces); + const testObject = namespaceArrayToObject( + formattedTotal, + formatByNamespace(RESPONSE.months[i].new_clients.namespaces), + `${i + 6}/23`, + original.timestamp + ); + const { root } = testObject; + const { root: expectedRoot } = expectedObject; - const { root } = testObject; - const { root: expectedRoot } = expectedObject; - assert.propEqual(root.new_clients, expectedRoot.new_clients, 'it formats namespaces new_clients'); - assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); - assert.propContains(root, expectedRoot, 'namespace has correct keys'); + assert.propEqual( + root?.new_clients, + expectedRoot?.new_clients, + `it formats namespaces new_clients for ${original.timestamp}` + ); + assert.propEqual(root.mounts_by_key, expectedRoot.mounts_by_key, 'it formats namespaces mounts_by_key'); + assert.propContains(root, expectedRoot, 'namespace has correct keys'); - assert.propEqual( - namespaceArrayToObject(formattedTotal, formatByNamespace([]), '9/23', '2023-09-01T00:00:00Z'), - {}, - 'returns an empty object when there are no new clients ' - ); - assert.propEqual(RESPONSE.months[1], original, 'it does not modify original month data'); + assert.propEqual(RESPONSE.months[i], original, 'it does not modify original month data'); + } }); // TESTS FOR COMBINED ACTIVITY DATA - no mount attribution < 1.10