1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-03-23 00:16:25 +01:00
unleash.unleash/src/lib/features/project-status/projects-status.e2e.test.ts
Thomas Heartman 04b2b488f6
chore(1-3133): change avg health to current health in project status (#8803)
This PR updates the project status service (and schemas and UI) to use
the project's current health instead of the 4-week average.

I nabbed the `calculateHealthRating` from
`src/lib/services/project-health-service.ts` instead of relying on the
service itself, because that service relies on the project service,
which relies on pretty much everything in the entire system.

However, I think we can split the health service into a service that
*does* need the project service (which is used for 1 of 3 methods) and a
service (or read model) that doesn't. We could then rely on the second
one for this service without too much overhead. Or we could extract the
`calculateHealthRating` into a shared function that takes its stores as
arguments. ... but I suggest doing that in a follow-up PR.

Because the calculation has been tested other places (especially if we
rely on a service / shared function for it), I've simplified the tests
to just verify that it's present.

I've changed the schema's `averageHealth` into an object in case we want
to include average health etc. in the future, but this is up for debate.
2024-11-20 11:41:45 +01:00

296 lines
8.0 KiB
TypeScript

import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import {
type IUnleashTest,
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
import {
FEATURE_CREATED,
type IUser,
RoleName,
type IAuditUser,
type IUnleashConfig,
} from '../../types';
import type { EventService } from '../../services';
import { createEventsService } from '../events/createEventsService';
import { createTestConfig } from '../../../test/config/test-config';
import { randomId } from '../../util';
import { ApiTokenType } from '../../types/models/api-token';
let app: IUnleashTest;
let db: ITestDb;
let eventService: EventService;
const TEST_USER_ID = -9999;
const config: IUnleashConfig = createTestConfig();
const insertHealthScore = (id: string, health: number) => {
const irrelevantFlagTrendDetails = {
total_flags: 10,
stale_flags: 10,
potentially_stale_flags: 10,
};
return db.rawDatabase('flag_trends').insert({
...irrelevantFlagTrendDetails,
id,
project: 'default',
health,
});
};
const getCurrentDateStrings = () => {
const today = new Date();
const todayString = today.toISOString().split('T')[0];
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const yesterdayString = yesterday.toISOString().split('T')[0];
return { todayString, yesterdayString };
};
beforeAll(async () => {
db = await dbInit('projects_status', getLogger);
app = await setupAppWithCustomConfig(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
},
},
},
db.rawDatabase,
);
eventService = createEventsService(db.rawDatabase, config);
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
beforeEach(async () => {
await db.stores.clientMetricsStoreV2.deleteAll();
await db.rawDatabase('flag_trends').delete();
});
test('project insights should return correct count for each day', async () => {
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'today-event' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'today-event-two' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'yesterday-event' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
const { events } = await eventService.getEvents();
const yesterdayEvent = events.find(
(e) => e.data.featureName === 'yesterday-event',
);
const { todayString, yesterdayString } = getCurrentDateStrings();
await db.rawDatabase.raw(`UPDATE events SET created_at = ? where id = ?`, [
yesterdayString,
yesterdayEvent?.id,
]);
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(body).toMatchObject({
activityCountByDate: [
{ date: yesterdayString, count: 1 },
{ date: todayString, count: 2 },
],
});
});
test('project resources should contain the right data', async () => {
const { body: noResourcesBody } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(noResourcesBody.resources).toMatchObject({
members: 0,
apiTokens: 0,
segments: 0,
});
const flagName = randomId();
await app.createFeature(flagName);
const environment = 'default';
await db.stores.clientMetricsStoreV2.batchInsertMetrics([
{
featureName: flagName,
appName: `web2`,
environment,
timestamp: new Date(),
yes: 5,
no: 2,
},
]);
await app.services.apiTokenService.createApiTokenWithProjects({
tokenName: 'test-token',
projects: ['default'],
type: ApiTokenType.CLIENT,
environment: 'default',
});
await app.services.segmentService.create(
{
name: 'test-segment',
project: 'default',
constraints: [],
},
{} as IAuditUser,
);
const admin = await app.services.userService.createUser({
username: 'admin',
rootRole: RoleName.ADMIN,
});
const user = await app.services.userService.createUser({
username: 'test-user',
rootRole: RoleName.EDITOR,
});
await app.services.projectService.addAccess('default', [4], [], [user.id], {
...admin,
ip: '',
} as IAuditUser);
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(body.resources).toMatchObject({
members: 1,
apiTokens: 1,
segments: 1,
});
});
test('project health contains the current health score', async () => {
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(body.health.current).toBe(100);
});
test('project status contains lifecycle data', async () => {
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(body.lifecycleSummary).toMatchObject({
initial: {
averageDays: null,
currentFlags: 0,
},
preLive: {
averageDays: null,
currentFlags: 0,
},
live: {
averageDays: null,
currentFlags: 0,
},
completed: {
averageDays: null,
currentFlags: 0,
},
archived: {
currentFlags: 0,
last30Days: 0,
},
});
});
test('project status includes stale flags', async () => {
const otherProject = await app.services.projectService.createProject(
{
name: 'otherProject',
id: randomId(),
},
{} as IUser,
{} as IAuditUser,
);
function cartesianProduct(...arrays: any[][]): any[][] {
return arrays.reduce(
(acc, array) => {
return acc.flatMap((accItem) =>
array.map((item) => [...accItem, item]),
);
},
[[]] as any[][],
);
}
// of all 16 (2^4) permutations, only 3 are unhealthy flags in a given project.
const combinations = cartesianProduct(
[false, true], // stale
[false, true], // potentially stale
[false, true], // archived
['default', otherProject.id], // project
);
for (const [stale, potentiallyStale, archived, project] of combinations) {
const name = `flag-${project}-stale-${stale}-potentially-stale-${potentiallyStale}-archived-${archived}`;
await app.createFeature(
{
name,
stale,
},
project,
);
if (potentiallyStale) {
await db
.rawDatabase('features')
.update('potentially_stale', true)
.where({ name });
}
if (archived) {
await app.archiveFeature(name, project);
}
}
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
expect(body.staleFlags).toMatchObject({
total: 3,
});
});