Skip to content

Commit

Permalink
test: project insights service test (#6661)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Mar 22, 2024
1 parent f5a7cc9 commit 86f229a
Show file tree
Hide file tree
Showing 12 changed files with 438 additions and 41 deletions.
20 changes: 18 additions & 2 deletions src/lib/features/feature-toggle/fakes/fake-feature-toggle-store.ts
Expand Up @@ -323,10 +323,26 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
throw new Error('Method not implemented.');
}

getFeatureTypeCounts(
async getFeatureTypeCounts(
params: IFeatureProjectUserParams,
): Promise<IFeatureTypeCount[]> {
throw new Error('Method not implemented.');
const typeCounts = this.features.reduce(
(acc, feature) => {
if (!feature.type) {
return acc;
}

if (!acc[feature.type]) {
acc[feature.type] = { type: feature.type, count: 0 };
}
acc[feature.type].count += 1;

return acc;
},
{} as Record<string, IFeatureTypeCount>,
);

return Object.values(typeCounts);
}

setCreatedByUserId(batchSize: number): Promise<number | undefined> {
Expand Down
15 changes: 10 additions & 5 deletions src/lib/features/project-insights/createProjectInsightsService.ts
Expand Up @@ -53,17 +53,14 @@ export const createProjectInsightsService = (
);
};

export const createFakeProjectInsightsService = (
config: IUnleashConfig,
): ProjectInsightsService => {
export const createFakeProjectInsightsService = () => {
const projectStore = new FakeProjectStore();
const featureToggleStore = new FakeFeatureToggleStore();
const featureTypeStore = new FakeFeatureTypeStore();
const projectStatsStore = new FakeProjectStatsStore();
const featureStrategiesStore = new FakeFeatureStrategiesStore();
const projectInsightsReadModel = new FakeProjectInsightsReadModel();

return new ProjectInsightsService(
const projectInsightsService = new ProjectInsightsService(
{
projectStore,
featureToggleStore,
Expand All @@ -73,4 +70,12 @@ export const createFakeProjectInsightsService = (
},
projectInsightsReadModel,
);

return {
projectInsightsService,
projectStatsStore,
featureToggleStore,
projectStore,
projectInsightsReadModel,
};
};
Expand Up @@ -8,12 +8,18 @@ const changeRequestCounts: ChangeRequestCounts = {
approved: 0,
applied: 0,
rejected: 0,
reviewRequired: 10,
reviewRequired: 0,
scheduled: 0,
};

export class FakeProjectInsightsReadModel implements IProjectInsightsReadModel {
private counts: Record<string, ChangeRequestCounts> = {};

async getChangeRequests(projectId: string): Promise<ChangeRequestCounts> {
return changeRequestCounts;
return this.counts[projectId] ?? changeRequestCounts;
}

async setChangeRequests(projectId: string, counts: ChangeRequestCounts) {
this.counts[projectId] = counts;
}
}
296 changes: 296 additions & 0 deletions src/lib/features/project-insights/project-insights-service.e2e.test.ts
@@ -0,0 +1,296 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import type FeatureToggleService from '../../../lib/features/feature-toggle/feature-toggle-service';
import type ProjectService from '../../../lib/features/project/project-service';
import { createTestConfig } from '../../../test/config/test-config';
import {
EventService,
type ProjectInsightsService,
} from '../../../lib/services';
import { FeatureEnvironmentEvent } from '../../../lib/types/events';
import { subDays } from 'date-fns';
import {
createFeatureToggleService,
createProjectService,
} from '../../../lib/features';
import type { IUnleashStores, IUser } from '../../../lib/types';
import type { User } from '../../../lib/server-impl';
import { createProjectInsightsService } from './createProjectInsightsService';

let stores: IUnleashStores;
let db: ITestDb;

let projectService: ProjectService;
let projectInsightsService: ProjectInsightsService;
let eventService: EventService;
let featureToggleService: FeatureToggleService;
let user: User; // many methods in this test use User instead of IUser
let opsUser: IUser;

beforeAll(async () => {
db = await dbInit('project_service_serial', getLogger);
stores = db.stores;
// @ts-ignore return type IUser type missing generateImageUrl
user = await stores.userStore.insert({
name: 'Some Name',
email: 'test@getunleash.io',
});
opsUser = await stores.userStore.insert({
name: 'Test user',
email: 'test@example.com',
});
const config = createTestConfig({
getLogger,
});
eventService = new EventService(stores, config);

featureToggleService = createFeatureToggleService(db.rawDatabase, config);

projectService = createProjectService(db.rawDatabase, config);

projectInsightsService = createProjectInsightsService(
db.rawDatabase,
config,
);
});

afterAll(async () => {
await db.destroy();
});

afterEach(async () => {
await stores.eventStore.deleteAll();
});

const updateFeature = async (featureName: string, update: any) => {
return db.rawDatabase
.table('features')
.update(update)
.where({ name: featureName });
};

test('should return average time to production per toggle', async () => {
const project = {
id: 'average-time-to-prod-per-toggle',
name: 'average-time-to-prod-per-toggle',
mode: 'open' as const,
defaultStickiness: 'clientId',
};

await projectService.createProject(project, user);

const toggles = [
{ name: 'average-prod-time-pt', subdays: 7 },
{ name: 'average-prod-time-pt-2', subdays: 14 },
{ name: 'average-prod-time-pt-3', subdays: 40 },
{ name: 'average-prod-time-pt-4', subdays: 15 },
{ name: 'average-prod-time-pt-5', subdays: 2 },
];

const featureToggles = await Promise.all(
toggles.map((toggle) => {
return featureToggleService.createFeatureToggle(
project.id,
toggle,
user.email,
opsUser.id,
);
}),
);

await Promise.all(
featureToggles.map((toggle) => {
return eventService.storeEvent(
new FeatureEnvironmentEvent({
enabled: true,
project: project.id,
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: opsUser.id,
}),
);
}),
);

await Promise.all(
toggles.map((toggle) =>
updateFeature(toggle.name, {
created_at: subDays(new Date(), toggle.subdays),
}),
),
);

const result = await projectInsightsService.getDoraMetrics(project.id);

expect(result.features).toHaveLength(5);
expect(result.features[0].timeToProduction).toBeTruthy();
expect(result.projectAverage).toBeTruthy();
});

test('should return average time to production per toggle for a specific project', async () => {
const project1 = {
id: 'average-time-to-prod-per-toggle-1',
name: 'Project 1',
mode: 'open' as const,
defaultStickiness: 'clientId',
};

const project2 = {
id: 'average-time-to-prod-per-toggle-2',
name: 'Project 2',
mode: 'open' as const,
defaultStickiness: 'clientId',
};

await projectService.createProject(project1, user);
await projectService.createProject(project2, user);

const togglesProject1 = [
{ name: 'average-prod-time-pt-10', subdays: 7 },
{ name: 'average-prod-time-pt-11', subdays: 14 },
{ name: 'average-prod-time-pt-12', subdays: 40 },
];

const togglesProject2 = [
{ name: 'average-prod-time-pt-13', subdays: 15 },
{ name: 'average-prod-time-pt-14', subdays: 2 },
];

const featureTogglesProject1 = await Promise.all(
togglesProject1.map((toggle) => {
return featureToggleService.createFeatureToggle(
project1.id,
toggle,
user.email,
opsUser.id,
);
}),
);

const featureTogglesProject2 = await Promise.all(
togglesProject2.map((toggle) => {
return featureToggleService.createFeatureToggle(
project2.id,
toggle,
user.email,
opsUser.id,
);
}),
);

await Promise.all(
featureTogglesProject1.map((toggle) => {
return eventService.storeEvent(
new FeatureEnvironmentEvent({
enabled: true,
project: project1.id,
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: opsUser.id,
}),
);
}),
);

await Promise.all(
featureTogglesProject2.map((toggle) => {
return eventService.storeEvent(
new FeatureEnvironmentEvent({
enabled: true,
project: project2.id,
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: opsUser.id,
}),
);
}),
);

await Promise.all(
togglesProject1.map((toggle) =>
updateFeature(toggle.name, {
created_at: subDays(new Date(), toggle.subdays),
}),
),
);

await Promise.all(
togglesProject2.map((toggle) =>
updateFeature(toggle.name, {
created_at: subDays(new Date(), toggle.subdays),
}),
),
);

const resultProject1 = await projectInsightsService.getDoraMetrics(
project1.id,
);
const resultProject2 = await projectInsightsService.getDoraMetrics(
project2.id,
);

expect(resultProject1.features).toHaveLength(3);
expect(resultProject2.features).toHaveLength(2);
});

test('should return average time to production per toggle and include archived toggles', async () => {
const project1 = {
id: 'average-time-to-prod-per-toggle-12',
name: 'Project 1',
mode: 'open' as const,
defaultStickiness: 'clientId',
};

await projectService.createProject(project1, user);

const togglesProject1 = [
{ name: 'average-prod-time-pta-10', subdays: 7 },
{ name: 'average-prod-time-pta-11', subdays: 14 },
{ name: 'average-prod-time-pta-12', subdays: 40 },
];

const featureTogglesProject1 = await Promise.all(
togglesProject1.map((toggle) => {
return featureToggleService.createFeatureToggle(
project1.id,
toggle,
user.email,
opsUser.id,
);
}),
);

await Promise.all(
featureTogglesProject1.map((toggle) => {
return eventService.storeEvent(
new FeatureEnvironmentEvent({
enabled: true,
project: project1.id,
featureName: toggle.name,
environment: 'default',
createdBy: 'Fredrik',
createdByUserId: opsUser.id,
}),
);
}),
);

await Promise.all(
togglesProject1.map((toggle) =>
updateFeature(toggle.name, {
created_at: subDays(new Date(), toggle.subdays),
}),
),
);

await featureToggleService.archiveToggle('average-prod-time-pta-12', user);

const resultProject1 = await projectInsightsService.getDoraMetrics(
project1.id,
);

expect(resultProject1.features).toHaveLength(3);
});

0 comments on commit 86f229a

Please sign in to comment.