Skip to content

Commit 105c91d

Browse files
author
Artem
committed
#RI-4489 filters + UTests + reworks
1 parent ceb2482 commit 105c91d

19 files changed

+732
-145
lines changed

redisinsight/api/config/default.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export default {
194194
},
195195
],
196196
connections: {
197-
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000 // 30 sec
197+
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000, // 30 sec
198198
},
199199
redisStack: {
200200
id: process.env.BUILD_TYPE === 'REDIS_STACK' ? process.env.REDIS_STACK_DATABASE_ID || 'redis-stack' : undefined,

redisinsight/api/src/__mocks__/common.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export const mockRepository = jest.fn(() => ({
6161
save: jest.fn(),
6262
insert: jest.fn(),
6363
update: jest.fn(),
64+
upsert: jest.fn(),
6465
delete: jest.fn(),
6566
remove: jest.fn(),
6667
createQueryBuilder: mockCreateQueryBuilder,

redisinsight/api/src/__mocks__/feature.ts

Lines changed: 127 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';
22
import {
33
FeatureConfig,
4-
FeatureConfigFilter,
4+
FeatureConfigFilter, FeatureConfigFilterAnd, FeatureConfigFilterOr,
55
FeaturesConfig,
66
FeaturesConfigData,
77
} from 'src/modules/feature/model/features-config';
88
import { classToClass } from 'src/utils';
9+
import { Feature } from 'src/modules/feature/model/feature';
10+
import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
11+
import { mockAppSettings } from 'src/__mocks__/app-settings';
12+
import config from 'src/utils/config';
13+
import * as defaultConfig from '../../config/features-config.json';
914

1015
export const mockFeaturesConfigId = '1';
11-
export const mockFeaturesConfigVersion = 1.111;
16+
export const mockFeaturesConfigVersion = defaultConfig.version + 0.111;
1217
export const mockControlNumber = 7.68;
18+
export const mockControlGroup = '7';
1319

1420
export const mockFeaturesConfigJson = {
1521
version: mockFeaturesConfigVersion,
1622
features: {
1723
liveRecommendations: {
18-
perc: [[0, 10]],
24+
perc: [[1.25, 8.45]],
1925
flag: true,
2026
filters: [
2127
{
@@ -28,6 +34,49 @@ export const mockFeaturesConfigJson = {
2834
},
2935
};
3036

37+
export const mockFeaturesConfigJsonComplex = {
38+
...mockFeaturesConfigJson,
39+
features: {
40+
liveRecommendations: {
41+
...mockFeaturesConfigJson.features.liveRecommendations,
42+
filters: [
43+
{
44+
or: [
45+
{
46+
name: 'env.FORCE_ENABLE_LIVE_RECOMMENDATIONS',
47+
value: 'true',
48+
type: 'eq',
49+
},
50+
{
51+
and: [
52+
{
53+
name: 'agreements.analytics',
54+
value: true,
55+
cond: 'eq',
56+
},
57+
{
58+
or: [
59+
{
60+
name: 'settings.scanThreshold',
61+
value: mockAppSettings.scanThreshold,
62+
cond: 'eq',
63+
},
64+
{
65+
name: 'settings.batchSize',
66+
value: mockAppSettings.batchSize,
67+
cond: 'eq',
68+
},
69+
],
70+
},
71+
],
72+
},
73+
],
74+
},
75+
],
76+
},
77+
},
78+
};
79+
3180
export const mockFeaturesConfigData = Object.assign(new FeaturesConfigData(), {
3281
...mockFeaturesConfigJson,
3382
features: new Map(Object.entries({
@@ -40,21 +89,96 @@ export const mockFeaturesConfigData = Object.assign(new FeaturesConfigData(), {
4089
})),
4190
});
4291

92+
export const mockFeaturesConfigDataComplex = Object.assign(new FeaturesConfigData(), {
93+
...mockFeaturesConfigJson,
94+
features: new Map(Object.entries({
95+
liveRecommendations: Object.assign(new FeatureConfig(), {
96+
...mockFeaturesConfigJson.features.liveRecommendations,
97+
filters: [
98+
Object.assign(new FeatureConfigFilterOr(), {
99+
or: [
100+
Object.assign(new FeatureConfigFilter(), {
101+
name: 'env.FORCE_ENABLE_LIVE_RECOMMENDATIONS',
102+
value: 'true',
103+
type: 'eq',
104+
}),
105+
Object.assign(new FeatureConfigFilterAnd(), {
106+
and: [
107+
Object.assign(new FeatureConfigFilter(), {
108+
name: 'agreements.analytics',
109+
value: true,
110+
cond: 'eq',
111+
}),
112+
Object.assign(new FeatureConfigFilterOr(), {
113+
or: [
114+
Object.assign(new FeatureConfigFilter(), {
115+
name: 'settings.scanThreshold',
116+
value: mockAppSettings.scanThreshold,
117+
cond: 'eq',
118+
}),
119+
Object.assign(new FeatureConfigFilter(), {
120+
name: 'settings.batchSize',
121+
value: mockAppSettings.batchSize,
122+
cond: 'eq',
123+
}),
124+
],
125+
}),
126+
],
127+
}),
128+
],
129+
}),
130+
],
131+
}),
132+
})),
133+
});
134+
43135
export const mockFeaturesConfig = Object.assign(new FeaturesConfig(), {
44136
controlNumber: mockControlNumber,
45137
data: mockFeaturesConfigData,
46138
});
47139

140+
export const mockFeaturesConfigComplex = Object.assign(new FeaturesConfig(), {
141+
controlNumber: mockControlNumber,
142+
data: mockFeaturesConfigDataComplex,
143+
});
144+
48145
export const mockFeaturesConfigEntity = Object.assign(new FeaturesConfigEntity(), {
49146
...classToClass(FeaturesConfigEntity, mockFeaturesConfig),
50147
id: mockFeaturesConfigId,
51148
});
52149

150+
export const mockFeaturesConfigEntityComplex = Object.assign(new FeaturesConfigEntity(), {
151+
...classToClass(FeaturesConfigEntity, mockFeaturesConfigComplex),
152+
id: mockFeaturesConfigId,
153+
});
154+
155+
export const mockFeature = Object.assign(new Feature(), {
156+
name: 'liveRecommendations',
157+
flag: true,
158+
});
159+
160+
export const mockFeatureEntity = Object.assign(new FeatureEntity(), {
161+
id: 'lr-1',
162+
name: 'liveRecommendations',
163+
flag: true,
164+
});
165+
166+
export const mockServerState = {
167+
settings: mockAppSettings,
168+
agreements: mockAppSettings.agreements,
169+
config: config.get(),
170+
env: process.env,
171+
};
172+
53173
export const mockFeaturesConfigRepository = jest.fn(() => ({
54174
getOrCreate: jest.fn().mockResolvedValue(mockFeaturesConfig),
55175
update: jest.fn().mockResolvedValue(mockFeaturesConfig),
56176
}));
57177

58178
export const mockFeaturesConfigService = () => ({
59179
sync: jest.fn(),
180+
getControlInfo: jest.fn().mockResolvedValue({
181+
controlNumber: mockControlNumber,
182+
controlGroup: mockControlGroup,
183+
}),
60184
});

redisinsight/api/src/constants/error-messages.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,4 @@ export default {
6464
APP_SETTINGS_NOT_FOUND: () => 'Could not find application settings.',
6565
SERVER_INFO_NOT_FOUND: () => 'Could not find server info.',
6666
INCREASE_MINIMUM_LIMIT: (count: string) => `Set MAXSEARCHRESULTS to at least ${count}.`,
67-
CONTROL_GROUP_NOT_EXIST: 'Control group not found.',
6867
};

redisinsight/api/src/modules/feature/feature.service.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { find, map } from 'lodash';
1+
import { find } from 'lodash';
22
import { Injectable, Logger } from '@nestjs/common';
33
import { FeatureRepository } from 'src/modules/feature/repositories/feature.repository';
44
import { FeatureServerEvents, FeatureStorage, knownFeatures } from 'src/modules/feature/constants';
@@ -17,6 +17,7 @@ export class FeatureService {
1717
private eventEmitter: EventEmitter2,
1818
) {}
1919

20+
// todo: disable recommendations
2021
/**
2122
*
2223
*/
@@ -40,6 +41,7 @@ export class FeatureService {
4041
return { features };
4142
}
4243

44+
// todo: add api doc + models
4345
/**
4446
* Recalculate flags for database features based on controlGroup and new conditions
4547
*/
Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,143 @@
11
import { Test, TestingModule } from '@nestjs/testing';
2-
import { InternalServerErrorException } from '@nestjs/common';
2+
import axios from 'axios';
33
import {
4+
mockControlGroup,
5+
mockControlNumber,
6+
mockFeaturesConfig,
7+
mockFeaturesConfigJson,
48
mockFeaturesConfigRepository,
5-
MockType
9+
MockType,
610
} from 'src/__mocks__';
7-
import config from 'src/utils/config';
8-
import { SettingsService } from 'src/modules/settings/settings.service';
911
import { FeaturesConfigService } from 'src/modules/feature/features-config.service';
10-
import { AgreementsRepository } from 'src/modules/settings/repositories/agreements.repository';
1112
import { FeaturesConfigRepository } from 'src/modules/feature/repositories/features-config.repository';
13+
import { EventEmitter2 } from '@nestjs/event-emitter';
14+
import { plainToClass } from 'class-transformer';
15+
import { FeaturesConfigData } from 'src/modules/feature/model/features-config';
16+
import { FeatureServerEvents } from 'src/modules/feature/constants';
17+
import * as defaultConfig from '../../../config/features-config.json';
1218

13-
const REDIS_SCAN_CONFIG = config.get('redis_scan');
14-
const WORKBENCH_CONFIG = config.get('workbench');
19+
jest.mock('axios');
20+
const mockedAxios = axios as jest.Mocked<typeof axios>;
1521

1622
describe('FeaturesConfigService', () => {
1723
let service: FeaturesConfigService;
1824
let repository: MockType<FeaturesConfigRepository>;
25+
let eventEmitter: EventEmitter2;
1926

2027
beforeEach(async () => {
2128
jest.clearAllMocks();
2229
const module: TestingModule = await Test.createTestingModule({
2330
providers: [
2431
FeaturesConfigService,
32+
{
33+
provide: EventEmitter2,
34+
useFactory: () => ({
35+
emit: jest.fn(),
36+
}),
37+
},
2538
{
2639
provide: FeaturesConfigRepository,
2740
useFactory: mockFeaturesConfigRepository,
28-
}
41+
},
2942
],
3043
}).compile();
3144

3245
service = module.get(FeaturesConfigService);
46+
repository = module.get(FeaturesConfigRepository);
47+
eventEmitter = module.get(EventEmitter2);
48+
49+
mockedAxios.get.mockResolvedValue({ data: mockFeaturesConfigJson });
50+
});
51+
52+
describe('onApplicationBootstrap', () => {
53+
it('should sync on bootstrap', async () => {
54+
const spy = jest.spyOn(service, 'sync');
55+
await service['onApplicationBootstrap']();
56+
57+
expect(spy).toHaveBeenCalledTimes(1);
58+
});
59+
});
60+
61+
describe('getNewConfig', () => {
62+
it('should return remote config', async () => {
63+
const result = await service['getNewConfig']();
64+
65+
expect(result).toEqual(mockFeaturesConfigJson);
66+
});
67+
it('should return default config when unable to fetch remote config', async () => {
68+
mockedAxios.get.mockRejectedValueOnce(new Error('404 not found'));
69+
70+
const result = await service['getNewConfig']();
71+
72+
expect(result).toEqual(defaultConfig);
73+
});
74+
it('should return default config when invalid remote config fetched', async () => {
75+
mockedAxios.get.mockResolvedValue({
76+
data: JSON.stringify({
77+
...mockFeaturesConfigJson,
78+
features: {
79+
liveRecommendations: {
80+
...mockFeaturesConfigJson.features.liveRecommendations,
81+
flag: 'not boolean flag',
82+
},
83+
},
84+
}),
85+
});
86+
87+
const result = await service['getNewConfig']();
88+
89+
expect(result).toEqual(defaultConfig);
90+
});
91+
it('should return default config when remote config version less then default', async () => {
92+
mockedAxios.get.mockResolvedValue({
93+
data: JSON.stringify({
94+
...mockFeaturesConfigJson,
95+
version: defaultConfig.version - 0.1,
96+
}),
97+
});
98+
99+
const result = await service['getNewConfig']();
100+
101+
expect(result).toEqual(defaultConfig);
102+
});
33103
});
34104

35105
describe('sync', () => {
36-
it('should sync', async () => {
106+
it('should update to the latest remote config', async () => {
107+
repository.getOrCreate.mockResolvedValue({
108+
...mockFeaturesConfig,
109+
data: plainToClass(FeaturesConfigData, defaultConfig),
110+
});
111+
37112
await service['sync']();
113+
114+
expect(repository.update).toHaveBeenCalledWith(mockFeaturesConfigJson);
115+
expect(eventEmitter.emit).toHaveBeenCalledWith(FeatureServerEvents.FeaturesRecalculate);
116+
});
117+
it('should not fail and not emit recalculate event in case of an error', async () => {
118+
repository.getOrCreate.mockResolvedValue({
119+
...mockFeaturesConfig,
120+
data: plainToClass(FeaturesConfigData, defaultConfig),
121+
});
122+
repository.update.mockRejectedValueOnce(new Error('update error'));
123+
124+
await service['sync']();
125+
126+
expect(repository.update).toHaveBeenCalledWith(mockFeaturesConfigJson);
127+
expect(eventEmitter.emit).not.toHaveBeenCalledWith(FeatureServerEvents.FeaturesRecalculate);
128+
});
129+
});
130+
131+
describe('getControlInfo', () => {
132+
it('should get controlNumber and controlGroup', async () => {
133+
repository.getOrCreate.mockResolvedValue(mockFeaturesConfig);
134+
135+
const result = await service['getControlInfo']();
136+
137+
expect(result).toEqual({
138+
controlNumber: mockControlNumber,
139+
controlGroup: mockControlGroup,
140+
});
38141
});
39142
});
40143
});

0 commit comments

Comments
 (0)