mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-13 13:48:59 +02:00
test: project insights service test (#6661)
This commit is contained in:
parent
f5a7cc9125
commit
86f229a69d
@ -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> {
|
||||
|
@ -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,
|
||||
@ -73,4 +70,12 @@ export const createFakeProjectInsightsService = (
|
||||
},
|
||||
projectInsightsReadModel,
|
||||
);
|
||||
|
||||
return {
|
||||
projectInsightsService,
|
||||
projectStatsStore,
|
||||
featureToggleStore,
|
||||
projectStore,
|
||||
projectInsightsReadModel,
|
||||
};
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,0 +1,70 @@
|
||||
import { createFakeProjectInsightsService } from './createProjectInsightsService';
|
||||
|
||||
test('Return basic insights', async () => {
|
||||
const {
|
||||
projectInsightsService,
|
||||
projectStatsStore,
|
||||
featureToggleStore,
|
||||
projectStore,
|
||||
projectInsightsReadModel,
|
||||
} = createFakeProjectInsightsService();
|
||||
await featureToggleStore.create('default', {
|
||||
name: 'irrelevant',
|
||||
createdByUserId: 1,
|
||||
type: 'release',
|
||||
});
|
||||
await projectInsightsReadModel.setChangeRequests('default', {
|
||||
total: 5,
|
||||
approved: 1,
|
||||
applied: 1,
|
||||
rejected: 1,
|
||||
reviewRequired: 1,
|
||||
scheduled: 1,
|
||||
});
|
||||
await projectStore.create({
|
||||
id: 'default',
|
||||
name: 'irrelevant',
|
||||
});
|
||||
await projectStatsStore.updateProjectStats('default', {
|
||||
archivedCurrentWindow: 1,
|
||||
archivedPastWindow: 1,
|
||||
createdCurrentWindow: 1,
|
||||
createdPastWindow: 1,
|
||||
avgTimeToProdCurrentWindow: 1,
|
||||
projectActivityCurrentWindow: 1,
|
||||
projectActivityPastWindow: 1,
|
||||
projectMembersAddedCurrentWindow: 1,
|
||||
});
|
||||
|
||||
const insights = await projectInsightsService.getProjectInsights('default');
|
||||
|
||||
expect(insights).toEqual({
|
||||
stats: {
|
||||
archivedCurrentWindow: 1,
|
||||
archivedPastWindow: 1,
|
||||
createdCurrentWindow: 1,
|
||||
createdPastWindow: 1,
|
||||
avgTimeToProdCurrentWindow: 1,
|
||||
projectActivityCurrentWindow: 1,
|
||||
projectActivityPastWindow: 1,
|
||||
projectMembersAddedCurrentWindow: 1,
|
||||
},
|
||||
featureTypeCounts: [{ type: 'release', count: 1 }],
|
||||
health: {
|
||||
activeCount: 0,
|
||||
potentiallyStaleCount: 0,
|
||||
staleCount: 0,
|
||||
rating: 100,
|
||||
},
|
||||
leadTime: { features: [], projectAverage: 0 },
|
||||
changeRequests: {
|
||||
total: 5,
|
||||
approved: 1,
|
||||
applied: 1,
|
||||
rejected: 1,
|
||||
reviewRequired: 1,
|
||||
scheduled: 1,
|
||||
},
|
||||
members: { currentMembers: 0, change: 0 },
|
||||
});
|
||||
});
|
@ -54,9 +54,7 @@ export class ProjectInsightsService {
|
||||
this.projectInsightsReadModel = projectInsightsReadModel;
|
||||
}
|
||||
|
||||
private async getDoraMetrics(
|
||||
projectId: string,
|
||||
): Promise<ProjectDoraMetricsSchema> {
|
||||
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
||||
const activeFeatureToggles = (
|
||||
await this.featureToggleStore.getAll({ project: projectId })
|
||||
).map((feature) => feature.name);
|
||||
|
@ -118,6 +118,7 @@ export default class ProjectController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
/** @deprecated use project insights instead */
|
||||
this.route({
|
||||
method: 'get',
|
||||
path: '/:projectId/dora',
|
||||
@ -245,6 +246,7 @@ export default class ProjectController extends Controller {
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated use projectInsights instead */
|
||||
async getProjectDora(
|
||||
req: IAuthRequest,
|
||||
res: Response<ProjectDoraMetricsSchema>,
|
||||
|
@ -1,31 +1,31 @@
|
||||
import dbInit, { type ITestDb } from '../helpers/database-init';
|
||||
import getLogger from '../../fixtures/no-logger';
|
||||
import type FeatureToggleService from '../../../lib/features/feature-toggle/feature-toggle-service';
|
||||
import type ProjectService from '../../../lib/features/project/project-service';
|
||||
import type { AccessService } from '../../../lib/services/access-service';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../../../lib/types/permissions';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import { RoleName } from '../../../lib/types/model';
|
||||
import { randomId } from '../../../lib/util/random-id';
|
||||
import EnvironmentService from '../../../lib/features/project-environments/environment-service';
|
||||
import IncompatibleProjectError from '../../../lib/error/incompatible-project-error';
|
||||
import { EventService } from '../../../lib/services';
|
||||
import { FeatureEnvironmentEvent } from '../../../lib/types/events';
|
||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import type FeatureToggleService from '../feature-toggle/feature-toggle-service';
|
||||
import type ProjectService from './project-service';
|
||||
import type { AccessService } from '../../services/access-service';
|
||||
import { MOVE_FEATURE_TOGGLE } from '../../types/permissions';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { RoleName } from '../../types/model';
|
||||
import { randomId } from '../../util/random-id';
|
||||
import EnvironmentService from '../project-environments/environment-service';
|
||||
import IncompatibleProjectError from '../../error/incompatible-project-error';
|
||||
import { EventService } from '../../services';
|
||||
import { FeatureEnvironmentEvent } from '../../types/events';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import {
|
||||
createAccessService,
|
||||
createFeatureToggleService,
|
||||
createProjectService,
|
||||
} from '../../../lib/features';
|
||||
} from '../index';
|
||||
import {
|
||||
type IGroup,
|
||||
type IUnleashStores,
|
||||
type IUser,
|
||||
SYSTEM_USER,
|
||||
SYSTEM_USER_ID,
|
||||
} from '../../../lib/types';
|
||||
import type { User } from '../../../lib/server-impl';
|
||||
import { InvalidOperationError } from '../../../lib/error';
|
||||
} from '../../types';
|
||||
import type { User } from '../../server-impl';
|
||||
import { InvalidOperationError } from '../../error';
|
||||
|
||||
let stores: IUnleashStores;
|
||||
let db: ITestDb;
|
@ -945,6 +945,7 @@ export default class ProjectService {
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated use projectInsightsService instead */
|
||||
async getDoraMetrics(projectId: string): Promise<ProjectDoraMetricsSchema> {
|
||||
const activeFeatureToggles = (
|
||||
await this.featureToggleStore.getAll({ project: projectId })
|
||||
|
@ -270,7 +270,7 @@ export const createServices = (
|
||||
: createFakeProjectService(config);
|
||||
const projectInsightsService = db
|
||||
? createProjectInsightsService(db, config)
|
||||
: createFakeProjectInsightsService(config);
|
||||
: createFakeProjectInsightsService().projectInsightsService;
|
||||
|
||||
const projectHealthService = new ProjectHealthService(
|
||||
stores,
|
||||
|
21
src/test/fixtures/fake-project-stats-store.ts
vendored
21
src/test/fixtures/fake-project-stats-store.ts
vendored
@ -3,25 +3,28 @@ import type {
|
||||
ICreateEnabledDates,
|
||||
IProjectStatsStore,
|
||||
} from '../../lib/types/stores/project-stats-store-type';
|
||||
import type { DoraFeaturesSchema } from '../../lib/openapi';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
export default class FakeProjectStatsStore implements IProjectStatsStore {
|
||||
updateProjectStats(
|
||||
private stats: Record<string, IProjectStats> = {};
|
||||
|
||||
async updateProjectStats(
|
||||
projectId: string,
|
||||
status: IProjectStats,
|
||||
stats: IProjectStats,
|
||||
): Promise<void> {
|
||||
throw new Error('not implemented');
|
||||
this.stats[projectId] = stats;
|
||||
}
|
||||
|
||||
getProjectStats(projectId: string): Promise<IProjectStats> {
|
||||
throw new Error('not implemented');
|
||||
async getProjectStats(projectId: string): Promise<IProjectStats> {
|
||||
return this.stats[projectId];
|
||||
}
|
||||
|
||||
getTimeToProdDates(): Promise<ICreateEnabledDates[]> {
|
||||
throw new Error('not implemented');
|
||||
async getTimeToProdDates(): Promise<ICreateEnabledDates[]> {
|
||||
return [];
|
||||
}
|
||||
|
||||
getTimeToProdDatesForFeatureToggles(): Promise<any> {
|
||||
throw new Error('not implemented');
|
||||
async getTimeToProdDatesForFeatureToggles(): Promise<DoraFeaturesSchema[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
4
src/test/fixtures/fake-project-store.ts
vendored
4
src/test/fixtures/fake-project-store.ts
vendored
@ -166,13 +166,13 @@ export default class FakeProjectStore implements IProjectStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getMembersCountByProjectAfterDate(
|
||||
async getMembersCountByProjectAfterDate(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
projectId: string,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
date: string,
|
||||
): Promise<number> {
|
||||
throw new Error('Method not implemented');
|
||||
return 0;
|
||||
}
|
||||
|
||||
updateDefaultStrategy(
|
||||
|
Loading…
Reference in New Issue
Block a user