Skip to content

Commit 629c5b4

Browse files
authored
Merge pull request #1642 from RedisInsight/main
fixed ITests
2 parents 7511bac + de10991 commit 629c5b4

File tree

8 files changed

+149
-99
lines changed

8 files changed

+149
-99
lines changed

redisinsight/api/src/constants/recommendations.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ export const ONE_NODE_RECOMMENDATIONS = [
2626
RECOMMENDATION_NAMES.AVOID_LOGICAL_DATABASES,
2727
RECOMMENDATION_NAMES.RTS,
2828
RECOMMENDATION_NAMES.REDIS_VERSION,
29+
RECOMMENDATION_NAMES.SET_PASSWORD,
2930
];

redisinsight/api/src/modules/database-analysis/models/database-analysis.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export class DatabaseAnalysis {
125125
@Expose()
126126
@Type(() => Recommendation)
127127
recommendations: Recommendation[];
128+
128129
@ApiPropertyOptional({
129130
description: 'Logical database number.',
130131
type: Number,

redisinsight/api/src/modules/recommendation/providers/recommendation.provider.ts

Lines changed: 52 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ import { convertRedisInfoReplyToObject, convertBulkStringsToObject } from 'src/u
77
import {
88
RECOMMENDATION_NAMES, IS_TIMESTAMP, IS_INTEGER_NUMBER_REGEX, IS_NUMBER_REGEX,
99
} from 'src/constants';
10-
import ERROR_MESSAGES from 'src/constants/error-messages';
11-
import { ClusterNodeNotFoundError } from 'src/modules/cli/constants/errors';
12-
import { checkRedirectionError, parseRedirectionError } from 'src/utils/cli-helper';
1310
import { RedisDataType } from 'src/modules/browser/dto';
1411
import { Recommendation } from 'src/modules/database-analysis/models/recommendation';
1512
import { Key } from 'src/modules/database-analysis/models';
@@ -426,73 +423,15 @@ export class RecommendationProvider {
426423
async determineSearchIndexesRecommendation(
427424
redisClient: Redis,
428425
keys: Key[],
429-
client: any,
426+
client: Redis | Cluster,
430427
): Promise<Recommendation> {
431428
try {
432429
if (client.isCluster) {
433-
let processedKeysNumber = 0;
434-
let isJSONOrHash = false;
435-
let sortedSetNumber = 0;
436-
while (
437-
processedKeysNumber < keys.length
438-
&& !isJSONOrHash
439-
&& sortedSetNumber <= sortedSetCountForCheck
440-
) {
441-
if (keys[processedKeysNumber].type !== RedisDataType.ZSet) {
442-
processedKeysNumber += 1;
443-
} else {
444-
let keyType: string;
445-
const sortedSetMember = await redisClient.sendCommand(
446-
new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }),
447-
) as string[];
448-
try {
449-
keyType = await redisClient.sendCommand(
450-
new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }),
451-
) as string;
452-
} catch (err) {
453-
if (err && checkRedirectionError(err)) {
454-
const { address } = parseRedirectionError(err);
455-
const nodes = client.nodes('master');
456-
457-
const node: any = nodes.find(({ options: { host, port } }: Redis) => `${host}:${port}` === address);
458-
if (!node) {
459-
throw new ClusterNodeNotFoundError(
460-
ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND(node),
461-
);
462-
}
463-
464-
keyType = await node.sendCommand(
465-
new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }),
466-
) as string;
467-
}
468-
}
469-
if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) {
470-
isJSONOrHash = true;
471-
}
472-
processedKeysNumber += 1;
473-
sortedSetNumber += 1;
474-
}
475-
}
476-
477-
return isJSONOrHash ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null;
430+
const res = await this.determineSearchIndexesForCluster(keys, client);
431+
return res ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null;
478432
}
479-
const sortedSets = keys
480-
.filter(({ type }) => type === RedisDataType.ZSet)
481-
.slice(0, 100);
482-
const res = await redisClient.pipeline(sortedSets.map(({ name }) => ([
483-
'zrange',
484-
name,
485-
0,
486-
0,
487-
]))).exec();
488-
489-
const types = await redisClient.pipeline(res.map(([, member]) => ([
490-
'type',
491-
member,
492-
]))).exec();
493-
494-
const isHashOrJSONName = types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash);
495-
return isHashOrJSONName ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null;
433+
const res = await this.determineSearchIndexesForStandalone(keys, redisClient);
434+
return res ? { name: RECOMMENDATION_NAMES.SEARCH_INDEXES } : null;
496435
} catch (err) {
497436
this.logger.error('Can not determine search indexes recommendation', err);
498437
return null;
@@ -579,4 +518,51 @@ export class RecommendationProvider {
579518
return false;
580519
}
581520
}
521+
522+
private async determineSearchIndexesForCluster(keys: Key[], client: Redis | Cluster): Promise<boolean> {
523+
let processedKeysNumber = 0;
524+
let isJSONOrHash = false;
525+
let sortedSetNumber = 0;
526+
while (
527+
processedKeysNumber < keys.length
528+
&& !isJSONOrHash
529+
&& sortedSetNumber <= sortedSetCountForCheck
530+
) {
531+
if (keys[processedKeysNumber].type !== RedisDataType.ZSet) {
532+
processedKeysNumber += 1;
533+
} else {
534+
const sortedSetMember = await client.sendCommand(
535+
new Command('zrange', [keys[processedKeysNumber].name, 0, 0], { replyEncoding: 'utf8' }),
536+
) as string[];
537+
const keyType = await client.sendCommand(
538+
new Command('type', [sortedSetMember[0]], { replyEncoding: 'utf8' }),
539+
) as string;
540+
if (keyType === RedisDataType.JSON || keyType === RedisDataType.Hash) {
541+
isJSONOrHash = true;
542+
}
543+
processedKeysNumber += 1;
544+
sortedSetNumber += 1;
545+
}
546+
}
547+
return isJSONOrHash;
548+
}
549+
550+
private async determineSearchIndexesForStandalone(keys: Key[], redisClient: Redis): Promise<boolean> {
551+
const sortedSets = keys
552+
.filter(({ type }) => type === RedisDataType.ZSet)
553+
.slice(0, 100);
554+
const res = await redisClient.pipeline(sortedSets.map(({ name }) => ([
555+
'zrange',
556+
name,
557+
0,
558+
0,
559+
]))).exec();
560+
561+
const types = await redisClient.pipeline(res.map(([, member]) => ([
562+
'type',
563+
member,
564+
]))).exec();
565+
566+
return types.some(([, type]) => type === RedisDataType.JSON || type === RedisDataType.Hash);
567+
}
582568
}

redisinsight/api/test/api/database-analysis/POST-databases-id-analysis.test.ts

Lines changed: 57 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -189,19 +189,20 @@ describe('POST /databases/:instanceId/analysis', () => {
189189
].map(mainCheckFn);
190190
});
191191

192-
describe('setPassword recommendation', () => {
193-
requirements('!rte.pass');
192+
describe('redisVersion recommendation', () => {
193+
// todo find solution for redis pass
194+
requirements('rte.version <= 6', '!rte.pass');
194195
[
195196
{
196-
name: 'Should create new database analysis with setPassword recommendation',
197+
name: 'Should create new database analysis with redisVersion recommendation',
197198
data: {
198199
delimiter: '-',
199200
},
200201
statusCode: 201,
201202
responseSchema,
202203
checkFn: async ({ body }) => {
203204
expect(body.recommendations).to.include.deep.members([
204-
constants.TEST_SET_PASSWORD_RECOMMENDATION,
205+
constants.TEST_REDIS_VERSION_RECOMMENDATION,
205206
]);
206207
},
207208
after: async () => {
@@ -211,19 +212,19 @@ describe('POST /databases/:instanceId/analysis', () => {
211212
].map(mainCheckFn);
212213
});
213214

214-
describe('redisVersion recommendation', () => {
215-
requirements('rte.version <= 6');
215+
describe('setPassword recommendation', () => {
216+
requirements('!rte.pass');
216217
[
217218
{
218-
name: 'Should create new database analysis with redisVersion recommendation',
219+
name: 'Should create new database analysis with setPassword recommendation',
219220
data: {
220221
delimiter: '-',
221222
},
222223
statusCode: 201,
223224
responseSchema,
224225
checkFn: async ({ body }) => {
225226
expect(body.recommendations).to.include.deep.members([
226-
constants.TEST_REDIS_VERSION_RECOMMENDATION,
227+
constants.TEST_SET_PASSWORD_RECOMMENDATION,
227228
]);
228229
},
229230
after: async () => {
@@ -257,7 +258,7 @@ describe('POST /databases/:instanceId/analysis', () => {
257258
].map(mainCheckFn);
258259
});
259260

260-
describe('rediSearch recommendation with ReJSON', () => {
261+
describe('recommendations with ReJSON', () => {
261262
requirements('rte.modules.rejson');
262263
[
263264
{
@@ -280,6 +281,53 @@ describe('POST /databases/:instanceId/analysis', () => {
280281
expect(await repository.count()).to.eq(5);
281282
}
282283
},
284+
{
285+
name: 'Should create new database analysis with searchIndexes recommendation',
286+
data: {
287+
delimiter: '-',
288+
},
289+
statusCode: 201,
290+
responseSchema,
291+
before: async () => {
292+
const jsonValue = JSON.stringify(constants.TEST_REJSON_VALUE_1);
293+
await rte.data.sendCommand('ZADD', [constants.TEST_ZSET_KEY_1, constants.TEST_ZSET_MEMBER_1_SCORE, constants.TEST_ZSET_MEMBER_1]);
294+
await rte.data.sendCommand('json.set', [constants.TEST_ZSET_MEMBER_1, '.', jsonValue]);
295+
},
296+
checkFn: async ({ body }) => {
297+
expect(body.recommendations).to.include.deep.members([
298+
constants.TEST_SEARCH_INDEXES_RECOMMENDATION,
299+
]);
300+
},
301+
after: async () => {
302+
expect(await repository.count()).to.eq(5);
303+
}
304+
},
305+
].map(mainCheckFn);
306+
});
307+
308+
describe('searchIndexes recommendation', () => {
309+
requirements('!rte.pass');
310+
[
311+
{
312+
name: 'Should create new database analysis with searchIndexes recommendation',
313+
data: {
314+
delimiter: '-',
315+
},
316+
statusCode: 201,
317+
responseSchema,
318+
before: async () => {
319+
await rte.data.sendCommand('ZADD', [constants.TEST_ZSET_KEY_1, constants.TEST_ZSET_MEMBER_1_SCORE, constants.TEST_ZSET_MEMBER_1]);
320+
await rte.data.sendCommand('HSET', [constants.TEST_ZSET_MEMBER_1, constants.TEST_HASH_FIELD_1_NAME, constants.TEST_HASH_FIELD_1_VALUE]);
321+
},
322+
checkFn: async ({ body }) => {
323+
expect(body.recommendations).to.include.deep.members([
324+
constants.TEST_SEARCH_INDEXES_RECOMMENDATION,
325+
]);
326+
},
327+
after: async () => {
328+
expect(await repository.count()).to.eq(5);
329+
}
330+
},
283331
].map(mainCheckFn);
284332
});
285333

@@ -490,26 +538,6 @@ describe('POST /databases/:instanceId/analysis', () => {
490538
expect(await repository.count()).to.eq(5);
491539
}
492540
},
493-
// update with new requirements
494-
// {
495-
// name: 'Should create new database analysis with RTS recommendation',
496-
// data: {
497-
// delimiter: '-',
498-
// },
499-
// statusCode: 201,
500-
// responseSchema,
501-
// before: async () => {
502-
// await rte.data.sendCommand('zadd', [constants.TEST_ZSET_TIMESTAMP_KEY, constants.TEST_ZSET_TIMESTAMP_MEMBER, constants.TEST_ZSET_TIMESTAMP_SCORE]);
503-
// },
504-
// checkFn: async ({ body }) => {
505-
// expect(body.recommendations).to.include.deep.members([
506-
// constants.TEST_RTS_RECOMMENDATION,
507-
// ]);
508-
// },
509-
// after: async () => {
510-
// expect(await repository.count()).to.eq(5);
511-
// }
512-
// },
513541
].map(mainCheckFn);
514542
});
515543
});

redisinsight/api/test/api/database/GET-databases.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const responseSchema = Joi.array().items(Joi.object().keys({
1111
port: Joi.number().integer().required(),
1212
db: Joi.number().integer().allow(null).required(),
1313
name: Joi.string().required(),
14+
provider: Joi.string().required(),
1415
new: Joi.boolean().allow(null).required(),
1516
connectionType: Joi.string().valid('STANDALONE', 'SENTINEL', 'CLUSTER', 'NOT CONNECTED').required(),
1617
lastConnection: Joi.string().isoDate().allow(null).required(),

redisinsight/api/test/helpers/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,11 @@ export const constants = {
524524
name: RECOMMENDATION_NAMES.REDIS_SEARCH,
525525
},
526526

527+
528+
TEST_SEARCH_INDEXES_RECOMMENDATION: {
529+
name: RECOMMENDATION_NAMES.SEARCH_INDEXES,
530+
},
531+
527532
TEST_LUA_SCRIPT_VOTE_RECOMMENDATION: {
528533
name: RECOMMENDATION_NAMES.LUA_SCRIPT,
529534
vote: 'useful',

redisinsight/ui/src/pages/databaseAnalysis/components/recommendation-voting/RecommendationVoting.spec.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@ import React from 'react'
22
import { cloneDeep } from 'lodash'
33
import { instance, mock } from 'ts-mockito'
44
import { setRecommendationVote } from 'uiSrc/slices/analytics/dbAnalysis'
5+
import { userSettingsConfigSelector } from 'uiSrc/slices/user/user-settings'
6+
import { Vote } from 'uiSrc/constants/recommendations'
57

68
import {
9+
act,
710
cleanup,
811
mockedStore,
912
fireEvent,
1013
render,
1114
screen,
1215
waitForEuiPopoverVisible,
16+
waitForEuiToolTipVisible,
1317
} from 'uiSrc/utils/test-utils'
1418

1519
import RecommendationVoting, { Props } from './RecommendationVoting'
@@ -29,9 +33,13 @@ jest.mock('uiSrc/telemetry', () => ({
2933
sendEventTelemetry: jest.fn(),
3034
}))
3135

32-
jest.mock('react-redux', () => ({
33-
...jest.requireActual('react-redux'),
34-
useSelector: jest.fn(),
36+
jest.mock('uiSrc/slices/user/user-settings', () => ({
37+
...jest.requireActual('uiSrc/slices/user/user-settings'),
38+
userSettingsConfigSelector: jest.fn().mockReturnValue({
39+
agreements: {
40+
analytics: true,
41+
}
42+
}),
3543
}))
3644

3745
describe('RecommendationVoting', () => {
@@ -41,6 +49,7 @@ describe('RecommendationVoting', () => {
4149

4250
it('should call "setRecommendationVote" action be called after click "very-useful-vote-btn"', () => {
4351
render(<RecommendationVoting {...instance(mockedProps)} />)
52+
expect(screen.queryByTestId('very-useful-vote-btn')).toBeInTheDocument()
4453
fireEvent.click(screen.getByTestId('very-useful-vote-btn'))
4554

4655
const expectedActions = [setRecommendationVote()]
@@ -75,10 +84,26 @@ describe('RecommendationVoting', () => {
7584
})
7685

7786
it('should render component where all buttons are disabled"', async () => {
78-
render(<RecommendationVoting {...instance(mockedProps)} vote="useful" />)
87+
render(<RecommendationVoting {...instance(mockedProps)} vote={Vote.Like} />)
7988

8089
expect(screen.getByTestId('very-useful-vote-btn')).toBeDisabled()
8190
expect(screen.getByTestId('useful-vote-btn')).toBeDisabled()
8291
expect(screen.getByTestId('not-useful-vote-btn')).toBeDisabled()
8392
})
93+
94+
it('should render popover after click "not-useful-vote-btn"', async () => {
95+
userSettingsConfigSelector.mockImplementation(() => ({
96+
agreements: {
97+
analytics: false,
98+
},
99+
}))
100+
render(<RecommendationVoting {...instance(mockedProps)} />)
101+
102+
await act(async () => {
103+
fireEvent.mouseOver(screen.getByTestId('not-useful-vote-btn'))
104+
})
105+
await waitForEuiToolTipVisible()
106+
107+
expect(screen.getByTestId('not-useful-vote-tooltip')).toHaveTextContent('Enable Analytics on the Settings page to vote for a recommendation')
108+
})
84109
})

0 commit comments

Comments
 (0)