diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index b635436495..f876f60043 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -3,6 +3,7 @@ export default { INVALID_DATABASE_INSTANCE_ID: 'Invalid database instance id.', COMMAND_EXECUTION_NOT_FOUND: 'Command execution was not found.', PROFILER_LOG_FILE_NOT_FOUND: 'Profiler log file was not found.', + CONSUMER_GROUP_NOT_FOUND: 'Consumer Group with such name was not found.', PLUGIN_STATE_NOT_FOUND: 'Plugin state was not found.', UNDEFINED_INSTANCE_ID: 'Undefined redis database instance id.', NO_CONNECTION_TO_REDIS_DB: 'No connection to the Redis Database.', diff --git a/redisinsight/api/src/constants/redis-error-codes.ts b/redisinsight/api/src/constants/redis-error-codes.ts index baf151099e..caf687a9a3 100644 --- a/redisinsight/api/src/constants/redis-error-codes.ts +++ b/redisinsight/api/src/constants/redis-error-codes.ts @@ -11,6 +11,7 @@ export enum RedisErrorCodes { Timeout = 'ETIMEDOUT', CommandSyntaxError = 'syntax error', BusyGroup = 'BUSYGROUP', + NoGroup = 'NOGROUP', UnknownCommand = 'unknown command', } diff --git a/redisinsight/api/src/modules/browser/dto/stream.dto.ts b/redisinsight/api/src/modules/browser/dto/stream.dto.ts index 52d79496ee..b8496825ae 100644 --- a/redisinsight/api/src/modules/browser/dto/stream.dto.ts +++ b/redisinsight/api/src/modules/browser/dto/stream.dto.ts @@ -5,7 +5,16 @@ import { ArrayNotEmpty, IsArray, IsDefined, - IsEnum, IsInt, IsNotEmpty, IsString, Min, ValidateNested, isString, IsOptional, NotEquals, ValidateIf, IsBoolean, + IsEnum, + IsInt, + IsNotEmpty, + IsString, + Min, + ValidateNested, + isString, + NotEquals, + ValidateIf, + IsBoolean, } from 'class-validator'; import { KeyDto, KeyWithExpireDto } from 'src/modules/browser/dto/keys.dto'; import { SortOrder } from 'src/constants'; @@ -278,7 +287,8 @@ export class DeleteConsumerGroupsDto extends KeyDto { @IsDefined() @IsArray() @ArrayNotEmpty() - @Type(() => String) + @IsNotEmpty({ each: true }) + @IsString({ each: true }) consumerGroups: string[]; } @@ -335,7 +345,8 @@ export class DeleteConsumersDto extends GetConsumersDto { @IsDefined() @IsArray() @ArrayNotEmpty() - @Type(() => String) + @IsString({ each: true }) + @IsNotEmpty({ each: true }) consumerNames: string[]; } @@ -421,7 +432,8 @@ export class AckPendingEntriesDto extends GetConsumersDto { @IsDefined() @IsArray() @ArrayNotEmpty() - @Type(() => String) + @IsString({ each: true }) + @IsNotEmpty({ each: true }) entries: string[]; } @@ -471,7 +483,8 @@ export class ClaimPendingEntryDto extends KeyDto { @IsDefined() @IsArray() @ArrayNotEmpty() - @Type(() => String) + @IsString({ each: true }) + @IsNotEmpty({ each: true }) entries: string[]; @ApiPropertyOptional({ diff --git a/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.ts b/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.ts index d9fa16881d..61619249d3 100644 --- a/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.ts +++ b/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.ts @@ -15,7 +15,7 @@ import { ConsumerGroupDto, CreateConsumerGroupsDto, DeleteConsumerGroupsDto, DeleteConsumerGroupsResponse, - UpdateConsumerGroupDto + UpdateConsumerGroupDto, } from 'src/modules/browser/dto/stream.dto'; @Injectable() @@ -197,6 +197,10 @@ export class ConsumerGroupService { throw new BadRequestException(error.message); } + if (error?.message.includes(RedisErrorCodes.NoGroup)) { + throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND); + } + throw catchAclError(error); } } diff --git a/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts b/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts index aea20b87ae..b31cae1230 100644 --- a/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts +++ b/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts @@ -3,7 +3,7 @@ import { } from '@nestjs/common'; import { IFindRedisClientInstanceByOptions } from 'src/modules/core/services/redis/redis.service'; import { RedisErrorCodes } from 'src/constants'; -import {catchAclError, catchTransactionError, convertStringsArrayToObject} from 'src/utils'; +import { catchAclError, catchTransactionError, convertStringsArrayToObject } from 'src/utils'; import { BrowserToolCommands, BrowserToolKeysCommands, BrowserToolStreamCommands, @@ -54,6 +54,10 @@ export class ConsumerService { throw error; } + if (error?.message.includes(RedisErrorCodes.NoGroup)) { + throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND); + } + if (error?.message.includes(RedisErrorCodes.WrongType)) { throw new BadRequestException(error.message); } @@ -108,6 +112,10 @@ export class ConsumerService { throw error; } + if (error?.message.includes(RedisErrorCodes.NoGroup)) { + throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND); + } + if (error?.message.includes(RedisErrorCodes.WrongType)) { throw new BadRequestException(error.message); } @@ -148,6 +156,10 @@ export class ConsumerService { throw error; } + if (error?.message.includes(RedisErrorCodes.NoGroup)) { + throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND); + } + if (error?.message.includes(RedisErrorCodes.WrongType)) { throw new BadRequestException(error.message); } @@ -259,6 +271,10 @@ export class ConsumerService { throw error; } + if (error?.message.includes(RedisErrorCodes.NoGroup)) { + throw new NotFoundException(ERROR_MESSAGES.CONSUMER_GROUP_NOT_FOUND); + } + if (error?.message.includes(RedisErrorCodes.WrongType)) { throw new BadRequestException(error.message); } diff --git a/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups-consumers.test.ts b/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups-consumers.test.ts new file mode 100644 index 0000000000..1eaa1a9916 --- /dev/null +++ b/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups-consumers.test.ts @@ -0,0 +1,232 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).delete(`/instance/${instanceId}/streams/consumer-groups/consumers`); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + groupName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + consumerNames: Joi.array().items(Joi.string().required().label('consumerNames').messages({ + 'any.required': '{#label} should not be empty', + })).min(1).required().messages({ + 'array.sparse': 'each value in consumerNames must be a string', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerNames: [constants.TEST_STREAM_GROUP_1], +}; + + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('DELETE /instance/:instanceId/streams/consumer-groups/consumers', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_1, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_1, + ]); + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_2, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_2, + ]); + }); + + [ + { + name: 'Should remove single consumer', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerNames: [constants.TEST_STREAM_CONSUMER_1], + }, + before: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + expect(consumers.length).to.eq(2); + }, + after: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + expect(consumers.length).to.eq(1); + }, + }, + { + name: 'Should remove multiple consumers', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerNames: [constants.TEST_STREAM_CONSUMER_1, constants.TEST_STREAM_CONSUMER_2], + }, + before: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + expect(consumers.length).to.eq(2); + }, + after: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + expect(consumers.length).to.eq(0); + }, + }, + { + name: 'Should remove single consumers and skip not existing consumers', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerNames: [constants.TEST_STREAM_CONSUMER_1, constants.getRandomString(), constants.getRandomString()], + }, + before: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + console.log('_c', consumers) + expect(consumers.length).to.eq(2); + }, + after: async () => { + const consumers = await rte.data.sendCommand('xinfo', ['consumers', constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_GROUP_1]); + expect(consumers.length).to.eq(1); + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if group does not exists', + data: { + ...validInputData, + groupName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Consumer Group with such name was not found.', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xgroup)" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xgroup') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups.test.ts b/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups.test.ts new file mode 100644 index 0000000000..5c82e61fee --- /dev/null +++ b/redisinsight/api/test/api/stream/DELETE-instance-id-streams-consumer_groups.test.ts @@ -0,0 +1,191 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).delete(`/instance/${instanceId}/streams/consumer-groups`); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + consumerGroups: Joi.array().items(Joi.string().label('consumerGroups').required().messages({ + 'any.required': '{#label} should not be empty', + })).required().min(1).messages({ + 'array.sparse': 'each value in consumerGroups must be a string', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [constants.TEST_STREAM_GROUP_1], +}; + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('DELETE /instance/:instanceId/streams/consumer-groups', () => { + beforeEach(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + [ + { + name: 'Should delete consumer group', + data: { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [constants.TEST_STREAM_GROUP_1], + }, + before: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(2); + }, + after: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(1); + }, + }, + { + name: 'Should delete multiple consumer group', + data: { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [constants.TEST_STREAM_GROUP_1, constants.TEST_STREAM_GROUP_2], + }, + before: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(2); + }, + after: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(0); + }, + }, + { + name: 'Should delete single consumer group and ignore not existing', + data: { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [constants.TEST_STREAM_GROUP_1, constants.getRandomString(), constants.getRandomString()], + }, + before: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(2); + }, + after: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(groups.length).to.eq(1); + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should remove consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xgroup" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xgroup') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/PATCH-instance-id-streams-consumer_groups.test.ts b/redisinsight/api/test/api/stream/PATCH-instance-id-streams-consumer_groups.test.ts new file mode 100644 index 0000000000..495dfaa9f8 --- /dev/null +++ b/redisinsight/api/test/api/stream/PATCH-instance-id-streams-consumer_groups.test.ts @@ -0,0 +1,182 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).patch(`/instance/${instanceId}/streams/consumer-groups`); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + name: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + lastDeliveredId: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + name: constants.TEST_STREAM_GROUP_1, + lastDeliveredId: '$', +}; + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('PATCH /instance/:instanceId/streams/consumer-groups', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.client.del(constants.TEST_STREAM_KEY_2); + await rte.client.xadd(constants.TEST_STREAM_KEY_2, '*', 'f', 'v'); + }); + + [ + { + name: 'Should update lastDeliveredId', + data: { + keyName: constants.TEST_STREAM_KEY_1, + name: constants.TEST_STREAM_GROUP_1, + lastDeliveredId: constants.TEST_STREAM_ID_2, + }, + before: async () => { + const [group] = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(group[7]).to.eq(constants.TEST_STREAM_ID_1); + }, + after: async () => { + const [group] = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_1]); + expect(group[7]).to.eq(constants.TEST_STREAM_ID_2); + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if group does not exists', + data: { + ...validInputData, + keyName: constants.TEST_STREAM_KEY_2, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Consumer Group with such name was not found.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xgroup" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xgroup') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-get.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-get.test.ts new file mode 100644 index 0000000000..1f62a60f60 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-get.test.ts @@ -0,0 +1,199 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups/consumers/get`); + +const consumerSchema = Joi.object().keys({ + name: Joi.string().required(), + idle: Joi.number().required(), + pending: Joi.number().required(), +}).strict(); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + groupName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, +}; + +const responseSchema = Joi.array().items(consumerSchema).min(0).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups/consumers/get', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_1, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_1, + ]); + }); + + [ + { + name: 'Should return empty array when no consumers', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_2, + }, + responseSchema, + responseBody: [], + }, + { + name: 'Should return consumers list', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + }, + responseSchema, + checkFn: ({ body }) => { + const [consumer] = body; + expect(consumer.name).to.eq(constants.TEST_STREAM_CONSUMER_1); + expect(consumer.pending).to.eq(0); + expect(consumer.idle).to.gte(0); + } + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if group does not exists', + data: { + ...validInputData, + groupName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Consumer Group with such name was not found.', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xinfo" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xinfo') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-ack.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-ack.test.ts new file mode 100644 index 0000000000..12435fc4a4 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-ack.test.ts @@ -0,0 +1,268 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups/consumers/pending-messages/ack`); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + groupName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + entries: Joi.array().items(Joi.string().required().label('entries').messages({ + 'any.required': '{#label} should not be empty', + })).required().min(1).messages({ + 'array.sparse': 'each value in entries must be a string', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + entries: [constants.TEST_STREAM_ID_1], +}; + +const responseSchema = Joi.object().keys({ + affected: Joi.number().required().min(0), +}).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups/consumers/pending-messages/ack', () => { + requirements('!rte.crdt'); + + beforeEach(async () => { + await rte.data.generateStrings(true); + await rte.data.generateStreamsWithoutStrictMode(); + }); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_3, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_4, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_1, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_2, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + }); + + [ + { + name: 'Should ack single entry', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + entries: [constants.TEST_STREAM_ID_3], + }, + responseSchema, + responseBody: { affected: 1 }, + before: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(2); + }, + after: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(1); + }, + }, + { + name: 'Should ack single entry and ignore not existing', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + entries: [constants.TEST_STREAM_ID_3, '9999-98', '9999-99'], + }, + responseSchema, + responseBody: { affected: 1 }, + before: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(2); + }, + after: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(1); + }, + }, + { + name: 'Should return affected:0 if group does not exists', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.getRandomString(), + entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4], + }, + responseSchema, + responseBody: { affected: 0 }, + before: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(2); + }, + after: async () => { + const pendingMessages = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + ]) + expect(pendingMessages.length).to.eql(2); + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xack" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xack') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-claim.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-claim.test.ts new file mode 100644 index 0000000000..984a8d2195 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-claim.test.ts @@ -0,0 +1,348 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups/consumers/pending-messages/claim`); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + groupName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + consumerName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + entries: Joi.array().items(Joi.string().required().label('entries').messages({ + 'any.required': '{#label} should not be empty', + })).required().min(1).messages({ + 'array.sparse': 'each value in entries must be a string', + }), + minIdleTime: Joi.number().integer().min(0), + time: Joi.number().integer(), + retryCount: Joi.number().integer().min(0), + force: Joi.boolean(), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_GROUP_1, + minIdleTime: 0, + entries: [constants.TEST_STREAM_ID_1], +}; + +const responseSchema = Joi.object().keys({ + affected: Joi.array().items(Joi.string()).required(), +}).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups/consumers/pending-messages/claim', () => { + requirements('!rte.crdt'); + + beforeEach(async () => { + await rte.data.generateStrings(true); + await rte.data.generateStreamsWithoutStrictMode(); + }); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_3, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_4, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_1, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_2, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + }); + + [ + { + name: 'Should claim single entry', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_2, + entries: [constants.TEST_STREAM_ID_3], + force: true, + }, + responseSchema, + responseBody: { affected: [constants.TEST_STREAM_ID_3] }, + before: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(2); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(0); + }, + after: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(1); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(1); + }, + }, + { + name: 'Should claim multiple entries', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_2, + entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4], + minIdleTime: 0, + idle: 0, + retryCount: 1, + }, + responseSchema, + responseBody: { affected: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4] }, + before: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(2); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(0); + }, + after: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(0); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(2); + }, + }, + { + name: 'Should claim multiple entries out of known consumer', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.getRandomString(), + entries: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4], + minIdleTime: 0, + time: 0, + retryCount: 1, + }, + responseSchema, + responseBody: { affected: [constants.TEST_STREAM_ID_3, constants.TEST_STREAM_ID_4] }, + before: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(2); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(0); + }, + after: async () => { + const consumerOneEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_1, + ]) + expect(consumerOneEntries.length).to.eql(0); + const consumerTwoEntries = await rte.data.sendCommand('xpending', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + '-', '+', 100, + constants.TEST_STREAM_CONSUMER_2, + ]) + expect(consumerTwoEntries.length).to.eql(0); + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if group does not exists', + data: { + ...validInputData, + groupName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Consumer Group with such name was not found.', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xclaim" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xclaim') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-get.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-get.test.ts new file mode 100644 index 0000000000..d036731d69 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-consumers-pending_messages-get.test.ts @@ -0,0 +1,305 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups/consumers/pending-messages/get`); + +const pendingMessageSchema = Joi.object().keys({ + id: Joi.string().required(), + consumerName: Joi.string().required(), + idle: Joi.number().required(), + delivered: Joi.number().required(), +}).strict(); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + groupName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + consumerName: Joi.string().required().messages({ + 'any.required': '{#label} should not be empty', + }), + count: Joi.number(), + start: Joi.string(), + end: Joi.string(), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, +}; + +const responseSchema = Joi.array().items(pendingMessageSchema).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups/consumers/pending-messages/get', () => { + requirements('!rte.crdt'); + + beforeEach(async () => { + await rte.data.generateStrings(true); + await rte.data.generateStreamsWithoutStrictMode(); + }); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_3, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xadd', [ + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_ID_4, + constants.TEST_STREAM_FIELD_1, + constants.TEST_STREAM_VALUE_1, + ]) + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_1, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + await rte.data.sendCommand('xreadgroup', [ + 'GROUP', + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_CONSUMER_2, + 'STREAMS', + constants.TEST_STREAM_KEY_1, + '>', + ]); + }); + + [ + { + name: 'Should return empty array when no pending messages', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_2, + consumerName: constants.TEST_STREAM_CONSUMER_2, + }, + responseSchema, + responseBody: [], + }, + { + name: 'Should return pending messages list with only 1 message', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + count: 1 + }, + responseSchema, + checkFn: ({ body }) => { + const [message] = body; + expect(body.length).to.eql(1); + expect(message.id).to.eq(constants.TEST_STREAM_ID_3); + expect(message.consumerName).to.eq(constants.TEST_STREAM_CONSUMER_1); + expect(message.idle).to.gte(0); + expect(message.delivered).to.eq(1); + } + }, + { + name: 'Should return pending messages list (2 messages)', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eql(2); + } + }, + { + name: 'Should return pending messages list (0 messages) filtered by end', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + start: '-', + end: '99-0', + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eql(0); + } + }, + { + name: 'Should return pending messages list (1 messages) filtered by end', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + start: '-', + end: '300-0', + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eql(1); + } + }, + { + name: 'Should return pending messages list (0 messages) filtered by start', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + start: '999-0', + end: '+', + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eql(0); + } + }, + { + name: 'Should return pending messages list (1 messages) filtered by start', + data: { + keyName: constants.TEST_STREAM_KEY_1, + groupName: constants.TEST_STREAM_GROUP_1, + consumerName: constants.TEST_STREAM_CONSUMER_1, + start: '400-0', + end: '+', + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eql(1); + } + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if group does not exists', + data: { + ...validInputData, + groupName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Consumer Group with such name was not found.', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xpending" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xpending') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-get.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-get.test.ts new file mode 100644 index 0000000000..3d464fcaf4 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups-get.test.ts @@ -0,0 +1,189 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups/get`); + +const consumerGroupSchema = Joi.object().keys({ + name: Joi.string().required(), + consumers: Joi.number().required(), + pending: Joi.number().required(), + lastDeliveredId: Joi.string().required(), + smallestPendingId: Joi.string().allow(null).required(), + greatestPendingId: Joi.string().allow(null).required(), +}); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, +}; + +const responseSchema = Joi.array().items(consumerGroupSchema).min(0).required(); + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups/get', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + [ + { + name: 'Should return empty array when no consumer groups', + data: { + keyName: constants.TEST_STREAM_KEY_2, + }, + responseSchema, + responseBody: [], + }, + { + name: 'Should return groups list', + data: { + keyName: constants.TEST_STREAM_KEY_1, + }, + responseSchema, + checkFn: ({ body }) => { + expect(body.length).to.eq(2); + expect(body[0].name).to.eq(constants.TEST_STREAM_GROUP_1); + expect(body[1].name).to.eq(constants.TEST_STREAM_GROUP_2); + expect(body[1].consumers).to.eq(0); + expect(body[1].pending).to.eq(0); + expect(body[1].lastDeliveredId).to.eq(constants.TEST_STREAM_ID_1); + expect(body[1].smallestPendingId).to.eq(null); + expect(body[1].greatestPendingId).to.eq(null); + } + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xpending" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xpending') + }, + { + name: 'Should throw error if no permissions for "xinfo" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xinfo') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups.test.ts b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups.test.ts new file mode 100644 index 0000000000..a443002535 --- /dev/null +++ b/redisinsight/api/test/api/stream/POST-instance-id-streams-consumer_groups.test.ts @@ -0,0 +1,245 @@ +import { + expect, + describe, + it, + before, + deps, + Joi, + requirements, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + validateApiCall +} from '../deps'; +const { server, request, constants, rte } = deps; + +// endpoint to test +const endpoint = (instanceId = constants.TEST_INSTANCE_ID) => + request(server).post(`/instance/${instanceId}/streams/consumer-groups`); + +const consumerGroupSchema = Joi.object().keys({ + name: Joi.string().label('consumerGroups.0.name').required(), + lastDeliveredId: Joi.string().label('consumerGroups.0.lastDeliveredId').required(), +}); + +const dataSchema = Joi.object({ + keyName: Joi.string().allow('').required(), + consumerGroups: Joi.array().items(consumerGroupSchema).required().messages({ + 'array.sparse': 'entries must be either object or array', + 'array.base': 'property {#label} must be either object or array', + 'any.required': '{#label} should not be empty', + }), +}).strict(); + +const validInputData = { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [ + { + name: 'group-1', + lastDeliveredId: '$', + } + ], +}; + +const mainCheckFn = async (testCase) => { + it(testCase.name, async () => { + // additional checks before test run + if (testCase.before) { + await testCase.before(); + } + + await validateApiCall({ + endpoint, + ...testCase, + }); + + // additional checks after test pass + if (testCase.after) { + await testCase.after(); + } + }); +}; + +describe('POST /instance/:instanceId/streams/consumer-groups', () => { + before(async () => await rte.data.generateKeys(true)); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + }); + + describe('Common', () => { + beforeEach(async () => { + await rte.client.del(constants.TEST_STREAM_KEY_2); + await rte.client.xadd(constants.TEST_STREAM_KEY_2, '*', 'f', 'v'); + }); + + [ + { + name: 'Should create single consumer group', + data: { + keyName: constants.TEST_STREAM_KEY_2, + consumerGroups: [ + { + name: constants.TEST_STREAM_GROUP_1, + lastDeliveredId: constants.TEST_STREAM_ID_1, + } + ], + }, + before: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_2]); + expect(groups.length).to.eq(0); + }, + statusCode: 201, + after: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_2]); + expect(groups).to.deep.eq([ + [ + 'name', constants.TEST_STREAM_GROUP_1, + 'consumers', 0, + 'pending', 0, + 'last-delivered-id', constants.TEST_STREAM_ID_1, + ] + ]); + }, + }, + { + name: 'Should create multiple consumer groups', + data: { + keyName: constants.TEST_STREAM_KEY_2, + consumerGroups: [ + { + name: constants.TEST_STREAM_GROUP_1, + lastDeliveredId: constants.TEST_STREAM_ID_1, + }, + { + name: constants.TEST_STREAM_GROUP_2, + lastDeliveredId: constants.TEST_STREAM_ID_1, + } + ], + }, + before: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_2]); + expect(groups.length).to.eq(0); + }, + statusCode: 201, + after: async () => { + const groups = await rte.data.sendCommand('xinfo', ['groups', constants.TEST_STREAM_KEY_2]); + expect(groups).to.deep.eq([ + [ + 'name', constants.TEST_STREAM_GROUP_1, + 'consumers', 0, + 'pending', 0, + 'last-delivered-id', constants.TEST_STREAM_ID_1, + ], + [ + 'name', constants.TEST_STREAM_GROUP_2, + 'consumers', 0, + 'pending', 0, + 'last-delivered-id', constants.TEST_STREAM_ID_1, + ] + ]); + }, + }, + { + name: 'Should return 409 Conflict error when group exists', + data: { + keyName: constants.TEST_STREAM_KEY_1, + consumerGroups: [ + { + name: constants.TEST_STREAM_GROUP_1, + lastDeliveredId: constants.TEST_STREAM_ID_1, + } + ], + }, + statusCode: 409, + responseBody: { + statusCode: 409, + error: 'Conflict', + }, + }, + { + name: 'Should return BadRequest error if key has another type', + data: { + ...validInputData, + keyName: constants.TEST_STRING_KEY_1, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + }, + { + name: 'Should return NotFound error if key does not exists', + data: { + ...validInputData, + keyName: constants.getRandomString(), + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Key with this name does not exist.', + }, + }, + { + name: 'Should return NotFound error if instance id does not exists', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + ...validInputData, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + error: 'Not Found', + message: 'Invalid database instance id.', + }, + }, + ].map(mainCheckFn); + }); + + describe('ACL', () => { + requirements('rte.acl'); + + before(async () => await rte.data.generateKeys(true)); + before(async () => rte.data.setAclUserRules('~* +@all')); + + [ + { + name: 'Should create consumer group', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + statusCode: 201, + data: { + ...validInputData, + }, + }, + { + name: 'Should throw error if no permissions for "exists" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -exists') + }, + { + name: 'Should throw error if no permissions for "xgroup" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + ...validInputData, + }, + statusCode: 403, + responseBody: { + statusCode: 403, + error: 'Forbidden', + }, + before: () => rte.data.setAclUserRules('~* +@all -xgroup') + }, + ].map(mainCheckFn); + }); +}); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index 80ae28a1d1..620e416051 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -55,6 +55,7 @@ export const constants = { TEST_RTE_ON_PREMISE: process.env.TEST_RTE_ON_PREMISE ? process.env.TEST_RTE_ON_PREMISE === 'true' : true, TEST_RTE_SHARED_DATA: process.env.TEST_RTE_SHARED_DATA ? process.env.TEST_RTE_SHARED_DATA === 'true' : false, TEST_RTE_BIG_DATA: process.env.TEST_RTE_BIG_DATA ? process.env.TEST_RTE_BIG_DATA === 'true' : false, + TEST_RTE_CRDT: process.env.TEST_RTE_CRDT ? process.env.TEST_RTE_CRDT === 'true' : false, TEST_RTE_TYPE: process.env.TEST_RTE_DISCOVERY_TYPE || 'STANDALONE', TEST_RTE_HOST: process.env.TEST_RTE_DISCOVERY_HOST, TEST_RTE_PORT: process.env.TEST_RTE_DISCOVERY_PORT, @@ -165,16 +166,23 @@ export const constants = { // Redis Stream TEST_STREAM_TYPE: 'stream', TEST_STREAM_KEY_1: TEST_RUN_ID + '_stream_1' + CLUSTER_HASH_SLOT, + TEST_STREAM_KEY_2: TEST_RUN_ID + '_stream_2' + CLUSTER_HASH_SLOT, TEST_STREAM_DATA_1: TEST_RUN_ID + '_stream_data_1', TEST_STREAM_DATA_2: TEST_RUN_ID + '_stream_data_2', TEST_STREAM_ID_1: '100-0', TEST_STREAM_FIELD_1: TEST_RUN_ID + '_stream_field_1', TEST_STREAM_VALUE_1: TEST_RUN_ID + '_stream_value_1', TEST_STREAM_ID_2: '200-0', + TEST_STREAM_ID_3: '300-0', + TEST_STREAM_ID_4: '400-0', TEST_STREAM_FIELD_2: TEST_RUN_ID + '_stream_field_2', TEST_STREAM_VALUE_2: TEST_RUN_ID + '_stream_value_2', TEST_STREAM_EXPIRE_1: KEY_TTL, TEST_STREAM_HUGE_KEY: TEST_RUN_ID + '_stream_huge' + CLUSTER_HASH_SLOT, + TEST_STREAM_GROUP_1: TEST_RUN_ID + '_stream_group_1', + TEST_STREAM_CONSUMER_1: TEST_RUN_ID + '_stream_consumer_1', + TEST_STREAM_GROUP_2: TEST_RUN_ID + '_stream_group_2', + TEST_STREAM_CONSUMER_2: TEST_RUN_ID + '_stream_consumer_2', // ReJSON-RL TEST_REJSON_TYPE: 'ReJSON-RL', diff --git a/redisinsight/api/test/helpers/data/redis.ts b/redisinsight/api/test/helpers/data/redis.ts index f383726a22..5184979fa9 100644 --- a/redisinsight/api/test/helpers/data/redis.ts +++ b/redisinsight/api/test/helpers/data/redis.ts @@ -1,10 +1,17 @@ import { get } from 'lodash'; import { constants } from '../constants'; import * as _ from 'lodash'; +import * as IORedis from 'ioredis'; export const initDataHelper = (rte) => { const client = rte.client; + const sendCommand = async (command: string, args: string[], replyEncoding = 'utf8'): Promise => { + return client.sendCommand(new IORedis.Command(command, args, { + replyEncoding, + })); + }; + const executeCommand = async (...args: string[]): Promise => { return client.nodes ? Promise.all(client.nodes('master').map(async (node) => { try { @@ -229,6 +236,40 @@ export const initDataHelper = (rte) => { } await client.xadd(constants.TEST_STREAM_KEY_1, '*', constants.TEST_STREAM_FIELD_1, constants.TEST_STREAM_VALUE_1) + await sendCommand('xgroup', [ + 'create', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_ID_1 + ]) + await sendCommand('xgroup', [ + 'create', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_2, + constants.TEST_STREAM_ID_1 + ]) + await client.xadd(constants.TEST_STREAM_KEY_2, '*', constants.TEST_STREAM_FIELD_1, constants.TEST_STREAM_VALUE_1) + }; + + const generateStreamsWithoutStrictMode = async (clean: boolean = false) => { + if (clean) { + await truncate(); + } + + await client.xadd(constants.TEST_STREAM_KEY_1, constants.TEST_STREAM_ID_1, constants.TEST_STREAM_FIELD_1, constants.TEST_STREAM_VALUE_1) + await sendCommand('xgroup', [ + 'create', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_1, + constants.TEST_STREAM_ID_1 + ]) + await sendCommand('xgroup', [ + 'create', + constants.TEST_STREAM_KEY_1, + constants.TEST_STREAM_GROUP_2, + constants.TEST_STREAM_ID_1 + ]) + await client.xadd(constants.TEST_STREAM_KEY_2, constants.TEST_STREAM_ID_1, constants.TEST_STREAM_FIELD_1, constants.TEST_STREAM_VALUE_1) }; const generateHugeStream = async (number: number = 100000, clean: boolean) => { @@ -329,6 +370,7 @@ export const initDataHelper = (rte) => { } return { + sendCommand, executeCommand, executeCommandAll, setAclUserRules, @@ -340,6 +382,9 @@ export const initDataHelper = (rte) => { generateNKeys, generateNReJSONs, generateNTimeSeries, + generateStrings, + generateStreams, + generateStreamsWithoutStrictMode, generateNStreams, generateNGraphs, getClientNodes, diff --git a/redisinsight/api/test/helpers/redis.ts b/redisinsight/api/test/helpers/redis.ts index 98aa29a81c..bd44806f65 100644 --- a/redisinsight/api/test/helpers/redis.ts +++ b/redisinsight/api/test/helpers/redis.ts @@ -185,6 +185,7 @@ export const initRTE = async () => { cloud: !!constants.TEST_CLOUD_RTE, sharedData: constants.TEST_RTE_SHARED_DATA, bigData: constants.TEST_RTE_BIG_DATA, + crdt: constants.TEST_RTE_CRDT, nodes: [], }; diff --git a/redisinsight/api/test/helpers/test.ts b/redisinsight/api/test/helpers/test.ts index 8d0c83679a..9b6c94f3da 100644 --- a/redisinsight/api/test/helpers/test.ts +++ b/redisinsight/api/test/helpers/test.ts @@ -5,7 +5,7 @@ import * as fs from 'fs'; import * as chai from 'chai'; import * as Joi from 'joi'; import * as diff from 'object-diff'; -import { cloneDeep, isMatch, isObject, set } from 'lodash'; +import { cloneDeep, isMatch, isObject, set, isArray } from 'lodash'; import { generateInvalidDataArray } from './test/dataGenerator'; export { _, fs } @@ -84,6 +84,10 @@ export const validateApiCall = async function ({ */ export const checkResponseBody = (body, expected) => { try { + if (isArray(expected)) { + return expect(body).to.deep.eq(expected); + } + if (isObject(expected)) { return expect(isMatch(body, expected)).to.eql(true); } diff --git a/redisinsight/api/test/test-runs/re-crdt/.env b/redisinsight/api/test/test-runs/re-crdt/.env index 0499f8d08a..91b052aa7e 100644 --- a/redisinsight/api/test/test-runs/re-crdt/.env +++ b/redisinsight/api/test/test-runs/re-crdt/.env @@ -3,3 +3,4 @@ TEST_RE_USER=demo@redislabs.com TEST_RE_PASS=123456 TEST_REDIS_PORT=12000 TEST_RTE_SHARED_DATA=true +TEST_RTE_CRDT=true