mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-13 11:17:26 +02:00
Fixes a bug where `registerInstance` and `register{Frontend|Backend}Client` would overwrite each other's data in the instance service, leading to the bulk update being made with partial data, often missing SDK version. There's a different issue in the actual store that causes sdk version and type to be overwritten when it's updated (because we don't use `setLastSeen` anymore), but I'll handle that in a different PR. This PR adds tests for the changes I've made. Additionally, I've made these semi-related bonus changes: - In registerInstance, don't expect a partial `IClientApp`. We used to validate that it was actual a metrics object instead. Instead, update the signature to expect the actual properties we need from the cilent metrics schema and set a default for instanceId the way Joi did. - In `metrics.ts`, use the `ClientMetricsSchema` type in the function signature, so that the request body is correctly typed in the function (instead of being `any`). - Delete two unused properties from the`createApplicationSchema`. They would get ignored and were never used as far as I can tell. (`appName` is taken from the URL, and applications don't store `sdkVersion` information). - Add `sdkVersion` to `IClientApp` because it's used in instance service. I've been very confused about all the weird type shenanigans we do in the instance service (expecting `IClientApp`, then validating with a different Joi schema etc). I think this makes it a little bit better and updates the bits I'm touching, but I'm happy to take input if you disagree.
392 lines
12 KiB
TypeScript
392 lines
12 KiB
TypeScript
import ClientInstanceService from './instance-service.js';
|
|
import type { IClientApp } from '../../../types/model.js';
|
|
import FakeEventStore from '../../../../test/fixtures/fake-event-store.js';
|
|
import { createTestConfig } from '../../../../test/config/test-config.js';
|
|
import { FakePrivateProjectChecker } from '../../private-project/fakePrivateProjectChecker.js';
|
|
import type {
|
|
IClientApplicationsStore,
|
|
IUnleashConfig,
|
|
} from '../../../types/index.js';
|
|
import FakeClientMetricsStoreV2 from '../client-metrics/fake-client-metrics-store-v2.js';
|
|
import FakeStrategiesStore from '../../../../test/fixtures/fake-strategies-store.js';
|
|
import FakeFeatureToggleStore from '../../feature-toggle/fakes/fake-feature-toggle-store.js';
|
|
import type { IApplicationOverview } from './models.js';
|
|
|
|
import { vi } from 'vitest';
|
|
|
|
let config: IUnleashConfig;
|
|
beforeAll(() => {
|
|
config = createTestConfig({});
|
|
});
|
|
test('Multiple registrations of same appname and instanceid within same time period should only cause one registration', async () => {
|
|
const appStoreSpy = vi.fn();
|
|
const bulkSpy = vi.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
const clientMetrics = new ClientInstanceService(
|
|
{
|
|
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
|
strategyStore: new FakeStrategiesStore(),
|
|
featureToggleStore: new FakeFeatureToggleStore(),
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: new FakeEventStore(),
|
|
},
|
|
config,
|
|
new FakePrivateProjectChecker(),
|
|
);
|
|
const client1: IClientApp = {
|
|
appName: 'test_app',
|
|
instanceId: 'ava',
|
|
strategies: [{ name: 'defaullt' }],
|
|
started: new Date(),
|
|
interval: 10,
|
|
};
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
|
|
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
|
|
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(1);
|
|
expect(bulkSpy).toHaveBeenCalledTimes(1);
|
|
|
|
const registrations: IClientApp[] = appStoreSpy.mock
|
|
.calls[0][0] as IClientApp[];
|
|
|
|
expect(registrations.length).toBe(1);
|
|
expect(registrations[0].appName).toBe(client1.appName);
|
|
expect(registrations[0].instanceId).toBe(client1.instanceId);
|
|
expect(registrations[0].started).toBe(client1.started);
|
|
expect(registrations[0].interval).toBe(client1.interval);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
test('Multiple unique clients causes multiple registrations', async () => {
|
|
const appStoreSpy = vi.fn();
|
|
const bulkSpy = vi.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
|
|
const clientMetrics = new ClientInstanceService(
|
|
{
|
|
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
|
strategyStore: new FakeStrategiesStore(),
|
|
featureToggleStore: new FakeFeatureToggleStore(),
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: new FakeEventStore(),
|
|
},
|
|
config,
|
|
new FakePrivateProjectChecker(),
|
|
);
|
|
const client1 = {
|
|
appName: 'test_app',
|
|
instanceId: 'client1',
|
|
strategies: [{ name: 'defaullt' }],
|
|
started: new Date(),
|
|
interval: 10,
|
|
};
|
|
const client2 = {
|
|
appName: 'test_app_2',
|
|
instanceId: 'client2',
|
|
strategies: [{ name: 'defaullt' }],
|
|
started: new Date(),
|
|
interval: 10,
|
|
};
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client2, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client2, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client2, '127.0.0.1');
|
|
|
|
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
|
|
const registrations: IClientApp[] = appStoreSpy.mock
|
|
.calls[0][0] as IClientApp[];
|
|
|
|
expect(registrations.length).toBe(2);
|
|
});
|
|
|
|
test('Same client registered outside of dedup interval will be registered twice', async () => {
|
|
const appStoreSpy = vi.fn();
|
|
const bulkSpy = vi.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
|
|
const clientMetrics = new ClientInstanceService(
|
|
{
|
|
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
|
strategyStore: new FakeStrategiesStore(),
|
|
featureToggleStore: new FakeFeatureToggleStore(),
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: new FakeEventStore(),
|
|
},
|
|
config,
|
|
new FakePrivateProjectChecker(),
|
|
);
|
|
const client1 = {
|
|
appName: 'test_app',
|
|
instanceId: 'client1',
|
|
strategies: [{ name: 'defaullt' }],
|
|
started: new Date(),
|
|
interval: 10,
|
|
};
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
|
|
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
|
|
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
await clientMetrics.registerBackendClient(client1, '127.0.0.1');
|
|
|
|
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
|
|
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(2);
|
|
expect(bulkSpy).toHaveBeenCalledTimes(2);
|
|
|
|
const firstRegistrations = appStoreSpy.mock.calls[0][0][0];
|
|
const secondRegistrations = appStoreSpy.mock.calls[1][0][0];
|
|
|
|
expect(firstRegistrations.appName).toBe(secondRegistrations.appName);
|
|
expect(firstRegistrations.instanceId).toBe(secondRegistrations.instanceId);
|
|
});
|
|
|
|
test('No registrations during a time period will not call stores', async () => {
|
|
const appStoreSpy = vi.fn();
|
|
const bulkSpy = vi.fn();
|
|
const clientApplicationsStore: any = {
|
|
bulkUpsert: appStoreSpy,
|
|
};
|
|
const clientInstanceStore: any = {
|
|
bulkUpsert: bulkSpy,
|
|
};
|
|
const clientMetrics = new ClientInstanceService(
|
|
{
|
|
clientMetricsStoreV2: new FakeClientMetricsStoreV2(),
|
|
strategyStore: new FakeStrategiesStore(),
|
|
featureToggleStore: new FakeFeatureToggleStore(),
|
|
clientApplicationsStore,
|
|
clientInstanceStore,
|
|
eventStore: new FakeEventStore(),
|
|
},
|
|
config,
|
|
new FakePrivateProjectChecker(),
|
|
);
|
|
|
|
await clientMetrics.bulkAdd(); // in prod called by a SchedulerService
|
|
|
|
expect(appStoreSpy).toHaveBeenCalledTimes(0);
|
|
expect(bulkSpy).toHaveBeenCalledTimes(0);
|
|
});
|
|
|
|
test('filter out private projects from overview', async () => {
|
|
const clientApplicationsStore = {
|
|
async getApplicationOverview(
|
|
appName: string,
|
|
): Promise<IApplicationOverview> {
|
|
return {
|
|
environments: [
|
|
{
|
|
name: 'development',
|
|
instanceCount: 1,
|
|
sdks: ['unleash-client-node:3.5.1'],
|
|
backendSdks: ['unleash-client-node:3.5.1'],
|
|
frontendSdks: [],
|
|
lastSeen: new Date(),
|
|
issues: {
|
|
missingFeatures: [],
|
|
outdatedSdks: [],
|
|
},
|
|
},
|
|
],
|
|
projects: ['privateProject', 'publicProject'],
|
|
issues: {
|
|
missingStrategies: [],
|
|
},
|
|
featureCount: 0,
|
|
};
|
|
},
|
|
} as IClientApplicationsStore;
|
|
const privateProjectsChecker = {
|
|
async filterUserAccessibleProjects(
|
|
userId: number,
|
|
projects: string[],
|
|
): Promise<string[]> {
|
|
return projects.filter((project) => !project.includes('private'));
|
|
},
|
|
} as FakePrivateProjectChecker;
|
|
const clientInstanceService = new ClientInstanceService(
|
|
{ clientApplicationsStore } as any,
|
|
config,
|
|
privateProjectsChecker,
|
|
);
|
|
|
|
const overview = await clientInstanceService.getApplicationOverview(
|
|
'appName',
|
|
123,
|
|
);
|
|
|
|
expect(overview).toMatchObject({
|
|
environments: [
|
|
{
|
|
name: 'development',
|
|
instanceCount: 1,
|
|
sdks: ['unleash-client-node:3.5.1'],
|
|
issues: {
|
|
missingFeatures: [],
|
|
outdatedSdks: ['unleash-client-node:3.5.1'],
|
|
},
|
|
},
|
|
],
|
|
projects: ['publicProject'],
|
|
issues: {
|
|
missingStrategies: [],
|
|
},
|
|
featureCount: 0,
|
|
});
|
|
});
|
|
|
|
test('`registerInstance` sets `instanceId` to `default` if it is not provided', async () => {
|
|
const instanceService = new ClientInstanceService(
|
|
{} as any,
|
|
config,
|
|
{} as any,
|
|
);
|
|
|
|
await instanceService.registerInstance(
|
|
{
|
|
appName: 'appName',
|
|
environment: '',
|
|
},
|
|
'::1',
|
|
);
|
|
|
|
expect(instanceService.seenClients.appName_default).toMatchObject({
|
|
appName: 'appName',
|
|
instanceId: 'default',
|
|
});
|
|
});
|
|
|
|
describe('upserting into `seenClients`', () => {
|
|
test('registerInstance merges its data', async () => {
|
|
const instanceService = new ClientInstanceService(
|
|
{} as any,
|
|
config,
|
|
{} as any,
|
|
);
|
|
|
|
const client = {
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
};
|
|
|
|
const key = instanceService.clientKey(client);
|
|
|
|
instanceService.seenClients = {
|
|
[key]: { ...client, sdkVersion: 'my-sdk' },
|
|
};
|
|
|
|
await instanceService.registerInstance(
|
|
{
|
|
...client,
|
|
environment: 'blue',
|
|
},
|
|
'::1',
|
|
);
|
|
|
|
expect(instanceService.seenClients[key]).toMatchObject({
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
environment: 'blue',
|
|
sdkVersion: 'my-sdk',
|
|
});
|
|
});
|
|
test('registerBackendClient merges its data', async () => {
|
|
const instanceService = new ClientInstanceService(
|
|
{} as any,
|
|
config,
|
|
{} as any,
|
|
);
|
|
|
|
const client = {
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
};
|
|
|
|
const key = instanceService.clientKey(client);
|
|
|
|
instanceService.seenClients = {
|
|
[key]: { ...client, environment: 'blue' },
|
|
};
|
|
|
|
await instanceService.registerBackendClient(
|
|
{
|
|
...client,
|
|
sdkVersion: 'my-sdk',
|
|
started: new Date(),
|
|
interval: 5,
|
|
},
|
|
'::1',
|
|
);
|
|
|
|
expect(instanceService.seenClients[key]).toMatchObject({
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
environment: 'blue',
|
|
sdkVersion: 'my-sdk',
|
|
});
|
|
});
|
|
test('registerFrontendClient merges its data', async () => {
|
|
const instanceService = new ClientInstanceService(
|
|
{} as any,
|
|
config,
|
|
{} as any,
|
|
);
|
|
|
|
const client = {
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
};
|
|
|
|
const key = instanceService.clientKey(client);
|
|
|
|
instanceService.seenClients = {
|
|
[key]: { ...client, metricsCount: 10 },
|
|
};
|
|
|
|
instanceService.registerFrontendClient({
|
|
...client,
|
|
sdkVersion: 'my-sdk',
|
|
sdkType: 'frontend',
|
|
environment: 'black',
|
|
});
|
|
|
|
expect(instanceService.seenClients[key]).toMatchObject({
|
|
appName: 'appName',
|
|
instanceId: 'instanceId',
|
|
sdkVersion: 'my-sdk',
|
|
sdkType: 'frontend',
|
|
environment: 'black',
|
|
metricsCount: 10,
|
|
});
|
|
});
|
|
});
|