Skip to content

Commit 5a1e2ce

Browse files
author
Artem
committed
ITests for Stream Consumer Groups functionality
1 parent f1af098 commit 5a1e2ce

17 files changed

+2221
-9
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export default {
33
INVALID_DATABASE_INSTANCE_ID: 'Invalid database instance id.',
44
COMMAND_EXECUTION_NOT_FOUND: 'Command execution was not found.',
55
PROFILER_LOG_FILE_NOT_FOUND: 'Profiler log file was not found.',
6+
CONSUMER_GROUP_NOT_FOUND: 'Consumer Group with such name was not found.',
67
PLUGIN_STATE_NOT_FOUND: 'Plugin state was not found.',
78
UNDEFINED_INSTANCE_ID: 'Undefined redis database instance id.',
89
NO_CONNECTION_TO_REDIS_DB: 'No connection to the Redis Database.',

redisinsight/api/src/constants/redis-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export enum RedisErrorCodes {
1111
Timeout = 'ETIMEDOUT',
1212
CommandSyntaxError = 'syntax error',
1313
BusyGroup = 'BUSYGROUP',
14+
NoGroup = 'NOGROUP',
1415
UnknownCommand = 'unknown command',
1516
}
1617

redisinsight/api/src/modules/browser/dto/stream.dto.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,16 @@ import {
55
ArrayNotEmpty,
66
IsArray,
77
IsDefined,
8-
IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateNested, isString, IsOptional, NotEquals, ValidateIf, IsBoolean,
8+
IsEnum,
9+
IsInt,
10+
IsNotEmpty,
11+
IsString,
12+
Min,
13+
ValidateNested,
14+
isString,
15+
NotEquals,
16+
ValidateIf,
17+
IsBoolean,
918
} from 'class-validator';
1019
import { KeyDto, KeyWithExpireDto } from 'src/modules/browser/dto/keys.dto';
1120
import { SortOrder } from 'src/constants';
@@ -278,7 +287,8 @@ export class DeleteConsumerGroupsDto extends KeyDto {
278287
@IsDefined()
279288
@IsArray()
280289
@ArrayNotEmpty()
281-
@Type(() => String)
290+
@IsNotEmpty({ each: true })
291+
@IsString({ each: true })
282292
consumerGroups: string[];
283293
}
284294

@@ -335,7 +345,8 @@ export class DeleteConsumersDto extends GetConsumersDto {
335345
@IsDefined()
336346
@IsArray()
337347
@ArrayNotEmpty()
338-
@Type(() => String)
348+
@IsString({ each: true })
349+
@IsNotEmpty({ each: true })
339350
consumerNames: string[];
340351
}
341352

@@ -421,7 +432,8 @@ export class AckPendingEntriesDto extends GetConsumersDto {
421432
@IsDefined()
422433
@IsArray()
423434
@ArrayNotEmpty()
424-
@Type(() => String)
435+
@IsString({ each: true })
436+
@IsNotEmpty({ each: true })
425437
entries: string[];
426438
}
427439

@@ -471,7 +483,8 @@ export class ClaimPendingEntryDto extends KeyDto {
471483
@IsDefined()
472484
@IsArray()
473485
@ArrayNotEmpty()
474-
@Type(() => String)
486+
@IsString({ each: true })
487+
@IsNotEmpty({ each: true })
475488
entries: string[];
476489

477490
@ApiPropertyOptional({

redisinsight/api/src/modules/browser/services/stream/consumer-group.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
ConsumerGroupDto,
1616
CreateConsumerGroupsDto,
1717
DeleteConsumerGroupsDto, DeleteConsumerGroupsResponse,
18-
UpdateConsumerGroupDto
18+
UpdateConsumerGroupDto,
1919
} from 'src/modules/browser/dto/stream.dto';
2020

2121
@Injectable()
@@ -197,6 +197,10 @@ export class ConsumerGroupService {
197197
throw new BadRequestException(error.message);
198198
}
199199

200+
if (error?.message.includes(RedisErrorCodes.NoGroup)) {
201+
throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);
202+
}
203+
200204
throw catchAclError(error);
201205
}
202206
}

redisinsight/api/src/modules/browser/services/stream/consumer.service.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
} from '@nestjs/common';
44
import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service';
55
import { RedisErrorCodes } from 'src/constants';
6-
import {catchAclError, catchTransactionError, convertStringsArrayToObject} from 'src/utils';
6+
import { catchAclError, catchTransactionError, convertStringsArrayToObject } from 'src/utils';
77
import {
88
BrowserToolCommands,
99
BrowserToolKeysCommands, BrowserToolStreamCommands,
@@ -54,6 +54,10 @@ export class ConsumerService {
5454
throw error;
5555
}
5656

57+
if (error?.message.includes(RedisErrorCodes.NoGroup)) {
58+
throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);
59+
}
60+
5761
if (error?.message.includes(RedisErrorCodes.WrongType)) {
5862
throw new BadRequestException(error.message);
5963
}
@@ -108,6 +112,10 @@ export class ConsumerService {
108112
throw error;
109113
}
110114

115+
if (error?.message.includes(RedisErrorCodes.NoGroup)) {
116+
throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);
117+
}
118+
111119
if (error?.message.includes(RedisErrorCodes.WrongType)) {
112120
throw new BadRequestException(error.message);
113121
}
@@ -148,6 +156,10 @@ export class ConsumerService {
148156
throw error;
149157
}
150158

159+
if (error?.message.includes(RedisErrorCodes.NoGroup)) {
160+
throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);
161+
}
162+
151163
if (error?.message.includes(RedisErrorCodes.WrongType)) {
152164
throw new BadRequestException(error.message);
153165
}
@@ -259,6 +271,10 @@ export class ConsumerService {
259271
throw error;
260272
}
261273

274+
if (error?.message.includes(RedisErrorCodes.NoGroup)) {
275+
throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND);
276+
}
277+
262278
if (error?.message.includes(RedisErrorCodes.WrongType)) {
263279
throw new BadRequestException(error.message);
264280
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import {
2+
expect,
3+
describe,
4+
it,
5+
before,
6+
deps,
7+
Joi,
8+
requirements,
9+
generateInvalidDataTestCases,
10+
validateInvalidDataTestCase,
11+
validateApiCall
12+
} from '../deps';
13+
const { server, request, constants, rte } = deps;
14+
15+
// endpoint to test
16+
const endpoint = (instanceId = constants.TEST_INSTANCE_ID) =>
17+
request(server).delete(`/instance/${instanceId}/streams/consumer-groups/consumers`);
18+
19+
const dataSchema = Joi.object({
20+
keyName: Joi.string().allow('').required(),
21+
groupName: Joi.string().required().messages({
22+
'any.required': '{#label} should not be empty',
23+
}),
24+
consumerNames: Joi.array().items(Joi.string().required().label('consumerNames').messages({
25+
'any.required': '{#label} should not be empty',
26+
})).min(1).required().messages({
27+
'array.sparse': 'each value in consumerNames must be a string',
28+
}),
29+
}).strict();
30+
31+
const validInputData = {
32+
keyName: constants.TEST_STREAM_KEY_1,
33+
groupName: constants.TEST_STREAM_GROUP_1,
34+
consumerNames: [constants.TEST_STREAM_GROUP_1],
35+
};
36+
37+
38+
const mainCheckFn = async (testCase) => {
39+
it(testCase.name, async () => {
40+
// additional checks before test run
41+
if (testCase.before) {
42+
await testCase.before();
43+
}
44+
45+
await validateApiCall({
46+
endpoint,
47+
...testCase,
48+
});
49+
50+
// additional checks after test pass
51+
if (testCase.after) {
52+
await testCase.after();
53+
}
54+
});
55+
};
56+
57+
describe('DELETE /instance/:instanceId/streams/consumer-groups/consumers', () => {
58+
before(async () => await rte.data.generateKeys(true));
59+
60+
describe('Validation', () => {
61+
generateInvalidDataTestCases(dataSchema, validInputData).map(
62+
validateInvalidDataTestCase(endpoint, dataSchema),
63+
);
64+
});
65+
66+
describe('Common', () => {
67+
beforeEach(async () => {
68+
await rte.data.sendCommand('xreadgroup', [
69+
'GROUP',
70+
constants.TEST_STREAM_GROUP_1,
71+
constants.TEST_STREAM_CONSUMER_1,
72+
'STREAMS',
73+
constants.TEST_STREAM_KEY_1,
74+
constants.TEST_STREAM_ID_1,
75+
]);
76+
await rte.data.sendCommand('xreadgroup', [
77+
'GROUP',
78+
constants.TEST_STREAM_GROUP_1,
79+
constants.TEST_STREAM_CONSUMER_2,
80+
'STREAMS',
81+
constants.TEST_STREAM_KEY_1,
82+
constants.TEST_STREAM_ID_2,
83+
]);
84+
});
85+
86+
[
87+
{
88+
name: 'Should remove single consumer',
89+
data: {
90+
keyName: constants.TEST_STREAM_KEY_1,
91+
groupName: constants.TEST_STREAM_GROUP_1,
92+
consumerNames: [constants.TEST_STREAM_CONSUMER_1],
93+
},
94+
before: async () => {
95+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
96+
expect(consumers.length).to.eq(2);
97+
},
98+
after: async () => {
99+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
100+
expect(consumers.length).to.eq(1);
101+
},
102+
},
103+
{
104+
name: 'Should remove multiple consumers',
105+
data: {
106+
keyName: constants.TEST_STREAM_KEY_1,
107+
groupName: constants.TEST_STREAM_GROUP_1,
108+
consumerNames: [constants.TEST_STREAM_CONSUMER_1, constants.TEST_STREAM_CONSUMER_2],
109+
},
110+
before: async () => {
111+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
112+
expect(consumers.length).to.eq(2);
113+
},
114+
after: async () => {
115+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
116+
expect(consumers.length).to.eq(0);
117+
},
118+
},
119+
{
120+
name: 'Should remove single consumers and skip not existing consumers',
121+
data: {
122+
keyName: constants.TEST_STREAM_KEY_1,
123+
groupName: constants.TEST_STREAM_GROUP_1,
124+
consumerNames: [constants.TEST_STREAM_CONSUMER_1, constants.getRandomString(), constants.getRandomString()],
125+
},
126+
before: async () => {
127+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
128+
console.log('_c', consumers)
129+
expect(consumers.length).to.eq(2);
130+
},
131+
after: async () => {
132+
const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]);
133+
expect(consumers.length).to.eq(1);
134+
},
135+
},
136+
{
137+
name: 'Should return BadRequest error if key has another type',
138+
data: {
139+
...validInputData,
140+
keyName: constants.TEST_STRING_KEY_1,
141+
},
142+
statusCode: 400,
143+
responseBody: {
144+
statusCode: 400,
145+
error: 'Bad Request',
146+
},
147+
},
148+
{
149+
name: 'Should return NotFound error if group does not exists',
150+
data: {
151+
...validInputData,
152+
groupName: constants.getRandomString(),
153+
},
154+
statusCode: 404,
155+
responseBody: {
156+
statusCode: 404,
157+
error: 'Not Found',
158+
message: 'Consumer Group with such name was not found.',
159+
},
160+
},
161+
{
162+
name: 'Should return NotFound error if key does not exists',
163+
data: {
164+
...validInputData,
165+
keyName: constants.getRandomString(),
166+
},
167+
statusCode: 404,
168+
responseBody: {
169+
statusCode: 404,
170+
error: 'Not Found',
171+
message: 'Key with this name does not exist.',
172+
},
173+
},
174+
{
175+
name: 'Should return NotFound error if instance id does not exists',
176+
endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID),
177+
data: {
178+
...validInputData,
179+
},
180+
statusCode: 404,
181+
responseBody: {
182+
statusCode: 404,
183+
error: 'Not Found',
184+
message: 'Invalid database instance id.',
185+
},
186+
},
187+
].map(mainCheckFn);
188+
});
189+
190+
describe('ACL', () => {
191+
requirements('rte.acl');
192+
193+
before(async () => await rte.data.generateKeys(true));
194+
before(async () => rte.data.setAclUserRules('~* +@all'));
195+
196+
[
197+
{
198+
name: 'Should create consumer group',
199+
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
200+
data: {
201+
...validInputData,
202+
},
203+
},
204+
{
205+
name: 'Should throw error if no permissions for "exists" command',
206+
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
207+
data: {
208+
...validInputData,
209+
},
210+
statusCode: 403,
211+
responseBody: {
212+
statusCode: 403,
213+
error: 'Forbidden',
214+
},
215+
before: () => rte.data.setAclUserRules('~* +@all -exists')
216+
},
217+
{
218+
name: 'Should throw error if no permissions for "xgroup)" command',
219+
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
220+
data: {
221+
...validInputData,
222+
},
223+
statusCode: 403,
224+
responseBody: {
225+
statusCode: 403,
226+
error: 'Forbidden',
227+
},
228+
before: () => rte.data.setAclUserRules('~* +@all -xgroup')
229+
},
230+
].map(mainCheckFn);
231+
});
232+
});

0 commit comments

Comments
 (0)