From 0a250a7526c3840746e7dbde55834436124cab05 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 8 Nov 2024 11:29:17 +0100 Subject: [PATCH] tests: add more tests for the lifecycle avg calculation query (#8698) This PR adds more tests to check a few more cases for the lifecycle calculation query. Specifically, it tests that: - If we don't have any data for a stage, we return `null`. - We filter on projects - It correctly takes `0` days into account when calculating averages. --- ...oject-lifecycle-summary-read-model.test.ts | 124 +++++++++++++++--- .../project-lifecycle-summary-read-model.ts | 16 +-- website/docusaurus.config.ts | 2 +- 3 files changed, 118 insertions(+), 24 deletions(-) diff --git a/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts b/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts index a77b894e20..62acab9111 100644 --- a/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts +++ b/src/lib/features/project-status/project-lifecycle-summary-read-model.test.ts @@ -1,4 +1,4 @@ -import { addDays } from 'date-fns'; +import { addDays, addMinutes } from 'date-fns'; import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import getLogger from '../../../test/fixtures/no-logger'; import { ProjectLifecycleSummaryReadModel } from './project-lifecycle-summary-read-model'; @@ -17,6 +17,12 @@ afterAll(async () => { } }); +afterEach(async () => { + await db.stores.projectStore.deleteAll(); + await db.stores.featureToggleStore.deleteAll(); + await db.stores.featureLifecycleStore.deleteAll(); +}); + const updateFeatureStageDate = async ( flagName: string, stage: string, @@ -30,22 +36,22 @@ const updateFeatureStageDate = async ( describe('Average time calculation', () => { test('it calculates the average time for each stage', async () => { - const project1 = await db.stores.projectStore.create({ - name: 'project1', - id: 'project1', + const project = await db.stores.projectStore.create({ + name: 'project', + id: randomId(), }); const now = new Date(); const flags = [ { name: randomId(), offsets: [2, 5, 6, 10] }, { name: randomId(), offsets: [1, null, 4, 7] }, - { name: randomId(), offsets: [12, 25, 8, 9] }, + { name: randomId(), offsets: [12, 25, 0, 9] }, { name: randomId(), offsets: [1, 2, 3, null] }, ]; for (const { name, offsets } of flags) { const created = await db.stores.featureToggleStore.create( - project1.id, + project.id, { name, createdByUserId: 1, @@ -79,27 +85,115 @@ describe('Average time calculation', () => { await updateFeatureStageDate( created.name, stage, - addDays(now, offsetFromInitial), + addMinutes( + addDays(now, offsetFromInitial), + 1 * (index + 1), + ), ); } } const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); - const result = await readModel.getAverageTimeInEachStage(project1.id); + const result = await readModel.getAverageTimeInEachStage(project.id); expect(result).toMatchObject({ initial: 4, // (2 + 1 + 12 + 1) / 4 = 4 - 'pre-live': 9, // (5 + 25 + 2 + 4) / 4 = 9 - live: 6, // (6 + 8 + 3) / 3 ~= 5.67 ~= 6 + 'pre-live': 9, // (5 + 4 + 25 + 2) / 4 = 9 + live: 3, // (6 + 0 + 3) / 3 = 3 completed: 9, // (10 + 7 + 9) / 3 ~= 8.67 ~= 9 }); }); - test('it returns `null` if it has no data for something', async () => {}); - test('it rounds to the nearest whole number', async () => {}); - test('it ignores flags in other projects', async () => {}); - test('it ignores flags in other projects', async () => {}); + test('it returns `null` if it has no data for something', async () => { + const project = await db.stores.projectStore.create({ + name: 'project', + id: randomId(), + }); + const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); - test("it ignores rows that don't have a next stage", async () => {}); + const result1 = await readModel.getAverageTimeInEachStage(project.id); + + expect(result1).toMatchObject({ + initial: null, + 'pre-live': null, + live: null, + completed: null, + }); + + const flag = await db.stores.featureToggleStore.create(project.id, { + name: randomId(), + createdByUserId: 1, + }); + await db.stores.featureLifecycleStore.insert([ + { + feature: flag.name, + stage: 'initial', + }, + ]); + + await db.stores.featureLifecycleStore.insert([ + { + feature: flag.name, + stage: 'pre-live', + }, + ]); + + await updateFeatureStageDate( + flag.name, + 'pre-live', + addDays(new Date(), 5), + ); + + const result2 = await readModel.getAverageTimeInEachStage(project.id); + + expect(result2).toMatchObject({ + initial: 5, + 'pre-live': null, + live: null, + completed: null, + }); + }); + + test('it ignores flags in other projects', async () => { + const project = await db.stores.projectStore.create({ + name: 'project', + id: randomId(), + }); + const readModel = new ProjectLifecycleSummaryReadModel(db.rawDatabase); + + const flag = await db.stores.featureToggleStore.create(project.id, { + name: randomId(), + createdByUserId: 1, + }); + await db.stores.featureLifecycleStore.insert([ + { + feature: flag.name, + stage: 'initial', + }, + ]); + + await db.stores.featureLifecycleStore.insert([ + { + feature: flag.name, + stage: 'pre-live', + }, + ]); + + await updateFeatureStageDate( + flag.name, + 'pre-live', + addDays(new Date(), 5), + ); + + const result = + await readModel.getAverageTimeInEachStage('some-other-project'); + + expect(result).toMatchObject({ + initial: null, + 'pre-live': null, + live: null, + completed: null, + }); + }); }); diff --git a/src/lib/features/project-status/project-lifecycle-summary-read-model.ts b/src/lib/features/project-status/project-lifecycle-summary-read-model.ts index b4bf1050ba..b37f3561ce 100644 --- a/src/lib/features/project-status/project-lifecycle-summary-read-model.ts +++ b/src/lib/features/project-status/project-lifecycle-summary-read-model.ts @@ -38,10 +38,10 @@ export class ProjectLifecycleSummaryReadModel } async getAverageTimeInEachStage(projectId: string): Promise<{ - initial: number; - 'pre-live': number; - live: number; - completed: number; + initial: number | null; + 'pre-live': number | null; + live: number | null; + completed: number | null; }> { const q = this.db .with( @@ -80,10 +80,10 @@ export class ProjectLifecycleSummaryReadModel return acc; }, { - initial: 0, - 'pre-live': 0, - live: 0, - completed: 0, + initial: null, + 'pre-live': null, + live: null, + completed: null, }, ); } diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index 39f98d0c40..1ed84a3389 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -294,7 +294,7 @@ const config: Config = { routeBasePath: '/', remarkPlugins: [[pluginNpm2Yarn, { sync: true }]], docItemComponent: '@theme/ApiItem', - sidebarPath: './sidebars.ts' + sidebarPath: './sidebars.ts', }, theme: { customCss: './src/css/custom.css',