diff --git a/src/lib/domain/project-health/project-health.test.ts b/src/lib/domain/project-health/project-health.test.ts new file mode 100644 index 0000000000..fa678c3773 --- /dev/null +++ b/src/lib/domain/project-health/project-health.test.ts @@ -0,0 +1,203 @@ +import { subDays } from 'date-fns'; +import type { IFeatureType } from 'lib/types/stores/feature-type-store'; +import { + calculateProjectHealth, + calculateHealthRating, +} from './project-health'; + +const exampleFeatureTypes: IFeatureType[] = [ + { + id: 'default', + name: 'Default', + description: '', + lifetimeDays: 30, + }, + { + id: 'short-lived', + name: 'Short lived', + description: '', + lifetimeDays: 7, + }, + { + id: 'non-expiring', + name: 'Long lived', + description: '', + lifetimeDays: null, + }, +]; + +describe('calculateProjectHealth', () => { + it('works with empty features', () => { + expect(calculateProjectHealth([], exampleFeatureTypes)).toEqual({ + activeCount: 0, + staleCount: 0, + potentiallyStaleCount: 0, + }); + }); + + it('counts active toggles', () => { + const features = [{ stale: false }, {}]; + + expect(calculateProjectHealth(features, exampleFeatureTypes)).toEqual({ + activeCount: 2, + staleCount: 0, + potentiallyStaleCount: 0, + }); + }); + + it('counts stale toggles', () => { + const features = [{ stale: true }, { stale: false }, {}]; + + expect(calculateProjectHealth(features, exampleFeatureTypes)).toEqual({ + activeCount: 2, + staleCount: 1, + potentiallyStaleCount: 0, + }); + }); + + it('takes feature type into account when calculating potentially stale toggles', () => { + expect( + calculateProjectHealth( + [ + { + stale: false, + createdAt: subDays(Date.now(), 15), + type: 'default', + }, + ], + exampleFeatureTypes, + ), + ).toEqual({ + activeCount: 1, + staleCount: 0, + potentiallyStaleCount: 0, + }); + + expect( + calculateProjectHealth( + [ + { + stale: false, + createdAt: subDays(Date.now(), 31), + type: 'default', + }, + ], + exampleFeatureTypes, + ), + ).toEqual({ + activeCount: 1, + staleCount: 0, + potentiallyStaleCount: 1, + }); + + expect( + calculateProjectHealth( + [ + { + stale: false, + createdAt: subDays(Date.now(), 15), + type: 'short-lived', + }, + ], + exampleFeatureTypes, + ), + ).toEqual({ + activeCount: 1, + staleCount: 0, + potentiallyStaleCount: 1, + }); + }); + + it("doesn't count stale toggles as potentially stale or stale as active", () => { + const features = [ + { + stale: true, + createdAt: subDays(Date.now(), 31), + type: 'default', + }, + { + stale: false, + createdAt: subDays(Date.now(), 31), + type: 'default', + }, + ]; + + expect(calculateProjectHealth(features, exampleFeatureTypes)).toEqual({ + activeCount: 1, + staleCount: 1, + potentiallyStaleCount: 1, + }); + }); + + it('counts non-expiring types properly', () => { + const features = [ + { + createdAt: subDays(Date.now(), 366), + type: 'non-expiring', + }, + { + createdAt: subDays(Date.now(), 366), + type: 'default', + }, + ]; + + expect(calculateProjectHealth(features, exampleFeatureTypes)).toEqual({ + activeCount: 2, + staleCount: 0, + potentiallyStaleCount: 1, + }); + }); +}); + +describe('calculateHealthRating', () => { + it('works with empty feature toggles', () => { + expect(calculateHealthRating([], exampleFeatureTypes)).toEqual(100); + }); + + it('works with stale and active feature toggles', () => { + expect( + calculateHealthRating( + [{ stale: true }, { stale: true }], + exampleFeatureTypes, + ), + ).toEqual(0); + expect( + calculateHealthRating( + [{ stale: true }, { stale: false }], + exampleFeatureTypes, + ), + ).toEqual(50); + expect( + calculateHealthRating( + [{ stale: false }, { stale: true }, { stale: false }], + exampleFeatureTypes, + ), + ).toEqual(67); + }); + + it('counts potentially stale toggles', () => { + expect( + calculateHealthRating( + [ + { createdAt: subDays(Date.now(), 1), type: 'default' }, + { + stale: true, + createdAt: subDays(Date.now(), 1), + type: 'default', + }, + { + stale: true, + createdAt: subDays(Date.now(), 31), + type: 'default', + }, + { + stale: false, + createdAt: subDays(Date.now(), 31), + type: 'default', + }, + ], + exampleFeatureTypes, + ), + ).toEqual(25); + }); +}); diff --git a/src/lib/domain/calculate-project-health/calculate-project-health.ts b/src/lib/domain/project-health/project-health.ts similarity index 82% rename from src/lib/domain/calculate-project-health/calculate-project-health.ts rename to src/lib/domain/project-health/project-health.ts index 36a00defd3..bd2299b889 100644 --- a/src/lib/domain/calculate-project-health/calculate-project-health.ts +++ b/src/lib/domain/project-health/project-health.ts @@ -2,12 +2,14 @@ import { hoursToMilliseconds } from 'date-fns'; import type { IProjectHealthReport } from 'lib/types'; import type { IFeatureType } from 'lib/types/stores/feature-type-store'; +type IPartialFeatures = Array<{ + stale?: boolean; + createdAt?: Date; + type?: string; +}>; + const getPotentiallyStaleCount = ( - features: Array<{ - stale?: boolean; - createdAt?: Date; - type?: string; - }>, + features: IPartialFeatures, featureTypes: IFeatureType[], ) => { const today = new Date().valueOf(); @@ -20,17 +22,14 @@ const getPotentiallyStaleCount = ( return ( !feature.stale && + featureTypeExpectedLifetime !== null && diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24) ); }).length; }; export const calculateProjectHealth = ( - features: Array<{ - stale?: boolean; - createdAt?: Date; - type?: string; - }>, + features: IPartialFeatures, featureTypes: IFeatureType[], ): Pick< IProjectHealthReport, @@ -41,12 +40,8 @@ export const calculateProjectHealth = ( staleCount: features.filter((f) => f.stale).length, }); -export const getHealthRating = ( - features: Array<{ - stale?: boolean; - createdAt?: Date; - type?: string; - }>, +export const calculateHealthRating = ( + features: IPartialFeatures, featureTypes: IFeatureType[], ): number => { const { potentiallyStaleCount, activeCount, staleCount } = diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index f28737774b..ede477bc85 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -11,8 +11,8 @@ import type { IProjectStore } from '../types/stores/project-store'; import ProjectService from './project-service'; import { calculateProjectHealth, - getHealthRating, -} from '../domain/calculate-project-health/calculate-project-health'; + calculateHealthRating, +} from '../domain/project-health/project-health'; export default class ProjectHealthService { private logger: Logger; @@ -82,7 +82,7 @@ export default class ProjectHealthService { archived: false, }); - return getHealthRating(toggles, this.featureTypes); + return calculateHealthRating(toggles, this.featureTypes); } async setHealthRating(): Promise {