mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01:00
Vitest Pros: * Automated failing test comments on github PRs * A nice local UI with incremental testing when changing files (`yarn test:ui`) * Also nicely supported in all major IDEs, click to run test works (so we won't miss what we had with jest). * Works well with ESM Vitest Cons: * The ESBuild transformer vitest uses takes a little longer to transform than our current SWC/jest setup, however, it is possible to setup SWC as the transformer for vitest as well (though it only does one transform, so we're paying ~7-10 seconds instead of ~ 2-3 seconds in transform phase). * Exposes how slow our tests are (tongue in cheek here)
217 lines
6.8 KiB
TypeScript
217 lines
6.8 KiB
TypeScript
import {
|
|
responseTimeMetrics,
|
|
storeRequestedRoute,
|
|
} from './response-time-metrics.js';
|
|
import { REQUEST_TIME } from '../metric-events.js';
|
|
import { vi } from 'vitest';
|
|
import type { IFlagResolver } from '../server-impl.js';
|
|
import EventEmitter from 'events';
|
|
|
|
const isDefined = async (timeInfo: any, limit = 10) => {
|
|
let counter = 0;
|
|
while (timeInfo === undefined) {
|
|
// Waiting for event to be triggered
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
counter++;
|
|
if (counter > limit) {
|
|
throw new Error('Event was not triggered');
|
|
}
|
|
}
|
|
};
|
|
|
|
const flagResolver = {
|
|
isEnabled: vi.fn(),
|
|
getAll: vi.fn(),
|
|
getVariant: vi.fn(),
|
|
getStaticContext: vi.fn(),
|
|
} as IFlagResolver;
|
|
|
|
// Make sure it's always cleaned up
|
|
let res: any;
|
|
beforeEach(() => {
|
|
res = {
|
|
statusCode: 200,
|
|
locals: {}, // res will always have locals (according to express RequestHandler type)
|
|
once: vi.fn((event: string, callback: () => void) => {
|
|
if (event === 'finish') {
|
|
callback();
|
|
}
|
|
}),
|
|
};
|
|
});
|
|
|
|
describe('responseTimeMetrics new behavior', () => {
|
|
const instanceStatsService = {
|
|
getAppCountSnapshot: vi.fn() as () => number | undefined,
|
|
};
|
|
const eventBus = new EventEmitter();
|
|
|
|
test('uses baseUrl and route path to report metrics with flag enabled, but no res.locals.route', async () => {
|
|
let timeInfo: any;
|
|
// register a listener
|
|
eventBus.on(REQUEST_TIME, (data) => {
|
|
timeInfo = data;
|
|
});
|
|
const middleware = responseTimeMetrics(
|
|
eventBus,
|
|
flagResolver,
|
|
instanceStatsService,
|
|
);
|
|
const req = {
|
|
baseUrl: '/api/admin',
|
|
route: {
|
|
path: '/features',
|
|
},
|
|
method: 'GET',
|
|
path: 'should-not-be-used',
|
|
headers: {},
|
|
};
|
|
|
|
// @ts-expect-error req doesn't have all properties and we're not passing next
|
|
middleware(req, res, () => {});
|
|
|
|
await isDefined(timeInfo);
|
|
expect(timeInfo).toMatchObject({
|
|
path: '/api/admin/features',
|
|
method: 'GET',
|
|
statusCode: 200,
|
|
time: expect.any(Number),
|
|
});
|
|
expect(timeInfo.time).toBeGreaterThan(0);
|
|
expect(res.once).toHaveBeenCalledWith('finish', expect.any(Function));
|
|
});
|
|
|
|
test('uses res.locals.route to report metrics when flag enabled', async () => {
|
|
let timeInfo: any;
|
|
// register a listener
|
|
eventBus.on(REQUEST_TIME, (data) => {
|
|
timeInfo = data;
|
|
});
|
|
const middleware = responseTimeMetrics(
|
|
eventBus,
|
|
flagResolver,
|
|
instanceStatsService,
|
|
);
|
|
const req = {
|
|
baseUrl: '/api/admin',
|
|
route: {
|
|
path: '/features',
|
|
},
|
|
method: 'GET',
|
|
path: 'should-not-be-used',
|
|
};
|
|
const reqWithoutRoute = {
|
|
method: 'GET',
|
|
headers: {},
|
|
};
|
|
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
storeRequestedRoute(req, res, () => {});
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
middleware(reqWithoutRoute, res, () => {});
|
|
|
|
await isDefined(timeInfo);
|
|
expect(timeInfo).toMatchObject({
|
|
path: '/api/admin/features',
|
|
method: 'GET',
|
|
statusCode: 200,
|
|
time: expect.any(Number),
|
|
});
|
|
expect(timeInfo.time).toBeGreaterThan(0);
|
|
expect(res.once).toHaveBeenCalledWith('finish', expect.any(Function));
|
|
});
|
|
|
|
test.each([undefined, '/'])(
|
|
'reports (hidden) when route is undefined and path is %s',
|
|
async (path: string) => {
|
|
let timeInfo: any;
|
|
// register a listener
|
|
eventBus.on(REQUEST_TIME, (data) => {
|
|
timeInfo = data;
|
|
});
|
|
const middleware = responseTimeMetrics(
|
|
eventBus,
|
|
flagResolver,
|
|
instanceStatsService,
|
|
);
|
|
const req = {
|
|
baseUrl: '/api/admin',
|
|
method: 'GET',
|
|
path: 'should-not-be-used',
|
|
};
|
|
const reqWithoutRoute = {
|
|
method: 'GET',
|
|
path,
|
|
headers: {},
|
|
};
|
|
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
storeRequestedRoute(req, res, () => {});
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
middleware(reqWithoutRoute, res, () => {});
|
|
|
|
await isDefined(timeInfo);
|
|
expect(timeInfo).toMatchObject({
|
|
path: '(hidden)',
|
|
method: 'GET',
|
|
statusCode: 200,
|
|
time: expect.any(Number),
|
|
});
|
|
expect(timeInfo.time).toBeGreaterThan(0);
|
|
expect(res.once).toHaveBeenCalledWith(
|
|
'finish',
|
|
expect.any(Function),
|
|
);
|
|
},
|
|
);
|
|
|
|
test.each([
|
|
['/api/admin/features', '/api/admin/(hidden)'],
|
|
['/api/admin/features/my-feature', '/api/admin/(hidden)'],
|
|
['/api/frontend/client/metrics', '/api/frontend/(hidden)'],
|
|
['/api/client/metrics', '/api/client/(hidden)'],
|
|
['/edge/validate', '/edge/(hidden)'],
|
|
['/whatever', '(hidden)'],
|
|
['/healthz', '(hidden)'],
|
|
['/internal-backstage/prometheus', '(hidden)'],
|
|
])(
|
|
'when path is %s and route is undefined, reports %s',
|
|
async (path: string, expected: string) => {
|
|
let timeInfo: any;
|
|
// register a listener
|
|
eventBus.on(REQUEST_TIME, (data) => {
|
|
timeInfo = data;
|
|
});
|
|
const middleware = responseTimeMetrics(
|
|
eventBus,
|
|
flagResolver,
|
|
instanceStatsService,
|
|
);
|
|
const req = {
|
|
baseUrl: '/api/admin',
|
|
method: 'GET',
|
|
path: 'should-not-be-used',
|
|
};
|
|
const reqWithoutRoute = {
|
|
method: 'GET',
|
|
path,
|
|
headers: {},
|
|
};
|
|
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
storeRequestedRoute(req, res, () => {});
|
|
// @ts-expect-error req and res doesn't have all properties
|
|
middleware(reqWithoutRoute, res, () => {});
|
|
|
|
await isDefined(timeInfo);
|
|
expect(timeInfo).toMatchObject({
|
|
path: expected,
|
|
time: expect.any(Number),
|
|
method: 'GET',
|
|
statusCode: 200,
|
|
});
|
|
expect(timeInfo.time).toBeGreaterThan(0);
|
|
},
|
|
);
|
|
});
|