From 2407e9adb6228eba5a6fbcbddbd8e3b7e17b2f55 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 22 Aug 2022 17:21:18 +0300 Subject: [PATCH 001/107] Update package.json --- redisinsight/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/package.json b/redisinsight/package.json index 1c26558377..385d8c0646 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.6.0", + "version": "2.10.0", "description": "RedisInsight", "main": "./main.prod.js", "author": { From 33ca8698798eac661a3845758766a26edcc45b3d Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Wed, 28 Sep 2022 15:16:39 +0300 Subject: [PATCH 002/107] increase version --- redisinsight/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/package.json b/redisinsight/package.json index 82dfb8fa53..926352537e 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.10.0", + "version": "2.12.0", "description": "RedisInsight", "main": "./main.prod.js", "author": { From 9471191b8a95998b15e470d3deff78dd7fc7e0b6 Mon Sep 17 00:00:00 2001 From: ALENA NABOKA Date: Wed, 26 Oct 2022 13:23:29 +0300 Subject: [PATCH 003/107] increase RI version --- redisinsight/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/package.json b/redisinsight/package.json index 82dfb8fa53..2cbd657ba9 100644 --- a/redisinsight/package.json +++ b/redisinsight/package.json @@ -2,7 +2,7 @@ "name": "redisinsight", "productName": "RedisInsight", "private": true, - "version": "2.10.0", + "version": "2.14.0", "description": "RedisInsight", "main": "./main.prod.js", "author": { From 22c4fa18d4f0a3eb1426a21ae6d253f4f4ee2e34 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 8 Nov 2022 10:42:12 +0300 Subject: [PATCH 004/107] #RI-3780 databases import. BE base implementation --- redisinsight/api/config/stack.ts | 1 + redisinsight/api/src/app.module.ts | 2 + .../database-import.controller.ts | 37 ++++++ .../database-import/database-import.module.ts | 11 ++ .../database-import.service.ts | 121 ++++++++++++++++++ .../dto/database-import.response.ts | 15 +++ .../dto/import.database.dto.ts | 3 + .../modules/database/database.controller.ts | 2 +- .../src/modules/database/database.module.ts | 1 + 9 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 redisinsight/api/src/modules/database-import/database-import.controller.ts create mode 100644 redisinsight/api/src/modules/database-import/database-import.module.ts create mode 100644 redisinsight/api/src/modules/database-import/database-import.service.ts create mode 100644 redisinsight/api/src/modules/database-import/dto/database-import.response.ts create mode 100644 redisinsight/api/src/modules/database-import/dto/import.database.dto.ts diff --git a/redisinsight/api/config/stack.ts b/redisinsight/api/config/stack.ts index 71e76d2b12..ea8df928a4 100644 --- a/redisinsight/api/config/stack.ts +++ b/redisinsight/api/config/stack.ts @@ -5,6 +5,7 @@ export default { excludeRoutes: [ 'redis-enterprise/*', 'redis-sentinel/*', + { path: 'databases/import' }, { path: 'databases', method: RequestMethod.POST }, { path: 'databases', method: RequestMethod.DELETE }, { path: 'databases/:id', method: RequestMethod.DELETE }, diff --git a/redisinsight/api/src/app.module.ts b/redisinsight/api/src/app.module.ts index 26dada6d44..8708586dd7 100644 --- a/redisinsight/api/src/app.module.ts +++ b/redisinsight/api/src/app.module.ts @@ -19,6 +19,7 @@ import { ServerModule } from 'src/modules/server/server.module'; import { LocalDatabaseModule } from 'src/local-database.module'; import { CoreModule } from 'src/core.module'; import { AutodiscoveryModule } from 'src/modules/autodiscovery/autodiscovery.module'; +import { DatabaseImportModule } from 'src/modules/database-import/database-import.module'; import { BrowserModule } from './modules/browser/browser.module'; import { RedisEnterpriseModule } from './modules/redis-enterprise/redis-enterprise.module'; import { RedisSentinelModule } from './modules/redis-sentinel/redis-sentinel.module'; @@ -52,6 +53,7 @@ const PATH_CONFIG = config.get('dir_path'); BulkActionsModule, ClusterMonitorModule, DatabaseAnalysisModule, + DatabaseImportModule, ...(SERVER_CONFIG.staticContent ? [ ServeStaticModule.forRoot({ diff --git a/redisinsight/api/src/modules/database-import/database-import.controller.ts b/redisinsight/api/src/modules/database-import/database-import.controller.ts new file mode 100644 index 0000000000..3379fd7950 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.controller.ts @@ -0,0 +1,37 @@ +import { + Controller, Post, UploadedFile, UseInterceptors, UsePipes, ValidationPipe, +} from '@nestjs/common'; +import { + ApiBody, ApiConsumes, ApiResponse, ApiTags, +} from '@nestjs/swagger'; +import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; + +@UsePipes(new ValidationPipe({ transform: true })) +@ApiTags('Database') +@Controller('/databases') +export class DatabaseImportController { + constructor(private readonly service: DatabaseImportService) {} + + @Post('import') + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + }, + }, + }) + @UseInterceptors(FileInterceptor('file')) + @ApiResponse({ type: DatabaseImportResponse }) + async import( + @UploadedFile() file: any, + ): Promise { + return this.service.import(file); + } +} diff --git a/redisinsight/api/src/modules/database-import/database-import.module.ts b/redisinsight/api/src/modules/database-import/database-import.module.ts new file mode 100644 index 0000000000..eac32e7d5f --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.module.ts @@ -0,0 +1,11 @@ +import { Module } from '@nestjs/common'; +import { DatabaseImportController } from 'src/modules/database-import/database-import.controller'; +import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; + +@Module({ + controllers: [DatabaseImportController], + providers: [ + DatabaseImportService, + ], +}) +export class DatabaseImportModule {} diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts new file mode 100644 index 0000000000..507b5276fb --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -0,0 +1,121 @@ +import { BadRequestException, Injectable } from '@nestjs/common'; +import { isArray, get, set } from 'lodash'; +import { Database } from 'src/modules/database/models/database'; +import { plainToClass } from 'class-transformer'; +import { ConnectionType } from 'src/modules/database/entities/database.entity'; +import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; +import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; + +@Injectable() +export class DatabaseImportService { + private fieldsMapSchema: Array<[string, string[]]> = [ + ['name', ['name', 'connectionName']], + ['username', ['username']], + ['password', ['password', 'auth']], + ['host', ['host']], + ['port', ['port']], + ['db', ['db']], + ['isCluster', ['cluster']], + ]; + + constructor( + private readonly databaseRepository: DatabaseRepository, + ) {} + + public async import(file): Promise { + const items = DatabaseImportService.parseFile(file); + + if (!isArray(items) || !items?.length) { + let filename = file?.originalname || 'import file'; + if (filename.length > 50) { + filename = `${filename.slice(0, 50)}...`; + } + return Promise.reject(new BadRequestException(`Unable to parse ${filename}`)); + } + + const response = { + total: items.length, + success: 0, + }; + + // it is very important to insert databases on-by-one to avoid db constraint errors + await items.reduce((prev, item) => prev.finally(() => this.createDatabase(item) + .then(() => { + response.success += 1; + }) + .catch(() => { /* just ignore errors */ })), Promise.resolve()); + + return plainToClass(DatabaseImportResponse, response); + } + + /** + * Map data to known model, validate it and create database if possible + * Note: will not create connection, simply create database + * @param item + * @private + */ + private async createDatabase(item: any[]): Promise { + const data: any = {}; + + this.fieldsMapSchema.forEach(([field, paths]) => { + let value; + + paths.every((path) => { + value = get(item, path); + return value === undefined; + }); + + set(data, field, value); + }); + + // set database name if needed + if (!data.name) { + data.name = `${data.host}:${data.port}`; + } + + // determine database type + if (data.isCluster) { + data.connectionType = ConnectionType.CLUSTER; + } else { + data.connectionType = ConnectionType.STANDALONE; + } + + const database = plainToClass(Database, data); + + return this.databaseRepository.create(database); + } + + /** + * Try to parse file based on mimetype and known\supported formats + * @param file + */ + static parseFile(file): any { + const data = file?.buffer?.toString(); + + let databases; + + if (file.mimetype === 'application/json') { + databases = DatabaseImportService.parseJson(data); + } else { + databases = DatabaseImportService.parseBase64(data); + } + + return databases; + } + + static parseBase64(data: string): any { + try { + return JSON.parse((Buffer.from(data, 'base64')).toString('utf8')); + } catch (e) { + return null; + } + } + + static parseJson(data: string): any { + try { + return JSON.parse(data); + } catch (e) { + return null; + } + } +} diff --git a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts new file mode 100644 index 0000000000..f6ec120409 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DatabaseImportResponse { + @ApiProperty({ + description: 'Total elements processed from the import file', + type: Number, + }) + total: number; + + @ApiProperty({ + description: 'Number of imported database', + type: Number, + }) + success: number; +} diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts new file mode 100644 index 0000000000..a2abaa7cac --- /dev/null +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -0,0 +1,3 @@ +export class ImportDatabaseDto { + +} diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index 7f99dfa4ce..da8e9e6c7b 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -16,7 +16,7 @@ import { BuildType } from 'src/modules/server/models/server'; import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dto'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; -@ApiTags('Database Instances') +@ApiTags('Database') @Controller('databases') export class DatabaseController { constructor( diff --git a/redisinsight/api/src/modules/database/database.module.ts b/redisinsight/api/src/modules/database/database.module.ts index 286e20da18..110b12cacf 100644 --- a/redisinsight/api/src/modules/database/database.module.ts +++ b/redisinsight/api/src/modules/database/database.module.ts @@ -41,6 +41,7 @@ export class DatabaseModule { }, ], exports: [ + DatabaseRepository, DatabaseService, DatabaseConnectionService, // todo: rethink everything below From 97417a13e96d67b8433b18076b247d2fe2ca8d9c Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 9 Nov 2022 10:13:43 +0300 Subject: [PATCH 005/107] Change cluster connection + add data validation --- .../database-import.service.ts | 34 +++++++++++++++++-- .../dto/import.database.dto.ts | 7 ++-- .../database/database-connection.service.ts | 20 +++++++++-- .../api/src/modules/redis/redis.service.ts | 7 ++-- 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 507b5276fb..323f22f49b 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -1,13 +1,20 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { isArray, get, set } from 'lodash'; import { Database } from 'src/modules/database/models/database'; import { plainToClass } from 'class-transformer'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; +import { Validator } from 'class-validator'; +import { ImportDatabaseDto } from 'src/modules/database-import/dto/import.database.dto'; +import { classToClass } from 'src/utils'; @Injectable() export class DatabaseImportService { + private logger = new Logger('DatabaseImportService'); + + private validator = new Validator(); + private fieldsMapSchema: Array<[string, string[]]> = [ ['name', ['name', 'connectionName']], ['username', ['username']], @@ -22,6 +29,10 @@ export class DatabaseImportService { private readonly databaseRepository: DatabaseRepository, ) {} + /** + * Import databases from the file + * @param file + */ public async import(file): Promise { const items = DatabaseImportService.parseFile(file); @@ -80,7 +91,26 @@ export class DatabaseImportService { data.connectionType = ConnectionType.STANDALONE; } - const database = plainToClass(Database, data); + const dto = plainToClass( + ImportDatabaseDto, + // additionally replace empty strings ("") with null + Object.keys(data) + .reduce((acc, key) => { + acc[key] = data[key] === '' ? null : data[key]; + return acc; + }, {}), + ); + + try { + await this.validator.validateOrReject(dto, { + whitelist: true, + }); + } catch (e) { + this.logger.warn('Invalid data for database import entry', e); + return Promise.reject(e); + } + + const database = classToClass(Database, dto); return this.databaseRepository.create(database); } diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index a2abaa7cac..dfb410531f 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -1,3 +1,6 @@ -export class ImportDatabaseDto { +import { PickType } from '@nestjs/swagger'; +import { Database } from 'src/modules/database/models/database'; -} +export class ImportDatabaseDto extends PickType(Database, [ + 'host', 'port', 'name', 'db', 'username', 'password', +] as const) {} diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index 0c9be37a1d..932458daaa 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -8,6 +8,7 @@ import { RedisService } from 'src/modules/redis/redis.service'; import { ClientMetadata } from 'src/modules/redis/models/client-metadata'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; +import { Database } from 'src/modules/database/models/database'; @Injectable() export class DatabaseConnectionService { @@ -38,10 +39,25 @@ export class DatabaseConnectionService { // refresh modules list and last connected time // will be refreshed after user navigate to particular database from the databases list // Note: move to a different place in case if we need to update such info more often - await this.repository.update(databaseId, { + const toUpdate: Partial = { lastConnection: new Date(), modules: await this.databaseInfoProvider.determineDatabaseModules(client), - }); + }; + + // !Temporary. Refresh cluster nodes on connection + if (client?.isCluster) { + const primaryNodeOptions = client.nodes('master')[0].options; + + toUpdate.host = primaryNodeOptions.host; + toUpdate.port = primaryNodeOptions.port; + + toUpdate.nodes = client.nodes().map(({ options }) => ({ + host: options.host, + port: options.port, + })); + } + + await this.repository.update(databaseId, toUpdate); this.logger.log(`Succeed to connect to database ${databaseId}`); } diff --git a/redisinsight/api/src/modules/redis/redis.service.ts b/redisinsight/api/src/modules/redis/redis.service.ts index 9199131181..1cd9fe10b6 100644 --- a/redisinsight/api/src/modules/redis/redis.service.ts +++ b/redisinsight/api/src/modules/redis/redis.service.ts @@ -96,14 +96,17 @@ export class RedisService { public async createClusterClient( database: Database, - nodes: IRedisClusterNodeAddress[], + nodes: IRedisClusterNodeAddress[] = [], useRetry: boolean = false, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, ): Promise { const config = await this.getRedisConnectionConfig(database); return new Promise((resolve, reject) => { try { - const cluster = new Redis.Cluster(nodes, { + const cluster = new Redis.Cluster([{ + host: database.host, + port: database.port, + }].concat(nodes), { clusterRetryStrategy: useRetry ? this.retryStrategy : () => undefined, redisOptions: { ...config, From 3e168fee0036dbe26411b6c026a8ba40653aeaa0 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 10 Nov 2022 12:18:11 +0300 Subject: [PATCH 006/107] finilize + Itests --- .../database-import.controller.ts | 13 +- .../database-import.service.ts | 2 +- .../dto/import.database.dto.ts | 1 + .../src/modules/database/models/database.ts | 3 +- .../POST-databases-import.test.ts | 281 ++++++++++++++++++ redisinsight/api/test/helpers/test.ts | 6 + 6 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 redisinsight/api/test/api/database-import/POST-databases-import.test.ts diff --git a/redisinsight/api/src/modules/database-import/database-import.controller.ts b/redisinsight/api/src/modules/database-import/database-import.controller.ts index 3379fd7950..4d1e9e5bfa 100644 --- a/redisinsight/api/src/modules/database-import/database-import.controller.ts +++ b/redisinsight/api/src/modules/database-import/database-import.controller.ts @@ -1,5 +1,7 @@ import { - Controller, Post, UploadedFile, UseInterceptors, UsePipes, ValidationPipe, + BadRequestException, + Controller, HttpCode, Post, UploadedFile, + UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiResponse, ApiTags, @@ -27,11 +29,20 @@ export class DatabaseImportController { }, }, }) + @HttpCode(200) @UseInterceptors(FileInterceptor('file')) @ApiResponse({ type: DatabaseImportResponse }) async import( @UploadedFile() file: any, ): Promise { + // todo: create FileValidation class + if (!file) { + throw new BadRequestException('No import file provided'); + } + if (file?.size > 1024 * 1024 * 10) { + throw new BadRequestException('Import file is too big. Maximum 10mb allowed'); + } + return this.service.import(file); } } diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 323f22f49b..efa63f2e23 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -124,7 +124,7 @@ export class DatabaseImportService { let databases; - if (file.mimetype === 'application/json') { + if (file?.mimetype === 'application/json') { databases = DatabaseImportService.parseJson(data); } else { databases = DatabaseImportService.parseBase64(data); diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index dfb410531f..dbc83070d9 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -3,4 +3,5 @@ import { Database } from 'src/modules/database/models/database'; export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', + 'connectionType', ] as const) {} diff --git a/redisinsight/api/src/modules/database/models/database.ts b/redisinsight/api/src/modules/database/models/database.ts index 27731772b7..8531689622 100644 --- a/redisinsight/api/src/modules/database/models/database.ts +++ b/redisinsight/api/src/modules/database/models/database.ts @@ -4,7 +4,7 @@ import { CaCertificate } from 'src/modules/certificate/models/ca-certificate'; import { ClientCertificate } from 'src/modules/certificate/models/client-certificate'; import { ConnectionType, HostingProvider } from 'src/modules/database/entities/database.entity'; import { - IsBoolean, + IsBoolean, IsEnum, IsInt, IsNotEmpty, IsNotEmptyObject, @@ -97,6 +97,7 @@ export class Database { enum: ConnectionType, }) @Expose() + @IsEnum(ConnectionType) connectionType: ConnectionType; @ApiPropertyOptional({ diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts new file mode 100644 index 0000000000..3786b9a10b --- /dev/null +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -0,0 +1,281 @@ +import { + Joi, + expect, + describe, + it, + deps, + requirements, + validateApiCall, + getMainCheckFn, generateInvalidDataArray, +} from '../deps'; +import { randomBytes } from 'crypto'; +import { cloneDeep, set } from 'lodash'; +const { rte, request, server, localDb, constants } = deps; + +const endpoint = () => request(server).post(`/${constants.API.DATABASES}/import`); + +// input data schema +const databaseSchema = Joi.object({ + name: Joi.string().allow(null, ''), + host: Joi.string().required(), + port: Joi.number().integer().required(), + db: Joi.number().integer().allow(null, ''), + username: Joi.string().allow(null, ''), + password: Joi.string().allow(null, ''), +}).messages({ + 'any.required': '{#label} should not be empty', +}).strict(true); + +const validInputData = { + name: constants.getRandomString(), + host: constants.getRandomString(), + port: 111, +}; + +const baseDatabaseData = { + name: 'someName', + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: constants.TEST_REDIS_USER || '', + password: constants.TEST_REDIS_PASSWORD || '', +} + +const baseSentinelData = { + name: constants.TEST_SENTINEL_MASTER_GROUP, + username: constants.TEST_SENTINEL_MASTER_USER || null, + password: constants.TEST_SENTINEL_MASTER_PASS || null, +} + +const importDatabaseFormat1 = { + name: baseDatabaseData.name, + host: baseDatabaseData.host, + port: baseDatabaseData.port, + username: baseDatabaseData.username, + auth: baseDatabaseData.password, +} + +const mainCheckFn = getMainCheckFn(endpoint); + +describe('POST /databases/import', () => { + describe('Validation', function () { + generateInvalidDataArray(databaseSchema) + .map(({ path, value }) => { + const database = path?.length ? set(cloneDeep(validInputData), path, value) : value; + return { + name: `Should not import when database: ${path.join('.')} = "${value}"`, + attach: ['file', Buffer.from(JSON.stringify([database])), 'file.json'], + responseBody: { + total: 1, + success: 0, + } + } + }) + .map(async (testCase) => { + it(testCase.name, async () => { + await validateApiCall({ + endpoint, + ...testCase, + }); + }); + }); + + [ + { + name: 'Should fail due to file was not provided', + statusCode: 400, + responseBody: { + statusCode: 400, + message: 'No import file provided', + error: 'Bad Request', + }, + }, + { + name: 'Should fail due to file size (>10mb)', + statusCode: 400, + attach: ['file', randomBytes(11 * 1024 * 1024), 'filename.json'], + responseBody: { + statusCode: 400, + message: 'Import file is too big. Maximum 10mb allowed', + error: 'Bad Request', + }, + }, + { + name: 'Should fail to incorrect file format', + statusCode: 400, + attach: ['file', randomBytes(10), 'filename.json'], + responseBody: { + statusCode: 400, + message: 'Unable to parse filename.json', + error: 'Bad Request', + }, + }, + { + name: 'Should truncate error message', + statusCode: 400, + attach: ['file', randomBytes(10), new Array(10_000).fill(1).join('')], + responseBody: { + statusCode: 400, + message: `Unable to parse ${new Array(50).fill(1).join('')}...`, + error: 'Bad Request', + }, + }, + { + name: 'Should return 0/0 imported if mandatory field was not defined (host)', + statusCode: 400, + attach: ['file', randomBytes(10), new Array(10_000).fill(1).join('')], + responseBody: { + statusCode: 400, + message: `Unable to parse ${new Array(50).fill(1).join('')}...`, + error: 'Bad Request', + }, + }, + ].map(mainCheckFn); + }); + describe('STANDALONE', () => { + requirements('rte.type=STANDALONE'); + describe('NO TLS', function () { + requirements('!rte.tls'); + it('Import standalone without tls (format 1)', async () => { + const name = constants.getRandomString(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: 1, + }, + }); + + // check connection + const database = await localDb.getInstanceByName(name); + await validateApiCall({ + endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), + statusCode: 200, + }); + }); + describe('Oss', () => { + requirements('!rte.re'); + it('Import standalone with particular db index', async () => { + const name = constants.getRandomString(); + const cliUuid = constants.getRandomString(); + const browserKeyName = constants.getRandomString(); + const cliKeyName = constants.getRandomString(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + name, + db: constants.TEST_REDIS_DB_INDEX, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: 1, + }, + }); + + // check connection + const database = await localDb.getInstanceByName(name); + await validateApiCall({ + endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), + statusCode: 200, + }); + + // Create string using Browser API to particular db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${database.id}/string`), + statusCode: 201, + data: { + keyName: browserKeyName, + value: 'somevalue' + }, + }); + + // Create client for CLI + await validateApiCall({ + endpoint: () => request(server).patch(`/${constants.API.DATABASES}/${database.id}/cli/${cliUuid}`), + statusCode: 200, + }); + + // Create string using CLI API to 0 db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${database.id}/cli/${cliUuid}/send-command`), + statusCode: 200, + data: { + command: `set ${cliKeyName} somevalue`, + }, + }); + + + // check data created by db index + await rte.data.executeCommand('select', `${constants.TEST_REDIS_DB_INDEX}`); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(1) + + // check data created by db index + await rte.data.executeCommand('select', '0'); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(0) + }); + }); + }); + xdescribe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + }); + xdescribe('TLS AUTH', function () { + requirements('rte.tls', 'rte.tlsAuth'); + }); + }); + describe('CLUSTER', () => { + requirements('rte.type=CLUSTER'); + describe('NO TLS', function () { + requirements('!rte.tls'); + it('should import cluster database (base64)', async () => { + const name = constants.getRandomString(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + name, + cluster: true, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: 1, + }, + }); + + + // check connection + const database = await localDb.getInstanceByName(name); + + expect(database.nodes).to.eq('[]'); + expect(database.connectionType).to.eq('CLUSTER'); + + await validateApiCall({ + endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), + statusCode: 200, + }); + + expect((await localDb.getInstanceByName(name)).nodes).to.not.eq('[]'); + }); + }); + xdescribe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + }); + }); + xdescribe('SENTINEL', () => { + requirements('rte.type=SENTINEL'); + }); +}); diff --git a/redisinsight/api/test/helpers/test.ts b/redisinsight/api/test/helpers/test.ts index d1fd25ccce..7b73ca38e9 100644 --- a/redisinsight/api/test/helpers/test.ts +++ b/redisinsight/api/test/helpers/test.ts @@ -19,6 +19,7 @@ export * from './test/dataGenerator'; interface ITestCaseInput { endpoint: Function; // function that returns prepared supertest with url data?: any; + attach?: any[]; query?: any; statusCode?: number; responseSchema?: Joi.AnySchema; @@ -35,6 +36,7 @@ interface ITestCaseInput { export const validateApiCall = async function ({ endpoint, data, + attach, query, statusCode = 200, responseSchema, @@ -48,6 +50,10 @@ export const validateApiCall = async function ({ request.send(typeof data === 'function' ? data() : data); } + if (attach) { + request.attach(...attach); + } + // data to send with url query string if (query) { request.query(query); From ebde0a562d58dcdd9453ff82cdb7db7092939e5b Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Mon, 14 Nov 2022 15:22:18 +0400 Subject: [PATCH 007/107] #RI-3779 - add import db connections --- .../ImportDatabasesDialog.spec.tsx | 122 ++++++++++++ .../ImportDatabasesDialog.tsx | 170 ++++++++++++++++ .../import-databases-dialog/index.ts | 3 + .../styles.module.scss | 70 +++++++ redisinsight/ui/src/components/index.ts | 2 + redisinsight/ui/src/constants/api.ts | 1 + .../components/HomeHeader/HomeHeader.spec.tsx | 40 +++- .../home/components/HomeHeader/HomeHeader.tsx | 185 +++++++++++------- .../components/HomeHeader/styles.module.scss | 13 +- .../ui/src/slices/instances/instances.ts | 64 ++++++ .../ui/src/slices/interfaces/instances.ts | 8 + .../slices/tests/instances/instances.spec.ts | 157 +++++++++++++++ redisinsight/ui/src/telemetry/events.ts | 3 + 13 files changed, 766 insertions(+), 72 deletions(-) create mode 100644 redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx create mode 100644 redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx create mode 100644 redisinsight/ui/src/components/import-databases-dialog/index.ts create mode 100644 redisinsight/ui/src/components/import-databases-dialog/styles.module.scss diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx new file mode 100644 index 0000000000..61be09a4f4 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx @@ -0,0 +1,122 @@ +import { waitFor } from '@testing-library/react' +import { cloneDeep } from 'lodash' +import React from 'react' +import { importInstancesFromFile, importInstancesSelector } from 'uiSrc/slices/instances/instances' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { render, screen, fireEvent, mockedStore, cleanup } from 'uiSrc/utils/test-utils' + +import ImportDatabasesDialog from './ImportDatabasesDialog' + +jest.mock('uiSrc/slices/instances/instances', () => ({ + ...jest.requireActual('uiSrc/slices/instances/instances'), + importInstancesSelector: jest.fn().mockReturnValue({ + loading: false, + error: '', + data: null + }) +})) + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('ImportDatabasesDialog', () => { + it('should render', () => { + expect(render()).toBeTruthy() + }) + + it('should call onClose', () => { + const onClose = jest.fn() + render() + + fireEvent.click(screen.getByTestId('cancel-btn')) + + expect(onClose).toBeCalled() + }) + + it('submit btn should be disabled without file', () => { + render() + + expect(screen.getByTestId('submit-btn')).toBeDisabled() + }) + + it('should call proper actions and send telemetry', async () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + render() + + const jsonString = JSON.stringify({}) + const blob = new Blob([jsonString]) + const file = new File([blob], 'empty.json', { + type: 'application/JSON', + }) + + await waitFor(() => { + fireEvent.change( + screen.getByTestId('import-databases-input-file'), + { + target: { files: [file] }, + } + ) + }) + + expect(screen.getByTestId('submit-btn')).not.toBeDisabled() + fireEvent.click(screen.getByTestId('submit-btn')) + + const expectedActions = [importInstancesFromFile()] + expect(store.getActions()).toEqual(expectedActions) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + }) + + it('should render loading indicator', () => { + (importInstancesSelector as jest.Mock).mockImplementation(() => ({ + loading: true, + data: null + })) + + render() + expect(screen.getByTestId('file-loading-indicator')).toBeInTheDocument() + }) + + it('should render success message when at least 1 database added', () => { + (importInstancesSelector as jest.Mock).mockImplementation(() => ({ + loading: false, + data: { + success: 1, + total: 2 + } + })) + + render() + expect(screen.getByTestId('result-success')).toBeInTheDocument() + expect(screen.queryByTestId('result-failed')).not.toBeInTheDocument() + }) + + it('should render error message when 0 success databases added', () => { + (importInstancesSelector as jest.Mock).mockImplementation(() => ({ + loading: false, + data: { + success: 0, + total: 2 + } + })) + + render() + expect(screen.getByTestId('result-failed')).toBeInTheDocument() + expect(screen.queryByTestId('result-success')).not.toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx new file mode 100644 index 0000000000..ea91f9f781 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -0,0 +1,170 @@ +import { + EuiButton, + EuiFilePicker, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiLoadingSpinner, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, + EuiTextColor, + EuiTitle +} from '@elastic/eui' +import React, { useState } from 'react' +import { useDispatch, useSelector } from 'react-redux' +import { + fetchInstancesAction, + importInstancesSelector, + resetImportInstances, + uploadInstancesFile +} from 'uiSrc/slices/instances/instances' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { Nullable } from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface Props { + onClose: (isCancelled: boolean) => void +} + +const MAX_MB_FILE = 10 +const MAX_FILE_SIZE = MAX_MB_FILE * 1024 * 1024 + +const ImportDatabasesDialog = ({ onClose }: Props) => { + const { loading, data, error } = useSelector(importInstancesSelector) + const [files, setFiles] = useState>(null) + const [isInvalid, setIsInvalid] = useState(false) + const [isSubmitDisabled, setIsSubmitDisabled] = useState(true) + + const dispatch = useDispatch() + + const onFileChange = (files: FileList | null) => { + setFiles(files) + setIsInvalid(!!files?.length && files?.[0].size > MAX_FILE_SIZE) + setIsSubmitDisabled(!files?.length || files[0].size > MAX_FILE_SIZE) + } + + const handleOnClose = () => { + dispatch(resetImportInstances()) + data?.success && dispatch(fetchInstancesAction()) + onClose(!data) + } + + const onSubmit = () => { + if (files) { + const formData = new FormData() + formData.append('file', files[0]) + + dispatch(uploadInstancesFile(formData)) + + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED + }) + } + } + + const isShowForm = !loading && !data && !error + return ( + + + + + Import Database Connections + + + + + + + + {isShowForm && ( + <> + + {isInvalid && ( + + File should not exceed {MAX_MB_FILE} mb + + )} + + )} + + {loading && ( +
+ + Uploading... +
+ )} + + {data && data.success !== 0 && ( +
+ + + Successfully added {data.success} of {data.total} database connections + +
+ )} + + {(data?.success === 0 || error) && ( +
+ + + Failed to add database connections + +
+ )} +
+
+
+ + {data && data.success !== 0 && ( + + + Ok + + + )} + + {isShowForm && ( + + + Cancel + + + + Import + + + )} +
+ ) +} + +export default ImportDatabasesDialog diff --git a/redisinsight/ui/src/components/import-databases-dialog/index.ts b/redisinsight/ui/src/components/import-databases-dialog/index.ts new file mode 100644 index 0000000000..2a083df701 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/index.ts @@ -0,0 +1,3 @@ +import ImportDatabasesDialog from './ImportDatabasesDialog' + +export default ImportDatabasesDialog diff --git a/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss new file mode 100644 index 0000000000..3f240cd6bf --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss @@ -0,0 +1,70 @@ +.modal { + background: var(--euiColorLightestShade) !important; + min-width: 500px !important; + min-height: 270px !important; + + :global { + .euiModalHeader { + padding: 4px 42px 42px 30px; + } + + .euiModalBody__overflow { + padding: 8px 30px; + } + + .euiModal__closeIcon { + top: 16px; + right: 16px; + background: none; + } + + .euiButtonEmpty.euiButtonEmpty--primary.euiFilePicker__clearButton, + .euiButtonEmpty.euiButtonEmpty--primary.euiFilePicker__clearButton .euiButtonEmpty__text { + color: var(--externalLinkColor) !important; + } + + .euiModalFooter { + margin-top: 12px; + } + } + + .errorFileMsg { + margin-top: 10px; + font-size: 12px; + } + + .fileDrop { + width: 300px; + + :global { + .euiFilePicker__showDrop .euiFilePicker__prompt, .euiFilePicker__input:focus + .euiFilePicker__prompt { + background-color: var(--euiColorEmptyShade); + } + + .euiFilePicker__prompt { + background-color: var(--euiColorEmptyShade); + height: 140px; + border-radius: 4px; + box-shadow: none; + border: 1px dashed var(--controlsBorderColor); + } + + .euiFilePicker { + width: 400px; + } + + .euiFilePicker__clearButton { + margin-top: 4px; + } + } + } + + .loading, .result { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + margin-top: 20px; + } +} diff --git a/redisinsight/ui/src/components/index.ts b/redisinsight/ui/src/components/index.ts index 3cfce1e0af..567df24019 100644 --- a/redisinsight/ui/src/components/index.ts +++ b/redisinsight/ui/src/components/index.ts @@ -20,6 +20,7 @@ import GlobalSubscriptions from './global-subscriptions' import MonitorWrapper from './monitor' import PagePlaceholder from './page-placeholder' import BulkActionsConfig from './bulk-actions-config' +import ImportDatabasesDialog from './import-databases-dialog' export { NavigationMenu, @@ -47,4 +48,5 @@ export { ShortcutsFlyout, PagePlaceholder, BulkActionsConfig, + ImportDatabasesDialog } diff --git a/redisinsight/ui/src/constants/api.ts b/redisinsight/ui/src/constants/api.ts index f910f787af..b1f78075f8 100644 --- a/redisinsight/ui/src/constants/api.ts +++ b/redisinsight/ui/src/constants/api.ts @@ -1,5 +1,6 @@ enum ApiEndpoints { DATABASES = 'databases', + DATABASES_IMPORT = 'databases/import', CA_CERTIFICATES = 'certificates/ca', CLIENT_CERTIFICATES = 'certificates/client', diff --git a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.spec.tsx b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.spec.tsx index 5b8613f679..a827593487 100644 --- a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.spec.tsx @@ -1,6 +1,8 @@ +import { within } from '@testing-library/react' import React from 'react' import { instance, mock } from 'ts-mockito' -import { render } from 'uiSrc/utils/test-utils' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' import HomeHeader, { Props } from './HomeHeader' const mockedProps = mock() @@ -16,8 +18,44 @@ jest.mock('uiSrc/slices/content/create-redis-buttons', () => { } }) +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + describe('HomeHeader', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should open import dbs dialog', () => { + render() + + fireEvent.click(screen.getByTestId('import-dbs-btn')) + + expect(screen.getByTestId('import-dbs-dialog')).toBeInTheDocument() + }) + + it('should call proper telemetry on open and close import databases dialog', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + render() + + fireEvent.click(screen.getByTestId('import-dbs-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_CLICKED + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + + fireEvent.click(within(screen.getByTestId('import-dbs-dialog')).getByTestId('cancel-btn')) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_CANCELLED + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + }) }) diff --git a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx index a3aa3dbcce..959ffc6da9 100644 --- a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx +++ b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx @@ -3,12 +3,15 @@ import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiSpacer, EuiText, + EuiToolTip, } from '@elastic/eui' import { isEmpty } from 'lodash' import { useSelector } from 'react-redux' import cx from 'classnames' +import { ImportDatabasesDialog } from 'uiSrc/components' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import HelpLinksMenu from 'uiSrc/pages/home/components/HelpLinksMenu' import PromoLink from 'uiSrc/components/promo-link/PromoLink' @@ -37,6 +40,7 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => const { loading, data } = useSelector(contentSelector) const [promoData, setPromoData] = useState() const [guides, setGuides] = useState([]) + const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) useEffect(() => { if (loading || !data || isEmpty(data)) { @@ -74,6 +78,20 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => } } + const handleClickImportDbBtn = () => { + setIsImportDialogOpen(true) + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_CLICKED, + }) + } + + const handleCloseImportDb = (isCancelled: boolean) => { + setIsImportDialogOpen(false) + isCancelled && sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_CANCELLED, + }) + } + const AddInstanceBtn = () => ( <> ) + const ImportDatabasesBtn = () => ( + + + + + + ) + const Guides = () => (
@@ -150,82 +185,92 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => ) } - return direction === 'column' - ? ( -
- - - - - - - -
- - - { !loading && !isEmpty(data) && ( - <> - {promoData && ( - - - - - - )} - - - )} - -
- ) : ( -
- - - - - -
- - { !loading && !isEmpty(data) && ( + return ( + <> + {isImportDialogOpen && } + {direction === 'column' ? ( +
+ + + + + + + + + + +
+ + + {!loading && !isEmpty(data) && ( <> - - - {promoData && ( - - - - )} - - + {promoData && ( + + + - - - handleClickLink(HELP_LINKS[link as keyof typeof HELP_LINKS]?.event)} - /> - - - handleClickLink(HELP_LINKS[link as keyof typeof HELP_LINKS]?.event)} - /> - + )} + )} - {instances.length > 0 && ( - - + +
+ ) : ( +
+ + + - )} - - -
- ) + + + + +
+ + {!loading && !isEmpty(data) && ( + <> + + + {promoData && ( + + + + )} + + + + + + + handleClickLink(HELP_LINKS[link as keyof typeof HELP_LINKS]?.event)} + /> + + + handleClickLink(HELP_LINKS[link as keyof typeof HELP_LINKS]?.event)} + /> + + + )} + {instances.length > 0 && ( + + + + )} + + +
+ )} + + ) } export default HomeHeader diff --git a/redisinsight/ui/src/pages/home/components/HomeHeader/styles.module.scss b/redisinsight/ui/src/pages/home/components/HomeHeader/styles.module.scss index c4fb2a4d8e..a4ca7d5f5a 100644 --- a/redisinsight/ui/src/pages/home/components/HomeHeader/styles.module.scss +++ b/redisinsight/ui/src/pages/home/components/HomeHeader/styles.module.scss @@ -21,6 +21,17 @@ font-weight: 500 !important; } } + +.importDatabasesBtn:global(.euiButton) { + height: 43px; + min-width: auto !important; + + :global(.euiButton__text) { + display: flex; + align-items: center; + } +} + .followText { padding-top: 7px; font-size: 12px !important; @@ -147,7 +158,7 @@ .smallGuides { display: none !important; - @media (min-width: 1101px) and (max-width: 1251px) { + @media (min-width: 1101px) and (max-width: 1249px) { display: flex !important; } } diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index d23e69f67e..8031e97067 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -43,6 +43,11 @@ export const initialState: InitialStateInstances = { instanceOverview: { version: '', }, + importInstances: { + loading: false, + error: '', + data: null + }, } // A slice for recipes @@ -169,6 +174,25 @@ const instancesSlice = createSlice({ resetConnectedInstance: (state) => { state.connectedInstance = initialState.connectedInstance }, + + importInstancesFromFile: (state) => { + state.importInstances.loading = true + state.importInstances.error = '' + }, + + importInstancesFromFileSuccess: (state, { payload }) => { + state.importInstances.loading = false + state.importInstances.data = payload + }, + + importInstancesFromFileFailure: (state, { payload }) => { + state.importInstances.loading = false + state.importInstances.error = payload + }, + + resetImportInstances: (state) => { + state.importInstances = initialState.importInstances + } }, }) @@ -196,6 +220,10 @@ export const { changeInstanceAliasFailure, resetInstanceUpdate, setEditedInstance, + importInstancesFromFile, + importInstancesFromFileSuccess, + importInstancesFromFileFailure, + resetImportInstances } = instancesSlice.actions // selectors @@ -206,6 +234,8 @@ export const editedInstanceSelector = (state: RootState) => state.connections.instances.editedInstance export const connectedInstanceOverviewSelector = (state: RootState) => state.connections.instances.instanceOverview +export const importInstancesSelector = (state: RootState) => + state.connections.instances.importInstances // The reducer export default instancesSlice.reducer @@ -484,3 +514,37 @@ export function resetInstanceUpdateAction() { sourceInstance?.cancel?.() } } + +// Asynchronous thunk action +export function uploadInstancesFile( + file: FormData, + onSuccessAction?: () => void, + onFailAction?: () => void +) { + return async (dispatch: AppDispatch) => { + dispatch(importInstancesFromFile()) + + try { + const { status, data } = await apiService.post( + ApiEndpoints.DATABASES_IMPORT, + file, + { + headers: { + Accept: 'application/json', + 'Content-Type': 'multipart/form-data' + } + } + ) + + if (isStatusSuccessful(status)) { + dispatch(importInstancesFromFileSuccess(data)) + onSuccessAction?.() + } + } catch (error) { + const errorMessage = getApiErrorMessage(error) + dispatch(importInstancesFromFileFailure(errorMessage)) + dispatch(addErrorNotification(error)) + onFailAction?.() + } + } +} diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 438e0f5231..2c591ea96a 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -283,6 +283,14 @@ export interface InitialStateInstances { connectedInstance: Instance editedInstance: InitialStateEditedInstances instanceOverview: DatabaseConfigInfo + importInstances: { + loading: boolean + error: string + data: Nullable<{ + success: number, + total: number + }> + } } export interface InitialStateEditedInstances { diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index 97d8bb10fb..a6073bbc65 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -43,6 +43,11 @@ import reducer, { setConnectedInstance, setConnectedInstanceFailure, setConnectedInstanceSuccess, + importInstancesFromFile, + importInstancesFromFileSuccess, + importInstancesFromFileFailure, + resetImportInstances, + importInstancesSelector, uploadInstancesFile } from '../../instances/instances' import { addErrorNotification, addMessageNotification, IAddInstanceErrorPayload } from '../../app/notifications' import { ConnectionType, InitialStateInstances, Instance } from '../../interfaces' @@ -512,6 +517,108 @@ describe('instances slice', () => { }) }) + describe('importInstancesFromFile', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState.importInstances, + loading: true, + error: '' + } + + // Act + const nextState = reducer(initialState, importInstancesFromFile()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(importInstancesSelector(rootState)).toEqual(state) + }) + }) + + describe('importInstancesFromFileSuccess', () => { + it('should properly set state', () => { + // Arrange + const data = { + success: 3, + total: 5 + } + const state = { + ...initialState.importInstances, + loading: false, + data + } + + // Act + const nextState = reducer(initialState, importInstancesFromFileSuccess(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(importInstancesSelector(rootState)).toEqual(state) + }) + }) + + describe('importInstancesFromFileFailure', () => { + it('should properly set state', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState.importInstances, + loading: false, + error + } + + // Act + const nextState = reducer(initialState, importInstancesFromFileFailure(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(importInstancesSelector(rootState)).toEqual(state) + }) + }) + + describe('resetImportInstances', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + importInstances: { + ...initialState.importInstances, + data: { + success: 1, + total: 2 + } + } + } + + const state = { + ...initialState.importInstances + } + + // Act + const nextState = reducer(currentState, resetImportInstances()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + connections: { + instances: nextState, + }, + }) + expect(importInstancesSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { describe('fetchInstances', () => { it('call both fetchInstances and loadInstancesSuccess when fetch is successed', async () => { @@ -997,5 +1104,55 @@ describe('instances slice', () => { expect(store.getActions()).toEqual(expectedActions) }) }) + + describe('uploadInstancesFile', () => { + it('should call proper actions on success', async () => { + // Arrange + const formData = new FormData() + const data = { + success: 0, + total: 1 + } + + const responsePayload = { data, status: 200 } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(uploadInstancesFile(formData)) + + // Assert + const expectedActions = [ + importInstancesFromFile(), + importInstancesFromFileSuccess(responsePayload.data) + ] + expect(store.getActions()).toEqual(expectedActions) + }) + + it('should call proper actions on fail', async () => { + // Arrange + const formData = new FormData() + const errorMessage = 'Some error' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + + apiService.post = jest.fn().mockRejectedValueOnce(responsePayload) + + // Act + await store.dispatch(uploadInstancesFile(formData)) + + // Assert + const expectedActions = [ + importInstancesFromFile(), + importInstancesFromFileFailure(responsePayload.response.data.message), + addErrorNotification(responsePayload as AxiosError), + ] + expect(store.getActions()).toEqual(expectedActions) + }) + }) }) }) diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index ea9b2bf88f..1185656b4c 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -27,6 +27,9 @@ export enum TelemetryEvent { CONFIG_DATABASES_DATABASE_CLONE_REQUESTED = 'CONFIG_DATABASES_DATABASE_CLONE_REQUESTED', CONFIG_DATABASES_DATABASE_CLONE_CANCELLED = 'CONFIG_DATABASES_DATABASE_CLONE_CANCELLED', CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED = 'CONFIG_DATABASES_DATABASE_CLONE_CONFIRMED', + CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED = 'CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED', + CONFIG_DATABASES_REDIS_IMPORT_CANCELLED = 'CONFIG_DATABASES_REDIS_IMPORT_CANCELLED', + CONFIG_DATABASES_REDIS_IMPORT_CLICKED = 'CONFIG_DATABASES_REDIS_IMPORT_CLICKED', BUILD_FROM_SOURCE_CLICKED = 'BUILD_FROM_SOURCE_CLICKED', BUILD_USING_DOCKER_CLICKED = 'BUILD_USING_DOCKER_CLICKED', From ce231de85cc2af8d19d6e905ba8a5d8d5a6ca05a Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 14 Nov 2022 17:22:28 +0300 Subject: [PATCH 008/107] Fix validation + add telemetry --- .../api/src/constants/telemetry-events.ts | 5 ++ .../database-import.analytics.ts | 42 ++++++++++ .../database-import.controller.ts | 9 -- .../database-import/database-import.module.ts | 2 + .../database-import.service.ts | 83 +++++++++++++------ .../dto/database-import.response.ts | 6 ++ .../dto/import.database.dto.ts | 10 ++- .../database-import/exceptions/index.ts | 3 + ...database-import-file-provided.exception.ts | 13 +++ ...exceeded-database-import-file.exception.ts | 13 +++ ...to-parse-database-import-file.exception.ts | 13 +++ .../POST-databases-import.test.ts | 14 ++-- 12 files changed, 169 insertions(+), 44 deletions(-) create mode 100644 redisinsight/api/src/modules/database-import/database-import.analytics.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/index.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/no-database-import-file-provided.exception.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/size-limit-exceeded-database-import-file.exception.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/unable-to-parse-database-import-file.exception.ts diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index 307a74ea47..8dff4b10e4 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -14,6 +14,11 @@ export enum TelemetryEvents { RedisInstanceConnectionFailed = 'DATABASE_CONNECTION_FAILED', RedisInstanceListReceived = 'CONFIG_DATABASES_DATABASE_LIST_DISPLAYED', + // Databases import + DatabaseImportParseFailed = 'CONFIG_DATABASES_REDIS_IMPORT_PARSE_FAILED', + DatabaseImportFailed = 'CONFIG_DATABASES_REDIS_IMPORT_FAILED', + DatabaseImportSucceeded = 'CONFIG_DATABASES_REDIS_IMPORT_SUCCEEDED', + // Events for autodiscovery flows REClusterDiscoverySucceed = 'CONFIG_DATABASES_RE_CLUSTER_AUTODISCOVERY_SUCCEEDED', REClusterDiscoveryFailed = 'CONFIG_DATABASES_RE_CLUSTER_AUTODISCOVERY_FAILED', diff --git a/redisinsight/api/src/modules/database-import/database-import.analytics.ts b/redisinsight/api/src/modules/database-import/database-import.analytics.ts new file mode 100644 index 0000000000..e363db5a0e --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.analytics.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { TelemetryEvents } from 'src/constants'; +import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; + +@Injectable() +export class DatabaseImportAnalytics extends TelemetryBaseService { + constructor(protected eventEmitter: EventEmitter2) { + super(eventEmitter); + } + + sendImportResults(importResult: DatabaseImportResponse): void { + if (importResult.success) { + this.sendEvent( + TelemetryEvents.DatabaseImportSucceeded, + { + succeed: importResult.success, + }, + ); + } + + if (importResult.errors?.length) { + this.sendEvent( + TelemetryEvents.DatabaseImportFailed, + { + failed: importResult.errors.length, + errors: importResult.errors.map((e) => (e?.constructor?.name || 'UncaughtError')), + }, + ); + } + } + + sendImportFailed(e: Error): void { + this.sendEvent( + TelemetryEvents.DatabaseImportParseFailed, + { + error: e?.constructor?.name || 'UncaughtError', + }, + ); + } +} diff --git a/redisinsight/api/src/modules/database-import/database-import.controller.ts b/redisinsight/api/src/modules/database-import/database-import.controller.ts index 4d1e9e5bfa..3bfabfb86e 100644 --- a/redisinsight/api/src/modules/database-import/database-import.controller.ts +++ b/redisinsight/api/src/modules/database-import/database-import.controller.ts @@ -1,5 +1,4 @@ import { - BadRequestException, Controller, HttpCode, Post, UploadedFile, UseInterceptors, UsePipes, ValidationPipe, } from '@nestjs/common'; @@ -35,14 +34,6 @@ export class DatabaseImportController { async import( @UploadedFile() file: any, ): Promise { - // todo: create FileValidation class - if (!file) { - throw new BadRequestException('No import file provided'); - } - if (file?.size > 1024 * 1024 * 10) { - throw new BadRequestException('Import file is too big. Maximum 10mb allowed'); - } - return this.service.import(file); } } diff --git a/redisinsight/api/src/modules/database-import/database-import.module.ts b/redisinsight/api/src/modules/database-import/database-import.module.ts index eac32e7d5f..ae7e036f30 100644 --- a/redisinsight/api/src/modules/database-import/database-import.module.ts +++ b/redisinsight/api/src/modules/database-import/database-import.module.ts @@ -1,11 +1,13 @@ import { Module } from '@nestjs/common'; import { DatabaseImportController } from 'src/modules/database-import/database-import.controller'; import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; +import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; @Module({ controllers: [DatabaseImportController], providers: [ DatabaseImportService, + DatabaseImportAnalytics, ], }) export class DatabaseImportModule {} diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index efa63f2e23..d83b14efb7 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { isArray, get, set } from 'lodash'; import { Database } from 'src/modules/database/models/database'; import { plainToClass } from 'class-transformer'; @@ -8,6 +8,11 @@ import { DatabaseImportResponse } from 'src/modules/database-import/dto/database import { Validator } from 'class-validator'; import { ImportDatabaseDto } from 'src/modules/database-import/dto/import.database.dto'; import { classToClass } from 'src/utils'; +import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; +import { + SizeLimitExceededDatabaseImportFileException, + NoDatabaseImportFileProvidedException, UnableToParseDatabaseImportFileException, +} from 'src/modules/database-import/exceptions'; @Injectable() export class DatabaseImportService { @@ -27,6 +32,7 @@ export class DatabaseImportService { constructor( private readonly databaseRepository: DatabaseRepository, + private readonly analytics: DatabaseImportAnalytics, ) {} /** @@ -34,29 +40,57 @@ export class DatabaseImportService { * @param file */ public async import(file): Promise { - const items = DatabaseImportService.parseFile(file); + try { + // todo: create FileValidation class + if (!file) { + throw new NoDatabaseImportFileProvidedException('No import file provided'); + } + if (file?.size > 1024 * 1024 * 10) { + throw new SizeLimitExceededDatabaseImportFileException('Import file is too big. Maximum 10mb allowed'); + } - if (!isArray(items) || !items?.length) { - let filename = file?.originalname || 'import file'; - if (filename.length > 50) { - filename = `${filename.slice(0, 50)}...`; + const items = DatabaseImportService.parseFile(file); + + if (!isArray(items) || !items?.length) { + let filename = file?.originalname || 'import file'; + if (filename.length > 50) { + filename = `${filename.slice(0, 50)}...`; + } + throw new UnableToParseDatabaseImportFileException(`Unable to parse ${filename}`); } - return Promise.reject(new BadRequestException(`Unable to parse ${filename}`)); - } - const response = { - total: items.length, - success: 0, - }; + let response = { + total: items.length, + success: 0, + errors: [], + }; + + // it is very important to insert databases on-by-one to avoid db constraint errors + await items.reduce((prev, item) => prev.finally(() => this.createDatabase(item) + .then(() => { + response.success += 1; + }) + .catch((e) => { + let error = e; + if (isArray(e)) { + [error] = e; + } + this.logger.warn(`Unable to import database: ${error?.constructor?.name || 'UncaughtError'}`, error); + response.errors.push(error); + })), Promise.resolve()); + + this.analytics.sendImportResults(response); + + response = plainToClass(DatabaseImportResponse, response); + + return response; + } catch (e) { + this.logger.warn(`Unable to import databases: ${e?.constructor?.name || 'UncaughtError'}`, e); - // it is very important to insert databases on-by-one to avoid db constraint errors - await items.reduce((prev, item) => prev.finally(() => this.createDatabase(item) - .then(() => { - response.success += 1; - }) - .catch(() => { /* just ignore errors */ })), Promise.resolve()); + this.analytics.sendImportFailed(e); - return plainToClass(DatabaseImportResponse, response); + throw e; + } } /** @@ -101,14 +135,9 @@ export class DatabaseImportService { }, {}), ); - try { - await this.validator.validateOrReject(dto, { - whitelist: true, - }); - } catch (e) { - this.logger.warn('Invalid data for database import entry', e); - return Promise.reject(e); - } + await this.validator.validateOrReject(dto, { + whitelist: true, + }); const database = classToClass(Database, dto); diff --git a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts index f6ec120409..dfe8f374da 100644 --- a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts +++ b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts @@ -1,15 +1,21 @@ import { ApiProperty } from '@nestjs/swagger'; +import { Exclude, Expose } from 'class-transformer'; export class DatabaseImportResponse { @ApiProperty({ description: 'Total elements processed from the import file', type: Number, }) + @Expose() total: number; @ApiProperty({ description: 'Number of imported database', type: Number, }) + @Expose() success: number; + + @Exclude() + errors: Error[]; } diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index dbc83070d9..7fd5f757fd 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -1,7 +1,15 @@ import { PickType } from '@nestjs/swagger'; import { Database } from 'src/modules/database/models/database'; +import { Expose, Type } from 'class-transformer'; +import { IsInt, IsNotEmpty } from 'class-validator'; export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', 'connectionType', -] as const) {} +] as const) { + @Expose() + @IsNotEmpty() + @IsInt({ always: true }) + @Type(() => Number) + port: number; +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/index.ts b/redisinsight/api/src/modules/database-import/exceptions/index.ts new file mode 100644 index 0000000000..d62d9a0259 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/index.ts @@ -0,0 +1,3 @@ +export * from './size-limit-exceeded-database-import-file.exception'; +export * from './no-database-import-file-provided.exception'; +export * from './unable-to-parse-database-import-file.exception'; diff --git a/redisinsight/api/src/modules/database-import/exceptions/no-database-import-file-provided.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/no-database-import-file-provided.exception.ts new file mode 100644 index 0000000000..8a69a18cc4 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/no-database-import-file-provided.exception.ts @@ -0,0 +1,13 @@ +import { HttpException } from '@nestjs/common'; + +export class NoDatabaseImportFileProvidedException extends HttpException { + constructor(message: string = 'No import file provided') { + const response = { + message, + statusCode: 400, + error: 'No Database Import File Provided', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/size-limit-exceeded-database-import-file.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/size-limit-exceeded-database-import-file.exception.ts new file mode 100644 index 0000000000..0dfb0b960c --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/size-limit-exceeded-database-import-file.exception.ts @@ -0,0 +1,13 @@ +import { HttpException } from '@nestjs/common'; + +export class SizeLimitExceededDatabaseImportFileException extends HttpException { + constructor(message: string = 'Invalid import file') { + const response = { + message, + statusCode: 400, + error: 'Invalid Database Import File', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/unable-to-parse-database-import-file.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/unable-to-parse-database-import-file.exception.ts new file mode 100644 index 0000000000..e73f1de712 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/unable-to-parse-database-import-file.exception.ts @@ -0,0 +1,13 @@ +import { HttpException } from '@nestjs/common'; + +export class UnableToParseDatabaseImportFileException extends HttpException { + constructor(message: string = 'Unable to parse import file') { + const response = { + message, + statusCode: 400, + error: 'Unable To Parse Database Import File', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index 3786b9a10b..d46dd73631 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -18,7 +18,7 @@ const endpoint = () => request(server).post(`/${constants.API.DATABASES}/import` const databaseSchema = Joi.object({ name: Joi.string().allow(null, ''), host: Joi.string().required(), - port: Joi.number().integer().required(), + port: Joi.number().integer().allow(true).required(), db: Joi.number().integer().allow(null, ''), username: Joi.string().allow(null, ''), password: Joi.string().allow(null, ''), @@ -49,7 +49,7 @@ const baseSentinelData = { const importDatabaseFormat1 = { name: baseDatabaseData.name, host: baseDatabaseData.host, - port: baseDatabaseData.port, + port: `${baseDatabaseData.port}`, username: baseDatabaseData.username, auth: baseDatabaseData.password, } @@ -86,7 +86,7 @@ describe('POST /databases/import', () => { responseBody: { statusCode: 400, message: 'No import file provided', - error: 'Bad Request', + error: 'No Database Import File Provided', }, }, { @@ -96,7 +96,7 @@ describe('POST /databases/import', () => { responseBody: { statusCode: 400, message: 'Import file is too big. Maximum 10mb allowed', - error: 'Bad Request', + error: 'Invalid Database Import File', }, }, { @@ -106,7 +106,7 @@ describe('POST /databases/import', () => { responseBody: { statusCode: 400, message: 'Unable to parse filename.json', - error: 'Bad Request', + error: 'Unable To Parse Database Import File', }, }, { @@ -116,7 +116,7 @@ describe('POST /databases/import', () => { responseBody: { statusCode: 400, message: `Unable to parse ${new Array(50).fill(1).join('')}...`, - error: 'Bad Request', + error: 'Unable To Parse Database Import File', }, }, { @@ -126,7 +126,7 @@ describe('POST /databases/import', () => { responseBody: { statusCode: 400, message: `Unable to parse ${new Array(50).fill(1).join('')}...`, - error: 'Bad Request', + error: 'Unable To Parse Database Import File', }, }, ].map(mainCheckFn); From aec6614da5bde5f453a9494963ef0d16a0b7421d Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 16 Nov 2022 13:55:57 +0300 Subject: [PATCH 009/107] #RI-3817 improve port validation + add UTests --- .../api/src/__mocks__/database-import.ts | 37 ++++ redisinsight/api/src/__mocks__/index.ts | 1 + .../database-import.analytics.spec.ts | 96 ++++++++++ .../database-import.service.spec.ts | 170 ++++++++++++++++++ .../database-import.service.ts | 2 +- .../dto/import.database.dto.ts | 6 +- 6 files changed, 310 insertions(+), 2 deletions(-) create mode 100644 redisinsight/api/src/__mocks__/database-import.ts create mode 100644 redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts create mode 100644 redisinsight/api/src/modules/database-import/database-import.service.spec.ts diff --git a/redisinsight/api/src/__mocks__/database-import.ts b/redisinsight/api/src/__mocks__/database-import.ts new file mode 100644 index 0000000000..2ce3ed50a5 --- /dev/null +++ b/redisinsight/api/src/__mocks__/database-import.ts @@ -0,0 +1,37 @@ +import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; +import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { mockDatabase } from 'src/__mocks__/databases'; +import { ValidationError } from 'class-validator'; + +export const mockDatabasesToImportArray = new Array(10).fill(mockDatabase); + +export const mockDatabaseImportFile = { + originalname: 'filename.json', + mimetype: 'application/json', + size: 1, + buffer: Buffer.from(JSON.stringify(mockDatabasesToImportArray)), +}; + +export const mockDatabaseImportResponse = Object.assign(new DatabaseImportResponse(), { + total: 10, + success: 7, + errors: [new ValidationError(), new BadRequestException(), new ForbiddenException()], +}); + +export const mockDatabaseImportParseFailedAnalyticsPayload = { + +}; + +export const mockDatabaseImportFailedAnalyticsPayload = { + failed: mockDatabaseImportResponse.errors.length, + errors: ['ValidationError', 'BadRequestException', 'ForbiddenException'], +}; + +export const mockDatabaseImportSucceededAnalyticsPayload = { + succeed: mockDatabaseImportResponse.success, +}; + +export const mockDatabaseImportAnalytics = jest.fn(() => ({ + sendImportResults: jest.fn(), + sendImportFailed: jest.fn(), +})); diff --git a/redisinsight/api/src/__mocks__/index.ts b/redisinsight/api/src/__mocks__/index.ts index f378d9dee6..ef2dacddb2 100644 --- a/redisinsight/api/src/__mocks__/index.ts +++ b/redisinsight/api/src/__mocks__/index.ts @@ -16,3 +16,4 @@ export * from './redis'; export * from './server'; export * from './redis-enterprise'; export * from './redis-sentinel'; +export * from './database-import'; diff --git a/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts b/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts new file mode 100644 index 0000000000..920cc215f4 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts @@ -0,0 +1,96 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { EventEmitter2 } from '@nestjs/event-emitter'; +import { + mockDatabaseImportFailedAnalyticsPayload, + mockDatabaseImportResponse, mockDatabaseImportSucceededAnalyticsPayload, +} from 'src/__mocks__'; +import { TelemetryEvents } from 'src/constants'; +import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; +import { + NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException, + UnableToParseDatabaseImportFileException, +} from 'src/modules/database-import/exceptions'; + +describe('DatabaseImportAnalytics', () => { + let service: DatabaseImportAnalytics; + let sendEventSpy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + EventEmitter2, + DatabaseImportAnalytics, + ], + }).compile(); + + service = await module.get(DatabaseImportAnalytics); + sendEventSpy = jest.spyOn(service as any, 'sendEvent'); + }); + + describe('sendImportResults', () => { + it('should emit 2 events with success and failed results', () => { + service.sendImportResults(mockDatabaseImportResponse); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 1, + TelemetryEvents.DatabaseImportSucceeded, + mockDatabaseImportSucceededAnalyticsPayload, + ); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 2, + TelemetryEvents.DatabaseImportFailed, + mockDatabaseImportFailedAnalyticsPayload, + ); + }); + }); + + describe('sendImportFailed', () => { + it('should emit 1 event with "Error" cause', () => { + service.sendImportFailed(new Error()); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 1, + TelemetryEvents.DatabaseImportParseFailed, + { + error: 'Error', + }, + ); + }); + it('should emit 1 event with "UnableToParseDatabaseImportFileException" cause', () => { + service.sendImportFailed(new UnableToParseDatabaseImportFileException()); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 1, + TelemetryEvents.DatabaseImportParseFailed, + { + error: 'UnableToParseDatabaseImportFileException', + }, + ); + }); + it('should emit 1 event with "NoDatabaseImportFileProvidedException" cause', () => { + service.sendImportFailed(new NoDatabaseImportFileProvidedException()); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 1, + TelemetryEvents.DatabaseImportParseFailed, + { + error: 'NoDatabaseImportFileProvidedException', + }, + ); + }); + it('should emit 1 event with "SizeLimitExceededDatabaseImportFileException" cause', () => { + service.sendImportFailed(new SizeLimitExceededDatabaseImportFileException()); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 1, + TelemetryEvents.DatabaseImportParseFailed, + { + error: 'SizeLimitExceededDatabaseImportFileException', + }, + ); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts new file mode 100644 index 0000000000..49de47ea42 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -0,0 +1,170 @@ +import { pick } from 'lodash'; +import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; +import { + mockDatabase, + mockDatabaseImportAnalytics, + mockDatabaseImportFile, + mockDatabaseImportResponse, + MockType, +} from 'src/__mocks__'; +import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; +import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConnectionType } from 'src/modules/database/entities/database.entity'; +import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { ValidationError } from 'class-validator'; +import { + NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException, + UnableToParseDatabaseImportFileException, +} from 'src/modules/database-import/exceptions'; + +describe('DatabaseImportService', () => { + let service: DatabaseImportService; + let databaseRepository: MockType; + let analytics: MockType; + let validatoSpy; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DatabaseImportService, + { + provide: DatabaseRepository, + useFactory: jest.fn(() => ({ + create: jest.fn().mockResolvedValue(mockDatabase), + })), + }, + { + provide: DatabaseImportAnalytics, + useFactory: mockDatabaseImportAnalytics, + }, + ], + }).compile(); + + service = await module.get(DatabaseImportService); + databaseRepository = await module.get(DatabaseRepository); + analytics = await module.get(DatabaseImportAnalytics); + validatoSpy = jest.spyOn(service['validator'], 'validateOrReject'); + }); + + describe('importDatabase', () => { + beforeEach(() => { + databaseRepository.create.mockRejectedValueOnce(new BadRequestException()); + databaseRepository.create.mockRejectedValueOnce(new ForbiddenException()); + validatoSpy.mockRejectedValueOnce([new ValidationError()]); + }); + + it('should import databases from json', async () => { + const response = await service.import(mockDatabaseImportFile); + + expect(response).toEqual({ + ...mockDatabaseImportResponse, + errors: undefined, // errors omitted from response + }); + expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse); + }); + + it('should import databases from base64', async () => { + const response = await service.import({ + ...mockDatabaseImportFile, + mimetype: 'binary/octet-stream', + buffer: Buffer.from(mockDatabaseImportFile.buffer.toString('base64')), + }); + + expect(response).toEqual({ + ...mockDatabaseImportResponse, + errors: undefined, // errors omitted from response + }); + expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse); + }); + + it('should fail due to file was not provided', async () => { + try { + await service.import(undefined); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(NoDatabaseImportFileProvidedException); + expect(e.message).toEqual('No import file provided'); + expect(analytics.sendImportFailed) + .toHaveBeenCalledWith(new NoDatabaseImportFileProvidedException('No import file provided')); + } + }); + + it('should fail due to file exceeded size limitations', async () => { + try { + await service.import({ + ...mockDatabaseImportFile, + size: 10 * 1024 * 1024 + 1, + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(SizeLimitExceededDatabaseImportFileException); + expect(e.message).toEqual('Import file is too big. Maximum 10mb allowed'); + } + }); + + it('should fail due to incorrect json', async () => { + try { + await service.import({ + ...mockDatabaseImportFile, + buffer: Buffer.from([0, 21]), + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException); + expect(e.message).toEqual(`Unable to parse ${mockDatabaseImportFile.originalname}`); + } + }); + + it('should faile due to incorrect base64 + truncate filename', async () => { + try { + await service.import({ + ...mockDatabaseImportFile, + originalname: (new Array(1_000).fill(1)).join(''), + mimetype: 'binary/octet-stream', + buffer: Buffer.from([0, 21]), + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(UnableToParseDatabaseImportFileException); + expect(e.message).toEqual(`Unable to parse ${(new Array(50).fill(1)).join('')}...`); + } + }); + }); + + describe('createDatabase', () => { + it('should create standalone database', async () => { + await service['createDatabase']({ + ...mockDatabase, + }); + + expect(databaseRepository.create).toHaveBeenCalledWith({ + ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), + }); + }); + it('should create standalone with created name', async () => { + await service['createDatabase']({ + ...mockDatabase, + name: undefined, + }); + + expect(databaseRepository.create).toHaveBeenCalledWith({ + ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), + name: `${mockDatabase.host}:${mockDatabase.port}`, + }); + }); + it('should create cluster database', async () => { + await service['createDatabase']({ + ...mockDatabase, + cluster: true, + }); + + expect(databaseRepository.create).toHaveBeenCalledWith({ + ...pick(mockDatabase, ['host', 'port', 'name']), + connectionType: ConnectionType.CLUSTER, + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index d83b14efb7..8fdc6c9fdd 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -99,7 +99,7 @@ export class DatabaseImportService { * @param item * @private */ - private async createDatabase(item: any[]): Promise { + private async createDatabase(item: any): Promise { const data: any = {}; this.fieldsMapSchema.forEach(([field, paths]) => { diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index 7fd5f757fd..92bd244a08 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -1,7 +1,9 @@ import { PickType } from '@nestjs/swagger'; import { Database } from 'src/modules/database/models/database'; import { Expose, Type } from 'class-transformer'; -import { IsInt, IsNotEmpty } from 'class-validator'; +import { + IsInt, IsNotEmpty, Max, Min, +} from 'class-validator'; export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', @@ -11,5 +13,7 @@ export class ImportDatabaseDto extends PickType(Database, [ @IsNotEmpty() @IsInt({ always: true }) @Type(() => Number) + @Min(0) + @Max(65536) port: number; } From 9661ccb43454457dfa7e09faf85d88969d4dc7fa Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 16 Nov 2022 16:11:38 +0300 Subject: [PATCH 010/107] fix max port validation --- .../api/src/modules/database-import/dto/import.database.dto.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index 92bd244a08..de696bcafc 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -14,6 +14,6 @@ export class ImportDatabaseDto extends PickType(Database, [ @IsInt({ always: true }) @Type(() => Number) @Min(0) - @Max(65536) + @Max(65535) port: number; } From 775325a109fb82eb5f768bb5a455ba5bed512572 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 16 Nov 2022 20:18:56 +0100 Subject: [PATCH 011/107] add tests for databases import --- .../InstanceForm/InstanceForm.tsx | 2 +- tests/e2e/common-actions/databases-actions.ts | 31 ++++ tests/e2e/helpers/api/api-database.ts | 14 ++ .../pageObjects/add-redis-database-page.ts | 2 + .../pageObjects/my-redis-databases-page.ts | 11 +- tests/e2e/test-data/ardm-valid.ano | 1 + tests/e2e/test-data/racompass-invalid.json | 171 ++++++++++++++++++ tests/e2e/test-data/racompass-valid.json | 171 ++++++++++++++++++ tests/e2e/test-data/rdm-valid.json | 62 +++++++ .../database/import-databases.e2e.ts | 99 ++++++++++ 10 files changed, 562 insertions(+), 2 deletions(-) create mode 100644 tests/e2e/common-actions/databases-actions.ts create mode 100644 tests/e2e/test-data/ardm-valid.ano create mode 100644 tests/e2e/test-data/racompass-invalid.json create mode 100644 tests/e2e/test-data/racompass-valid.json create mode 100644 tests/e2e/test-data/rdm-valid.json create mode 100644 tests/e2e/tests/critical-path/database/import-databases.e2e.ts diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx index c9af186d30..42e85eef0a 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx @@ -448,7 +448,7 @@ const AddStandaloneForm = (props: Props) => { label={( Connection Type: - + {capitalize(connectionType)} diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts new file mode 100644 index 0000000000..129b24dbf7 --- /dev/null +++ b/tests/e2e/common-actions/databases-actions.ts @@ -0,0 +1,31 @@ +import { t } from 'testcafe'; +import { MyRedisDatabasePage } from '../pageObjects'; + +const myRedisDatabasePage = new MyRedisDatabasePage(); + +export class DatabasesActions { + /** + * Verify that databases are displayed + * @param databases The list of databases to verify + */ + async verifyDatabasesDisplayed(databases: string[]): Promise { + for (const db in databases) { + const databaseName = myRedisDatabasePage.dbNameList.withText(db); + await t.expect(databaseName.exists).ok(`"${db}" database doesn't exist`); + } + } + + /** + * Import database using file + * @param pathToFile The path to file for import + * @param databaseType The database type + */ + async importDatabase(pathToFile: string, databaseType = ''): Promise { + await t + .click(myRedisDatabasePage.importDatabasesBtn) + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [pathToFile]) + .click(myRedisDatabasePage.submitImportBtn) + .expect(myRedisDatabasePage.successImportMessage.exists).ok(`Successfully added ${databaseType} databases message not displayed`); + } + +} diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 4f87be5e4c..8cd2693be8 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -153,6 +153,20 @@ export async function deleteStandaloneDatabaseApi(databaseParameters: AddNewData .expect(200); } +/** + * Delete Standalone databases using their names through api + * @param databaseNames Databases names + */ +export async function deleteStandaloneDatabasesByNamesApi(databaseNames: string[]): Promise { + databaseNames.forEach(async databaseName => { + const databaseId = await getDatabaseByName(databaseName); + await request(endpoint).delete('/databases') + .send({ 'ids': [`${databaseId}`] }) + .set('Accept', 'application/json') + .expect(200); + }); +} + /** * Delete database from OSS Cluster through api * @param databaseParameters The database parameters diff --git a/tests/e2e/pageObjects/add-redis-database-page.ts b/tests/e2e/pageObjects/add-redis-database-page.ts index ff93abfd52..22fcad871b 100644 --- a/tests/e2e/pageObjects/add-redis-database-page.ts +++ b/tests/e2e/pageObjects/add-redis-database-page.ts @@ -28,6 +28,7 @@ export class AddRedisDatabasePage { sentinelDatabaseNavigation = Selector('[data-testid=database-nav-group]'); cloneSentinelDatabaseNavigation = Selector('[data-testid=database-nav-group-clone]'); cancelButton = Selector('[data-testid=btn-cancel]'); + showPasswordBtn = Selector('[aria-label^="Show password"]'); //TEXT INPUTS (also referred to as 'Text fields') hostInput = Selector('[data-testid=host]'); portInput = Selector('[data-testid=port]'); @@ -42,6 +43,7 @@ export class AddRedisDatabasePage { databaseIndexMessage = Selector('[data-testid=db-index-message]'); primaryGroupNameInput = Selector('[data-testid=primary-group]'); masterGroupPassword = Selector('[data-testid=sentinel-master-password]'); + connectionType = Selector('[data-testid=connection-type]'); //Links buildFromSource = Selector('a').withExactText('Build from source'); buildFromDocker = Selector('a').withExactText('Docker'); diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index e749a9d795..89a78e35da 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -15,7 +15,7 @@ export class MyRedisDatabasePage { githubButton = Selector('[data-testid=github-repo-icon]'); browserButton = Selector('[data-testid=browser-page-btn]'); pubSubButton = Selector('[data-testid=pub-sub-page-btn]'); - myRedisDBButton = Selector('[data-test-subj=home-page-btn]'); + myRedisDBButton = Selector('[data-test-subj=home-page-btn]', { timeout: 1000 }); deleteDatabaseButton = Selector('[data-testid^=delete-instance-]'); confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove'); toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); @@ -30,6 +30,10 @@ export class MyRedisDatabasePage { sortByHostAndPort = Selector('span').withAttribute('title', 'Host:Port'); sortByConnectionType = Selector('span').withAttribute('title', 'Connection Type'); sortByLastConnection = Selector('span').withAttribute('title', 'Last connection'); + importDatabasesBtn = Selector('[data-testid=import-dbs-btn]'); + submitImportBtn = Selector('[data-testid=submit-btn]'); + closeDialogBtn = Selector('[aria-label="Closes this modal window"]'); + okDialogBtn = Selector('[data-testid=ok-btn]'); //CHECKBOXES selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]'); //ICONS @@ -46,6 +50,7 @@ export class MyRedisDatabasePage { //TEXT INPUTS (also referred to as 'Text fields') aliasInput = Selector('[data-testid=alias-input]'); searchInput = Selector('[data-testid=search-database-list]'); + importDatabaseInput = Selector('[data-testid=import-databases-input-file]'); //TEXT ELEMENTS moduleTooltip = Selector('.euiToolTipPopover'); moduleQuantifier = Selector('[data-testid=_module]'); @@ -55,6 +60,10 @@ export class MyRedisDatabasePage { hostPort = Selector('[data-testid=host-port]'); noResultsFoundMessage = Selector('div').withExactText('No results found'); noResultsFoundText = Selector('div').withExactText('No databases matched your search. Try reducing the criteria.'); + failedImportMessage = Selector('[data-testid=result-failed]'); + successImportMessage = Selector('[data-testid=result-success]'); + // DIALOG + importDbDialog = Selector('[data-testid=import-dbs-dialog]'); /** * Click on the database by name diff --git a/tests/e2e/test-data/ardm-valid.ano b/tests/e2e/test-data/ardm-valid.ano new file mode 100644 index 0000000000..7c557d1989 --- /dev/null +++ b/tests/e2e/test-data/ardm-valid.ano @@ -0,0 +1 @@ +W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9XQ== \ No newline at end of file diff --git a/tests/e2e/test-data/racompass-invalid.json b/tests/e2e/test-data/racompass-invalid.json new file mode 100644 index 0000000000..aab8b3e325 --- /dev/null +++ b/tests/e2e/test-data/racompass-invalid.json @@ -0,0 +1,171 @@ +[ + { + "srvRecords": [ + { + "priority": null, + "weight": null, + "port": null, + "name": null + } + ], + "useSRVRecords": false, + "natMaps": [ + { + "privateHost": null, + "privatePort": null, + "publicHost": null, + "publicPort": null + } + ], + "enableNatMaps": false, + "clusterOptions": { + "slotsRefreshInterval": 5000, + "slotsRefreshTimeout": 1000, + "retryDelayOnTryAgain": 100, + "retryDelayOnClusterDown": 100, + "retryDelayOnFailover": 100, + "retryDelayOnMoved": 0, + "maxRedirections": 16, + "dnsLookup": false, + "scaleReads": "master", + "startupNodes": [ + { + "host": null, + "port": null + } + ] + }, + "enableStartupNodes": false, + "sentinelOptions": { + "tls": { + "key": null, + "keyBookmark": null, + "ca": null, + "caBookmark": null, + "cert": null, + "certBookmark": null + }, + "preferredSlaves": [ + { + "host": null, + "port": null, + "priority": null + } + ], + "role": null, + "sentinelPassword": null, + "name": null + }, + "enablePreferredSlaves": false, + "sshKeyPassphrase": null, + "sshKeyFileBookmark": null, + "sshKeyFile": null, + "sshUser": null, + "sshPort": null, + "sshHost": null, + "ssh": false, + "caCertBookmark": null, + "caCert": null, + "certificateBookmark": null, + "certificate": null, + "keyFileBookmark": null, + "keyFile": null, + "ssl": false, + "default": false, + "star": false, + "totalDb": 16, + "db": 0, + "password": "", + "color": "#4B5563", + "host": "localhost", + "keyPrefix": null, + "type": "standalone", + "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9" + }, + { + "srvRecords": [ + { + "priority": null, + "weight": null, + "port": null, + "name": null + } + ], + "useSRVRecords": false, + "natMaps": [ + { + "privateHost": null, + "privatePort": null, + "publicHost": null, + "publicPort": null + } + ], + "enableNatMaps": false, + "clusterOptions": { + "slotsRefreshInterval": 5000, + "slotsRefreshTimeout": 1000, + "retryDelayOnTryAgain": 100, + "retryDelayOnClusterDown": 100, + "retryDelayOnFailover": 100, + "retryDelayOnMoved": 0, + "maxRedirections": 16, + "dnsLookup": false, + "scaleReads": "master", + "startupNodes": [ + { + "host": null, + "port": null + } + ] + }, + "enableStartupNodes": false, + "sentinelOptions": { + "tls": { + "key": null, + "keyBookmark": null, + "ca": null, + "caBookmark": null, + "cert": null, + "certBookmark": null + }, + "preferredSlaves": [ + { + "host": null, + "port": null, + "priority": null + } + ], + "role": null, + "sentinelPassword": null, + "name": null + }, + "enablePreferredSlaves": false, + "sshKeyPassphrase": null, + "sshKeyFileBookmark": null, + "sshKeyFile": null, + "sshUser": null, + "sshPort": null, + "sshHost": null, + "ssh": false, + "caCertBookmark": null, + "caCert": null, + "certificateBookmark": null, + "certificate": null, + "keyFileBookmark": null, + "keyFile": null, + "ssl": false, + "default": false, + "star": false, + "totalDb": 16, + "db": 1, + "password": "vfsd", + "color": "#4B5563", + "port": 1111, + "keyPrefix": null, + "type": "standalone", + "connectionName": "vd", + "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", + "cluster": true, + "name": "vd long host" + } +] \ No newline at end of file diff --git a/tests/e2e/test-data/racompass-valid.json b/tests/e2e/test-data/racompass-valid.json new file mode 100644 index 0000000000..dc4e32dcf5 --- /dev/null +++ b/tests/e2e/test-data/racompass-valid.json @@ -0,0 +1,171 @@ +[ + { + "srvRecords": [ + { + "priority": null, + "weight": null, + "port": null, + "name": null + } + ], + "useSRVRecords": false, + "natMaps": [ + { + "privateHost": null, + "privatePort": null, + "publicHost": null, + "publicPort": null + } + ], + "enableNatMaps": false, + "clusterOptions": { + "slotsRefreshInterval": 5000, + "slotsRefreshTimeout": 1000, + "retryDelayOnTryAgain": 100, + "retryDelayOnClusterDown": 100, + "retryDelayOnFailover": 100, + "retryDelayOnMoved": 0, + "maxRedirections": 16, + "dnsLookup": false, + "scaleReads": "master", + "startupNodes": [ + { + "host": null, + "port": null + } + ] + }, + "enableStartupNodes": false, + "sentinelOptions": { + "tls": { + "key": null, + "keyBookmark": null, + "ca": null, + "caBookmark": null, + "cert": null, + "certBookmark": null + }, + "preferredSlaves": [ + { + "host": null, + "port": null, + "priority": null + } + ], + "role": null, + "sentinelPassword": null, + "name": null + }, + "enablePreferredSlaves": false, + "sshKeyPassphrase": null, + "sshKeyFileBookmark": null, + "sshKeyFile": null, + "sshUser": null, + "sshPort": null, + "sshHost": null, + "ssh": false, + "caCertBookmark": null, + "caCert": null, + "certificateBookmark": null, + "certificate": null, + "keyFileBookmark": null, + "keyFile": null, + "ssl": false, + "default": false, + "star": false, + "totalDb": 16, + "db": 1, + "password": "", + "color": "#4B5563", + "port": 8100, + "host": "racompassDbWithIndex", + "keyPrefix": null, + "type": "standalone", + "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9" + }, + { + "srvRecords": [ + { + "priority": null, + "weight": null, + "port": null, + "name": null + } + ], + "useSRVRecords": false, + "natMaps": [ + { + "privateHost": null, + "privatePort": null, + "publicHost": null, + "publicPort": null + } + ], + "enableNatMaps": false, + "clusterOptions": { + "slotsRefreshInterval": 5000, + "slotsRefreshTimeout": 1000, + "retryDelayOnTryAgain": 100, + "retryDelayOnClusterDown": 100, + "retryDelayOnFailover": 100, + "retryDelayOnMoved": 0, + "maxRedirections": 16, + "dnsLookup": false, + "scaleReads": "master", + "startupNodes": [ + { + "host": null, + "port": null + } + ] + }, + "enableStartupNodes": false, + "sentinelOptions": { + "tls": { + "key": null, + "keyBookmark": null, + "ca": null, + "caBookmark": null, + "cert": null, + "certBookmark": null + }, + "preferredSlaves": [ + { + "host": null, + "port": null, + "priority": null + } + ], + "role": null, + "sentinelPassword": null, + "name": null + }, + "enablePreferredSlaves": false, + "sshKeyPassphrase": null, + "sshKeyFileBookmark": null, + "sshKeyFile": null, + "sshUser": null, + "sshPort": null, + "sshHost": null, + "ssh": false, + "caCertBookmark": null, + "caCert": null, + "certificateBookmark": null, + "certificate": null, + "keyFileBookmark": null, + "keyFile": null, + "ssl": false, + "default": false, + "star": false, + "totalDb": 16, + "db": 0, + "password": "vfsd", + "color": "#4B5563", + "host": "localhost", + "port": 1111, + "keyPrefix": null, + "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", + "cluster": true, + "name": "racompassCluster" + } +] \ No newline at end of file diff --git a/tests/e2e/test-data/rdm-valid.json b/tests/e2e/test-data/rdm-valid.json new file mode 100644 index 0000000000..845fe507e7 --- /dev/null +++ b/tests/e2e/test-data/rdm-valid.json @@ -0,0 +1,62 @@ +[ + { + "password": "new", + "filter_history": { + "*": 1, + "stream1": 1 + }, + "host": "localhost", + "name": "vfd das jashd ashdkjh kjhasfh hfjks dahfjk shdk fhskjad hfkj sdhkj fashk dhfk sahkj fhsak dhfskja hsd kfjh dsakj fhsdjk fhskdjal fhksjd hfkjsd hfkjs hakdjfh sjkdah fjksdh fjksdh jkfh ksdh fsdkjhfksjhfkhsdkf hksjdhf kjsdhf skdhf sdf sdfsda fsdfsd fsd fsdf sd f sdf sd f sd fsd f sd f sd fsd f sad fs df sd f s dsa fsdf vfd das jashd ashdkjh", + "ssh_port": 22, + "timeout_connect": 60000, + "timeout_execute": 60000, + "cluster": false, + "invalid_field": "inv", + "db": 0 + }, + { + "host": "rdmWithUsernameAndPass1", + "port": "1561", + "cluster": true, + "username": "rdmUsername", + "auth": "rdmAuth" + }, + { + "auth": "", + "host": "172.30.100.151", + "name": "oss cluster", + "ssh_port": 22, + "cluster": true, + "timeout_connect": 60000, + "timeout_execute": 60000 + }, + { + "auth": "longname database >500 symbols invalid", + "host": "172.30.100.181", + "port": 1000, + "keys_pattern": "*", + "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla rutrum nec libero aliquet ultricies. Donec ut blandit dui, ac hendrerit risus. Vestibulum sodales sed risus ac auctor. Integer quis justo vel leo gravida volutpat quis et risus. Nam vestibulum fermentum eros, in ullamcorper nulla laoreet quis. Vestibulum ut arcu nec turpis elementum malesuada. Maecenas congue felis nec posuere ullamcorper. Phasellus auctor leo sit amet ligula scelerisque, quis elementum ligula auctor. Quisque id leo r", + "namespace_separator": ":", + "ssh_port": 22, + "ssl": true, + "ssl_ca_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/ca.crt", + "ssl_local_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.crt", + "ssl_private_key_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.key", + "timeout_connect": 60000, + "timeout_execute": 60000 + }, + { + "auth": "pass", + "host": "localhost", + "name": "plain-certs+pass", + "port": 65537, + "ssh_port": 22, + "ssl_local_cert_path": "C:/Users/enaboko/Downloads/certificates", + "timeout_connect": 60000, + "timeout_execute": 60000 + }, + { + "host": "rdmOnlyHostPortDB2", + "port": 6379 + } +] diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts new file mode 100644 index 0000000000..db100c0727 --- /dev/null +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -0,0 +1,99 @@ +import { rte } from '../../../helpers/constants'; +import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; +import { commonUrl } from '../../../helpers/conf'; +import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; +import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; +import { DatabasesActions } from '../../../common-actions/databases-actions'; + +const browserPage = new BrowserPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); +const databasesActions = new DatabasesActions(); +const addRedisDatabasePage = new AddRedisDatabasePage(); + +const invalidJsonPath = '../../../test-data/racompass-invalid.json'; +const rdmData = { + type: 'rdm', + path: '../../../test-data/rdm-valid.json', + dbNames: ['rdmWithUsernameAndPass1:1561', 'rdmOnlyHostPortDB2:6379'], + userName: 'rdmUsername', + password: 'rdmAuth', + connectionType: 'Cluster' +}; +const dbData = [ + { + type: 'racompass', + path: '../../../test-data/racompass-valid.json', + dbNames: ['racompassCluster', 'racompassDbWithIndex:8100 [1]'] + }, + { + type: 'ardm', + path: '../../../test-data/ardm-valid.ano', + dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] + } +]; +const databases = [ + rdmData.dbNames[0], + rdmData.dbNames[1], + dbData[0].dbNames[0], + dbData[0].dbNames[1].split(' ')[0], + dbData[1].dbNames[0], + dbData[1].dbNames[1] +]; + +fixture `Import databases` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl); +test + .before(async() => { + await acceptLicenseTerms(); + }) + .after(async() => { + // Delete databases + deleteStandaloneDatabasesByNamesApi(databases); + })('Connection import from JSON', async t => { + const tooltipText = 'Import Database Connections'; + const partialImportedMsg = 'Successfully added 2 of 6 database connections'; + + // Verify that user can see the “Import Database Connections” tooltip + await t.hover(myRedisDatabasePage.importDatabasesBtn); + await t.hover(myRedisDatabasePage.importDatabasesBtn); + await t.expect(browserPage.tooltip.innerText).contains(tooltipText, 'The tooltip message not displayed/correct'); + + // Verify that Import dialogue is not closed when clicking any area outside the box + await t.click(myRedisDatabasePage.importDatabasesBtn); + await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not opened'); + await t.click(myRedisDatabasePage.myRedisDBButton); + await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not displayed'); + + // Verify that user see the message when parse error appears + await t + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [invalidJsonPath]) + .click(myRedisDatabasePage.submitImportBtn) + .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed'); + + // Verify that success message is displayed + await t.click(myRedisDatabasePage.closeDialogBtn); + await databasesActions.importDatabase(rdmData.path); + await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Successfully added databases number not correct'); + + // Verify that list of databases is reloaded when database added + await t.click(myRedisDatabasePage.okDialogBtn); + await databasesActions.verifyDatabasesDisplayed(rdmData.dbNames); + + // Verify that user can import database with all data + await clickOnEditDatabaseByName(rdmData.dbNames[0]); + // Verify username imported + await t.expect(addRedisDatabasePage.usernameInput.value).eql(rdmData.userName); + // Verify password imported + await t.click(addRedisDatabasePage.showPasswordBtn); + await t.expect(addRedisDatabasePage.passwordInput.value).eql(rdmData.password); + // Verify cluster connection type imported + await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType); + + // Verify that user can import files from Racompass, ARDM, RDM + for (const db of dbData) { + await databasesActions.importDatabase(db.path, db.type); + await t.click(myRedisDatabasePage.okDialogBtn); + await databasesActions.verifyDatabasesDisplayed(db.dbNames); + } + }); From a984c4d016c502b908e4bc3dba690e5fed2946bb Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 16 Nov 2022 21:40:41 +0100 Subject: [PATCH 012/107] Revert "add tests for databases import" This reverts commit 775325a109fb82eb5f768bb5a455ba5bed512572. --- .../InstanceForm/InstanceForm.tsx | 2 +- tests/e2e/common-actions/databases-actions.ts | 31 ---- tests/e2e/helpers/api/api-database.ts | 14 -- .../pageObjects/add-redis-database-page.ts | 2 - .../pageObjects/my-redis-databases-page.ts | 11 +- tests/e2e/test-data/ardm-valid.ano | 1 - tests/e2e/test-data/racompass-invalid.json | 171 ------------------ tests/e2e/test-data/racompass-valid.json | 171 ------------------ tests/e2e/test-data/rdm-valid.json | 62 ------- .../database/import-databases.e2e.ts | 99 ---------- 10 files changed, 2 insertions(+), 562 deletions(-) delete mode 100644 tests/e2e/common-actions/databases-actions.ts delete mode 100644 tests/e2e/test-data/ardm-valid.ano delete mode 100644 tests/e2e/test-data/racompass-invalid.json delete mode 100644 tests/e2e/test-data/racompass-valid.json delete mode 100644 tests/e2e/test-data/rdm-valid.json delete mode 100644 tests/e2e/tests/critical-path/database/import-databases.e2e.ts diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx index 42e85eef0a..c9af186d30 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx @@ -448,7 +448,7 @@ const AddStandaloneForm = (props: Props) => { label={( Connection Type: - + {capitalize(connectionType)} diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts deleted file mode 100644 index 129b24dbf7..0000000000 --- a/tests/e2e/common-actions/databases-actions.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { t } from 'testcafe'; -import { MyRedisDatabasePage } from '../pageObjects'; - -const myRedisDatabasePage = new MyRedisDatabasePage(); - -export class DatabasesActions { - /** - * Verify that databases are displayed - * @param databases The list of databases to verify - */ - async verifyDatabasesDisplayed(databases: string[]): Promise { - for (const db in databases) { - const databaseName = myRedisDatabasePage.dbNameList.withText(db); - await t.expect(databaseName.exists).ok(`"${db}" database doesn't exist`); - } - } - - /** - * Import database using file - * @param pathToFile The path to file for import - * @param databaseType The database type - */ - async importDatabase(pathToFile: string, databaseType = ''): Promise { - await t - .click(myRedisDatabasePage.importDatabasesBtn) - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [pathToFile]) - .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.successImportMessage.exists).ok(`Successfully added ${databaseType} databases message not displayed`); - } - -} diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 8cd2693be8..4f87be5e4c 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -153,20 +153,6 @@ export async function deleteStandaloneDatabaseApi(databaseParameters: AddNewData .expect(200); } -/** - * Delete Standalone databases using their names through api - * @param databaseNames Databases names - */ -export async function deleteStandaloneDatabasesByNamesApi(databaseNames: string[]): Promise { - databaseNames.forEach(async databaseName => { - const databaseId = await getDatabaseByName(databaseName); - await request(endpoint).delete('/databases') - .send({ 'ids': [`${databaseId}`] }) - .set('Accept', 'application/json') - .expect(200); - }); -} - /** * Delete database from OSS Cluster through api * @param databaseParameters The database parameters diff --git a/tests/e2e/pageObjects/add-redis-database-page.ts b/tests/e2e/pageObjects/add-redis-database-page.ts index 22fcad871b..ff93abfd52 100644 --- a/tests/e2e/pageObjects/add-redis-database-page.ts +++ b/tests/e2e/pageObjects/add-redis-database-page.ts @@ -28,7 +28,6 @@ export class AddRedisDatabasePage { sentinelDatabaseNavigation = Selector('[data-testid=database-nav-group]'); cloneSentinelDatabaseNavigation = Selector('[data-testid=database-nav-group-clone]'); cancelButton = Selector('[data-testid=btn-cancel]'); - showPasswordBtn = Selector('[aria-label^="Show password"]'); //TEXT INPUTS (also referred to as 'Text fields') hostInput = Selector('[data-testid=host]'); portInput = Selector('[data-testid=port]'); @@ -43,7 +42,6 @@ export class AddRedisDatabasePage { databaseIndexMessage = Selector('[data-testid=db-index-message]'); primaryGroupNameInput = Selector('[data-testid=primary-group]'); masterGroupPassword = Selector('[data-testid=sentinel-master-password]'); - connectionType = Selector('[data-testid=connection-type]'); //Links buildFromSource = Selector('a').withExactText('Build from source'); buildFromDocker = Selector('a').withExactText('Docker'); diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 89a78e35da..e749a9d795 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -15,7 +15,7 @@ export class MyRedisDatabasePage { githubButton = Selector('[data-testid=github-repo-icon]'); browserButton = Selector('[data-testid=browser-page-btn]'); pubSubButton = Selector('[data-testid=pub-sub-page-btn]'); - myRedisDBButton = Selector('[data-test-subj=home-page-btn]', { timeout: 1000 }); + myRedisDBButton = Selector('[data-test-subj=home-page-btn]'); deleteDatabaseButton = Selector('[data-testid^=delete-instance-]'); confirmDeleteButton = Selector('[data-testid^=delete-instance-]').withExactText('Remove'); toastCloseButton = Selector('[data-test-subj=toastCloseButton]'); @@ -30,10 +30,6 @@ export class MyRedisDatabasePage { sortByHostAndPort = Selector('span').withAttribute('title', 'Host:Port'); sortByConnectionType = Selector('span').withAttribute('title', 'Connection Type'); sortByLastConnection = Selector('span').withAttribute('title', 'Last connection'); - importDatabasesBtn = Selector('[data-testid=import-dbs-btn]'); - submitImportBtn = Selector('[data-testid=submit-btn]'); - closeDialogBtn = Selector('[aria-label="Closes this modal window"]'); - okDialogBtn = Selector('[data-testid=ok-btn]'); //CHECKBOXES selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]'); //ICONS @@ -50,7 +46,6 @@ export class MyRedisDatabasePage { //TEXT INPUTS (also referred to as 'Text fields') aliasInput = Selector('[data-testid=alias-input]'); searchInput = Selector('[data-testid=search-database-list]'); - importDatabaseInput = Selector('[data-testid=import-databases-input-file]'); //TEXT ELEMENTS moduleTooltip = Selector('.euiToolTipPopover'); moduleQuantifier = Selector('[data-testid=_module]'); @@ -60,10 +55,6 @@ export class MyRedisDatabasePage { hostPort = Selector('[data-testid=host-port]'); noResultsFoundMessage = Selector('div').withExactText('No results found'); noResultsFoundText = Selector('div').withExactText('No databases matched your search. Try reducing the criteria.'); - failedImportMessage = Selector('[data-testid=result-failed]'); - successImportMessage = Selector('[data-testid=result-success]'); - // DIALOG - importDbDialog = Selector('[data-testid=import-dbs-dialog]'); /** * Click on the database by name diff --git a/tests/e2e/test-data/ardm-valid.ano b/tests/e2e/test-data/ardm-valid.ano deleted file mode 100644 index 7c557d1989..0000000000 --- a/tests/e2e/test-data/ardm-valid.ano +++ /dev/null @@ -1 +0,0 @@ -W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9XQ== \ No newline at end of file diff --git a/tests/e2e/test-data/racompass-invalid.json b/tests/e2e/test-data/racompass-invalid.json deleted file mode 100644 index aab8b3e325..0000000000 --- a/tests/e2e/test-data/racompass-invalid.json +++ /dev/null @@ -1,171 +0,0 @@ -[ - { - "srvRecords": [ - { - "priority": null, - "weight": null, - "port": null, - "name": null - } - ], - "useSRVRecords": false, - "natMaps": [ - { - "privateHost": null, - "privatePort": null, - "publicHost": null, - "publicPort": null - } - ], - "enableNatMaps": false, - "clusterOptions": { - "slotsRefreshInterval": 5000, - "slotsRefreshTimeout": 1000, - "retryDelayOnTryAgain": 100, - "retryDelayOnClusterDown": 100, - "retryDelayOnFailover": 100, - "retryDelayOnMoved": 0, - "maxRedirections": 16, - "dnsLookup": false, - "scaleReads": "master", - "startupNodes": [ - { - "host": null, - "port": null - } - ] - }, - "enableStartupNodes": false, - "sentinelOptions": { - "tls": { - "key": null, - "keyBookmark": null, - "ca": null, - "caBookmark": null, - "cert": null, - "certBookmark": null - }, - "preferredSlaves": [ - { - "host": null, - "port": null, - "priority": null - } - ], - "role": null, - "sentinelPassword": null, - "name": null - }, - "enablePreferredSlaves": false, - "sshKeyPassphrase": null, - "sshKeyFileBookmark": null, - "sshKeyFile": null, - "sshUser": null, - "sshPort": null, - "sshHost": null, - "ssh": false, - "caCertBookmark": null, - "caCert": null, - "certificateBookmark": null, - "certificate": null, - "keyFileBookmark": null, - "keyFile": null, - "ssl": false, - "default": false, - "star": false, - "totalDb": 16, - "db": 0, - "password": "", - "color": "#4B5563", - "host": "localhost", - "keyPrefix": null, - "type": "standalone", - "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9" - }, - { - "srvRecords": [ - { - "priority": null, - "weight": null, - "port": null, - "name": null - } - ], - "useSRVRecords": false, - "natMaps": [ - { - "privateHost": null, - "privatePort": null, - "publicHost": null, - "publicPort": null - } - ], - "enableNatMaps": false, - "clusterOptions": { - "slotsRefreshInterval": 5000, - "slotsRefreshTimeout": 1000, - "retryDelayOnTryAgain": 100, - "retryDelayOnClusterDown": 100, - "retryDelayOnFailover": 100, - "retryDelayOnMoved": 0, - "maxRedirections": 16, - "dnsLookup": false, - "scaleReads": "master", - "startupNodes": [ - { - "host": null, - "port": null - } - ] - }, - "enableStartupNodes": false, - "sentinelOptions": { - "tls": { - "key": null, - "keyBookmark": null, - "ca": null, - "caBookmark": null, - "cert": null, - "certBookmark": null - }, - "preferredSlaves": [ - { - "host": null, - "port": null, - "priority": null - } - ], - "role": null, - "sentinelPassword": null, - "name": null - }, - "enablePreferredSlaves": false, - "sshKeyPassphrase": null, - "sshKeyFileBookmark": null, - "sshKeyFile": null, - "sshUser": null, - "sshPort": null, - "sshHost": null, - "ssh": false, - "caCertBookmark": null, - "caCert": null, - "certificateBookmark": null, - "certificate": null, - "keyFileBookmark": null, - "keyFile": null, - "ssl": false, - "default": false, - "star": false, - "totalDb": 16, - "db": 1, - "password": "vfsd", - "color": "#4B5563", - "port": 1111, - "keyPrefix": null, - "type": "standalone", - "connectionName": "vd", - "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", - "cluster": true, - "name": "vd long host" - } -] \ No newline at end of file diff --git a/tests/e2e/test-data/racompass-valid.json b/tests/e2e/test-data/racompass-valid.json deleted file mode 100644 index dc4e32dcf5..0000000000 --- a/tests/e2e/test-data/racompass-valid.json +++ /dev/null @@ -1,171 +0,0 @@ -[ - { - "srvRecords": [ - { - "priority": null, - "weight": null, - "port": null, - "name": null - } - ], - "useSRVRecords": false, - "natMaps": [ - { - "privateHost": null, - "privatePort": null, - "publicHost": null, - "publicPort": null - } - ], - "enableNatMaps": false, - "clusterOptions": { - "slotsRefreshInterval": 5000, - "slotsRefreshTimeout": 1000, - "retryDelayOnTryAgain": 100, - "retryDelayOnClusterDown": 100, - "retryDelayOnFailover": 100, - "retryDelayOnMoved": 0, - "maxRedirections": 16, - "dnsLookup": false, - "scaleReads": "master", - "startupNodes": [ - { - "host": null, - "port": null - } - ] - }, - "enableStartupNodes": false, - "sentinelOptions": { - "tls": { - "key": null, - "keyBookmark": null, - "ca": null, - "caBookmark": null, - "cert": null, - "certBookmark": null - }, - "preferredSlaves": [ - { - "host": null, - "port": null, - "priority": null - } - ], - "role": null, - "sentinelPassword": null, - "name": null - }, - "enablePreferredSlaves": false, - "sshKeyPassphrase": null, - "sshKeyFileBookmark": null, - "sshKeyFile": null, - "sshUser": null, - "sshPort": null, - "sshHost": null, - "ssh": false, - "caCertBookmark": null, - "caCert": null, - "certificateBookmark": null, - "certificate": null, - "keyFileBookmark": null, - "keyFile": null, - "ssl": false, - "default": false, - "star": false, - "totalDb": 16, - "db": 1, - "password": "", - "color": "#4B5563", - "port": 8100, - "host": "racompassDbWithIndex", - "keyPrefix": null, - "type": "standalone", - "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9" - }, - { - "srvRecords": [ - { - "priority": null, - "weight": null, - "port": null, - "name": null - } - ], - "useSRVRecords": false, - "natMaps": [ - { - "privateHost": null, - "privatePort": null, - "publicHost": null, - "publicPort": null - } - ], - "enableNatMaps": false, - "clusterOptions": { - "slotsRefreshInterval": 5000, - "slotsRefreshTimeout": 1000, - "retryDelayOnTryAgain": 100, - "retryDelayOnClusterDown": 100, - "retryDelayOnFailover": 100, - "retryDelayOnMoved": 0, - "maxRedirections": 16, - "dnsLookup": false, - "scaleReads": "master", - "startupNodes": [ - { - "host": null, - "port": null - } - ] - }, - "enableStartupNodes": false, - "sentinelOptions": { - "tls": { - "key": null, - "keyBookmark": null, - "ca": null, - "caBookmark": null, - "cert": null, - "certBookmark": null - }, - "preferredSlaves": [ - { - "host": null, - "port": null, - "priority": null - } - ], - "role": null, - "sentinelPassword": null, - "name": null - }, - "enablePreferredSlaves": false, - "sshKeyPassphrase": null, - "sshKeyFileBookmark": null, - "sshKeyFile": null, - "sshUser": null, - "sshPort": null, - "sshHost": null, - "ssh": false, - "caCertBookmark": null, - "caCert": null, - "certificateBookmark": null, - "certificate": null, - "keyFileBookmark": null, - "keyFile": null, - "ssl": false, - "default": false, - "star": false, - "totalDb": 16, - "db": 0, - "password": "vfsd", - "color": "#4B5563", - "host": "localhost", - "port": 1111, - "keyPrefix": null, - "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", - "cluster": true, - "name": "racompassCluster" - } -] \ No newline at end of file diff --git a/tests/e2e/test-data/rdm-valid.json b/tests/e2e/test-data/rdm-valid.json deleted file mode 100644 index 845fe507e7..0000000000 --- a/tests/e2e/test-data/rdm-valid.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "password": "new", - "filter_history": { - "*": 1, - "stream1": 1 - }, - "host": "localhost", - "name": "vfd das jashd ashdkjh kjhasfh hfjks dahfjk shdk fhskjad hfkj sdhkj fashk dhfk sahkj fhsak dhfskja hsd kfjh dsakj fhsdjk fhskdjal fhksjd hfkjsd hfkjs hakdjfh sjkdah fjksdh fjksdh jkfh ksdh fsdkjhfksjhfkhsdkf hksjdhf kjsdhf skdhf sdf sdfsda fsdfsd fsd fsdf sd f sdf sd f sd fsd f sd f sd fsd f sad fs df sd f s dsa fsdf vfd das jashd ashdkjh", - "ssh_port": 22, - "timeout_connect": 60000, - "timeout_execute": 60000, - "cluster": false, - "invalid_field": "inv", - "db": 0 - }, - { - "host": "rdmWithUsernameAndPass1", - "port": "1561", - "cluster": true, - "username": "rdmUsername", - "auth": "rdmAuth" - }, - { - "auth": "", - "host": "172.30.100.151", - "name": "oss cluster", - "ssh_port": 22, - "cluster": true, - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "auth": "longname database >500 symbols invalid", - "host": "172.30.100.181", - "port": 1000, - "keys_pattern": "*", - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla rutrum nec libero aliquet ultricies. Donec ut blandit dui, ac hendrerit risus. Vestibulum sodales sed risus ac auctor. Integer quis justo vel leo gravida volutpat quis et risus. Nam vestibulum fermentum eros, in ullamcorper nulla laoreet quis. Vestibulum ut arcu nec turpis elementum malesuada. Maecenas congue felis nec posuere ullamcorper. Phasellus auctor leo sit amet ligula scelerisque, quis elementum ligula auctor. Quisque id leo r", - "namespace_separator": ":", - "ssh_port": 22, - "ssl": true, - "ssl_ca_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/ca.crt", - "ssl_local_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.crt", - "ssl_private_key_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.key", - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "auth": "pass", - "host": "localhost", - "name": "plain-certs+pass", - "port": 65537, - "ssh_port": 22, - "ssl_local_cert_path": "C:/Users/enaboko/Downloads/certificates", - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "host": "rdmOnlyHostPortDB2", - "port": 6379 - } -] diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts deleted file mode 100644 index db100c0727..0000000000 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { rte } from '../../../helpers/constants'; -import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; -import { commonUrl } from '../../../helpers/conf'; -import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; -import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; -import { DatabasesActions } from '../../../common-actions/databases-actions'; - -const browserPage = new BrowserPage(); -const myRedisDatabasePage = new MyRedisDatabasePage(); -const databasesActions = new DatabasesActions(); -const addRedisDatabasePage = new AddRedisDatabasePage(); - -const invalidJsonPath = '../../../test-data/racompass-invalid.json'; -const rdmData = { - type: 'rdm', - path: '../../../test-data/rdm-valid.json', - dbNames: ['rdmWithUsernameAndPass1:1561', 'rdmOnlyHostPortDB2:6379'], - userName: 'rdmUsername', - password: 'rdmAuth', - connectionType: 'Cluster' -}; -const dbData = [ - { - type: 'racompass', - path: '../../../test-data/racompass-valid.json', - dbNames: ['racompassCluster', 'racompassDbWithIndex:8100 [1]'] - }, - { - type: 'ardm', - path: '../../../test-data/ardm-valid.ano', - dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] - } -]; -const databases = [ - rdmData.dbNames[0], - rdmData.dbNames[1], - dbData[0].dbNames[0], - dbData[0].dbNames[1].split(' ')[0], - dbData[1].dbNames[0], - dbData[1].dbNames[1] -]; - -fixture `Import databases` - .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl); -test - .before(async() => { - await acceptLicenseTerms(); - }) - .after(async() => { - // Delete databases - deleteStandaloneDatabasesByNamesApi(databases); - })('Connection import from JSON', async t => { - const tooltipText = 'Import Database Connections'; - const partialImportedMsg = 'Successfully added 2 of 6 database connections'; - - // Verify that user can see the “Import Database Connections” tooltip - await t.hover(myRedisDatabasePage.importDatabasesBtn); - await t.hover(myRedisDatabasePage.importDatabasesBtn); - await t.expect(browserPage.tooltip.innerText).contains(tooltipText, 'The tooltip message not displayed/correct'); - - // Verify that Import dialogue is not closed when clicking any area outside the box - await t.click(myRedisDatabasePage.importDatabasesBtn); - await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not opened'); - await t.click(myRedisDatabasePage.myRedisDBButton); - await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not displayed'); - - // Verify that user see the message when parse error appears - await t - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [invalidJsonPath]) - .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed'); - - // Verify that success message is displayed - await t.click(myRedisDatabasePage.closeDialogBtn); - await databasesActions.importDatabase(rdmData.path); - await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Successfully added databases number not correct'); - - // Verify that list of databases is reloaded when database added - await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(rdmData.dbNames); - - // Verify that user can import database with all data - await clickOnEditDatabaseByName(rdmData.dbNames[0]); - // Verify username imported - await t.expect(addRedisDatabasePage.usernameInput.value).eql(rdmData.userName); - // Verify password imported - await t.click(addRedisDatabasePage.showPasswordBtn); - await t.expect(addRedisDatabasePage.passwordInput.value).eql(rdmData.password); - // Verify cluster connection type imported - await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType); - - // Verify that user can import files from Racompass, ARDM, RDM - for (const db of dbData) { - await databasesActions.importDatabase(db.path, db.type); - await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(db.dbNames); - } - }); From dbe45dfbe789c66e8a9b49a79c0882e088169be3 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Wed, 16 Nov 2022 22:00:33 +0100 Subject: [PATCH 013/107] upd --- .../e2e/tests/critical-path/database/import-databases.e2e.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index db100c0727..ae0319325c 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -71,16 +71,17 @@ test .click(myRedisDatabasePage.submitImportBtn) .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed'); - // Verify that success message is displayed + // Verify that user can import database with mandatory fields await t.click(myRedisDatabasePage.closeDialogBtn); await databasesActions.importDatabase(rdmData.path); + // Verify that success message is displayed await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Successfully added databases number not correct'); // Verify that list of databases is reloaded when database added await t.click(myRedisDatabasePage.okDialogBtn); await databasesActions.verifyDatabasesDisplayed(rdmData.dbNames); - // Verify that user can import database with all data + // Verify that user can import database with mandatory+optional data await clickOnEditDatabaseByName(rdmData.dbNames[0]); // Verify username imported await t.expect(addRedisDatabasePage.usernameInput.value).eql(rdmData.userName); From 3fc18725dfbfc442ed72637468d6f2eafd1e7df4 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Nov 2022 10:53:23 +0100 Subject: [PATCH 014/107] fix --- tests/e2e/common-actions/databases-actions.ts | 2 +- tests/e2e/tests/critical-path/database/import-databases.e2e.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index 129b24dbf7..a33e0b22f7 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -9,7 +9,7 @@ export class DatabasesActions { * @param databases The list of databases to verify */ async verifyDatabasesDisplayed(databases: string[]): Promise { - for (const db in databases) { + for (const db of databases) { const databaseName = myRedisDatabasePage.dbNameList.withText(db); await t.expect(databaseName.exists).ok(`"${db}" database doesn't exist`); } diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index ae0319325c..dcbe4ed2bf 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -31,6 +31,7 @@ const dbData = [ dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] } ]; +// List of all created databases to delete const databases = [ rdmData.dbNames[0], rdmData.dbNames[1], From aa9bd15cf2b1373843ed36f6de73f107e7f5be81 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 17 Nov 2022 12:27:14 +0100 Subject: [PATCH 015/107] add test for remove file --- tests/e2e/pageObjects/my-redis-databases-page.ts | 1 + .../critical-path/database/import-databases.e2e.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 89a78e35da..668c1d5f40 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -34,6 +34,7 @@ export class MyRedisDatabasePage { submitImportBtn = Selector('[data-testid=submit-btn]'); closeDialogBtn = Selector('[aria-label="Closes this modal window"]'); okDialogBtn = Selector('[data-testid=ok-btn]'); + removeImportedFileBtn = Selector('[aria-label="Clear selected files"]'); //CHECKBOXES selectAllCheckbox = Selector('[data-test-subj=checkboxSelectAll]'); //ICONS diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index dcbe4ed2bf..2e7a1e91ce 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -17,7 +17,8 @@ const rdmData = { dbNames: ['rdmWithUsernameAndPass1:1561', 'rdmOnlyHostPortDB2:6379'], userName: 'rdmUsername', password: 'rdmAuth', - connectionType: 'Cluster' + connectionType: 'Cluster', + fileName: 'rdm-valid.json' }; const dbData = [ { @@ -54,6 +55,7 @@ test })('Connection import from JSON', async t => { const tooltipText = 'Import Database Connections'; const partialImportedMsg = 'Successfully added 2 of 6 database connections'; + const defaultText = 'Select or drag and drop a file'; // Verify that user can see the “Import Database Connections” tooltip await t.hover(myRedisDatabasePage.importDatabasesBtn); @@ -72,6 +74,15 @@ test .click(myRedisDatabasePage.submitImportBtn) .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed'); + // Verify that user can remove file from import input + await t.click(myRedisDatabasePage.closeDialogBtn); + await t.click(myRedisDatabasePage.importDatabasesBtn); + await t.setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [rdmData.path]); + await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(rdmData.fileName, 'Filename not displayed in import input'); + // Click on remove button + await t.click(myRedisDatabasePage.removeImportedFileBtn); + await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); + // Verify that user can import database with mandatory fields await t.click(myRedisDatabasePage.closeDialogBtn); await databasesActions.importDatabase(rdmData.path); From 000a6327e7e5ee1c38b5aa4db751519511a4cb4d Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 18 Nov 2022 10:07:01 +0400 Subject: [PATCH 016/107] #RI-3852 - update text --- .../import-databases-dialog/ImportDatabasesDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx index ea91f9f781..8215352ca8 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -95,7 +95,7 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { /> {isInvalid && ( - File should not exceed {MAX_MB_FILE} mb + File should not exceed {MAX_MB_FILE} MB )} From 4be338b0e43cf83a5e1a1cc77b85e53f587ab029 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 18 Nov 2022 10:09:21 +0100 Subject: [PATCH 017/107] fixes by comments --- tests/e2e/common-actions/databases-actions.ts | 29 +++++++++++++++---- .../database/import-databases.e2e.ts | 8 ++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index a33e0b22f7..3167ce1b2c 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -17,15 +17,34 @@ export class DatabasesActions { /** * Import database using file - * @param pathToFile The path to file for import - * @param databaseType The database type + * @param fileParameters The arguments of imported file */ - async importDatabase(pathToFile: string, databaseType = ''): Promise { + async importDatabase(fileParameters: ImportDatabaseParameters): Promise { await t .click(myRedisDatabasePage.importDatabasesBtn) - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [pathToFile]) + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [fileParameters.path]) .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.successImportMessage.exists).ok(`Successfully added ${databaseType} databases message not displayed`); + .expect(myRedisDatabasePage.successImportMessage.exists).ok(`Successfully added ${fileParameters.type} databases message not displayed`); } } + +/** + * Import database parameters + * @param path The path to file + * @param type The type of application + * @param dbNames The names of databases + * @param userName The username of db + * @param password The password of db + * @param connectionType The connection type of db + * @param fileName The file name + */ +export type ImportDatabaseParameters = { + path: string, + type?: string, + dbNames?: string[], + userName?: string, + password?: string, + connectionType?: string, + fileName?: string +}; diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index 2e7a1e91ce..ce738b9dac 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -58,7 +58,7 @@ test const defaultText = 'Select or drag and drop a file'; // Verify that user can see the “Import Database Connections” tooltip - await t.hover(myRedisDatabasePage.importDatabasesBtn); + await t.expect(myRedisDatabasePage.importDatabasesBtn.visible).ok('The import databases button not displayed'); await t.hover(myRedisDatabasePage.importDatabasesBtn); await t.expect(browserPage.tooltip.innerText).contains(tooltipText, 'The tooltip message not displayed/correct'); @@ -85,9 +85,9 @@ test // Verify that user can import database with mandatory fields await t.click(myRedisDatabasePage.closeDialogBtn); - await databasesActions.importDatabase(rdmData.path); + await databasesActions.importDatabase(rdmData); // Verify that success message is displayed - await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Successfully added databases number not correct'); + await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Databases not imported successfully'); // Verify that list of databases is reloaded when database added await t.click(myRedisDatabasePage.okDialogBtn); @@ -105,7 +105,7 @@ test // Verify that user can import files from Racompass, ARDM, RDM for (const db of dbData) { - await databasesActions.importDatabase(db.path, db.type); + await databasesActions.importDatabase(db); await t.click(myRedisDatabasePage.okDialogBtn); await databasesActions.verifyDatabasesDisplayed(db.dbNames); } From 588e10c3981014851810f7c7fabfb9ff95302cad Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Mon, 21 Nov 2022 16:59:00 +0400 Subject: [PATCH 018/107] #RI-3804 - optimize LS instances data --- .../browser/components/key-tree/KeyTree.tsx | 6 +- .../KeyTreeDelimiter.spec.tsx | 32 ---------- .../KeyTreeDelimiter/KeyTreeDelimiter.tsx | 19 ++---- .../components/header/Header.tsx | 14 ++--- redisinsight/ui/src/pages/home/HomePage.tsx | 9 ++- .../ui/src/pages/instance/InstancePage.tsx | 2 + .../ui/src/pages/slowLog/SlowLogPage.tsx | 4 +- .../SlowLogConfig/SlowLogConfig.tsx | 10 ++-- redisinsight/ui/src/services/storage.ts | 4 ++ .../ui/src/slices/analytics/slowlog.ts | 31 ++++------ redisinsight/ui/src/slices/app/context.ts | 29 +++++++-- .../ui/src/slices/interfaces/analytics.ts | 2 - redisinsight/ui/src/slices/interfaces/app.ts | 6 +- .../ui/src/slices/interfaces/instances.ts | 2 +- .../slices/tests/analytics/slowlog.spec.ts | 13 ++-- .../ui/src/slices/tests/app/context.spec.ts | 59 +++++++++++++++++-- .../ui/src/styles/components/_buttons.scss | 10 +++- redisinsight/ui/src/utils/index.ts | 1 + redisinsight/ui/src/utils/optimizations.ts | 39 ++++++++++++ 19 files changed, 181 insertions(+), 111 deletions(-) create mode 100644 redisinsight/ui/src/utils/optimizations.ts diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index e75e65f742..a3a438441d 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -1,10 +1,11 @@ -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, useTransition } from 'react' +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useTransition } from 'react' import cx from 'classnames' import { EuiResizableContainer } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' import { appContextBrowserTree, + appContextDbConfig, setBrowserTreeNodesOpen, setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' @@ -39,7 +40,8 @@ const KeyTree = forwardRef((props: Props, ref) => { const firstPanelId = 'tree' const secondPanelId = 'keys' - const { delimiter, panelSizes, openNodes, selectedLeaf } = useSelector(appContextBrowserTree) + const { panelSizes, openNodes, selectedLeaf } = useSelector(appContextBrowserTree) + const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) const [,startTransition] = useTransition() diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx index 8140e17b4a..3b14d916bc 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx @@ -3,7 +3,6 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { DEFAULT_DELIMITER } from 'uiSrc/constants' import { setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' -import { localStorageService } from 'uiSrc/services' import { cleanup, clearStoreActions, @@ -57,37 +56,6 @@ describe('KeyTreeDelimiter', () => { expect(screen.getByTestId(DELIMITER_INPUT)).toBeInTheDocument() }) - it('"setBrowserTreeDelimiter" should be called with value for LocalStorage after render', async () => { - jest.useFakeTimers() - const localStorageValue = 'test' - localStorageService.get = jest.fn().mockReturnValue(localStorageValue) - - render() - - jest.advanceTimersByTime(0) - - const expectedActions = [setBrowserTreeDelimiter(localStorageValue)] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) - - it('"setBrowserTreeDelimiter" should be called with DEFAULT_DELIMITER after render', async () => { - jest.useFakeTimers() - const localStorageValue = '' - localStorageService.get = jest.fn().mockReturnValue(localStorageValue) - render() - - jest.advanceTimersByTime(0) - - const expectedActions = [setBrowserTreeDelimiter(DEFAULT_DELIMITER)] - - expect(clearStoreActions(store.getActions())).toEqual( - clearStoreActions(expectedActions) - ) - }) - it('"setBrowserTreeDelimiter" should be called after Apply change delimiter', async () => { const value = 'val' render() diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx index 2e0d6edf95..0422408031 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx @@ -1,15 +1,14 @@ -import React, { useEffect, useState } from 'react' +import React, { useState } from 'react' import cx from 'classnames' import { useDispatch, useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import { EuiIcon, EuiPopover } from '@elastic/eui' import { replaceSpaces } from 'uiSrc/utils' -import { localStorageService } from 'uiSrc/services' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import InlineItemEditor from 'uiSrc/components/inline-item-editor' -import { BrowserStorageItem, DEFAULT_DELIMITER } from 'uiSrc/constants' -import { appContextBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' +import { DEFAULT_DELIMITER } from 'uiSrc/constants' +import { appContextDbConfig, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' @@ -19,7 +18,7 @@ export interface Props { const MAX_DELIMITER_LENGTH = 5 const KeyTreeDelimiter = ({ loading }: Props) => { const { instanceId = '' } = useParams<{ instanceId: string }>() - const { delimiter = '' } = useSelector(appContextBrowserTree) + const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) const [isPopoverOpen, setIsPopoverOpen] = useState(false) const dispatch = useDispatch() @@ -27,16 +26,6 @@ const KeyTreeDelimiter = ({ loading }: Props) => { const onButtonClick = () => setIsPopoverOpen((isPopoverOpen) => !isPopoverOpen) const closePopover = () => setIsPopoverOpen(false) - useEffect(() => { - const delimiterStorage = localStorageService.get(BrowserStorageItem.treeViewDelimiter + instanceId) - || delimiter - || DEFAULT_DELIMITER - - setTimeout(() => { - dispatch(setBrowserTreeDelimiter(delimiterStorage)) - }, 0) - }, []) - const button = (
{ const { instanceId } = useParams<{ instanceId: string }>() const dispatch = useDispatch() - const { delimiter = '' } = useSelector(appContextBrowserTree) - - const delimiterStorage = localStorageService.get(BrowserStorageItem.treeViewDelimiter + instanceId) - || delimiter - || DEFAULT_DELIMITER + const { treeViewDelimiter: delimiter = '' } = useSelector(appContextDbConfig) const analysisOptions: EuiSuperSelectOption[] = items.map((item) => { const { createdAt, id } = item @@ -80,7 +74,7 @@ const Header = (props: Props) => { databaseId: instanceId, } }) - dispatch(createNewAnalysis(instanceId, delimiterStorage)) + dispatch(createNewAnalysis(instanceId, delimiter)) } return ( @@ -135,7 +129,7 @@ const Header = (props: Props) => { )} - + { name: TelemetryPageView.DATABASES_LIST_PAGE }) } + if (instances.length && !isPageViewSent) { + optimizeLSInstances(instances) + } }, [instances, analyticsIdentified, isPageViewSent, isChangedInstance]) useEffect(() => { @@ -168,6 +171,10 @@ const HomePage = () => { dispatch(setEditedInstance(null)) setEditDialogIsOpen(false) } + + instances.forEach((instance) => { + localStorageService.remove(BrowserStorageItem.dbConfig + instance.id) + }) } const onResize = ({ width: innerWidth }: { width: number }) => { diff --git a/redisinsight/ui/src/pages/instance/InstancePage.tsx b/redisinsight/ui/src/pages/instance/InstancePage.tsx index 025d20fc7e..cb7767c06e 100644 --- a/redisinsight/ui/src/pages/instance/InstancePage.tsx +++ b/redisinsight/ui/src/pages/instance/InstancePage.tsx @@ -15,6 +15,7 @@ import { appContextSelector, setAppContextConnectedInstanceId, setAppContextInitialState, + setDbConfig, } from 'uiSrc/slices/app/context' import { resetKeysData } from 'uiSrc/slices/browser/keys' import { BrowserStorageItem } from 'uiSrc/constants' @@ -78,6 +79,7 @@ const InstancePage = ({ routes = [] }: Props) => { } dispatch(setAppContextConnectedInstanceId(connectionInstanceId)) + dispatch(setDbConfig(localStorageService.get(BrowserStorageItem.dbConfig + connectionInstanceId))) }, []) useEffect(() => () => { diff --git a/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx b/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx index 32fcf6cea4..20edd3f27c 100644 --- a/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx +++ b/redisinsight/ui/src/pages/slowLog/SlowLogPage.tsx @@ -15,6 +15,7 @@ import { AutoSizer } from 'react-virtualized' import { DEFAULT_SLOWLOG_MAX_LEN } from 'uiSrc/constants' import { DATE_FORMAT } from 'uiSrc/pages/slowLog/components/SlowLogTable/SlowLogTable' import { convertNumberByUnits } from 'uiSrc/pages/slowLog/utils' +import { appContextDbConfig } from 'uiSrc/slices/app/context' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { ConnectionType } from 'uiSrc/slices/interfaces' @@ -51,7 +52,8 @@ const countOptions: EuiSuperSelectOption[] = [ const SlowLogPage = () => { const { connectionType, name: connectedInstanceName, db } = useSelector(connectedInstanceSelector) - const { data, loading, durationUnit, config } = useSelector(slowLogSelector) + const { data, loading, config } = useSelector(slowLogSelector) + const { slowLogDurationUnit: durationUnit } = useSelector(appContextDbConfig) const { slowlogLogSlowerThan = 0, slowlogMaxLen } = useSelector(slowLogConfigSelector) const { identified: analyticsIdentified } = useSelector(appAnalyticsInfoSelector) const { viewTab } = useSelector(analyticsSettingsSelector) diff --git a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx index 56b137d720..05420e1d69 100644 --- a/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx +++ b/redisinsight/ui/src/pages/slowLog/components/SlowLogConfig/SlowLogConfig.tsx @@ -21,9 +21,8 @@ import { DURATION_UNITS, MINUS_ONE, } from 'uiSrc/constants' +import { appContextDbConfig } from 'uiSrc/slices/app/context' import { ConnectionType } from 'uiSrc/slices/interfaces' -import { ConfigDBStorageItem } from 'uiSrc/constants/storage' -import { setDBConfigStorageField } from 'uiSrc/services' import { patchSlowLogConfigAction, slowLogConfigSelector, slowLogSelector } from 'uiSrc/slices/analytics/slowlog' import { errorValidateNegativeInteger, validateNumber } from 'uiSrc/utils' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' @@ -40,13 +39,14 @@ const SlowLogConfig = ({ closePopover, onRefresh }: Props) => { const options = DURATION_UNITS const { instanceId } = useParams<{ instanceId: string }>() const { connectionType } = useSelector(connectedInstanceSelector) - const { loading, durationUnit: durationUnitStore } = useSelector(slowLogSelector) + const { loading } = useSelector(slowLogSelector) + const { slowLogDurationUnit } = useSelector(appContextDbConfig) const { slowlogMaxLen = DEFAULT_SLOWLOG_MAX_LEN, slowlogLogSlowerThan = DEFAULT_SLOWLOG_SLOWER_THAN, } = useSelector(slowLogConfigSelector) - const [durationUnit, setDurationUnit] = useState(durationUnitStore ?? DEFAULT_SLOWLOG_DURATION_UNIT) + const [durationUnit, setDurationUnit] = useState(slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT) const [maxLen, setMaxLen] = useState(`${slowlogMaxLen}`) const [slowerThan, setSlowerThan] = useState(slowlogLogSlowerThan !== MINUS_ONE @@ -96,8 +96,6 @@ const SlowLogConfig = ({ closePopover, onRefresh }: Props) => { } const onSuccess = () => { - setDBConfigStorageField(instanceId, ConfigDBStorageItem.slowLogDurationUnit, durationUnit) - onRefresh(maxLen ? toNumber(maxLen) : DEFAULT_SLOWLOG_MAX_LEN) closePopover() } diff --git a/redisinsight/ui/src/services/storage.ts b/redisinsight/ui/src/services/storage.ts index 80c620cb8b..938ee6e70d 100644 --- a/redisinsight/ui/src/services/storage.ts +++ b/redisinsight/ui/src/services/storage.ts @@ -26,6 +26,10 @@ class StorageService { return null } + getAll() { + return this.storage + } + set(itemName: string = '', item: any) { try { if (isObjectLike(item)) { diff --git a/redisinsight/ui/src/slices/analytics/slowlog.ts b/redisinsight/ui/src/slices/analytics/slowlog.ts index 63b07bf6ee..7cde630e9a 100644 --- a/redisinsight/ui/src/slices/analytics/slowlog.ts +++ b/redisinsight/ui/src/slices/analytics/slowlog.ts @@ -1,11 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { AxiosError } from 'axios' -import { ApiEndpoints, DEFAULT_SLOWLOG_DURATION_UNIT, DurationUnits } from 'uiSrc/constants' -import { apiService, getDBConfigStorageField } from 'uiSrc/services' +import { ApiEndpoints, DurationUnits } from 'uiSrc/constants' +import { apiService } from 'uiSrc/services' +import { setSlowLogUnits } from 'uiSrc/slices/app/context' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import { StateSlowLog } from 'uiSrc/slices/interfaces/analytics' -import { ConfigDBStorageItem } from 'uiSrc/constants/storage' -import { getApiErrorMessage, getUrl, isStatusSuccessful, Nullable } from 'uiSrc/utils' +import { getApiErrorMessage, getUrl, isStatusSuccessful } from 'uiSrc/utils' import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' import { AppDispatch, RootState } from '../store' @@ -15,7 +15,6 @@ export const initialState: StateSlowLog = { error: '', data: [], lastRefreshTime: null, - durationUnit: DurationUnits.microSeconds, config: null } @@ -29,11 +28,10 @@ const slowLogSlice = createSlice({ }, getSlowLogsSuccess: ( state, - { payload: [data, durationUnit] }: PayloadAction<[SlowLog[], DurationUnits]> + { payload: data }: PayloadAction ) => { state.loading = false state.data = data - state.durationUnit = durationUnit state.lastRefreshTime = Date.now() }, getSlowLogsError: (state, { payload }) => { @@ -56,14 +54,10 @@ const slowLogSlice = createSlice({ }, getSlowLogConfigSuccess: ( state, - { payload: [data, durationUnit] }: PayloadAction<[SlowLogConfig, Nullable ]> + { payload: data }: PayloadAction ) => { state.loading = false state.config = data - - if (durationUnit) { - state.durationUnit = durationUnit - } }, getSlowLogConfigError: (state, { payload }) => { state.loading = false @@ -113,13 +107,7 @@ export function fetchSlowLogsAction( ) if (isStatusSuccessful(status)) { - dispatch( - getSlowLogsSuccess([ - data, - getDBConfigStorageField(instanceId, ConfigDBStorageItem.slowLogDurationUnit) - || DEFAULT_SLOWLOG_DURATION_UNIT - ]) - ) + dispatch(getSlowLogsSuccess(data)) onSuccessAction?.(data) } @@ -183,7 +171,7 @@ export function getSlowLogConfigAction( ) if (isStatusSuccessful(status)) { - dispatch(getSlowLogConfigSuccess([data, null])) + dispatch(getSlowLogConfigSuccess(data)) onSuccessAction?.() } @@ -218,7 +206,8 @@ export function patchSlowLogConfigAction( ) if (isStatusSuccessful(status)) { - dispatch(getSlowLogConfigSuccess([data, durationUnit])) + dispatch(getSlowLogConfigSuccess(data)) + dispatch(setSlowLogUnits(durationUnit)) onSuccessAction?.() } diff --git a/redisinsight/ui/src/slices/app/context.ts b/redisinsight/ui/src/slices/app/context.ts index 3bf65167ed..2b5614ea2d 100644 --- a/redisinsight/ui/src/slices/app/context.ts +++ b/redisinsight/ui/src/slices/app/context.ts @@ -1,14 +1,19 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' +import { ConfigDBStorageItem } from 'uiSrc/constants/storage' import { getTreeLeafField, Nullable } from 'uiSrc/utils' -import { BrowserStorageItem, DEFAULT_DELIMITER, KeyTypes } from 'uiSrc/constants' -import { localStorageService } from 'uiSrc/services' +import { BrowserStorageItem, DEFAULT_DELIMITER, DEFAULT_SLOWLOG_DURATION_UNIT, KeyTypes } from 'uiSrc/constants' +import { localStorageService, setDBConfigStorageField } from 'uiSrc/services' import { RootState } from '../store' import { RedisResponseBuffer, StateAppContext } from '../interfaces' export const initialState: StateAppContext = { contextInstanceId: '', lastPage: '', + dbConfig: { + treeViewDelimiter: DEFAULT_DELIMITER, + slowLogDurationUnit: DEFAULT_SLOWLOG_DURATION_UNIT + }, browser: { keyList: { isDataLoaded: false, @@ -70,6 +75,18 @@ const appContextSlice = createSlice({ setAppContextConnectedInstanceId: (state, { payload }: { payload: string }) => { state.contextInstanceId = payload }, + setDbConfig: (state, { payload }) => { + state.dbConfig.treeViewDelimiter = payload?.treeViewDelimiter ?? DEFAULT_DELIMITER + state.dbConfig.slowLogDurationUnit = payload?.slowLogDurationUnit ?? DEFAULT_SLOWLOG_DURATION_UNIT + }, + setSlowLogUnits: (state, { payload }) => { + state.dbConfig.slowLogDurationUnit = payload + setDBConfigStorageField(state.contextInstanceId, ConfigDBStorageItem.slowLogDurationUnit, payload) + }, + setBrowserTreeDelimiter: (state, { payload }: { payload: string }) => { + state.dbConfig.treeViewDelimiter = payload + setDBConfigStorageField(state.contextInstanceId, BrowserStorageItem.treeViewDelimiter, payload) + }, setBrowserSelectedKey: (state, { payload }: { payload: Nullable }) => { state.browser.keyList.selectedKey = payload }, @@ -120,10 +137,6 @@ const appContextSlice = createSlice({ setBrowserTreePanelSizes: (state, { payload }: { payload: any }) => { state.browser.tree.panelSizes = payload }, - setBrowserTreeDelimiter: (state, { payload }: { payload: string }) => { - localStorageService.set(BrowserStorageItem.treeViewDelimiter + state.contextInstanceId, payload) - state.browser.tree.delimiter = payload - }, setWorkbenchScript: (state, { payload }: { payload: string }) => { state.workbench.script = payload }, @@ -176,6 +189,8 @@ const appContextSlice = createSlice({ export const { setAppContextInitialState, setAppContextConnectedInstanceId, + setDbConfig, + setSlowLogUnits, setBrowserKeyListDataLoaded, setBrowserSelectedKey, setBrowserPatternScrollPosition, @@ -203,6 +218,8 @@ export const { // Selectors export const appContextSelector = (state: RootState) => state.app.context +export const appContextDbConfig = (state: RootState) => + state.app.context.dbConfig export const appContextBrowser = (state: RootState) => state.app.context.browser export const appContextBrowserTree = (state: RootState) => diff --git a/redisinsight/ui/src/slices/interfaces/analytics.ts b/redisinsight/ui/src/slices/interfaces/analytics.ts index a28cddd868..5018ad11b9 100644 --- a/redisinsight/ui/src/slices/interfaces/analytics.ts +++ b/redisinsight/ui/src/slices/interfaces/analytics.ts @@ -1,4 +1,3 @@ -import { DurationUnits } from 'uiSrc/constants' import { Nullable } from 'uiSrc/utils' import { SlowLog, SlowLogConfig } from 'apiSrc/modules/slow-log/models' import { ClusterDetails } from 'apiSrc/modules/cluster-monitor/models/cluster-details' @@ -10,7 +9,6 @@ export interface StateSlowLog { data: SlowLog[] lastRefreshTime: Nullable config: Nullable - durationUnit: DurationUnits } export interface StateClusterDetails { diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index 6959095533..600bc89a50 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -1,7 +1,7 @@ import { AxiosError } from 'axios' import { RelativeWidthSizes } from 'uiSrc/components/virtual-table/interfaces' import { Nullable } from 'uiSrc/utils' -import { ICommands } from 'uiSrc/constants' +import { DurationUnits, ICommands } from 'uiSrc/constants' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { GetServerInfoResponse } from 'apiSrc/modules/server/dto/server.dto' import { RedisString as RedisStringAPI } from 'apiSrc/common/constants/redis-string' @@ -38,6 +38,10 @@ export interface StateAppInfo { export interface StateAppContext { contextInstanceId: string lastPage: string + dbConfig: { + treeViewDelimiter: string + slowLogDurationUnit: DurationUnits + } browser: { keyList: { isDataLoaded: boolean diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 70898baede..64260caed9 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -26,7 +26,7 @@ export interface Instance extends DatabaseInstanceResponse { password?: Nullable username?: Nullable name?: string - db?: string + db?: number tls?: boolean tlsClientAuthRequired?: boolean verifyServerCert?: boolean diff --git a/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts b/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts index c7ba590eaf..7075baa82d 100644 --- a/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts +++ b/redisinsight/ui/src/slices/tests/analytics/slowlog.spec.ts @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' import { DEFAULT_SLOWLOG_DURATION_UNIT } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' +import { setSlowLogUnits } from 'uiSrc/slices/app/context' import { cleanup, mockedStore, initialStateDefault } from 'uiSrc/utils/test-utils' import { addErrorNotification } from 'uiSrc/slices/app/notifications' @@ -111,12 +112,11 @@ describe('slowLog slice', () => { ...initialState, loading: false, data, - durationUnit: DEFAULT_SLOWLOG_DURATION_UNIT, lastRefreshTime: timestamp } // Act - const nextState = reducer(initialState, getSlowLogsSuccess([data, DEFAULT_SLOWLOG_DURATION_UNIT])) + const nextState = reducer(initialState, getSlowLogsSuccess(data)) // Assert const rootState = Object.assign(initialStateDefault, { @@ -240,7 +240,7 @@ describe('slowLog slice', () => { } // Act - const nextState = reducer(initialState, getSlowLogConfigSuccess([config, null])) + const nextState = reducer(initialState, getSlowLogConfigSuccess(config)) // Assert const rootState = Object.assign(initialStateDefault, { @@ -296,7 +296,7 @@ describe('slowLog slice', () => { // Assert const expectedActions = [ getSlowLogs(), - getSlowLogsSuccess([data, DEFAULT_SLOWLOG_DURATION_UNIT]), + getSlowLogsSuccess(data), ] expect(store.getActions()).toEqual(expectedActions) @@ -386,7 +386,7 @@ describe('slowLog slice', () => { // Assert const expectedActions = [ getSlowLogConfig(), - getSlowLogConfigSuccess([data, null]), + getSlowLogConfigSuccess(data), ] expect(store.getActions()).toEqual(expectedActions) @@ -442,7 +442,8 @@ describe('slowLog slice', () => { // Assert const expectedActions = [ getSlowLogConfig(), - getSlowLogConfigSuccess([data, DEFAULT_SLOWLOG_DURATION_UNIT]), + getSlowLogConfigSuccess(data), + setSlowLogUnits(DEFAULT_SLOWLOG_DURATION_UNIT) ] expect(store.getActions()).toEqual(expectedActions) diff --git a/redisinsight/ui/src/slices/tests/app/context.spec.ts b/redisinsight/ui/src/slices/tests/app/context.spec.ts index 040a05c701..5a08c54455 100644 --- a/redisinsight/ui/src/slices/tests/app/context.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/context.spec.ts @@ -33,7 +33,11 @@ import reducer, { updateBrowserTreeSelectedLeaf, setBrowserTreeDelimiter, setBrowserIsNotRendered, - setBrowserRedisearchScrollPosition, updateKeyDetailsSizes, appContextBrowserKeyDetails, + setBrowserRedisearchScrollPosition, + updateKeyDetailsSizes, + appContextBrowserKeyDetails, + appContextDbConfig, + setSlowLogUnits, setDbConfig, } from '../../app/context' jest.mock('uiSrc/services', () => ({ @@ -526,14 +530,61 @@ describe('slices', () => { }) }) + describe('setDbConfig', () => { + it('should properly set db config', () => { + // Arrange + const data = { + slowLogDurationUnit: 'ms', + treeViewDelimiter: ':-' + } + + const state = { + ...initialState.dbConfig, + ...data + } + + // Act + const nextState = reducer(initialState, setDbConfig(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { context: nextState }, + }) + + expect(appContextDbConfig(rootState)).toEqual(state) + }) + }) + + describe('setSlowLogUnits', () => { + it('should properly set slow log units', () => { + // Arrange + const slowLogDurationUnit = 'ms' + + const state = { + ...initialState.dbConfig, + slowLogDurationUnit + } + + // Act + const nextState = reducer(initialState, setSlowLogUnits(slowLogDurationUnit)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { context: nextState }, + }) + + expect(appContextDbConfig(rootState)).toEqual(state) + }) + }) + describe('setBrowserTreeDelimiter', () => { it('should properly set browser tree delimiter', () => { // Arrange const delimiter = '_' const state = { - ...initialState.browser.tree, - delimiter + ...initialState.dbConfig, + treeViewDelimiter: delimiter } // Act @@ -544,7 +595,7 @@ describe('slices', () => { app: { context: nextState }, }) - expect(appContextBrowserTree(rootState)).toEqual(state) + expect(appContextDbConfig(rootState)).toEqual(state) }) }) diff --git a/redisinsight/ui/src/styles/components/_buttons.scss b/redisinsight/ui/src/styles/components/_buttons.scss index 9570448709..658ec77acd 100644 --- a/redisinsight/ui/src/styles/components/_buttons.scss +++ b/redisinsight/ui/src/styles/components/_buttons.scss @@ -20,7 +20,8 @@ } &:focus:not(:hover), &:focus-within:not(:hover) { - border: 2px solid var(--euiColorPrimary); + border: 1px solid var(--euiColorPrimary); + outline: 1px solid var(--euiColorPrimary); } .euiTextColor--default { color: currentColor; @@ -38,7 +39,8 @@ } &:focus:not(:hover), &:focus-within:not(:hover) { - border: 2px solid var(--euiColorPrimary); + border: 1px solid var(--euiColorPrimary); + outline: 1px solid var(--euiColorPrimary); } } } @@ -54,7 +56,8 @@ } &:focus:not(:hover), &:focus-within:not(:hover) { - border: 2px solid var(--euiColorPrimary); + border: 1px solid var(--euiColorPrimary); + outline: 1px solid var(--euiColorPrimary); } } } @@ -118,6 +121,7 @@ &:focus-within { background-color: var(--buttonWarningHoverColor); border-color: var(--buttonWarningHoverColor); + outline-color: var(--buttonWarningHoverColor); } } } diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 5431580aa1..42982ce13e 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -25,6 +25,7 @@ export * from './pubSubUtils' export * from './formatters' export * from './groupTypes' export * from './modules' +export * from './optimizations' export { Maybe, diff --git a/redisinsight/ui/src/utils/optimizations.ts b/redisinsight/ui/src/utils/optimizations.ts new file mode 100644 index 0000000000..1740f7e4cb --- /dev/null +++ b/redisinsight/ui/src/utils/optimizations.ts @@ -0,0 +1,39 @@ +import { some } from 'lodash' +import { BrowserStorageItem } from 'uiSrc/constants' +import { localStorageService, setDBConfigStorageField } from 'uiSrc/services' +import { Instance } from 'uiSrc/slices/interfaces' + +export const optimizeLSInstances = (instances: Instance[]) => { + try { + const isOptimized = localStorageService.get('optimizedInstances') + if (isOptimized) { + return + } + + const lsItems = localStorageService.getAll() + Object.keys(lsItems) + .forEach((item) => { + if (item.startsWith(BrowserStorageItem.treeViewDelimiter)) { + const instanceId = item.replace(BrowserStorageItem.treeViewDelimiter, '') + + if (some(instances, ['id', instanceId])) { + setDBConfigStorageField(instanceId, BrowserStorageItem.treeViewDelimiter, lsItems[item]) + } + + localStorageService.remove(item) + } + + if (item.startsWith(BrowserStorageItem.dbConfig)) { + const instanceId = item.replace(BrowserStorageItem.dbConfig, '') + + if (!some(instances, ['id', instanceId])) { + localStorageService.remove(item) + } + } + + localStorageService.set('optimizedInstances', true) + }) + } catch (e) { + console.error(e) + } +} From 139065a20014dfe8347bc982b0a2ceaee0dcd0b0 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 24 Nov 2022 18:06:21 +0300 Subject: [PATCH 019/107] show "No Results Found" message for redisearch --- .../browser/components/key-list/KeyList.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index d683c90f7b..cef51c1e89 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -159,15 +159,25 @@ const KeyList = forwardRef((props: Props, ref) => { if (isNotRendered.current) { return '' } - if (searchMode === SearchMode.Redisearch && !selectedIndex) { - return NoSelectedIndexText + + if (searchMode === SearchMode.Redisearch) { + if (!selectedIndex) { + return NoSelectedIndexText + } + + if (total === 0) { + return NoResultsFoundText + } + + if (isSearched) { + return keysState.scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText + } } + if (total === 0) { return NoKeysToDisplayText(Pages.workbench(instanceId), onNoKeysLinkClick) } - if (isSearched && searchMode === SearchMode.Redisearch) { - return keysState.scanned < total ? NoResultsFoundText : FullScanNoResultsFoundText - } + if (isSearched) { return keysState.scanned < total ? ScanNoResultsFoundText : FullScanNoResultsFoundText } From dea8ffa41bb72042e2e35318b3077d28812014c5 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 24 Nov 2022 20:05:17 +0100 Subject: [PATCH 020/107] fixes for flaky tests --- tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts | 2 +- tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts index b4ac8cbd17..4559d1526b 100644 --- a/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/bulk-delete.e2e.ts @@ -56,7 +56,7 @@ test('Verify that user can access the bulk actions screen in the Browser', async }); test('Verify that user can see summary of scanned level', async t => { const expectedAmount = new RegExp('Expected amount: ~(9|10) \\d{3} keys'); - const scannedKeys = new RegExp('Scanned (5|10)% \\(\\d{3,5}/10 \\d{3}\\) and found \\d{3,5} keys'); + const scannedKeys = new RegExp('Scanned (5|10)% \\((500|1000)/10 \\d{3}\\) and found \\d{3,5} keys'); const messageTitle = 'No pattern or key type set'; const messageText = 'To perform a bulk action, set the pattern or select the key type'; diff --git a/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts b/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts index d367710887..c6d1777e41 100644 --- a/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts +++ b/tests/e2e/tests/regression/browser/filtering-iteratively.e2e.ts @@ -31,7 +31,7 @@ test await browserPage.searchByKeyName('*'); const keysNumberOfResults = await browserPage.keysNumberOfResults.textContent; // Verify that number of results is 500 - await t.expect(keysNumberOfResults).contains('500', 'Number of results is not 500'); + await t.expect(keysNumberOfResults).match(/50[0-9]/, 'Number of results is not 500'); }); test .meta({ rte: rte.standalone })('Verify that user can search iteratively via Scan more for search pattern and selected data type', async t => { From 67fd844b56d1c7317d7cf38d3a9bf2951ddcfba5 Mon Sep 17 00:00:00 2001 From: Gnanesh Date: Sun, 27 Nov 2022 12:44:44 +0530 Subject: [PATCH 021/107] Don't convert graph Ids to string. --- .../ui/src/packages/redisgraph/src/parser.ts | 16 ++++++++-------- .../ui/src/packages/redisgraph/src/utils.ts | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/redisinsight/ui/src/packages/redisgraph/src/parser.ts b/redisinsight/ui/src/packages/redisgraph/src/parser.ts index 705d31c76e..2cbb20f9c8 100644 --- a/redisinsight/ui/src/packages/redisgraph/src/parser.ts +++ b/redisinsight/ui/src/packages/redisgraph/src/parser.ts @@ -71,7 +71,7 @@ function responseParser(data: any): IResponseParser { if (Array.isArray(item)) { if (item[0][0] === 'id' && item[1][0] === 'labels') { const node: INode = { - id: item[0][1].toString(), + id: item[0][1], labels: item[1][1], properties: {} } @@ -81,15 +81,15 @@ function responseParser(data: any): IResponseParser { const v = resolveProps(x) node['properties'][v.key] = v.value }) - if (nodes.findIndex((e: INode) => e.id === item[0][1].toString()) === -1) { + if (nodes.findIndex((e: INode) => e.id === item[0][1]) === -1) { nodes.push(node) } } else if (item[0][0] === 'id' && item[1][0] === 'type') { const edge: IEdge = { - id: item[0][1].toString(), + id: item[0][1], type: item[1][1], - source: item[2][1].toString(), - target: item[3][1].toString(), + source: item[2][1], + target: item[3][1], properties: {} } types[item[1][1]] = (types[item[1][1]] + 1) || 1 @@ -155,7 +155,7 @@ function ResultsParser(data: any[][]) : {headers: any[], results: any[] }{ if (Array.isArray(entity)) { if (entity[0][0] === 'id') { const item: any = { - id: entity[0][1].toString(), + id: entity[0][1], properties: {} } let propValues = [] @@ -164,8 +164,8 @@ function ResultsParser(data: any[][]) : {headers: any[], results: any[] }{ propValues = entity[2][1] } else if (entity[1][0] === 'type') { item.type = entity[1][1] - item.source = entity[2][1].toString() - item.target = entity[3][1].toString() + item.source = entity[2][1] + item.target = entity[3][1] propValues = entity[4][1] } propValues.map((x: any) => { diff --git a/redisinsight/ui/src/packages/redisgraph/src/utils.ts b/redisinsight/ui/src/packages/redisgraph/src/utils.ts index 67a83abcb5..60b8353737 100644 --- a/redisinsight/ui/src/packages/redisgraph/src/utils.ts +++ b/redisinsight/ui/src/packages/redisgraph/src/utils.ts @@ -189,23 +189,23 @@ export function commandIsSuccess(resp: [{ response: any, status: string }]) { } -export function getFetchNodesByIdQuery(graphKey: string, nodeIds: string[]): string { +export function getFetchNodesByIdQuery(graphKey: string, nodeIds: number[]): string { return `graph.ro_query ${graphKey} "MATCH (n) WHERE id(n) IN [${nodeIds}] RETURN DISTINCT n"` } -export function getFetchNodesByEdgeIdQuery(graphKey: string, edgeIds: string[], existingNodeIds: string[]): string { +export function getFetchNodesByEdgeIdQuery(graphKey: string, edgeIds: number[], existingNodeIds: number[]): string { return `graph.ro_query ${graphKey} "MATCH (n)-[t]->(m) WHERE id(t) IN [${edgeIds}] AND NOT id(n) IN [${existingNodeIds}] AND NOT id(m) IN [${existingNodeIds}] RETURN n, m"` } -export function getFetchEdgesByIdQuery(graphKey: string, edgeIds: string[]): string { +export function getFetchEdgesByIdQuery(graphKey: string, edgeIds: number[]): string { return `graph.ro_query ${graphKey} "MATCH ()-[t]->() WHERE id(t) IN [${edgeIds}] RETURN DISTINCT t"` } -export function getFetchDirectNeighboursOfNodeQuery(graphKey: string, nodeId: string): string { +export function getFetchDirectNeighboursOfNodeQuery(graphKey: string, nodeId: number): string { return `graph.ro_query "${graphKey}" "MATCH (n)-[t]-(m) WHERE id(n)=${nodeId} RETURN t, m"` } -export function getFetchNodeRelationshipsQuery(graphKey: string, sourceNodeIds: string[], destNodeIds: string[], existingEdgeIds: string[]): string { +export function getFetchNodeRelationshipsQuery(graphKey: string, sourceNodeIds: number[], destNodeIds: number[], existingEdgeIds: number[]): string { return `graph.ro_query ${graphKey} "MATCH (n)-[t]->(m) WHERE (ID(n) IN [${sourceNodeIds}] OR ID(m) IN [${destNodeIds}]) AND NOT ID(t) IN [${existingEdgeIds}] RETURN DISTINCT t"` } From 8fa03e3f18e278847d5d93261f87395a7c81f365 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Mon, 28 Nov 2022 12:47:48 +0100 Subject: [PATCH 022/107] #RI-3842 - Selected key not displayed in key list when switching between pages/views --- .../browser/components/key-list/KeyList.tsx | 15 ------- redisinsight/ui/src/slices/browser/keys.ts | 45 +++++++++++++++---- .../ui/src/slices/browser/redisearch.ts | 15 +++++++ .../ui/src/slices/tests/browser/keys.spec.ts | 35 ++++++++++++++- .../slices/tests/browser/redisearch.spec.ts | 36 ++++++++++++++- 5 files changed, 120 insertions(+), 26 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx index d683c90f7b..54537cc702 100644 --- a/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-list/KeyList.tsx @@ -19,7 +19,6 @@ import { formatLongName, bufferToString, bufferFormatRangeItems, - isEqualBuffers, Nullable, } from 'uiSrc/utils' import { @@ -33,9 +32,7 @@ import { fetchKeysMetadata, keysDataSelector, keysSelector, - resetKeysData, selectedKeySelector, - setLastBatchKeys, sourceKeysFetch, } from 'uiSrc/slices/browser/keys' import { @@ -126,18 +123,6 @@ const KeyList = forwardRef((props: Props, ref) => { rerender({}) }, [keysState.keys]) - useEffect(() => { - if (!selectedKey || !selectedKey?.data) return - - const indexKeyForUpdate = itemsRef.current.findIndex(({ name }) => - isEqualBuffers(name, selectedKey?.data?.name)) - - if (indexKeyForUpdate === -1) return - - itemsRef.current[indexKeyForUpdate] = selectedKey.data - rerender({}) - }, [selectedKey]) - const cancelAllMetadataRequests = () => { controller.current?.abort() } diff --git a/redisinsight/ui/src/slices/browser/keys.ts b/redisinsight/ui/src/slices/browser/keys.ts index ac69773609..8a685ac94b 100644 --- a/redisinsight/ui/src/slices/browser/keys.ts +++ b/redisinsight/ui/src/slices/browser/keys.ts @@ -41,6 +41,7 @@ import { fetchStreamEntries, setStreamInitialState } from './stream' import { deleteRedisearchKeyFromList, editRedisearchKeyFromList, + editRedisearchKeyTTLFromList, fetchMoreRedisearchKeysAction, fetchRedisearchKeysAction, resetRedisearchKeysData, @@ -253,6 +254,20 @@ const keysSlice = createSlice({ } }, + editPatternKeyTTLFromList: (state, { payload: [key, ttl] }: PayloadAction<[RedisResponseBuffer, number]>) => { + const keys = state.data.keys.map((keyData) => { + if (isEqualBuffers(keyData.name, key)) { + keyData.ttl = ttl + } + return keyData + }) + + state.data = { + ...state.data, + keys, + } + }, + // update length for Selected Key updateSelectedKeyLength: (state, { payload }) => { state.selectedKey = { @@ -367,6 +382,7 @@ export const { deleteKeyFailure, deletePatternKeyFromList, editPatternKeyFromList, + editPatternKeyTTLFromList, updateSelectedKeyLength, setPatternSearchMatch, setFilter, @@ -797,7 +813,7 @@ export function deleteKeyAction(key: RedisResponseBuffer, onSuccessAction?: () = } }) dispatch(deleteKeySuccess()) - dispatch(deleteKeyFromList(key)) + dispatch(deleteKeyFromList(key)) onSuccessAction?.() dispatch(addMessageNotification(successMessages.DELETED_KEY(key))) } @@ -832,7 +848,7 @@ export function editKey( ) if (isStatusSuccessful(status)) { - dispatch(editKeyFromList({ key, newKey })) + dispatch(editKeyFromList({ key, newKey })) onSuccess?.() } } catch (error) { @@ -845,7 +861,7 @@ export function editKey( } // Asynchronous thunk action -export function editKeyTTL(key: string, ttl: number) { +export function editKeyTTL(key: RedisResponseBuffer, ttl: number) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { dispatch(defaultSelectedKeyAction()) @@ -874,10 +890,11 @@ export function editKeyTTL(key: string, ttl: number) { } }) if (ttl !== 0) { + dispatch(editKeyTTLFromList([key, ttl])) dispatch(fetchKeyInfo(key)) } else { dispatch(deleteKeySuccess()) - dispatch(deleteKeyFromList(key)) + dispatch(deleteKeyFromList(key)) } dispatch(defaultSelectedKeyActionSuccess()) } @@ -932,8 +949,8 @@ export function fetchKeys( const isRedisearchExists = isRedisearchAvailable(state.connections.instances.connectedInstance.modules) return searchMode === SearchMode.Pattern || !isRedisearchExists - ? dispatch(fetchPatternKeysAction(cursor, count, onSuccess, onFailed)) - : dispatch(fetchRedisearchKeysAction(cursor, count, onSuccess, onFailed)) + ? dispatch(fetchPatternKeysAction(cursor, count, onSuccess, onFailed)) + : dispatch(fetchRedisearchKeysAction(cursor, count, onSuccess, onFailed)) } } @@ -977,12 +994,22 @@ export function deleteKeyFromList(key: RedisResponseBuffer) { } } -export function editKeyFromList(key: RedisResponseBuffer) { +export function editKeyFromList(data: { key: RedisResponseBuffer, newKey: RedisResponseBuffer }) { + return async (dispatch: AppDispatch, stateInit: () => RootState) => { + const state = stateInit() + + return state.browser.keys?.searchMode === SearchMode.Pattern + ? dispatch(editPatternKeyFromList(data)) + : dispatch(editRedisearchKeyFromList(data)) + } +} + +export function editKeyTTLFromList(data: [RedisResponseBuffer, number]) { return async (dispatch: AppDispatch, stateInit: () => RootState) => { const state = stateInit() return state.browser.keys?.searchMode === SearchMode.Pattern - ? dispatch(editPatternKeyFromList(key)) - : dispatch(editRedisearchKeyFromList(key)) + ? dispatch(editPatternKeyTTLFromList(data)) + : dispatch(editRedisearchKeyTTLFromList(data)) } } diff --git a/redisinsight/ui/src/slices/browser/redisearch.ts b/redisinsight/ui/src/slices/browser/redisearch.ts index eec5b69454..0f92c62a35 100644 --- a/redisinsight/ui/src/slices/browser/redisearch.ts +++ b/redisinsight/ui/src/slices/browser/redisearch.ts @@ -183,6 +183,20 @@ const redisearchSlice = createSlice({ keys, } }, + + editRedisearchKeyTTLFromList: (state, { payload: [key, ttl] }: PayloadAction<[RedisResponseBuffer, number]>) => { + const keys = state.data.keys.map((keyData) => { + if (isEqualBuffers(keyData.name, key)) { + keyData.ttl = ttl + } + return keyData + }) + + state.data = { + ...state.data, + keys, + } + }, }, }) @@ -207,6 +221,7 @@ export const { resetRedisearchKeysData, deleteRedisearchKeyFromList, editRedisearchKeyFromList, + editRedisearchKeyTTLFromList, } = redisearchSlice.actions // Selectors diff --git a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts index aafb794c7a..5489f7f6cc 100644 --- a/redisinsight/ui/src/slices/tests/browser/keys.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/keys.spec.ts @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash' import { AxiosError } from 'axios' import { KeyTypes, KeyValueFormat } from 'uiSrc/constants' import { apiService } from 'uiSrc/services' -import { parseKeysListResponse, stringToBuffer } from 'uiSrc/utils' +import { parseKeysListResponse, stringToBuffer, UTF8ToBuffer } from 'uiSrc/utils' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' import successMessages from 'uiSrc/components/notifications/success-messages' @@ -59,6 +59,7 @@ import reducer, { resetKeyInfo, resetKeys, fetchKeysMetadata, + editPatternKeyTTLFromList, } from '../../browser/keys' import { getString } from '../../browser/string' @@ -750,6 +751,37 @@ describe('keys slice', () => { }) }) + describe('editPatternKeyTTLFromList', () => { + it('should properly set the state before the edit key ttl', () => { + // Arrange + + const key = UTF8ToBuffer('key') + const ttl = 12000 + + const initialStateMock = { + ...initialState, + data: { + keys: [{ name: key }], + }, + } + const state = { + ...initialState, + data: { + keys: [{ name: key, ttl }], + }, + } + + // Act + const nextState = reducer(initialStateMock, editPatternKeyTTLFromList([key, ttl])) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { keys: nextState }, + }) + expect(keysSelector(rootState)).toEqual(state) + }) + }) + describe('resetKeyInfo', () => { it('should properly save viewFormat', () => { // Arrange @@ -1256,6 +1288,7 @@ describe('keys slice', () => { const expectedActions = [ defaultSelectedKeyAction(), // fetch keyInfo + editPatternKeyTTLFromList([key, ttl]), defaultSelectedKeyAction(), defaultSelectedKeyActionSuccess(), ] diff --git a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts index 671a483be4..2a36c22b25 100644 --- a/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts +++ b/redisinsight/ui/src/slices/tests/browser/redisearch.spec.ts @@ -4,7 +4,7 @@ import successMessages from 'uiSrc/components/notifications/success-messages' import { apiService } from 'uiSrc/services' import { cleanup, initialStateDefault, mockedStore, mockStore } from 'uiSrc/utils/test-utils' import { addErrorNotification, addMessageNotification } from 'uiSrc/slices/app/notifications' -import { stringToBuffer } from 'uiSrc/utils' +import { stringToBuffer, UTF8ToBuffer } from 'uiSrc/utils' import { REDISEARCH_LIST_DATA_MOCK } from 'uiSrc/mocks/handlers/browser/redisearchHandlers' import { SearchMode } from 'uiSrc/slices/interfaces/keys' import { fetchKeys, fetchMoreKeys } from 'uiSrc/slices/browser/keys' @@ -35,6 +35,7 @@ import reducer, { resetRedisearchKeysData, deleteRedisearchKeyFromList, editRedisearchKeyFromList, + editRedisearchKeyTTLFromList, } from '../../browser/redisearch' let store: typeof mockedStore @@ -713,6 +714,39 @@ describe('redisearch slice', () => { }) }) + describe('editRedisearchKeyTTLFromList', () => { + it('should properly set the state before the edit key ttl', () => { + // Arrange + + const key = UTF8ToBuffer('key') + const ttl = 12000 + + const prevState = { + ...initialState, + data: { + ...initialState.data, + keys: [{ name: key }], + }, + } + const state = { + ...initialState, + data: { + ...initialState.data, + keys: [{ name: key, ttl }], + }, + } + + // Act + const nextState = reducer(prevState, editRedisearchKeyTTLFromList([key, ttl])) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { redisearch: nextState }, + }) + expect(redisearchSelector(rootState)).toEqual(state) + }) + }) + describe('thunks', () => { describe('fetchRedisearchListAction', () => { it('call both fetchRedisearchListAction, loadListSuccess when fetch is successed', async () => { From 0eb7a24d360a6a1fefe5fce8d96ced9edf16c86e Mon Sep 17 00:00:00 2001 From: ALENA NABOKA Date: Mon, 28 Nov 2022 16:05:35 +0300 Subject: [PATCH 023/107] Add e2e test for Saved Context in Browser --- .../critical-path/browser/context.e2e.ts | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/context.e2e.ts b/tests/e2e/tests/critical-path/browser/context.e2e.ts index 41050d6f8f..ebbddcfe50 100644 --- a/tests/e2e/tests/critical-path/browser/context.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/context.e2e.ts @@ -4,7 +4,7 @@ import { BrowserPage, CliPage } from '../../../pageObjects'; -import { commonUrl, ossStandaloneConfig } from '../../../helpers/conf'; +import { commonUrl, ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { Common } from '../../../helpers/common'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; @@ -92,19 +92,32 @@ test('Verify that user can see saved executed commands in CLI on Browser page wh } }); test + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + }) .after(async() => { - // Clear and delete database - await browserPage.deleteKeyByName(keyName); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); + // Delete database + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify that user can see key details selected when he returns back to Browser page', async t => { - // Add and open new key details and navigate to Settings - keyName = common.generateWord(10); - await browserPage.addHashKey(keyName); - await browserPage.openKeyDetails(keyName); + // Scroll keys elements + const scrollY = 1000; + await t.scroll(browserPage.cssSelectorGrid, 0, scrollY); + + const keysCount = await browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).count; + const targetKey = browserPage.virtualTableContainer.find(browserPage.cssVirtualTableRow).nth(Math.floor(keysCount / 2)); + const targetKeyName = await targetKey.find(browserPage.cssSelectorKey).innerText; + // Open key details + await t.click(targetKey); + await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); + await t.click(myRedisDatabasePage.settingsButton); // Return back to Browser and check key details selected await t.click(myRedisDatabasePage.browserButton); - await t.expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('The key details is selected'); + // Check Keys details saved + await t.expect(browserPage.keyNameFormDetails.innerText).eql(targetKeyName, 'Key details is not saved as context'); + // Check Key selected in Key List + console.log(`targetKey.getAttribute('class'): ${await targetKey.getAttribute('class')}`); + await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); }); test .after(async() => { From 9a2b9b6098e94e27b55d86fa01da1fd8fc95fead Mon Sep 17 00:00:00 2001 From: ALENA NABOKA Date: Mon, 28 Nov 2022 16:30:32 +0300 Subject: [PATCH 024/107] remove console.log --- tests/e2e/tests/critical-path/browser/context.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/browser/context.e2e.ts b/tests/e2e/tests/critical-path/browser/context.e2e.ts index ebbddcfe50..72d46c4785 100644 --- a/tests/e2e/tests/critical-path/browser/context.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/context.e2e.ts @@ -116,7 +116,6 @@ test // Check Keys details saved await t.expect(browserPage.keyNameFormDetails.innerText).eql(targetKeyName, 'Key details is not saved as context'); // Check Key selected in Key List - console.log(`targetKey.getAttribute('class'): ${await targetKey.getAttribute('class')}`); await t.expect(await targetKey.getAttribute('class')).contains('table-row-selected', 'Not correct key selected in key list'); }); test From 9ac8c984f064f50a154d1041a36c68ca437c1f14 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 28 Nov 2022 15:57:54 +0100 Subject: [PATCH 025/107] fix pika db black screen --- .../components/top-keys/Table.tsx | 18 ++++++++++++++++-- .../components/top-namespace/Table.tsx | 2 +- .../ui/src/slices/instances/instances.ts | 4 ++-- .../src/utils/comparisons/compareVersions.ts | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-keys/Table.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-keys/Table.tsx index 05efa6a91d..4a67d28aad 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-keys/Table.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-keys/Table.tsx @@ -7,7 +7,7 @@ import { PropertySort } from '@elastic/eui' import cx from 'classnames' -import { isNull } from 'lodash' +import { isNil } from 'lodash' import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' @@ -138,6 +138,13 @@ const Table = (props: Props) => { sortable: true, align: 'left', render: (value: number, { name }) => { + if (isNil(value)) { + return ( + + - + + ) + } if (value === -1) { return ( @@ -174,6 +181,13 @@ const Table = (props: Props) => { sortable: true, align: 'right', render: (value: number, { type }) => { + if (isNil(value)) { + return ( + + - + + ) + } const [number, size] = formatBytes(value, 3, true) const isHighlight = isBigKey(type, HighlightType.Memory, value) return ( @@ -207,7 +221,7 @@ const Table = (props: Props) => { sortable: ({ length }) => length ?? -1, align: 'right', render: (value: number, { name, type }) => { - if (isNull(value)) { + if (isNil(value)) { return ( - diff --git a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/Table.tsx b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/Table.tsx index 5a476322b1..e810921d07 100644 --- a/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/Table.tsx +++ b/redisinsight/ui/src/pages/databaseAnalysis/components/top-namespace/Table.tsx @@ -66,7 +66,7 @@ const NameSpacesTable = (props: Props) => { dispatch(setBrowserTreeDelimiter(delimiter)) dispatch(setFilter(filter)) dispatch(setSearchMatch(`${nsp}${delimiter}*`, SearchMode.Pattern)) - dispatch(resetKeysData()) + dispatch(resetKeysData(SearchMode.Pattern)) dispatch(fetchKeys( SearchMode.Pattern, '0', diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index f3b8352ecc..c4f73575df 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -126,8 +126,8 @@ const instancesSlice = createSlice({ getDatabaseConfigInfoSuccess: (state, { payload }) => { state.loading = false state.instanceOverview = { - version: state.instanceOverview.version, - ...payload + ...payload, + version: payload?.version || state.instanceOverview.version || '', } }, getDatabaseConfigInfoFailure: (state, { payload }) => { diff --git a/redisinsight/ui/src/utils/comparisons/compareVersions.ts b/redisinsight/ui/src/utils/comparisons/compareVersions.ts index 2faa280e9a..b3c3501837 100644 --- a/redisinsight/ui/src/utils/comparisons/compareVersions.ts +++ b/redisinsight/ui/src/utils/comparisons/compareVersions.ts @@ -1,4 +1,4 @@ -export const isVersionHigherOrEquals = (sourceVersion: string, comparableVersion: string) => { +export const isVersionHigherOrEquals = (sourceVersion: string = '', comparableVersion: string = '') => { const sourceVersionArray = sourceVersion.split('.') const comparableVersionArray = comparableVersion.split('.') From 0f4d700cfa34ca981e55eac5297bafe9509c7d2f Mon Sep 17 00:00:00 2001 From: Gnanesh Date: Mon, 28 Nov 2022 21:50:14 +0530 Subject: [PATCH 026/107] Convert id to string before rendering node label --- redisinsight/ui/src/packages/redisgraph/src/Graph.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/packages/redisgraph/src/Graph.tsx b/redisinsight/ui/src/packages/redisgraph/src/Graph.tsx index ab3013d363..7d5dc79ff7 100644 --- a/redisinsight/ui/src/packages/redisgraph/src/Graph.tsx +++ b/redisinsight/ui/src/packages/redisgraph/src/Graph.tsx @@ -239,7 +239,7 @@ export default function Graph(props: { graphKey: string, data: any[] }) { graphData: graphData, infoPanel: true, // nodeRadius: 25, - onLabelNode: (node) => node.properties?.name || node.properties?.title || node.id || (node.labels ? node.labels[0] : ''), + onLabelNode: (node) => node.properties?.name || node.properties?.title || node.id.toString() || (node.labels ? node.labels[0] : ''), onNodeClick: (nodeSvg, node, event) => { if (d3.select(nodeSvg).attr('class').indexOf('selected') > 0) { d3.select(nodeSvg) From 71efe71320c0fb8a83ec2f0c2714d58de8dc580c Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 28 Nov 2022 23:36:16 +0200 Subject: [PATCH 027/107] #RI-3775 add dummy auth middleware + Session object + ClientMetadata object --- redisinsight/api/config/default.ts | 1 + redisinsight/api/src/__mocks__/common.ts | 34 + redisinsight/api/src/app.module.ts | 6 + redisinsight/api/src/common/constants/api.ts | 2 + .../api/src/common/constants/index.ts | 1 + .../client-metadata.decorator.ts | 72 ++ .../decorators/client-metadata/index.ts | 1 + .../api/src/common/decorators/index.ts | 2 + .../src/common/decorators/session/index.ts | 1 + .../decorators/session/session.decorator.ts | 11 + .../middlewares/dummy-auth.middleware.ts | 18 + .../api/src/common/models/client-metadata.ts | 18 + redisinsight/api/src/common/models/index.ts | 2 + redisinsight/api/src/common/models/session.ts | 13 + .../src/models/redis-consumer.interface.ts | 6 +- .../autodiscovery/autodiscovery.service.ts | 3 +- .../controllers/hash/hash.controller.ts | 42 +- .../controllers/keys/keys.controller.ts | 62 +- .../controllers/list/list.controller.ts | 58 +- .../redisearch/redisearch.controller.ts | 30 +- .../rejson-rl/rejson-rl.controller.ts | 48 +- .../browser/controllers/set/set.controller.ts | 40 +- .../stream/consumer-group.controller.ts | 23 +- .../controllers/stream/consumer.controller.ts | 26 +- .../controllers/stream/stream.controller.ts | 25 +- .../controllers/string/string.controller.ts | 37 +- .../controllers/z-set/z-set.controller.ts | 58 +- .../browser-tool-cluster.service.spec.ts | 25 +- .../browser-tool-cluster.service.ts | 26 +- .../browser-tool/browser-tool.service.spec.ts | 19 +- .../browser-tool/browser-tool.service.ts | 18 +- .../hash-business.service.spec.ts | 101 +-- .../hash-business/hash-business.service.ts | 42 +- .../key-info-manager.interface.ts | 4 +- .../graph-type-info.strategy.spec.ts | 25 +- .../graph-type-info.strategy.ts | 12 +- .../hash-type-info.strategy.spec.ts | 19 +- .../hash-type-info/hash-type-info.strategy.ts | 6 +- .../list-type-info.strategy.spec.ts | 19 +- .../list-type-info/list-type-info.strategy.ts | 6 +- .../rejson-rl-type-info.strategy.spec.ts | 39 +- .../rejson-rl-type-info.strategy.ts | 18 +- .../set-type-info.strategy.spec.ts | 19 +- .../set-type-info/set-type-info.strategy.ts | 6 +- .../stream-type-info.strategy.spec.ts | 19 +- .../stream-type-info.strategy.ts | 6 +- .../string-type-info.strategy.spec.ts | 19 +- .../string-type-info.strategy.ts | 6 +- .../ts-type-info.strategy.spec.ts | 25 +- .../ts-type-info/ts-type-info.strategy.ts | 12 +- .../unsupported-type-info.strategy.spec.ts | 19 +- .../unsupported-type-info.strategy.ts | 6 +- .../z-set-type-info.strategy.spec.ts | 19 +- .../z-set-type-info.strategy.ts | 6 +- .../keys-business.service.spec.ts | 95 +- .../keys-business/keys-business.service.ts | 48 +- .../scanner/scanner.interface.ts | 4 +- .../strategies/abstract.strategy.spec.ts | 9 +- .../strategies/cluster.strategy.spec.ts | 108 ++- .../scanner/strategies/cluster.strategy.ts | 6 +- .../strategies/standalone.strategy.spec.ts | 66 +- .../list-business.service.spec.ts | 129 ++- .../list-business/list-business.service.ts | 50 +- .../redisearch/redisearch.service.spec.ts | 29 +- .../services/redisearch/redisearch.service.ts | 20 +- .../rejson-rl-business.service.spec.ts | 157 ++-- .../rejson-rl-business.service.ts | 86 +- .../set-business/set-business.service.spec.ts | 103 +-- .../set-business/set-business.service.ts | 42 +- .../stream/consumer-group.service.spec.ts | 94 +- .../services/stream/consumer-group.service.ts | 42 +- .../services/stream/consumer.service.spec.ts | 135 ++- .../services/stream/consumer.service.ts | 42 +- .../services/stream/stream.service.spec.ts | 87 +- .../browser/services/stream/stream.service.ts | 50 +- .../string-business.service.spec.ts | 55 +- .../string-business.service.ts | 27 +- .../z-set-business.service.spec.ts | 143 ++- .../z-set-business/z-set-business.service.ts | 64 +- .../providers/bulk-actions.provider.ts | 6 +- .../api/src/modules/cli/cli.module.ts | 4 +- .../modules/cli/controllers/cli.controller.ts | 35 +- .../cli-business/cli-business.service.spec.ts | 160 ++-- .../cli-business/cli-business.service.ts | 73 +- .../cluster-monitor.controller.ts | 12 +- .../cluster-monitor.service.ts | 11 +- .../database-analysis.controller.ts | 7 +- .../database-analysis.service.ts | 14 +- .../database-connection.service.spec.ts | 6 +- .../database/database-connection.service.ts | 40 +- .../database/database-info.controller.ts | 20 +- .../database/database-info.service.spec.ts | 7 +- .../modules/database/database-info.service.ts | 29 +- .../modules/database/database.controller.ts | 11 +- .../src/modules/database/database.service.ts | 8 +- .../database/providers/database.factory.ts | 6 +- .../providers/redis-observer.provider.ts | 25 +- .../providers/redis-client.provider.spec.ts | 7 +- .../providers/redis-client.provider.ts | 13 +- .../providers/user-session.provider.ts | 8 +- .../src/modules/pub-sub/pub-sub.controller.ts | 9 +- .../modules/pub-sub/pub-sub.service.spec.ts | 13 +- .../src/modules/pub-sub/pub-sub.service.ts | 16 +- .../redis-sentinel/redis-sentinel.service.ts | 5 +- .../modules/redis/models/client-metadata.ts | 13 - .../redis-consumer.abstract.service.spec.ts | 37 +- .../redis/redis-consumer.abstract.service.ts | 56 +- .../src/modules/redis/redis-tool.factory.ts | 6 +- .../src/modules/redis/redis-tool.service.ts | 53 +- .../src/modules/redis/redis.service.spec.ts | 835 +++++++++--------- .../api/src/modules/redis/redis.service.ts | 62 +- .../modules/slow-log/slow-log.controller.ts | 33 +- .../modules/slow-log/slow-log.service.spec.ts | 45 +- .../src/modules/slow-log/slow-log.service.ts | 54 +- .../modules/workbench/plugins.controller.ts | 27 +- .../modules/workbench/plugins.service.spec.ts | 36 +- .../src/modules/workbench/plugins.service.ts | 26 +- .../plugin-commands-whitelist.provider.ts | 18 +- .../workbench-commands.executor.spec.ts | 75 +- .../providers/workbench-commands.executor.ts | 36 +- .../modules/workbench/workbench.controller.ts | 24 +- .../src/modules/workbench/workbench.module.ts | 4 +- .../workbench/workbench.service.spec.ts | 78 +- .../modules/workbench/workbench.service.ts | 33 +- 124 files changed, 2319 insertions(+), 2649 deletions(-) create mode 100644 redisinsight/api/src/common/constants/api.ts create mode 100644 redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts create mode 100644 redisinsight/api/src/common/decorators/client-metadata/index.ts create mode 100644 redisinsight/api/src/common/decorators/session/index.ts create mode 100644 redisinsight/api/src/common/decorators/session/session.decorator.ts create mode 100644 redisinsight/api/src/common/middlewares/dummy-auth.middleware.ts create mode 100644 redisinsight/api/src/common/models/client-metadata.ts create mode 100644 redisinsight/api/src/common/models/session.ts delete mode 100644 redisinsight/api/src/modules/redis/models/client-metadata.ts diff --git a/redisinsight/api/config/default.ts b/redisinsight/api/config/default.ts index 7f089d358f..cba8f4a827 100644 --- a/redisinsight/api/config/default.ts +++ b/redisinsight/api/config/default.ts @@ -57,6 +57,7 @@ export default { appVersion: process.env.APP_VERSION || '2.0.0', requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 10000, excludeRoutes: [], + excludeAuthRoutes: [], }, sockets: { cors: process.env.SOCKETS_CORS ? process.env.SOCKETS_CORS === 'true' : false, diff --git a/redisinsight/api/src/__mocks__/common.ts b/redisinsight/api/src/__mocks__/common.ts index 7c74f143e2..466a8bd0fd 100644 --- a/redisinsight/api/src/__mocks__/common.ts +++ b/redisinsight/api/src/__mocks__/common.ts @@ -1,3 +1,7 @@ +import { ClientContext, ClientMetadata, Session } from 'src/common/models'; +import { mockDatabase } from 'src/__mocks__/databases'; +import { v4 as uuidv4 } from 'uuid'; + export type MockType = { [P in keyof T]: jest.Mock; }; @@ -57,3 +61,33 @@ export const mockRepository = jest.fn(() => ({ remove: jest.fn(), createQueryBuilder: mockCreateQueryBuilder, })); + +export const mockSession: Session = { + userId: uuidv4(), + sessionId: uuidv4(), +}; + +export const mockCliClientMetadata: ClientMetadata = { + session: mockSession, + databaseId: mockDatabase.id, + context: ClientContext.CLI, + uniqueId: uuidv4(), +}; + +export const mockWorkbenchClientMetadata: ClientMetadata = { + session: mockSession, + databaseId: mockDatabase.id, + context: ClientContext.Workbench, +}; + +export const mockBrowserClientMetadata: ClientMetadata = { + session: mockSession, + databaseId: mockDatabase.id, + context: ClientContext.Browser, +}; + +export const mockCommonClientMetadata: ClientMetadata = { + session: mockSession, + databaseId: mockDatabase.id, + context: ClientContext.Common, +}; diff --git a/redisinsight/api/src/app.module.ts b/redisinsight/api/src/app.module.ts index 26dada6d44..c7d9ca717d 100644 --- a/redisinsight/api/src/app.module.ts +++ b/redisinsight/api/src/app.module.ts @@ -19,6 +19,7 @@ import { ServerModule } from 'src/modules/server/server.module'; import { LocalDatabaseModule } from 'src/local-database.module'; import { CoreModule } from 'src/core.module'; import { AutodiscoveryModule } from 'src/modules/autodiscovery/autodiscovery.module'; +import { DummyAuthMiddleware } from 'src/common/middlewares/dummy-auth.middleware'; import { BrowserModule } from './modules/browser/browser.module'; import { RedisEnterpriseModule } from './modules/redis-enterprise/redis-enterprise.module'; import { RedisSentinelModule } from './modules/redis-sentinel/redis-sentinel.module'; @@ -97,6 +98,11 @@ export class AppModule implements OnModuleInit, NestModule { } configure(consumer: MiddlewareConsumer) { + consumer + .apply(DummyAuthMiddleware) + .exclude(...SERVER_CONFIG.excludeAuthRoutes) + .forRoutes('*'); + consumer .apply(ExcludeRouteMiddleware) .forRoutes( diff --git a/redisinsight/api/src/common/constants/api.ts b/redisinsight/api/src/common/constants/api.ts new file mode 100644 index 0000000000..5d9c060dd8 --- /dev/null +++ b/redisinsight/api/src/common/constants/api.ts @@ -0,0 +1,2 @@ +export const API_PARAM_DATABASE_ID = 'dbInstance'; +export const API_PARAM_CLI_CLIENT_ID = 'uuid'; diff --git a/redisinsight/api/src/common/constants/index.ts b/redisinsight/api/src/common/constants/index.ts index e4d4980682..86cecbb8fd 100644 --- a/redisinsight/api/src/common/constants/index.ts +++ b/redisinsight/api/src/common/constants/index.ts @@ -1 +1,2 @@ export * from './redis-string'; +export * from './api'; diff --git a/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts b/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts new file mode 100644 index 0000000000..504c293a59 --- /dev/null +++ b/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts @@ -0,0 +1,72 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; +import { ClientContext, ClientMetadata } from 'src/common/models'; +import { sessionFromRequestFactory } from 'src/common/decorators'; +import { API_PARAM_CLI_CLIENT_ID, API_PARAM_DATABASE_ID } from 'src/common/constants'; + +export interface IClientMetadataDecoratorOptions { + paramPath?: string, + queryPath?: string, + bodyPath?: string, + context?: ClientContext, + uniqueId?: string, +} + +export const clientMetadataFromRequestFactory = (options: IClientMetadataDecoratorOptions, ctx: ExecutionContext) => { + const opts: IClientMetadataDecoratorOptions = { + context: ClientContext.Common, + ...options, + }; + + const request = ctx.switchToHttp().getRequest(); + + let databaseId; + if (opts.paramPath) { + databaseId = request.params?.[opts.paramPath]; + } else if (opts.queryPath) { + // TBD + } else if (opts.bodyPath) { + // TBD + } + + // todo: add validation + if (!databaseId) { + // todo: define proper error + throw new Error('No databaseId found'); + } + + return plainToClass(ClientMetadata, { + session: sessionFromRequestFactory(undefined, ctx), + databaseId, + context: opts.context, + uniqueId: opts.uniqueId, + }); +}; + +export const ClientMetadataFromRequest = createParamDecorator(clientMetadataFromRequestFactory); + +export const browserClientMetadataFactory = ( + param = API_PARAM_DATABASE_ID, + ctx: ExecutionContext, +): ClientMetadata => clientMetadataFromRequestFactory({ + paramPath: param, + context: ClientContext.Browser, +}, ctx); + +export const BrowserClientMetadata = createParamDecorator(browserClientMetadataFactory); + +export const cliClientMetadataFactory = ( + options = { databaseParam: API_PARAM_DATABASE_ID, uuidParam: API_PARAM_CLI_CLIENT_ID }, + ctx: ExecutionContext, +): ClientMetadata => { + const request = ctx.switchToHttp().getRequest(); + + // todo: add validation + return clientMetadataFromRequestFactory({ + paramPath: options.databaseParam, + context: ClientContext.CLI, + uniqueId: request.params?.[options.uuidParam], + }, ctx); +}; + +export const CliClientMetadata = createParamDecorator(cliClientMetadataFactory); diff --git a/redisinsight/api/src/common/decorators/client-metadata/index.ts b/redisinsight/api/src/common/decorators/client-metadata/index.ts new file mode 100644 index 0000000000..e2c348e087 --- /dev/null +++ b/redisinsight/api/src/common/decorators/client-metadata/index.ts @@ -0,0 +1 @@ +export * from './client-metadata.decorator'; diff --git a/redisinsight/api/src/common/decorators/index.ts b/redisinsight/api/src/common/decorators/index.ts index f107314f61..432b2d6ea1 100644 --- a/redisinsight/api/src/common/decorators/index.ts +++ b/redisinsight/api/src/common/decorators/index.ts @@ -2,3 +2,5 @@ export * from './redis-string'; export * from './zset-score'; export * from './default'; export * from './data-as-json-string.decorator'; +export * from './session'; +export * from './client-metadata'; diff --git a/redisinsight/api/src/common/decorators/session/index.ts b/redisinsight/api/src/common/decorators/session/index.ts new file mode 100644 index 0000000000..f258bbf20b --- /dev/null +++ b/redisinsight/api/src/common/decorators/session/index.ts @@ -0,0 +1 @@ +export * from './session.decorator'; diff --git a/redisinsight/api/src/common/decorators/session/session.decorator.ts b/redisinsight/api/src/common/decorators/session/session.decorator.ts new file mode 100644 index 0000000000..de8e502c5d --- /dev/null +++ b/redisinsight/api/src/common/decorators/session/session.decorator.ts @@ -0,0 +1,11 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { plainToClass } from 'class-transformer'; +import { Session } from 'src/common/models'; + +export const sessionFromRequestFactory = (data: unknown, ctx: ExecutionContext): Session => { + const request = ctx.switchToHttp().getRequest(); + + return plainToClass(Session, request.session); +}; + +export const SessionFromRequest = createParamDecorator(sessionFromRequestFactory); diff --git a/redisinsight/api/src/common/middlewares/dummy-auth.middleware.ts b/redisinsight/api/src/common/middlewares/dummy-auth.middleware.ts new file mode 100644 index 0000000000..bedcdb9060 --- /dev/null +++ b/redisinsight/api/src/common/middlewares/dummy-auth.middleware.ts @@ -0,0 +1,18 @@ +import { + Injectable, + NestMiddleware, +} from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; +import { ISession } from 'src/common/models/session'; + +@Injectable() +export class DummyAuthMiddleware implements NestMiddleware { + async use(req: Request, res: Response, next: NextFunction): Promise { + req['session'] = Object.freeze({ + userId: '1', + sessionId: '1', + }); + + next(); + } +} diff --git a/redisinsight/api/src/common/models/client-metadata.ts b/redisinsight/api/src/common/models/client-metadata.ts new file mode 100644 index 0000000000..4841b0ac98 --- /dev/null +++ b/redisinsight/api/src/common/models/client-metadata.ts @@ -0,0 +1,18 @@ +import { Session } from 'src/common/models/session'; + +export enum ClientContext { + Common = 'Common', + Browser = 'Browser', + CLI = 'CLI', + Workbench = 'Workbench', +} + +export class ClientMetadata { + session: Session; + + databaseId: string; + + context: ClientContext; + + uniqueId?: string; +} diff --git a/redisinsight/api/src/common/models/index.ts b/redisinsight/api/src/common/models/index.ts index 3112f95f99..20ae751ab7 100644 --- a/redisinsight/api/src/common/models/index.ts +++ b/redisinsight/api/src/common/models/index.ts @@ -1,2 +1,4 @@ export * from './common'; export * from './endpoint'; +export * from './session'; +export * from './client-metadata'; diff --git a/redisinsight/api/src/common/models/session.ts b/redisinsight/api/src/common/models/session.ts new file mode 100644 index 0000000000..39ef6a3903 --- /dev/null +++ b/redisinsight/api/src/common/models/session.ts @@ -0,0 +1,13 @@ +export interface ISession { + userId: string; + sessionId: string; + uniqueId?: string; +} + +export class Session implements ISession { + userId: string; + + sessionId: string; + + uniqueId?: string; +} diff --git a/redisinsight/api/src/models/redis-consumer.interface.ts b/redisinsight/api/src/models/redis-consumer.interface.ts index 6b8090b123..c34c8abc07 100644 --- a/redisinsight/api/src/models/redis-consumer.interface.ts +++ b/redisinsight/api/src/models/redis-consumer.interface.ts @@ -1,16 +1,16 @@ -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { ReplyError } from 'src/models/redis-client'; import { Cluster, Redis } from 'ioredis'; +import { ClientMetadata } from 'src/common/models'; export interface IRedisConsumer { execCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: any, args: Array, ): any; execPipeline( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommands: Array< [toolCommand: any, ...args: Array] >, diff --git a/redisinsight/api/src/modules/autodiscovery/autodiscovery.service.ts b/redisinsight/api/src/modules/autodiscovery/autodiscovery.service.ts index bd12eedd64..9e1937e71a 100644 --- a/redisinsight/api/src/modules/autodiscovery/autodiscovery.service.ts +++ b/redisinsight/api/src/modules/autodiscovery/autodiscovery.service.ts @@ -9,6 +9,7 @@ import config from 'src/utils/config'; import { SettingsService } from 'src/modules/settings/settings.service'; import { Database } from 'src/modules/database/models/database'; import { DatabaseService } from 'src/modules/database/database.service'; +import { ClientContext } from 'src/common/models'; const SERVER_CONFIG = config.get('server'); @@ -74,7 +75,7 @@ export class AutodiscoveryService implements OnModuleInit { try { const client = await this.redisService.createStandaloneClient( endpoint as Database, - AppTool.Common, + ClientContext.Common, false, 'redisinsight-auto-discovery', ); diff --git a/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts b/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts index 1ccf7c25a5..ee1711537d 100644 --- a/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts @@ -3,18 +3,16 @@ import { Controller, Delete, HttpCode, - Param, Post, Put, - UsePipes, - ValidationPipe, } from '@nestjs/common'; import { ApiBody, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; import { AddFieldsToHashDto, CreateHashWithExpireDto, @@ -38,15 +36,10 @@ export class HashController extends BaseController { @ApiBody({ type: CreateHashWithExpireDto }) @ApiQueryRedisStringEncoding() async createHash( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateHashWithExpireDto, ): Promise { - return await this.hashBusinessService.createHash( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.hashBusinessService.createHash(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -63,15 +56,10 @@ export class HashController extends BaseController { }) @ApiQueryRedisStringEncoding() async getMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetHashFieldsDto, ): Promise { - return await this.hashBusinessService.getFields( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.hashBusinessService.getFields(clientMetadata, dto); } @Put('') @@ -82,15 +70,10 @@ export class HashController extends BaseController { @ApiBody({ type: AddFieldsToHashDto }) @ApiQueryRedisStringEncoding() async addMember( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: AddFieldsToHashDto, ): Promise { - return await this.hashBusinessService.addFields( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.hashBusinessService.addFields(clientMetadata, dto); } @Delete('/fields') @@ -101,14 +84,9 @@ export class HashController extends BaseController { @ApiBody({ type: DeleteFieldsFromHashDto }) @ApiQueryRedisStringEncoding() async deleteFields( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteFieldsFromHashDto, ): Promise { - return await this.hashBusinessService.deleteFields( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.hashBusinessService.deleteFields(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts index e06533185e..be54e66324 100644 --- a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts @@ -2,12 +2,9 @@ import { Body, Controller, Delete, - Get, HttpCode, - Param, Patch, Post, - Query, } from '@nestjs/common'; import { ApiBody, ApiOkResponse, ApiOperation, ApiTags, @@ -16,7 +13,8 @@ import { KeysBusinessService } from 'src/modules/browser/services/keys-business/ import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { RedisService } from 'src/modules/redis/redis.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; import { DeleteKeysDto, DeleteKeysResponse, @@ -50,15 +48,10 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async getKeys( - @Param('dbInstance') dbInstance: string, - @Body() getKeysDto: GetKeysDto, + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: GetKeysDto, ): Promise { - return this.keysBusinessService.getKeys( - { - instanceId: dbInstance, - }, - getKeysDto, - ); + return this.keysBusinessService.getKeys(clientMetadata, dto); } @Post('get-metadata') @@ -72,15 +65,10 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async getKeysInfo( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetKeysInfoDto, ): Promise { - return this.keysBusinessService.getKeysInfo( - { - instanceId: dbInstance, - }, - dto, - ); + return this.keysBusinessService.getKeysInfo(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -95,15 +83,10 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async getKeyInfo( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetKeyInfoDto, ): Promise { - return await this.keysBusinessService.getKeyInfo( - { - instanceId: dbInstance, - }, - dto.keyName, - ); + return await this.keysBusinessService.getKeyInfo(clientMetadata, dto.keyName); } @Delete('') @@ -116,15 +99,10 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async deleteKey( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteKeysDto, ): Promise { - return await this.keysBusinessService.deleteKeys( - { - instanceId: dbInstance, - }, - dto.keyNames, - ); + return await this.keysBusinessService.deleteKeys(clientMetadata, dto.keyNames); } @Patch('/name') @@ -137,15 +115,10 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async renameKey( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: RenameKeyDto, ): Promise { - return await this.keysBusinessService.renameKey( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.keysBusinessService.renameKey(clientMetadata, dto); } @Patch('/ttl') @@ -158,14 +131,9 @@ export class KeysController extends BaseController { }) @ApiQueryRedisStringEncoding() async updateTtl( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: UpdateKeyTtlDto, ): Promise { - return await this.keysBusinessService.updateTtl( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.keysBusinessService.updateTtl(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts b/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts index 8f31c6c675..23e0d14a35 100644 --- a/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts @@ -30,8 +30,9 @@ import { DeleteListElementsResponse, PushListElementsResponse, } from 'src/modules/browser/dto'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; +import { ClientMetadata } from 'src/common/models'; import { ListBusinessService } from '../../services/list-business/list-business.service'; @ApiTags('List') @@ -47,15 +48,10 @@ export class ListController extends BaseController { @ApiBody({ type: CreateListWithExpireDto }) @ApiQueryRedisStringEncoding() async createList( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateListWithExpireDto, ): Promise { - return await this.listBusinessService.createList( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.listBusinessService.createList(clientMetadata, dto); } @Put('') @@ -72,15 +68,10 @@ export class ListController extends BaseController { }) @ApiQueryRedisStringEncoding() async pushElement( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: PushElementToListDto, ): Promise { - return await this.listBusinessService.pushElement( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.listBusinessService.pushElement(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -96,15 +87,10 @@ export class ListController extends BaseController { }) @ApiQueryRedisStringEncoding() async getElements( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetListElementsDto, ): Promise { - return this.listBusinessService.getElements( - { - instanceId: dbInstance, - }, - dto, - ); + return this.listBusinessService.getElements(clientMetadata, dto); } @Patch('') @@ -115,15 +101,10 @@ export class ListController extends BaseController { @ApiBody({ type: SetListElementDto }) @ApiQueryRedisStringEncoding() async updateElement( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: SetListElementDto, ): Promise { - return await this.listBusinessService.setElement( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.listBusinessService.setElement(clientMetadata, dto); } @Post('/get-elements/:index') @@ -149,17 +130,11 @@ export class ListController extends BaseController { }) @ApiQueryRedisStringEncoding() async getElement( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Param('index') index: number, @Body() dto: KeyDto, ): Promise { - return this.listBusinessService.getElement( - { - instanceId: dbInstance, - }, - index, - dto, - ); + return this.listBusinessService.getElement(clientMetadata, index, dto); } @Delete('/elements') @@ -177,14 +152,9 @@ export class ListController extends BaseController { }) @ApiQueryRedisStringEncoding() async deleteElement( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteListElementsDto, ): Promise { - return this.listBusinessService.deleteElements( - { - instanceId: dbInstance, - }, - dto, - ); + return this.listBusinessService.deleteElements(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts b/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts index f9569a1134..aef0db0e8f 100644 --- a/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Get, HttpCode, - Param, Post, } from '@nestjs/common'; import { @@ -12,7 +11,7 @@ import { ApiTags, } from '@nestjs/swagger'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; import { CreateRedisearchIndexDto, @@ -21,6 +20,7 @@ import { } from 'src/modules/browser/dto/redisearch'; import { RedisearchService } from 'src/modules/browser/services/redisearch/redisearch.service'; import { GetKeysWithDetailsResponse } from 'src/modules/browser/dto'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('RediSearch') @Controller('redisearch') @@ -35,13 +35,9 @@ export class RedisearchController extends BaseController { @ApiRedisParams() @ApiQueryRedisStringEncoding() async list( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, ): Promise { - return this.service.list( - { - instanceId: dbInstance, - }, - ); + return this.service.list(clientMetadata); } @Post('') @@ -50,15 +46,10 @@ export class RedisearchController extends BaseController { @HttpCode(201) @ApiBody({ type: CreateRedisearchIndexDto }) async createIndex( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateRedisearchIndexDto, ): Promise { - return await this.service.createIndex( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.service.createIndex(clientMetadata, dto); } @Post('search') @@ -68,14 +59,9 @@ export class RedisearchController extends BaseController { @ApiRedisParams() @ApiQueryRedisStringEncoding() async search( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: SearchRedisearchDto, ): Promise { - return await this.service.search( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.service.search(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts b/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts index 729d44aa18..3b3fd148da 100644 --- a/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Delete, - Param, Patch, Post, UsePipes, @@ -20,6 +19,8 @@ import { } from 'src/modules/browser/dto'; import { RejsonRlBusinessService } from 'src/modules/browser/services/rejson-rl-business/rejson-rl-business.service'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; +import { BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('REJSON-RL') @Controller('rejson-rl') @@ -41,15 +42,10 @@ export class RejsonRlController { ], }) async getJson( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetRejsonRlDto, ): Promise { - return this.service.getJson( - { - instanceId: dbInstance, - }, - dto, - ); + return this.service.getJson(clientMetadata, dto); } @Post('') @@ -58,15 +54,10 @@ export class RejsonRlController { statusCode: 201, }) async createJson( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateRejsonRlWithExpireDto, ): Promise { - return this.service.create( - { - instanceId: dbInstance, - }, - dto, - ); + return this.service.create(clientMetadata, dto); } @Patch('/set') @@ -75,15 +66,10 @@ export class RejsonRlController { statusCode: 200, }) async jsonSet( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: ModifyRejsonRlSetDto, ): Promise { - return this.service.jsonSet( - { - instanceId: dbInstance, - }, - dto, - ); + return this.service.jsonSet(clientMetadata, dto); } @Patch('/arrappend') @@ -92,15 +78,10 @@ export class RejsonRlController { statusCode: 200, }) async arrAppend( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: ModifyRejsonRlArrAppendDto, ): Promise { - return this.service.arrAppend( - { - instanceId: dbInstance, - }, - dto, - ); + return this.service.arrAppend(clientMetadata, dto); } @Delete('') @@ -109,14 +90,9 @@ export class RejsonRlController { statusCode: 200, }) async remove( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: RemoveRejsonRlDto, ): Promise { - return this.service.remove( - { - instanceId: dbInstance, - }, - dto, - ); + return this.service.remove(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts b/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts index 690a2b01da..3e45af8125 100644 --- a/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts @@ -3,7 +3,6 @@ import { Controller, Delete, HttpCode, - Param, Post, Put, } from '@nestjs/common'; @@ -11,8 +10,9 @@ import { ApiBody, ApiOkResponse, ApiOperation, ApiTags, } from '@nestjs/swagger'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; +import { ClientMetadata } from 'src/common/models'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; import { AddMembersToSetDto, CreateSetWithExpireDto, @@ -36,15 +36,10 @@ export class SetController extends BaseController { @ApiBody({ type: CreateSetWithExpireDto }) @ApiQueryRedisStringEncoding() async createSet( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateSetWithExpireDto, ): Promise { - return await this.setBusinessService.createSet( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.setBusinessService.createSet(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -61,15 +56,10 @@ export class SetController extends BaseController { }) @ApiQueryRedisStringEncoding() async getMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetSetMembersDto, ): Promise { - return await this.setBusinessService.getMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.setBusinessService.getMembers(clientMetadata, dto); } @Put('') @@ -80,15 +70,10 @@ export class SetController extends BaseController { @ApiBody({ type: AddMembersToSetDto }) @ApiQueryRedisStringEncoding() async addMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: AddMembersToSetDto, ): Promise { - return await this.setBusinessService.addMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.setBusinessService.addMembers(clientMetadata, dto); } @Delete('/members') @@ -99,14 +84,9 @@ export class SetController extends BaseController { @ApiBody({ type: DeleteMembersFromSetDto }) @ApiQueryRedisStringEncoding() async deleteMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteMembersFromSetDto, ): Promise { - return await this.setBusinessService.deleteMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.setBusinessService.deleteMembers(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts index 2a2a7ff92e..4e04635767 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts @@ -1,10 +1,8 @@ import { Body, Controller, Delete, - Param, Patch, + Patch, Post, - UsePipes, - ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; @@ -18,7 +16,8 @@ import { import { ConsumerGroupService } from 'src/modules/browser/services/stream/consumer-group.service'; import { KeyDto } from 'src/modules/browser/dto'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') @Controller('streams/consumer-groups') @@ -42,10 +41,10 @@ export class ConsumerGroupController extends BaseController { }) @ApiQueryRedisStringEncoding() async getGroups( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: KeyDto, ): Promise { - return this.service.getGroups({ instanceId }, dto); + return this.service.getGroups(clientMetadata, dto); } @Post('') @@ -54,10 +53,10 @@ export class ConsumerGroupController extends BaseController { statusCode: 201, }) async createGroups( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateConsumerGroupsDto, ): Promise { - return this.service.createGroups({ instanceId }, dto); + return this.service.createGroups(clientMetadata, dto); } @Patch('') @@ -66,10 +65,10 @@ export class ConsumerGroupController extends BaseController { statusCode: 200, }) async updateGroup( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: UpdateConsumerGroupDto, ): Promise { - return this.service.updateGroup({ instanceId }, dto); + return this.service.updateGroup(clientMetadata, dto); } @Delete('') @@ -85,9 +84,9 @@ export class ConsumerGroupController extends BaseController { ], }) async deleteGroup( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteConsumerGroupsDto, ): Promise { - return this.service.deleteGroup({ instanceId }, dto); + return this.service.deleteGroup(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts index 93575c0913..a3bae2edee 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts @@ -1,10 +1,7 @@ import { Body, Controller, Delete, - Param, Post, - UsePipes, - ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; @@ -16,7 +13,8 @@ import { } from 'src/modules/browser/dto/stream.dto'; import { ConsumerService } from 'src/modules/browser/services/stream/consumer.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') @Controller('streams/consumer-groups/consumers') @@ -39,10 +37,10 @@ export class ConsumerController extends BaseController { }) @ApiQueryRedisStringEncoding() async getConsumers( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetConsumersDto, ): Promise { - return this.service.getConsumers({ instanceId }, dto); + return this.service.getConsumers(clientMetadata, dto); } @Delete('') @@ -51,10 +49,10 @@ export class ConsumerController extends BaseController { statusCode: 200, }) async deleteConsumers( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteConsumersDto, ): Promise { - return this.service.deleteConsumers({ instanceId }, dto); + return this.service.deleteConsumers(clientMetadata, dto); } @Post('/pending-messages/get') @@ -71,10 +69,10 @@ export class ConsumerController extends BaseController { }) @ApiQueryRedisStringEncoding() async getPendingEntries( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetPendingEntriesDto, ): Promise { - return this.service.getPendingEntries({ instanceId }, dto); + return this.service.getPendingEntries(clientMetadata, dto); } @Post('/pending-messages/ack') @@ -89,10 +87,10 @@ export class ConsumerController extends BaseController { ], }) async ackPendingEntries( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: AckPendingEntriesDto, ): Promise { - return this.service.ackPendingEntries({ instanceId }, dto); + return this.service.ackPendingEntries(clientMetadata, dto); } @Post('/pending-messages/claim') @@ -107,9 +105,9 @@ export class ConsumerController extends BaseController { ], }) async claimPendingEntries( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: ClaimPendingEntryDto, ): Promise { - return this.service.claimPendingEntries({ instanceId }, dto); + return this.service.claimPendingEntries(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts index 40c0938edf..7681564f5d 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Delete, - Param, Post, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; @@ -17,7 +16,8 @@ import { } from 'src/modules/browser/dto/stream.dto'; import { StreamService } from 'src/modules/browser/services/stream/stream.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') @Controller('streams') @@ -32,10 +32,10 @@ export class StreamController extends BaseController { statusCode: 201, }) async createStream( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateStreamDto, ): Promise { - return this.service.createStream({ instanceId }, dto); + return this.service.createStream(clientMetadata, dto); } @Post('entries') @@ -52,10 +52,10 @@ export class StreamController extends BaseController { }) @ApiQueryRedisStringEncoding() async addEntries( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: AddStreamEntriesDto, ): Promise { - return this.service.addEntries({ instanceId }, dto); + return this.service.addEntries(clientMetadata, dto); } @Post('/entries/get') @@ -72,10 +72,10 @@ export class StreamController extends BaseController { }) @ApiQueryRedisStringEncoding() async getEntries( - @Param('dbInstance') instanceId: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetStreamEntriesDto, ): Promise { - return this.service.getEntries({ instanceId }, dto); + return this.service.getEntries(clientMetadata, dto); } @Delete('/entries') @@ -91,14 +91,9 @@ export class StreamController extends BaseController { ], }) async deleteEntries( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteStreamEntriesDto, ): Promise { - return await this.service.deleteEntries( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.service.deleteEntries(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts b/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts index ec4a652b2b..1252e376b8 100644 --- a/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, HttpCode, - Param, Post, Put, } from '@nestjs/common'; @@ -17,7 +16,8 @@ import { } from 'src/modules/browser/dto/string.dto'; import { GetKeyInfoDto } from 'src/modules/browser/dto'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; import { StringBusinessService } from '../../services/string-business/string-business.service'; @ApiTags('String') @@ -33,15 +33,10 @@ export class StringController extends BaseController { @ApiBody({ type: SetStringWithExpireDto }) @ApiQueryRedisStringEncoding() async setString( - @Param('dbInstance') dbInstance: string, - @Body() stringDto: SetStringWithExpireDto, + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: SetStringWithExpireDto, ): Promise { - return this.stringBusinessService.setString( - { - instanceId: dbInstance, - }, - stringDto, - ); + return this.stringBusinessService.setString(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -56,15 +51,10 @@ export class StringController extends BaseController { }) @ApiQueryRedisStringEncoding() async getStringValue( - @Param('dbInstance') dbInstance: string, - @Body() getKeyInfoDto: GetKeyInfoDto, + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: GetKeyInfoDto, ): Promise { - return this.stringBusinessService.getStringValue( - { - instanceId: dbInstance, - }, - getKeyInfoDto.keyName, - ); + return this.stringBusinessService.getStringValue(clientMetadata, dto); } @Put('') @@ -73,14 +63,9 @@ export class StringController extends BaseController { @ApiBody({ type: SetStringDto }) @ApiQueryRedisStringEncoding() async updateStringValue( - @Param('dbInstance') dbInstance: string, - @Body() setStringDto: SetStringDto, + @BrowserClientMetadata() clientMetadata: ClientMetadata, + @Body() dto: SetStringDto, ): Promise { - return this.stringBusinessService.updateStringValue( - { - instanceId: dbInstance, - }, - setStringDto, - ); + return this.stringBusinessService.updateStringValue(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts b/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts index b11c462dcc..1d80221b39 100644 --- a/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts @@ -2,7 +2,6 @@ import { Body, Controller, Delete, - Param, Patch, Post, Put, @@ -10,7 +9,8 @@ import { import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; import { AddMembersToZSetDto, CreateZSetWithExpireDto, @@ -38,15 +38,10 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async createSet( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateZSetWithExpireDto, ): Promise { - return await this.zSetBusinessService.createZSet( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.createZSet(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -64,15 +59,10 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async getZSet( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: GetZSetMembersDto, ): Promise { - return await this.zSetBusinessService.getMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.getMembers(clientMetadata, dto); } @Put('') @@ -82,15 +72,10 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async addMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: AddMembersToZSetDto, ): Promise { - return await this.zSetBusinessService.addMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.addMembers(clientMetadata, dto); } @Patch('') @@ -100,15 +85,10 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async updateMember( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: UpdateMemberInZSetDto, ): Promise { - return await this.zSetBusinessService.updateMember( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.updateMember(clientMetadata, dto); } @Delete('/members') @@ -125,15 +105,10 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async deleteMembers( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: DeleteMembersFromZSetDto, ): Promise { - return await this.zSetBusinessService.deleteMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.deleteMembers(clientMetadata, dto); } // The key name can be very large, so it is better to send it in the request body @@ -151,14 +126,9 @@ export class ZSetController extends BaseController { }) @ApiQueryRedisStringEncoding() async searchZSet( - @Param('dbInstance') dbInstance: string, + @BrowserClientMetadata() clientMetadata: ClientMetadata, @Body() dto: SearchZSetMembersDto, ): Promise { - return await this.zSetBusinessService.searchMembers( - { - instanceId: dbInstance, - }, - dto, - ); + return await this.zSetBusinessService.searchMembers(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts index 6a06c71238..68f567b956 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.spec.ts @@ -1,9 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as IORedis from 'ioredis'; import * as Redis from 'ioredis-mock'; -import { mockDatabase } from 'src/__mocks__'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; import { @@ -17,10 +15,7 @@ import { import { ClusterNodeNotFoundError } from 'src/modules/cli/constants/errors'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { DatabaseService } from 'src/modules/database/database.service'; - -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; +import { mockBrowserClientMetadata } from 'src/__mocks__'; const mockClient = new Redis(); const mockCluster = new Redis.Cluster([]); @@ -74,7 +69,7 @@ describe('BrowserToolClusterService', () => { getRedisClient.mockResolvedValue(mockClient); await service.execCommand( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ); @@ -92,7 +87,7 @@ describe('BrowserToolClusterService', () => { await expect( service.execCommand( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ), @@ -113,7 +108,7 @@ describe('BrowserToolClusterService', () => { getRedisClient.mockResolvedValue(mockClient); execPipelineFromClient.mockResolvedValue(); - await service.execPipeline(mockClientOptions, args); + await service.execPipeline(mockBrowserClientMetadata, args); expect(execPipelineFromClient).toHaveBeenCalledWith(mockClient, args); }); @@ -124,7 +119,7 @@ describe('BrowserToolClusterService', () => { getRedisClient.mockRejectedValue(error); await expect( - service.execPipeline(mockClientOptions, args), + service.execPipeline(mockBrowserClientMetadata, args), ).rejects.toThrow(InternalServerErrorException); expect(execPipelineFromClient).not.toHaveBeenCalled(); }); @@ -141,7 +136,7 @@ describe('BrowserToolClusterService', () => { mockCluster.nodes.mockReturnValue([mockClusterNode1, mockClusterNode2]); const result = await service.execCommandFromNodes( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], 'all', @@ -168,7 +163,7 @@ describe('BrowserToolClusterService', () => { await expect( service.execCommandFromNodes( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], 'all', @@ -187,7 +182,7 @@ describe('BrowserToolClusterService', () => { mockCluster.nodes.mockReturnValue([mockClusterNode1, mockClusterNode2]); const result = await service.execCommandFromNode( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], { ...mockClusterNode1.options }, @@ -218,7 +213,7 @@ describe('BrowserToolClusterService', () => { await expect( service.execCommandFromNode( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], nodeOptions, @@ -233,7 +228,7 @@ describe('BrowserToolClusterService', () => { await expect( service.execCommandFromNode( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], { ...mockClusterNode1.options }, diff --git a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts index 9af1886c94..d072018265 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service.ts @@ -1,12 +1,10 @@ import { Injectable, Logger } from '@nestjs/common'; import IORedis, { NodeRole, Redis } from 'ioredis'; -import { AppTool } from 'src/models'; import { RedisConsumerAbstractService } from 'src/modules/redis/redis-consumer.abstract.service'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; -import { Endpoint } from 'src/common/models'; +import { ClientContext, ClientMetadata, Endpoint } from 'src/common/models'; import { BrowserToolCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { ClusterNodeNotFoundError } from 'src/modules/cli/constants/errors'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -28,15 +26,15 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { protected redisService: RedisService, protected databaseService: DatabaseService, ) { - super(AppTool.Browser, redisService, databaseService); + super(ClientContext.Browser, redisService, databaseService); } async execCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: BrowserToolCommands, args: Array, ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); const [command, ...commandArgs] = toolCommand.split(' '); // TODO: use sendCommand method @@ -44,12 +42,12 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { } async execPipeline( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommands: Array< [toolCommand: BrowserToolCommands, ...args: Array] >, ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); const pipelineSummery = getRedisPipelineSummary(toolCommands); this.logger.log( `Execute pipeline ${pipelineSummery.summary}, length: ${pipelineSummery.length}, connectionName: ${getConnectionName(client)}`, @@ -58,12 +56,12 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { } async execCommandFromNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: BrowserToolCommands, args: Array, nodeRole: NodeRole = 'all', ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); const nodes: Redis[] = client.nodes(nodeRole); this.logger.log(`Execute command '${toolCommand}' from nodes, connectionName: ${getConnectionName(client)}`); return await Promise.all( @@ -86,13 +84,13 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { } async execCommandFromNode( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: BrowserToolCommands, args: Array, exactNode: Endpoint, replyEncoding: BufferEncoding = 'utf8', ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); this.logger.log(`Execute command '${toolCommand}' from node, connectionName: ${getConnectionName(client)}`); const [command, ...commandArgs] = toolCommand.split(' '); @@ -130,10 +128,10 @@ export class BrowserToolClusterService extends RedisConsumerAbstractService { } async getNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, nodeRole: NodeRole = 'all', ) { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); return client.nodes(nodeRole); } } diff --git a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.spec.ts b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.spec.ts index 3aed45a585..f2de6b44ff 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.spec.ts @@ -1,9 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as IORedis from 'ioredis'; import * as Redis from 'ioredis-mock'; -import { mockDatabase } from 'src/__mocks__'; +import { mockBrowserClientMetadata } from 'src/__mocks__'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; @@ -17,10 +16,6 @@ import { mockKeyDto } from 'src/modules/browser/__mocks__'; import { RedisString } from 'src/common/constants'; import { DatabaseService } from 'src/modules/database/database.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockClient = new Redis(); const mockConnectionErrorMessage = 'Could not connect to localhost, please check the connection details.'; const { keyName } = mockKeyDto; @@ -67,7 +62,7 @@ describe('BrowserToolService', () => { getRedisClient.mockResolvedValue(mockClient); await service.execCommand( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ); @@ -91,7 +86,7 @@ describe('BrowserToolService', () => { await expect( service.execCommand( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ), @@ -111,7 +106,7 @@ describe('BrowserToolService', () => { getRedisClient.mockResolvedValue(mockClient); execPipelineFromClient.mockResolvedValue(); - await service.execPipeline(mockClientOptions, args); + await service.execPipeline(mockBrowserClientMetadata, args); expect(execPipelineFromClient).toHaveBeenCalledWith(mockClient, args); }); @@ -122,7 +117,7 @@ describe('BrowserToolService', () => { getRedisClient.mockRejectedValue(error); await expect( - service.execPipeline(mockClientOptions, args), + service.execPipeline(mockBrowserClientMetadata, args), ).rejects.toThrow(InternalServerErrorException); expect(execPipelineFromClient).not.toHaveBeenCalled(); }); @@ -139,7 +134,7 @@ describe('BrowserToolService', () => { getRedisClient.mockResolvedValue(mockClient); execPipelineFromClient.mockResolvedValue(); - await service.execMulti(mockClientOptions, args); + await service.execMulti(mockBrowserClientMetadata, args); expect(execMultiFromClient).toHaveBeenCalledWith(mockClient, args); }); @@ -149,7 +144,7 @@ describe('BrowserToolService', () => { ); getRedisClient.mockRejectedValue(error); - await expect(service.execMulti(mockClientOptions, args)).rejects.toThrow( + await expect(service.execMulti(mockBrowserClientMetadata, args)).rejects.toThrow( InternalServerErrorException, ); expect(execMultiFromClient).not.toHaveBeenCalled(); diff --git a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts index a979a6b729..8649dfcdc6 100644 --- a/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts +++ b/redisinsight/api/src/modules/browser/services/browser-tool/browser-tool.service.ts @@ -1,8 +1,7 @@ import * as Redis from 'ioredis'; import { Injectable, Logger } from '@nestjs/common'; -import { AppTool, ReplyError } from 'src/models'; +import { ReplyError } from 'src/models'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; import { RedisConsumerAbstractService } from 'src/modules/redis/redis-consumer.abstract.service'; @@ -10,6 +9,7 @@ import { BrowserToolCommands } from 'src/modules/browser/constants/browser-tool- import { getRedisPipelineSummary } from 'src/utils/cli-helper'; import { getConnectionName } from 'src/utils/redis-connection-helper'; import { DatabaseService } from 'src/modules/database/database.service'; +import { ClientContext, ClientMetadata } from 'src/common/models'; @Injectable() export class BrowserToolService extends RedisConsumerAbstractService { @@ -19,16 +19,16 @@ export class BrowserToolService extends RedisConsumerAbstractService { protected redisService: RedisService, protected databaseService: DatabaseService, ) { - super(AppTool.Browser, redisService, databaseService); + super(ClientContext.Browser, redisService, databaseService); } async execCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: BrowserToolCommands, args: Array, replyEncoding: BufferEncoding = null, ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); const [command, ...commandArgs] = toolCommand.split(' '); return client.sendCommand( @@ -39,10 +39,10 @@ export class BrowserToolService extends RedisConsumerAbstractService { } async execPipeline( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommands: Array<[toolCommand: BrowserToolCommands, ...args: Array]>, ): Promise<[ReplyError | null, any]> { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); const pipelineSummery = getRedisPipelineSummary(toolCommands); this.logger.log( `Execute pipeline ${pipelineSummery.summary}, length: ${pipelineSummery.length}, connectionName: ${getConnectionName(client)}`, @@ -51,10 +51,10 @@ export class BrowserToolService extends RedisConsumerAbstractService { } async execMulti( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommands: Array<[toolCommand: BrowserToolCommands, ...args: Array]>, ): Promise<[ReplyError | null, any]> { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); const pipelineSummery = getRedisPipelineSummary(toolCommands); this.logger.log( `Execute pipeline ${pipelineSummery.summary}, length: ${pipelineSummery.length}, connectionName: ${getConnectionName(client)}`, diff --git a/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.spec.ts index 57f4db0bfb..84f5e046ea 100644 --- a/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.spec.ts @@ -12,7 +12,7 @@ import { mockRedisConsumer, mockRedisNoPermError, mockRedisWrongTypeError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { GetHashFieldsDto, @@ -23,7 +23,6 @@ import { BrowserToolHashCommands, BrowserToolKeysCommands, } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { mockAddFieldsDto, mockDeleteFieldsDto, mockGetFieldsDto, @@ -32,10 +31,6 @@ import { } from 'src/modules/browser/__mocks__'; import { HashBusinessService } from './hash-business.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('HashBusinessService', () => { let service: HashBusinessService; let browserTool; @@ -58,7 +53,7 @@ describe('HashBusinessService', () => { describe('createHash', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddFieldsDto.keyName, ]) .mockResolvedValue(false); @@ -72,10 +67,10 @@ describe('HashBusinessService', () => { const commandArgs = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); await expect( - service.createHash(mockClientOptions, { ...mockAddFieldsDto, expire }), + service.createHash(mockBrowserClientMetadata, { ...mockAddFieldsDto, expire }), ).resolves.not.toThrow(); expect(service.createHashWithExpiration).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, keyName, commandArgs, expire, @@ -87,14 +82,14 @@ describe('HashBusinessService', () => { const commandArgs = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HSet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HSet, [ keyName, ...commandArgs, ]) .mockResolvedValue(1); await expect( - service.createHash(mockClientOptions, mockAddFieldsDto), + service.createHash(mockBrowserClientMetadata, mockAddFieldsDto), ).resolves.not.toThrow(); expect(service.createHashWithExpiration).not.toHaveBeenCalled(); }); @@ -103,18 +98,18 @@ describe('HashBusinessService', () => { const args = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ keyName, ]) .mockResolvedValue(true); await expect( - service.createHash(mockClientOptions, mockAddFieldsDto), + service.createHash(mockBrowserClientMetadata, mockAddFieldsDto), ).rejects.toThrow(ConflictException); expect( browserTool.execCommand, ).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, [keyName, ...args], ); @@ -127,7 +122,7 @@ describe('HashBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.createHash(mockClientOptions, mockAddFieldsDto), + service.createHash(mockBrowserClientMetadata, mockAddFieldsDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -135,7 +130,7 @@ describe('HashBusinessService', () => { describe('getFields', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HLen, [ mockAddFieldsDto.keyName, ]) .mockResolvedValue(mockAddFieldsDto.fields.length); @@ -143,19 +138,19 @@ describe('HashBusinessService', () => { it('succeed to get fields of the hash', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HScan, expect.anything(), ) .mockResolvedValue(mockRedisHScanResponse); const result = await service.getFields( - mockClientOptions, + mockBrowserClientMetadata, mockGetFieldsDto, ); expect(result).toEqual(mockGetFieldsResponse); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HScan, expect.anything(), ); @@ -167,17 +162,17 @@ describe('HashBusinessService', () => { match: item.field.toString(), }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HGet, [ dto.keyName, dto.match, ]) .mockResolvedValue(item.value); - const result = await service.getFields(mockClientOptions, dto); + const result = await service.getFields(mockBrowserClientMetadata, dto); expect(result).toEqual(mockGetFieldsResponse); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HScan, expect.anything(), ); @@ -188,13 +183,13 @@ describe('HashBusinessService', () => { match: 'field', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HGet, [ dto.keyName, dto.match, ]) .mockResolvedValue(null); - const result = await service.getFields(mockClientOptions, dto); + const result = await service.getFields(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockGetFieldsResponse, fields: [] }); }); @@ -208,17 +203,17 @@ describe('HashBusinessService', () => { match: 'fi\\[a-e\\]ld', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HGet, [ dto.keyName, item.field.toString(), ]) .mockResolvedValue('value'); - const result = await service.getFields(mockClientOptions, dto); + const result = await service.getFields(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockGetFieldsResponse, fields: [item] }); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HScan, expect.anything(), ); @@ -235,25 +230,25 @@ describe('HashBusinessService', () => { // ); // when(browserTool.execCommand) // .calledWith( - // mockClientOptions, + // mockBrowserClientMetadata, // BrowserToolHashCommands.HScan, // expect.anything(), // ) // .mockResolvedValue(['200', []]); // - // await service.getFields(mockClientOptions, dto); + // await service.getFields(mockBrowserClientMetadata, dto); // // expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1); // }); it('key with this name does not exist for getFields', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolHashCommands.HLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolHashCommands.HLen, [ mockGetFieldsDto.keyName, ]) .mockResolvedValue(0); await expect( - service.getFields(mockClientOptions, mockGetFieldsDto), + service.getFields(mockBrowserClientMetadata, mockGetFieldsDto), ).rejects.toThrow(NotFoundException); }); it("try to use 'HLEN' command not for hash data type", async () => { @@ -264,7 +259,7 @@ describe('HashBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getFields(mockClientOptions, mockGetFieldsDto), + service.getFields(mockBrowserClientMetadata, mockGetFieldsDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for getFields", async () => { @@ -275,7 +270,7 @@ describe('HashBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getFields(mockClientOptions, mockGetFieldsDto), + service.getFields(mockBrowserClientMetadata, mockGetFieldsDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -283,7 +278,7 @@ describe('HashBusinessService', () => { describe('addFields', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddFieldsDto.keyName, ]) .mockResolvedValue(true); @@ -291,7 +286,7 @@ describe('HashBusinessService', () => { it('succeed to add/update fields to the Hash data type', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, expect.anything(), ) @@ -300,31 +295,31 @@ describe('HashBusinessService', () => { const commandArgs = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); await expect( - service.addFields(mockClientOptions, mockAddFieldsDto), + service.addFields(mockBrowserClientMetadata, mockAddFieldsDto), ).resolves.not.toThrow(); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, [keyName, ...commandArgs], ); }); it('key with this name does not exist for addFields', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddFieldsDto.keyName, ]) .mockResolvedValue(false); await expect( - service.addFields(mockClientOptions, mockAddFieldsDto), + service.addFields(mockBrowserClientMetadata, mockAddFieldsDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, expect.anything(), ); @@ -336,14 +331,14 @@ describe('HashBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.addFields(mockClientOptions, mockAddFieldsDto), + service.addFields(mockBrowserClientMetadata, mockAddFieldsDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for addFields", async () => { @@ -353,14 +348,14 @@ describe('HashBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HSet, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.addFields(mockClientOptions, mockAddFieldsDto), + service.addFields(mockBrowserClientMetadata, mockAddFieldsDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -368,7 +363,7 @@ describe('HashBusinessService', () => { describe('deleteFields', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockDeleteFieldsDto.keyName, ]) .mockResolvedValue(true); @@ -377,14 +372,14 @@ describe('HashBusinessService', () => { const { fields } = mockDeleteFieldsDto; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HDel, expect.anything(), ) .mockResolvedValue(fields.length); const result = await service.deleteFields( - mockClientOptions, + mockBrowserClientMetadata, mockDeleteFieldsDto, ); @@ -392,16 +387,16 @@ describe('HashBusinessService', () => { }); it('key with this name does not exist for deleteFields', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockDeleteFieldsDto.keyName, ]) .mockResolvedValue(false); await expect( - service.deleteFields(mockClientOptions, mockDeleteFieldsDto), + service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolHashCommands.HDel, expect.anything(), ); @@ -414,7 +409,7 @@ describe('HashBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.deleteFields(mockClientOptions, mockDeleteFieldsDto), + service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for deleteFields", async () => { @@ -425,7 +420,7 @@ describe('HashBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.deleteFields(mockClientOptions, mockDeleteFieldsDto), + service.deleteFields(mockBrowserClientMetadata, mockDeleteFieldsDto), ).rejects.toThrow(ForbiddenException); }); }); diff --git a/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.ts b/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.ts index 60e7a601bb..fee5302c0f 100644 --- a/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/hash-business/hash-business.service.ts @@ -11,7 +11,7 @@ import { catchAclError, catchTransactionError, unescapeGlob } from 'src/utils'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { RedisErrorCodes } from 'src/constants'; import config from 'src/utils/config'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolHashCommands, @@ -41,14 +41,14 @@ export class HashBusinessService { ) {} public async createHash( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateHashWithExpireDto, ): Promise { this.logger.log('Creating Hash data type.'); const { keyName, fields } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -63,13 +63,13 @@ export class HashBusinessService { const args = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); if (dto.expire) { await this.createHashWithExpiration( - clientOptions, + clientMetadata, keyName, args, dto.expire, ); } else { - await this.createSimpleHash(clientOptions, keyName, args); + await this.createSimpleHash(clientMetadata, keyName, args); } this.logger.log('Succeed to create Hash data type.'); } catch (error) { @@ -80,7 +80,7 @@ export class HashBusinessService { } public async getFields( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetHashFieldsDto, ): Promise { this.logger.log('Getting fields of the Hash data type stored at key.'); @@ -93,7 +93,7 @@ export class HashBusinessService { }; try { result.total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HLen, [keyName], ); @@ -109,7 +109,7 @@ export class HashBusinessService { const field = unescapeGlob(dto.match); result.nextCursor = 0; const value = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HGet, [keyName, field], ); @@ -117,7 +117,7 @@ export class HashBusinessService { result.fields.push(plainToClass(HashFieldDto, { field, value })); } } else { - const scanResult = await this.scanHash(clientOptions, dto); + const scanResult = await this.scanHash(clientMetadata, dto); result = { ...result, ...scanResult }; } this.logger.log('Succeed to get fields of the Hash data type.'); @@ -132,14 +132,14 @@ export class HashBusinessService { } public async addFields( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AddFieldsToHashDto, ): Promise { this.logger.log('Adding fields to the Hash data type.'); const { keyName, fields } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -153,7 +153,7 @@ export class HashBusinessService { } const args = flatMap(fields, ({ field, value }: HashFieldDto) => [field, value]); await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HSet, [keyName, ...args], ); @@ -169,7 +169,7 @@ export class HashBusinessService { } public async deleteFields( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteFieldsFromHashDto, ): Promise { this.logger.log('Deleting fields from the Hash data type.'); @@ -177,7 +177,7 @@ export class HashBusinessService { let result; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -190,7 +190,7 @@ export class HashBusinessService { ); } result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HDel, [keyName, ...fields], ); @@ -206,19 +206,19 @@ export class HashBusinessService { } public async createSimpleHash( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, args: RedisString[], ): Promise { await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HSet, [key, ...args], ); } public async createHashWithExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, args: RedisString[], expire, @@ -226,7 +226,7 @@ export class HashBusinessService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, [ + ] = await this.browserTool.execMulti(clientMetadata, [ [BrowserToolHashCommands.HSet, key, ...args], [BrowserToolKeysCommands.Expire, key, expire], ]); @@ -234,7 +234,7 @@ export class HashBusinessService { } public async scanHash( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetHashFieldsDto, ): Promise { const { keyName } = dto; @@ -247,7 +247,7 @@ export class HashBusinessService { }; while (result.nextCursor !== 0 && result.fields.length < count) { const scanResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolHashCommands.HScan, [ keyName, diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/key-info-manager.interface.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/key-info-manager.interface.ts index 41dab4f3c0..bdcfc72a06 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/key-info-manager.interface.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/key-info-manager.interface.ts @@ -1,10 +1,10 @@ -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { GetKeyInfoResponse } from 'src/modules/browser/dto'; import { RedisString } from 'src/common/constants'; +import { ClientMetadata } from 'src/common/models'; export interface IKeyInfoStrategy { getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise; diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.spec.ts index 7cac86ed7c..398bb4a7a6 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.spec.ts @@ -1,9 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { when } from 'jest-when'; import { + mockBrowserClientMetadata, mockRedisConsumer, mockRedisNoPermError, - mockDatabase, } from 'src/__mocks__'; import { BrowserToolKeysCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { GraphTypeInfoStrategy } from './graph-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testGraph', type: 'graphdata', @@ -58,7 +53,7 @@ describe('GraphTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; beforeEach(() => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -70,7 +65,7 @@ describe('GraphTypeInfoStrategy', () => { ], ]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolGraphCommands.GraphQuery, [ + .calledWith(mockBrowserClientMetadata, BrowserToolGraphCommands.GraphQuery, [ key, 'MATCH (r) RETURN count(r)', '--compact', @@ -79,7 +74,7 @@ describe('GraphTypeInfoStrategy', () => { }); it('should return appropriate value', async () => { const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Graph, ); @@ -92,14 +87,14 @@ describe('GraphTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.Graph); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.Graph); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -112,7 +107,7 @@ describe('GraphTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -125,7 +120,7 @@ describe('GraphTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Graph, ); @@ -139,7 +134,7 @@ describe('GraphTypeInfoStrategy', () => { message: "ERR unknown command 'graph.query", }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolGraphCommands.GraphQuery, [ + .calledWith(mockBrowserClientMetadata, BrowserToolGraphCommands.GraphQuery, [ key, 'MATCH (r) RETURN count(r)', '--compact', @@ -147,7 +142,7 @@ describe('GraphTypeInfoStrategy', () => { .mockResolvedValue(replyError); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Graph, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.ts index 6c4f293542..1acb860dc5 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/graph-type-info/graph-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolGraphCommands, @@ -20,7 +20,7 @@ export class GraphTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class GraphTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]); @@ -39,7 +39,7 @@ export class GraphTypeInfoStrategy implements IKeyInfoStrategy { (item: [ReplyError, any]) => item[1], ); const [ttl, size] = result; - const length = await this.getNodesCount(clientOptions, key); + const length = await this.getNodesCount(clientMetadata, key); return { name: key, type, @@ -51,12 +51,12 @@ export class GraphTypeInfoStrategy implements IKeyInfoStrategy { } private async getNodesCount( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, ): Promise { try { const queryReply = await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolGraphCommands.GraphQuery, [key, 'MATCH (r) RETURN count(r)', '--compact'], ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.spec.ts index 8ede2ffb25..c8532370dd 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { BrowserToolHashCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { HashTypeInfoStrategy } from './hash-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testHash', type: 'hash', @@ -49,7 +44,7 @@ describe('HashTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolHashCommands.HLen, key], @@ -64,7 +59,7 @@ describe('HashTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Hash, ); @@ -77,7 +72,7 @@ describe('HashTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolHashCommands.HLen, key], @@ -85,7 +80,7 @@ describe('HashTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.Hash); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.Hash); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('HashTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolHashCommands.HLen, key], @@ -113,7 +108,7 @@ describe('HashTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Hash, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.ts index 6595095e18..c46c564334 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/hash-type-info/hash-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolHashCommands, @@ -20,7 +20,7 @@ export class HashTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class HashTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolHashCommands.HLen, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.spec.ts index 6e4deb75ff..e87c8bbb2d 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { BrowserToolKeysCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { ListTypeInfoStrategy } from './list-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testList', type: 'list', @@ -49,7 +44,7 @@ describe('ListTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolListCommands.LLen, key], @@ -64,7 +59,7 @@ describe('ListTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.List, ); @@ -77,7 +72,7 @@ describe('ListTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolListCommands.LLen, key], @@ -85,7 +80,7 @@ describe('ListTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.List); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.List); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('ListTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolListCommands.LLen, key], @@ -113,7 +108,7 @@ describe('ListTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.List, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.ts index 1e70e021fd..365a735cb4 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/list-type-info/list-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -20,7 +20,7 @@ export class ListTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class ListTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolListCommands.LLen, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.spec.ts index 713a8d7654..bcb4b62a73 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import { @@ -12,14 +12,9 @@ import { } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { mockKeyDto } from 'src/modules/browser/__mocks__'; import { RejsonRlTypeInfoStrategy } from './rejson-rl-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: mockKeyDto.keyName, type: 'ReJSON-RL', @@ -52,7 +47,7 @@ describe('RejsonRlTypeInfoStrategy', () => { const path = '.'; beforeEach(() => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -64,13 +59,13 @@ describe('RejsonRlTypeInfoStrategy', () => { ], ]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ key, path, ], 'utf8') .mockResolvedValue('object'); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonObjLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonObjLen, [ key, path, ], 'utf8') @@ -78,7 +73,7 @@ describe('RejsonRlTypeInfoStrategy', () => { }); it('should return appropriate value for key that store object', async () => { const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.JSON, ); @@ -87,20 +82,20 @@ describe('RejsonRlTypeInfoStrategy', () => { }); it('should return appropriate value for key that store string', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ key, path, ]) .mockResolvedValue('string'); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonStrLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonStrLen, [ key, path, ], 'utf8') .mockResolvedValue(10); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.JSON, ); @@ -109,20 +104,20 @@ describe('RejsonRlTypeInfoStrategy', () => { }); it('should return appropriate value for key that store array', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ key, path, ], 'utf8') .mockResolvedValue('array'); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonArrLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [ key, path, ], 'utf8') .mockResolvedValue(10); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.JSON, ); @@ -131,14 +126,14 @@ describe('RejsonRlTypeInfoStrategy', () => { }); it('should return appropriate value for key that store not iterable type', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ key, path, ], 'utf8') .mockResolvedValue('boolean'); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.JSON, ); @@ -151,14 +146,14 @@ describe('RejsonRlTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.JSON); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.JSON); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -171,7 +166,7 @@ describe('RejsonRlTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -184,7 +179,7 @@ describe('RejsonRlTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.JSON, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.ts index bae9243e2d..b7c16aa942 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/rejson-rl-type-info/rejson-rl-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -21,7 +21,7 @@ export class RejsonRlTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -29,7 +29,7 @@ export class RejsonRlTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]); @@ -40,7 +40,7 @@ export class RejsonRlTypeInfoStrategy implements IKeyInfoStrategy { (item: [ReplyError, any]) => item[1], ); const [ttl, size] = result; - const length = await this.getLength(clientOptions, key); + const length = await this.getLength(clientMetadata, key); return { name: key, type, @@ -52,12 +52,12 @@ export class RejsonRlTypeInfoStrategy implements IKeyInfoStrategy { } private async getLength( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, ): Promise { try { const objectKeyType = await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonType, [key, '.'], 'utf8', @@ -66,21 +66,21 @@ export class RejsonRlTypeInfoStrategy implements IKeyInfoStrategy { switch (objectKeyType) { case 'object': return await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonObjLen, [key, '.'], 'utf8', ); case 'array': return await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [key, '.'], 'utf8', ); case 'string': return await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonStrLen, [key, '.'], 'utf8', diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.spec.ts index a0c90d0cea..44e739c5b8 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { BrowserToolKeysCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { SetTypeInfoStrategy } from './set-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testSet', type: 'set', @@ -49,7 +44,7 @@ describe('SetTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolSetCommands.SCard, key], @@ -64,7 +59,7 @@ describe('SetTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Set, ); @@ -77,7 +72,7 @@ describe('SetTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolSetCommands.SCard, key], @@ -85,7 +80,7 @@ describe('SetTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.Set); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.Set); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('SetTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolSetCommands.SCard, key], @@ -113,7 +108,7 @@ describe('SetTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Set, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.ts index f05b3e8ecb..8ebc676e92 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/set-type-info/set-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -20,7 +20,7 @@ export class SetTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class SetTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolSetCommands.SCard, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.spec.ts index 372fbb111d..67d07cab62 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import { @@ -12,13 +12,8 @@ import { } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { StreamTypeInfoStrategy } from './stream-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testStream', type: 'stream', @@ -49,7 +44,7 @@ describe('StreamTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStreamCommands.XLen, key], @@ -64,7 +59,7 @@ describe('StreamTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Stream, ); @@ -77,7 +72,7 @@ describe('StreamTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStreamCommands.XLen, key], @@ -85,7 +80,7 @@ describe('StreamTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.Stream); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.Stream); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('StreamTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStreamCommands.XLen, key], @@ -113,7 +108,7 @@ describe('StreamTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.Stream, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.ts index 99d2c8fecb..3a3be0a2e5 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/stream-type-info/stream-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -20,7 +20,7 @@ export class StreamTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class StreamTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStreamCommands.XLen, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.spec.ts index 477f9fffd3..47382d89ce 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { BrowserToolKeysCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { StringTypeInfoStrategy } from './string-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testString', type: 'string', @@ -49,7 +44,7 @@ describe('StringTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStringCommands.StrLen, key], @@ -64,7 +59,7 @@ describe('StringTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.String, ); @@ -77,7 +72,7 @@ describe('StringTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStringCommands.StrLen, key], @@ -85,7 +80,7 @@ describe('StringTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.String); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.String); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('StringTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStringCommands.StrLen, key], @@ -113,7 +108,7 @@ describe('StringTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.String, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.ts index 6709da6363..68b5f57639 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/string-type-info/string-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -20,7 +20,7 @@ export class StringTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class StringTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolStringCommands.StrLen, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.spec.ts index 9e239a0d3a..009468b417 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { BrowserToolKeysCommands, @@ -12,13 +12,8 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { TSTypeInfoStrategy } from './ts-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testTS', type: 'TSDB-TYPE', @@ -66,7 +61,7 @@ describe('TSTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; beforeEach(() => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -78,12 +73,12 @@ describe('TSTypeInfoStrategy', () => { ], ]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolTSCommands.TSInfo, [key], 'utf8') + .calledWith(mockBrowserClientMetadata, BrowserToolTSCommands.TSInfo, [key], 'utf8') .mockResolvedValue(mockTSInfoReply); }); it('should return appropriate value', async () => { const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.TS, ); @@ -96,14 +91,14 @@ describe('TSTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.TS); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.TS); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -116,7 +111,7 @@ describe('TSTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -129,7 +124,7 @@ describe('TSTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.TS, ); @@ -143,11 +138,11 @@ describe('TSTypeInfoStrategy', () => { message: "ERR unknown command 'ts.info'", }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolTSCommands.TSInfo, [key], 'utf8') + .calledWith(mockBrowserClientMetadata, BrowserToolTSCommands.TSInfo, [key], 'utf8') .mockResolvedValue(replyError); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.TS, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.ts index 50d696bdf7..c2d8b42502 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/ts-type-info/ts-type-info.strategy.ts @@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { convertStringsArrayToObject } from 'src/utils'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -21,7 +21,7 @@ export class TSTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -29,7 +29,7 @@ export class TSTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]); @@ -40,7 +40,7 @@ export class TSTypeInfoStrategy implements IKeyInfoStrategy { (item: [ReplyError, any]) => item[1], ); const [ttl, size] = result; - const length = await this.getTotalSamples(clientOptions, key); + const length = await this.getTotalSamples(clientMetadata, key); return { name: key, type, @@ -52,12 +52,12 @@ export class TSTypeInfoStrategy implements IKeyInfoStrategy { } private async getTotalSamples( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, ): Promise { try { const info = await this.redisManager.execCommand( - clientOptions, + clientMetadata, BrowserToolTSCommands.TSInfo, [key], 'utf8', diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.spec.ts index 853073911e..6de4ed2153 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.spec.ts @@ -3,19 +3,14 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { UnsupportedTypeInfoStrategy } from './unsupported-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testKey', type: 'custom-type', @@ -45,7 +40,7 @@ describe('UnsupportedTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -58,7 +53,7 @@ describe('UnsupportedTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, 'custom-type', ); @@ -71,14 +66,14 @@ describe('UnsupportedTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Ttl, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, 'custom-type'); + await strategy.getInfo(mockBrowserClientMetadata, key, 'custom-type'); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -91,7 +86,7 @@ describe('UnsupportedTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]) @@ -104,7 +99,7 @@ describe('UnsupportedTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, 'custom-type', ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.ts index 7a5faf7050..dd891c17af 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/unsupported-type-info/unsupported-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { RedisString } from 'src/common/constants'; @@ -17,7 +17,7 @@ export class UnsupportedTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -25,7 +25,7 @@ export class UnsupportedTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], ]); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.spec.ts index 834d5ea103..8e5a3f430a 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.spec.ts @@ -3,7 +3,7 @@ import { when } from 'jest-when'; import { mockRedisConsumer, mockRedisNoPermError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import { @@ -12,13 +12,8 @@ import { } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { ZSetTypeInfoStrategy } from './z-set-type-info.strategy'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const getKeyInfoResponse: GetKeyInfoResponse = { name: 'testZSet', type: 'zset', @@ -49,7 +44,7 @@ describe('ZSetTypeInfoStrategy', () => { const key = getKeyInfoResponse.name; it('should return appropriate value', async () => { when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolZSetCommands.ZCard, key], @@ -64,7 +59,7 @@ describe('ZSetTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.ZSet, ); @@ -77,7 +72,7 @@ describe('ZSetTypeInfoStrategy', () => { command: BrowserToolKeysCommands.Type, }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolZSetCommands.ZCard, key], @@ -85,7 +80,7 @@ describe('ZSetTypeInfoStrategy', () => { .mockResolvedValue([replyError, []]); try { - await strategy.getInfo(mockClientOptions, key, RedisDataType.ZSet); + await strategy.getInfo(mockBrowserClientMetadata, key, RedisDataType.ZSet); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -98,7 +93,7 @@ describe('ZSetTypeInfoStrategy', () => { message: "ERR unknown command 'memory'", }; when(browserTool.execPipeline) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolZSetCommands.ZCard, key], @@ -113,7 +108,7 @@ describe('ZSetTypeInfoStrategy', () => { ]); const result = await strategy.getInfo( - mockClientOptions, + mockBrowserClientMetadata, key, RedisDataType.ZSet, ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.ts index a122a2ea6e..a3e5d23dc9 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/key-info-manager/strategies/z-set-type-info/z-set-type-info.strategy.ts @@ -1,7 +1,7 @@ import { Logger } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands, @@ -20,7 +20,7 @@ export class ZSetTypeInfoStrategy implements IKeyInfoStrategy { } public async getInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, type: string, ): Promise { @@ -28,7 +28,7 @@ export class ZSetTypeInfoStrategy implements IKeyInfoStrategy { const [ transactionError, transactionResults, - ] = await this.redisManager.execPipeline(clientOptions, [ + ] = await this.redisManager.execPipeline(clientMetadata, [ [BrowserToolKeysCommands.Ttl, key], [BrowserToolKeysCommands.MemoryUsage, key, 'samples', '0'], [BrowserToolZSetCommands.ZCard, key], diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts index ad94e8c11c..a9f8f870fb 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.spec.ts @@ -15,10 +15,9 @@ import { mockDatabase, mockClusterDatabaseWithTlsAuth, mockDatabaseService, - MockType, + MockType, mockBrowserClientMetadata } from 'src/__mocks__'; import ERROR_MESSAGES from 'src/constants/error-messages'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { GetKeyInfoResponse, GetKeysDto, @@ -35,7 +34,6 @@ import { SettingsService } from 'src/modules/settings/settings.service'; import IORedis from 'ioredis'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { DatabaseService } from 'src/modules/database/database.service'; -import { RedisString } from 'src/common/constants'; import { KeysBusinessService } from './keys-business.service'; import { StringTypeInfoStrategy } from './key-info-manager/strategies/string-type-info/string-type-info.strategy'; @@ -46,10 +44,6 @@ const getKeyInfoResponse: GetKeyInfoResponse = { size: 50, }; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockGetKeysWithDetailsResponse: GetKeysWithDetailsResponse = { cursor: 0, total: 1, @@ -114,7 +108,7 @@ describe('KeysBusinessService', () => { describe('getKeyInfo', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Type, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Type, [ getKeyInfoResponse.name, ], 'utf8') .mockResolvedValue(RedisDataType.String); @@ -128,7 +122,7 @@ describe('KeysBusinessService', () => { stringTypeInfoManager.getInfo = jest.fn().mockResolvedValue(mockResult); const result = await service.getKeyInfo( - mockClientOptions, + mockBrowserClientMetadata, getKeyInfoResponse.name, ); @@ -136,13 +130,13 @@ describe('KeysBusinessService', () => { }); it('throw NotFound error when key not found for getKeyInfo', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Type, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Type, [ getKeyInfoResponse.name, ], 'utf8') .mockResolvedValue('none'); await expect( - service.getKeyInfo(mockClientOptions, getKeyInfoResponse.name), + service.getKeyInfo(mockBrowserClientMetadata, getKeyInfoResponse.name), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for getKeyInfo", async () => { @@ -151,13 +145,13 @@ describe('KeysBusinessService', () => { command: 'TYPE', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Type, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Type, [ getKeyInfoResponse.name, ], 'utf8') .mockRejectedValue(replyError); await expect( - service.getKeyInfo(mockClientOptions, getKeyInfoResponse.name), + service.getKeyInfo(mockBrowserClientMetadata, getKeyInfoResponse.name), ).rejects.toThrow(ForbiddenException); }); }); @@ -165,14 +159,14 @@ describe('KeysBusinessService', () => { describe('getKeysInfo', () => { beforeEach(() => { when(browserTool.getRedisClient) - .calledWith(mockClientOptions) + .calledWith(mockBrowserClientMetadata) .mockResolvedValue(nodeClient); standaloneScanner['getKeysInfo'] = jest.fn().mockResolvedValue([getKeyInfoResponse]); }); it('should return keys with info', async () => { const result = await service.getKeysInfo( - mockClientOptions, + mockBrowserClientMetadata, { keys: [getKeyInfoResponse.name] }, ); @@ -187,7 +181,7 @@ describe('KeysBusinessService', () => { standaloneScanner['getKeysInfo'] = jest.fn().mockRejectedValueOnce(replyError); await expect( - service.getKeysInfo(mockClientOptions, { keys: [getKeyInfoResponse.name] }), + service.getKeysInfo(mockBrowserClientMetadata, { keys: [getKeyInfoResponse.name] }), ).rejects.toThrow(ForbiddenException); }); }); @@ -199,21 +193,18 @@ describe('KeysBusinessService', () => { .fn() .mockResolvedValue([mockGetKeysWithDetailsResponse]); - const result = await service.getKeys(mockClientOptions, getKeysDto); + const result = await service.getKeys(mockBrowserClientMetadata, getKeysDto); expect(standaloneScanner.getKeys).toHaveBeenCalled(); expect(result).toEqual([mockGetKeysWithDetailsResponse]); }); it('should return appropriate value for cluster', async () => { - const clientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockClusterDatabaseWithTlsAuth.id, - }; databaseService.get.mockResolvedValueOnce(mockClusterDatabaseWithTlsAuth); clusterScanner.getKeys = jest .fn() .mockResolvedValue([mockGetKeysWithDetailsResponse]); - const result = await service.getKeys(clientOptions, getKeysDto); + const result = await service.getKeys(mockBrowserClientMetadata, getKeysDto); expect(clusterScanner.getKeys).toHaveBeenCalled(); expect(result).toEqual([mockGetKeysWithDetailsResponse]); @@ -226,7 +217,7 @@ describe('KeysBusinessService', () => { standaloneScanner.getKeys = jest.fn().mockRejectedValue(replyError); await expect( - service.getKeys(mockClientOptions, getKeysDto), + service.getKeys(mockBrowserClientMetadata, getKeysDto), ).rejects.toThrow(ForbiddenException); }); it('scan per type not supported', async () => { @@ -242,7 +233,7 @@ describe('KeysBusinessService', () => { standaloneScanner.getKeys = jest.fn().mockRejectedValue(replyError); try { - await service.getKeys(mockClientOptions, dto); + await service.getKeys(mockBrowserClientMetadata, dto); fail('Should throw an error'); } catch (err) { expect(err).toBeInstanceOf(BadRequestException); @@ -258,12 +249,12 @@ describe('KeysBusinessService', () => { it('succeeded to delete keys', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Del, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Del, [ ...keyNames, ]) .mockResolvedValue(keyNames.length); - const result = await service.deleteKeys(mockClientOptions, [ + const result = await service.deleteKeys(mockBrowserClientMetadata, [ 'testString1', 'testString2', ]); @@ -271,13 +262,13 @@ describe('KeysBusinessService', () => { }); it('keys not found', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Del, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Del, [ ...keyNames, ]) .mockResolvedValue(null); await expect( - service.deleteKeys(mockClientOptions, keyNames), + service.deleteKeys(mockBrowserClientMetadata, keyNames), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for deleteKeys", async () => { @@ -288,7 +279,7 @@ describe('KeysBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.deleteKeys(mockClientOptions, keyNames), + service.deleteKeys(mockBrowserClientMetadata, keyNames), ).rejects.toThrow(ForbiddenException); }); }); @@ -301,47 +292,47 @@ describe('KeysBusinessService', () => { it('succeeded to rename key', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ renameKeyDto.keyName, ]) .mockResolvedValue(true); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.RenameNX, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.RenameNX, [ renameKeyDto.keyName, renameKeyDto.newKeyName, ]) .mockResolvedValue(1); await expect( - service.renameKey(mockClientOptions, renameKeyDto), + service.renameKey(mockBrowserClientMetadata, renameKeyDto), ).resolves.not.toThrow(); }); it('key with keyName not exist', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ renameKeyDto.keyName, ]) .mockResolvedValue(false); await expect( - service.renameKey(mockClientOptions, renameKeyDto), + service.renameKey(mockBrowserClientMetadata, renameKeyDto), ).rejects.toThrow(NotFoundException); }); it('key with newKeyName already exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ renameKeyDto.keyName, ]) .mockResolvedValue(true); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ renameKeyDto.keyName, renameKeyDto.newKeyName, ]) .mockResolvedValue(0); await expect( - service.renameKey(mockClientOptions, renameKeyDto), + service.renameKey(mockBrowserClientMetadata, renameKeyDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for renameKey", async () => { @@ -352,7 +343,7 @@ describe('KeysBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.renameKey(mockClientOptions, renameKeyDto), + service.renameKey(mockBrowserClientMetadata, renameKeyDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -362,42 +353,42 @@ describe('KeysBusinessService', () => { it('set expiration time', async () => { const dto = { keyName, ttl: 1000 }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [keyName]) .mockResolvedValue(-1); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Expire, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Expire, [ keyName, dto.ttl, ]) .mockResolvedValue(1); - const result = await service.updateTtl(mockClientOptions, dto); + const result = await service.updateTtl(mockBrowserClientMetadata, dto); expect(result).toEqual({ ttl: dto.ttl }); }); it('remove the existing timeout on key', async () => { const dto = { keyName, ttl: -1 }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [keyName]) .mockResolvedValue(1000); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Persist, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Persist, [ keyName, ]) .mockResolvedValue(1); - const result = await service.updateTtl(mockClientOptions, dto); + const result = await service.updateTtl(mockBrowserClientMetadata, dto); expect(result).toEqual({ ttl: dto.ttl }); }); it('key not found', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Expire, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Expire, [ keyName, ]) .mockResolvedValue(0); await expect( - service.updateTtl(mockClientOptions, { keyName, ttl: 1000 }), + service.updateTtl(mockBrowserClientMetadata, { keyName, ttl: 1000 }), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for updateTtl", async () => { @@ -408,7 +399,7 @@ describe('KeysBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.updateTtl(mockClientOptions, { keyName, ttl: 1000 }), + service.updateTtl(mockBrowserClientMetadata, { keyName, ttl: 1000 }), ).rejects.toThrow(ForbiddenException); }); }); @@ -417,27 +408,27 @@ describe('KeysBusinessService', () => { const keyName = 'testString'; it('should remove key expiration', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [keyName]) .mockResolvedValue(1000); - const result = await service.removeKeyExpiration(mockClientOptions, { + const result = await service.removeKeyExpiration(mockBrowserClientMetadata, { keyName, ttl: -1, }); expect(result).toEqual({ ttl: -1 }); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Persist, [keyName], ); }); it('key not found', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [keyName]) .mockResolvedValue(-2); await expect( - service.removeKeyExpiration(mockClientOptions, { keyName, ttl: -1 }), + service.removeKeyExpiration(mockBrowserClientMetadata, { keyName, ttl: -1 }), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for removeKeyExpiration", async () => { @@ -448,7 +439,7 @@ describe('KeysBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.removeKeyExpiration(mockClientOptions, { keyName, ttl: -1 }), + service.removeKeyExpiration(mockBrowserClientMetadata, { keyName, ttl: -1 }), ).rejects.toThrow(ForbiddenException); }); }); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts index 3f0222eaac..a483c30820 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/keys-business.service.ts @@ -20,7 +20,7 @@ import { UpdateKeyTtlDto, } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolClusterService, @@ -118,17 +118,17 @@ export class KeysBusinessService { } public async getKeys( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetKeysDto, ): Promise { try { this.logger.log('Getting keys with details.'); // todo: refactor. no need entire entity here const databaseInstance = await this.databaseService.get( - clientOptions.instanceId, + clientMetadata.databaseId, ); const scanner = this.scanner.getStrategy(databaseInstance.connectionType); - const result = await scanner.getKeys(clientOptions, dto); + const result = await scanner.getKeys(clientMetadata, dto); return result.map((nodeResult) => plainToClass(GetKeysWithDetailsResponse, nodeResult)); } catch (error) { @@ -151,15 +151,15 @@ export class KeysBusinessService { * Fetch additional keys info (type, size, ttl) * For standalone instances will use pipeline * For cluster instances will use single commands - * @param clientOptions + * @param clientMetadata * @param dto */ public async getKeysInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetKeysInfoDto, ): Promise { try { - const client = await this.browserTool.getRedisClient(clientOptions); + const client = await this.browserTool.getRedisClient(clientMetadata); const scanner = this.scanner.getStrategy(client.isCluster ? ConnectionType.CLUSTER : ConnectionType.STANDALONE); const result = await scanner.getKeysInfo(client, dto.keys); @@ -171,13 +171,13 @@ export class KeysBusinessService { } public async getKeyInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, key: RedisString, ): Promise { this.logger.log('Getting key info.'); try { const type = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Type, [key], 'utf8', @@ -189,7 +189,7 @@ export class KeysBusinessService { ); } const infoManager = this.keyInfoManager.getStrategy(type); - const result = await infoManager.getInfo(clientOptions, key, type); + const result = await infoManager.getInfo(clientMetadata, key, type); this.logger.log('Succeed to get key info'); return plainToClass(GetKeyInfoResponse, result); } catch (error) { @@ -199,14 +199,14 @@ export class KeysBusinessService { } public async deleteKeys( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keys: RedisString[], ): Promise { this.logger.log('Deleting keys'); let result; try { result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Del, keys, ); @@ -223,7 +223,7 @@ export class KeysBusinessService { } public async renameKey( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: RenameKeyDto, ): Promise { this.logger.log('Renaming key'); @@ -231,7 +231,7 @@ export class KeysBusinessService { let result; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -244,7 +244,7 @@ export class KeysBusinessService { ); } result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.RenameNX, [keyName, newKeyName], ); @@ -263,17 +263,17 @@ export class KeysBusinessService { } public async updateTtl( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: UpdateKeyTtlDto, ): Promise { if (dto.ttl === -1) { - return await this.removeKeyExpiration(clientOptions, dto); + return await this.removeKeyExpiration(clientMetadata, dto); } - return await this.setKeyExpiration(clientOptions, dto); + return await this.setKeyExpiration(clientMetadata, dto); } public async setKeyExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: UpdateKeyTtlDto, ): Promise { this.logger.log('Setting a timeout on key.'); @@ -281,12 +281,12 @@ export class KeysBusinessService { let result; try { await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Ttl, [keyName], ); result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Expire, [keyName, ttl], ); @@ -305,14 +305,14 @@ export class KeysBusinessService { } public async removeKeyExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: UpdateKeyTtlDto, ): Promise { this.logger.log('Removing the existing timeout on key.'); const { keyName } = dto; try { const currentTtl = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Ttl, [keyName], ); @@ -326,7 +326,7 @@ export class KeysBusinessService { } if (currentTtl > 0) { await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Persist, [keyName], ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts index 31f0a8b3be..0b3887a26b 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/scanner.interface.ts @@ -1,7 +1,7 @@ import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { Cluster, Redis } from 'ioredis'; import { RedisString } from 'src/common/constants'; +import { ClientMetadata } from 'src/common/models'; interface IGetKeysArgs { cursor: string; @@ -22,7 +22,7 @@ export interface IGetNodeKeysResult { export interface IScannerStrategy { getKeys( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, args: IGetKeysArgs, ): Promise; diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts index dcc0969414..4094c989b3 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { when } from 'jest-when'; import { - mockDatabase, + mockBrowserClientMetadata, mockRedisConsumer, mockRedisWrongTypeError, mockSettingsService, @@ -9,17 +9,12 @@ import { import { ReplyError } from 'src/models'; import { GetKeyInfoResponse, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { StandaloneStrategy } from 'src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy'; import { AbstractStrategy } from 'src/modules/browser/services/keys-business/scanner/strategies/abstract.strategy'; import IORedis from 'ioredis'; import { SettingsService } from 'src/modules/settings/settings.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const nodeClient = Object.create(IORedis.prototype); const clusterClient = Object.create(IORedis.Cluster.prototype); @@ -127,7 +122,7 @@ describe('RedisScannerAbstract', () => { expect(result).toEqual(mockResult); expect(browserTool.execPipeline).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, keys.map((key: string) => [BrowserToolKeysCommands.Type, key]), ); }); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts index 1bb55337a1..1fe31fc998 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.spec.ts @@ -4,7 +4,7 @@ import { mockAppSettingsInitial, mockRedisClusterConsumer, mockRedisNoPermError, mockSettingsService, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import config from 'src/utils/config'; @@ -14,7 +14,6 @@ import { BrowserToolClusterService, } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { IGetNodeKeysResult } from 'src/modules/browser/services/keys-business/scanner/scanner.interface'; import IORedis from 'ioredis'; import { SettingsService } from 'src/modules/settings/settings.service'; @@ -22,9 +21,6 @@ import * as Utils from 'src/modules/database/utils/database.total.util'; import { ClusterStrategy } from './cluster.strategy'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); @@ -120,7 +116,7 @@ describe('Cluster Scanner Strategy', () => { when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), expect.anything(), @@ -130,7 +126,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -156,7 +152,7 @@ describe('Cluster Scanner Strategy', () => { expect(browserTool.execCommandFromNode).toBeCalledTimes(3); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', args.match, 'COUNT', args.count, 'TYPE', args.type], mockClusterNodes[0], @@ -164,7 +160,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 2, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', args.match, 'COUNT', args.count, 'TYPE', args.type], mockClusterNodes[1], @@ -172,7 +168,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 3, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', args.match, 'COUNT', args.count, 'TYPE', args.type], mockClusterNodes[2], @@ -187,7 +183,7 @@ describe('Cluster Scanner Strategy', () => { when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -196,7 +192,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['1', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -205,7 +201,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['2', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['2', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -214,7 +210,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['0', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -223,7 +219,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['1', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -232,7 +228,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['0', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), mockClusterNodes[2], @@ -242,7 +238,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = mockGetKeysInfoFn; - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -268,7 +264,7 @@ describe('Cluster Scanner Strategy', () => { expect(browserTool.execCommandFromNode).toBeCalledTimes(6); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -276,7 +272,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 2, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -284,7 +280,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 3, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[2], @@ -292,7 +288,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 4, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -300,7 +296,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 5, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -308,7 +304,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 6, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['2', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -323,7 +319,7 @@ describe('Cluster Scanner Strategy', () => { when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -332,7 +328,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['1', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -341,7 +337,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['2', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['2', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -350,7 +346,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['0', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -359,7 +355,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['1', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -368,7 +364,7 @@ describe('Cluster Scanner Strategy', () => { .mockResolvedValue({ result: ['0', [Buffer.from(getKeyInfoResponse.name)]] }); when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), mockClusterNodes[2], @@ -378,7 +374,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = mockGetKeysInfoFn; - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -410,7 +406,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -418,7 +414,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 2, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -426,7 +422,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 3, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[2], @@ -434,7 +430,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 4, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -442,7 +438,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 5, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[1], @@ -450,7 +446,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 6, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[2], @@ -458,7 +454,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 7, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['2', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[0], @@ -466,7 +462,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 8, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[2], @@ -474,7 +470,7 @@ describe('Cluster Scanner Strategy', () => { ); expect(browserTool.execCommandFromNode).toHaveBeenNthCalledWith( 9, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', args.count], mockClusterNodes[2], @@ -487,7 +483,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = mockGetKeysInfoFn; - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -520,7 +516,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = mockGetKeysInfoFn; - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -554,7 +550,7 @@ describe('Cluster Scanner Strategy', () => { strategy.getKeysInfo = mockGetKeysInfoFn; - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -573,7 +569,7 @@ describe('Cluster Scanner Strategy', () => { ...getKeysDto, cursor: '172.1.0.1asd00@0||172.1.0.1:7001@0||172.1.0.1:7002@0', }; - await strategy.getKeys(mockClientOptions, args); + await strategy.getKeys(mockBrowserClientMetadata, args); fail(); } catch (err) { expect(err.message).toEqual( @@ -592,7 +588,7 @@ describe('Cluster Scanner Strategy', () => { when(browserTool.execCommandFromNode) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), expect.anything(), @@ -601,7 +597,7 @@ describe('Cluster Scanner Strategy', () => { .mockRejectedValue(replyError); try { - await strategy.getKeys(mockClientOptions, args); + await strategy.getKeys(mockBrowserClientMetadata, args); fail(); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -619,7 +615,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).toHaveBeenCalled(); }); @@ -629,7 +625,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).toHaveBeenCalled(); }); @@ -639,7 +635,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).toHaveBeenCalled(); }); @@ -649,7 +645,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).toHaveBeenCalled(); }); @@ -659,7 +655,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).toHaveBeenCalled(); }); @@ -669,7 +665,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scanNodes).not.toHaveBeenCalled(); }); @@ -690,7 +686,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue(getKeyInfoResponse); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -720,7 +716,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue({ ...getKeyInfoResponse, name: searchPattern }); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -753,7 +749,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -783,7 +779,7 @@ describe('Cluster Scanner Strategy', () => { size: null, }); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -814,7 +810,7 @@ describe('Cluster Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts index b05a56b4b4..d7e1088cce 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/cluster.strategy.ts @@ -5,7 +5,7 @@ import { unescapeGlob } from 'src/utils'; import { BrowserToolClusterService, } from 'src/modules/browser/services/browser-tool-cluster/browser-tool-cluster.service'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { GetKeyInfoResponse, @@ -104,11 +104,11 @@ export class ClusterStrategy extends AbstractStrategy { } private async getNodesToScan( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, initialCursor: string, ): Promise { const nodesClients = await this.redisManager.getNodes( - clientOptions, + clientMetadata, 'master', ); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts index 779d638ecb..b7c06c2ce0 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts @@ -4,14 +4,13 @@ import { mockAppSettingsInitial, mockRedisConsumer, mockRedisNoPermError, mockSettingsService, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import config from 'src/utils/config'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { GetKeysDto, RedisDataType } from 'src/modules/browser/dto'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { IGetNodeKeysResult } from 'src/modules/browser/services/keys-business/scanner/scanner.interface'; import IORedis from 'ioredis'; import { SettingsService } from 'src/modules/settings/settings.service'; @@ -19,9 +18,6 @@ import * as Utils from 'src/modules/database/utils/database.total.util'; import { StandaloneStrategy } from './standalone.strategy'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; const nodeClient = Object.create(IORedis.prototype); nodeClient.sendCommand = jest.fn(); @@ -77,7 +73,7 @@ describe('Standalone Scanner Strategy', () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), null, @@ -86,7 +82,7 @@ describe('Standalone Scanner Strategy', () => { strategy.getKeysInfo = jest.fn().mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(result).toEqual([ { @@ -99,7 +95,7 @@ describe('Standalone Scanner Strategy', () => { expect(strategy.getKeysInfo).toHaveBeenCalled(); expect(browserTool.execCommand).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', args.match, 'COUNT', args.count, 'TYPE', args.type], null, @@ -113,7 +109,7 @@ describe('Standalone Scanner Strategy', () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), null, @@ -122,7 +118,7 @@ describe('Standalone Scanner Strategy', () => { strategy.getKeysInfo = jest.fn(); - const result = await strategy.getKeys(mockClientOptions, args); + const result = await strategy.getKeys(mockBrowserClientMetadata, args); expect(strategy.getKeysInfo).not.toHaveBeenCalled(); expect(result).toEqual([ @@ -138,7 +134,7 @@ describe('Standalone Scanner Strategy', () => { jest.spyOn(Utils, 'getTotal').mockResolvedValue(mockGetTotalResponse_2); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Scan, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, [ '0', 'MATCH', '*', @@ -147,7 +143,7 @@ describe('Standalone Scanner Strategy', () => { ], null) .mockResolvedValue(['1', new Array(3).fill(getKeyInfoResponse.name)]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Scan, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, [ '1', 'MATCH', '*', @@ -156,7 +152,7 @@ describe('Standalone Scanner Strategy', () => { ], null) .mockResolvedValue(['2', new Array(3).fill(getKeyInfoResponse.name)]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Scan, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, [ '2', 'MATCH', '*', @@ -169,7 +165,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue(new Array(9).fill(getKeyInfoResponse)); - const result = await strategy.getKeys(mockClientOptions, getKeysDto); + const result = await strategy.getKeys(mockBrowserClientMetadata, getKeysDto); expect(result).toEqual([ { @@ -183,21 +179,21 @@ describe('Standalone Scanner Strategy', () => { expect(browserTool.execCommand).toBeCalledTimes(3); expect(browserTool.execCommand).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['0', 'MATCH', '*', 'COUNT', getKeysDto.count], null, ); expect(browserTool.execCommand).toHaveBeenNthCalledWith( 2, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['1', 'MATCH', '*', 'COUNT', getKeysDto.count], null, ); expect(browserTool.execCommand).toHaveBeenNthCalledWith( 3, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, ['2', 'MATCH', '*', 'COUNT', getKeysDto.count], null, @@ -208,7 +204,7 @@ describe('Standalone Scanner Strategy', () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), null, @@ -217,7 +213,7 @@ describe('Standalone Scanner Strategy', () => { strategy.getKeysInfo = jest.fn().mockResolvedValue([]); - const result = await strategy.getKeys(mockClientOptions, getKeysDto); + const result = await strategy.getKeys(mockBrowserClientMetadata, getKeysDto); expect(result).toEqual([ { @@ -238,7 +234,7 @@ describe('Standalone Scanner Strategy', () => { strategy.getKeysInfo = jest.fn().mockResolvedValue([]); - const result = await strategy.getKeys(mockClientOptions, getKeysDto); + const result = await strategy.getKeys(mockBrowserClientMetadata, getKeysDto); expect(result).toEqual([ { @@ -254,13 +250,13 @@ describe('Standalone Scanner Strategy', () => { strategy.getKeysInfo = jest.fn().mockResolvedValue([]); strategy.scan = jest.fn().mockResolvedValue(undefined); - const result = await strategy.getKeys(mockClientOptions, { + const result = await strategy.getKeys(mockBrowserClientMetadata, { cursor: '0', type: RedisDataType.String, }); expect(strategy.scan).toHaveBeenLastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, mockNodeEmptyResult, '*', REDIS_SCAN_CONFIG.countDefault, @@ -278,7 +274,7 @@ describe('Standalone Scanner Strategy', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Scan, expect.anything(), null, @@ -286,7 +282,7 @@ describe('Standalone Scanner Strategy', () => { .mockRejectedValue(replyError); try { - await strategy.getKeys(mockClientOptions, getKeysDto); + await strategy.getKeys(mockBrowserClientMetadata, getKeysDto); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); @@ -304,7 +300,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).toHaveBeenCalled(); }); @@ -314,7 +310,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).toHaveBeenCalled(); }); @@ -324,7 +320,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).toHaveBeenCalled(); }); @@ -334,7 +330,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).toHaveBeenCalled(); }); @@ -344,7 +340,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).toHaveBeenCalled(); }); @@ -354,7 +350,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - await strategy.getKeys(mockClientOptions, dto); + await strategy.getKeys(mockBrowserClientMetadata, dto); expect(strategy.scan).not.toHaveBeenCalled(); }); @@ -373,7 +369,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -395,7 +391,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([{ ...getKeyInfoResponse, name: mockSearchPattern }]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -418,7 +414,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -440,7 +436,7 @@ describe('Standalone Scanner Strategy', () => { }, ]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { @@ -461,7 +457,7 @@ describe('Standalone Scanner Strategy', () => { .fn() .mockResolvedValue([getKeyInfoResponse]); - const result = await strategy.getKeys(mockClientOptions, dto); + const result = await strategy.getKeys(mockBrowserClientMetadata, dto); expect(result).toEqual([ { diff --git a/redisinsight/api/src/modules/browser/services/list-business/list-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/list-business/list-business.service.spec.ts index a3cbfb42be..aea51f7898 100644 --- a/redisinsight/api/src/modules/browser/services/list-business/list-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/list-business/list-business.service.spec.ts @@ -12,9 +12,8 @@ import { mockRedisNoPermError, mockRedisWrongNumberOfArgumentsError, mockRedisWrongTypeError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { CreateListWithExpireDto, ListElementDestination, @@ -34,10 +33,6 @@ import { } from 'src/modules/browser/__mocks__'; import { ListBusinessService } from './list-business.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('ListBusinessService', () => { let service: ListBusinessService; let browserTool; @@ -60,7 +55,7 @@ describe('ListBusinessService', () => { describe('createList', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockPushElementDto.keyName, ]) .mockResolvedValue(false); @@ -72,7 +67,7 @@ describe('ListBusinessService', () => { .mockResolvedValue(undefined); await expect( - service.createList(mockClientOptions, { + service.createList(mockBrowserClientMetadata, { ...mockPushElementDto, expire: 1000, }), @@ -81,26 +76,26 @@ describe('ListBusinessService', () => { }); it('create list without expiration', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LPush, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LPush, [ mockPushElementDto.keyName, mockPushElementDto.element, ]) .mockResolvedValue(1); await expect( - service.createList(mockClientOptions, mockPushElementDto), + service.createList(mockBrowserClientMetadata, mockPushElementDto), ).resolves.not.toThrow(); expect(service.createListWithExpiration).not.toHaveBeenCalled(); }); it('key with this name exist', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockPushElementDto.keyName, ]) .mockResolvedValue(true); await expect( - service.createList(mockClientOptions, mockPushElementDto), + service.createList(mockBrowserClientMetadata, mockPushElementDto), ).rejects.toThrow(ConflictException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); expect(browserTool.execMulti).not.toHaveBeenCalled(); @@ -113,7 +108,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.createList(mockClientOptions, mockPushElementDto), + service.createList(mockBrowserClientMetadata, mockPushElementDto), ).rejects.toThrow(ForbiddenException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); expect(browserTool.execMulti).not.toHaveBeenCalled(); @@ -123,25 +118,25 @@ describe('ListBusinessService', () => { describe('pushElement', () => { it('succeed to insert element at the tail of the list data type', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPushX, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPushX, [ mockPushElementDto.keyName, mockPushElementDto.element, ]) .mockResolvedValue(1); await expect( - service.pushElement(mockClientOptions, mockPushElementDto), + service.pushElement(mockBrowserClientMetadata, mockPushElementDto), ).resolves.not.toThrow(); }); it('succeed to insert element at the head of the list data type', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LPushX, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LPushX, [ mockPushElementDto.keyName, mockPushElementDto.element, ]) .mockResolvedValue(12); - const result = await service.pushElement(mockClientOptions, { + const result = await service.pushElement(mockBrowserClientMetadata, { ...mockPushElementDto, destination: ListElementDestination.Head, }); @@ -150,14 +145,14 @@ describe('ListBusinessService', () => { }); it('key with this name does not exist for pushElement', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPushX, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPushX, [ mockPushElementDto.keyName, mockPushElementDto.element, ]) .mockResolvedValue(0); await expect( - service.pushElement(mockClientOptions, mockPushElementDto), + service.pushElement(mockBrowserClientMetadata, mockPushElementDto), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for pushElement", async () => { @@ -168,7 +163,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.pushElement(mockClientOptions, mockPushElementDto), + service.pushElement(mockBrowserClientMetadata, mockPushElementDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -176,7 +171,7 @@ describe('ListBusinessService', () => { describe('getElements', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LLen, [ mockPushElementDto.keyName, ]) .mockResolvedValue(mockListElements.length); @@ -184,27 +179,27 @@ describe('ListBusinessService', () => { it('succeed to get elements of the list', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.Lrange, expect.anything(), ) .mockResolvedValue(mockListElements); const result = await service.getElements( - mockClientOptions, + mockBrowserClientMetadata, mockGetListElementsDto, ); await expect(result).toEqual(mockGetListElementsResponse); }); it('key with this name does not exist for getElements', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LLen, [ mockPushElementDto.keyName, ]) .mockResolvedValue(0); await expect( - service.getElements(mockClientOptions, mockGetListElementsDto), + service.getElements(mockBrowserClientMetadata, mockGetListElementsDto), ).rejects.toThrow(NotFoundException); }); it("try to use 'LLEN' command not for list data type", async () => { @@ -213,13 +208,13 @@ describe('ListBusinessService', () => { command: 'LLEN', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LLen, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LLen, [ mockPushElementDto.keyName, ]) .mockRejectedValue(replyError); await expect( - service.getElements(mockClientOptions, mockGetListElementsDto), + service.getElements(mockBrowserClientMetadata, mockGetListElementsDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for getElements", async () => { @@ -230,7 +225,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getElements(mockClientOptions, mockGetListElementsDto), + service.getElements(mockBrowserClientMetadata, mockGetListElementsDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -238,7 +233,7 @@ describe('ListBusinessService', () => { describe('getElement', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockKeyDto.keyName, ]) .mockResolvedValue(1); @@ -249,14 +244,14 @@ describe('ListBusinessService', () => { command: 'LINDEX', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LIndex, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LIndex, [ mockKeyDto.keyName, expect.anything(), ]) .mockRejectedValue(replyError); await expect( - service.getElement(mockClientOptions, mockIndex, mockKeyDto), + service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto), ).rejects.toThrow(BadRequestException); }); it("user hasn't permissions to LINDEX", async () => { @@ -266,14 +261,14 @@ describe('ListBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.LIndex, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.getElement(mockClientOptions, mockIndex, mockKeyDto), + service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto), ).rejects.toThrow(ForbiddenException); }); it("user hasn't permissions to EXISTS", async () => { @@ -283,51 +278,51 @@ describe('ListBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.getElement(mockClientOptions, mockIndex, mockKeyDto), + service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto), ).rejects.toThrow(ForbiddenException); }); it('key with this name does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockKeyDto.keyName, ]) .mockResolvedValue(0); await expect( - service.getElement(mockClientOptions, mockIndex, mockKeyDto), + service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto), ).rejects.toThrow(NotFoundException); }); it('index is out of range', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.LIndex, expect.anything(), ) .mockResolvedValue(null); await expect( - service.getElement(mockClientOptions, mockIndex, mockKeyDto), + service.getElement(mockBrowserClientMetadata, mockIndex, mockKeyDto), ).rejects.toThrow(NotFoundException); }); it('succeed to get List element by index', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.LIndex, expect.anything(), ) .mockResolvedValue(mockGetListElementResponse.value); const result = await service.getElement( - mockClientOptions, + mockBrowserClientMetadata, mockIndex, mockKeyDto, ); @@ -338,7 +333,7 @@ describe('ListBusinessService', () => { describe('setElement', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockSetListElementDto.keyName, ]) .mockResolvedValue(true); @@ -346,7 +341,7 @@ describe('ListBusinessService', () => { it('succeed to set the list element at index', async () => { const { keyName, index, element } = mockSetListElementDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LSet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LSet, [ keyName, index, element, @@ -354,18 +349,18 @@ describe('ListBusinessService', () => { .mockResolvedValue('OK'); await expect( - service.setElement(mockClientOptions, mockSetListElementDto), + service.setElement(mockBrowserClientMetadata, mockSetListElementDto), ).resolves.not.toThrow(); }); it('key with this name does not exist for setElement', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockSetListElementDto.keyName, ]) .mockResolvedValue(false); await expect( - service.setElement(mockClientOptions, mockSetListElementDto), + service.setElement(mockBrowserClientMetadata, mockSetListElementDto), ).rejects.toThrow(NotFoundException); }); it("try to use 'LSET' command not for list data type", async () => { @@ -376,7 +371,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.setElement(mockClientOptions, mockSetListElementDto), + service.setElement(mockBrowserClientMetadata, mockSetListElementDto), ).rejects.toThrow(BadRequestException); }); it('index for LSET coomand is of out of range', async () => { @@ -388,7 +383,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.setElement(mockClientOptions, mockSetListElementDto), + service.setElement(mockBrowserClientMetadata, mockSetListElementDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions", async () => { @@ -399,7 +394,7 @@ describe('ListBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.setElement(mockClientOptions, mockSetListElementDto), + service.setElement(mockBrowserClientMetadata, mockSetListElementDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -407,13 +402,13 @@ describe('ListBusinessService', () => { describe('deleteElements', () => { it('succeed to remove element from the tail', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPop, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPop, [ mockDeleteElementsDto.keyName, ]) .mockResolvedValue(mockListElements[0]); const result = await service.deleteElements( - mockClientOptions, + mockBrowserClientMetadata, mockDeleteElementsDto, ); @@ -421,12 +416,12 @@ describe('ListBusinessService', () => { }); it('succeed to remove element from the head', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.LPop, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.LPop, [ mockDeleteElementsDto.keyName, ]) .mockResolvedValue(mockListElements[0]); - const result = await service.deleteElements(mockClientOptions, { + const result = await service.deleteElements(mockBrowserClientMetadata, { ...mockDeleteElementsDto, destination: ListElementDestination.Head, }); @@ -436,13 +431,13 @@ describe('ListBusinessService', () => { it('succeed to remove multiple elements from the tail', async () => { const mockDeletedElements = [mockListElement, mockListElement2]; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPop, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPop, [ mockDeleteElementsDto.keyName, 2, ]) .mockResolvedValue(mockDeletedElements); - const result = await service.deleteElements(mockClientOptions, { + const result = await service.deleteElements(mockBrowserClientMetadata, { ...mockDeleteElementsDto, count: 2, }); @@ -455,14 +450,14 @@ describe('ListBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.RPop, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.deleteElements(mockClientOptions, mockDeleteElementsDto), + service.deleteElements(mockBrowserClientMetadata, mockDeleteElementsDto), ).rejects.toThrow(BadRequestException); }); it("redis doesn't support 'RPOP' with 'count' argument", async () => { @@ -474,14 +469,14 @@ describe('ListBusinessService', () => { }, }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPop, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPop, [ mockDeleteElementsDto.keyName, 2, ]) .mockRejectedValue(replyError); await expect( - service.deleteElements(mockClientOptions, { + service.deleteElements(mockBrowserClientMetadata, { ...mockDeleteElementsDto, count: 2, }), @@ -494,25 +489,25 @@ describe('ListBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolListCommands.RPop, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.deleteElements(mockClientOptions, mockDeleteElementsDto), + service.deleteElements(mockBrowserClientMetadata, mockDeleteElementsDto), ).rejects.toThrow(ForbiddenException); }); it('key with this name does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolListCommands.RPop, [ + .calledWith(mockBrowserClientMetadata, BrowserToolListCommands.RPop, [ mockDeleteElementsDto.keyName, ]) .mockResolvedValue(null); await expect( - service.deleteElements(mockClientOptions, mockDeleteElementsDto), + service.deleteElements(mockBrowserClientMetadata, mockDeleteElementsDto), ).rejects.toThrow(NotFoundException); }); }); @@ -524,7 +519,7 @@ describe('ListBusinessService', () => { }; it("shouldn't throw error", async () => { when(browserTool.execMulti) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolListCommands.LPush, dto.keyName, dto.element], [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire], ]) @@ -537,7 +532,7 @@ describe('ListBusinessService', () => { ]); await expect( - service.createListWithExpiration(mockClientOptions, dto), + service.createListWithExpiration(mockBrowserClientMetadata, dto), ).resolves.not.toThrow(); }); it('should throw error', async () => { @@ -546,14 +541,14 @@ describe('ListBusinessService', () => { command: 'LPUSH', }; when(browserTool.execMulti) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolListCommands.LPush, dto.keyName, dto.element], [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire], ]) .mockResolvedValue([replyError, []]); try { - await service.createListWithExpiration(mockClientOptions, dto); + await service.createListWithExpiration(mockBrowserClientMetadata, dto); fail('Should throw an error'); } catch (err) { expect(err.message).toEqual(replyError.message); diff --git a/redisinsight/api/src/modules/browser/services/list-business/list-business.service.ts b/redisinsight/api/src/modules/browser/services/list-business/list-business.service.ts index f8847f6803..cc808c61d3 100644 --- a/redisinsight/api/src/modules/browser/services/list-business/list-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/list-business/list-business.service.ts @@ -9,7 +9,7 @@ import { isNull, isArray } from 'lodash'; import { RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { catchAclError, catchTransactionError } from 'src/utils'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { CreateListWithExpireDto, DeleteListElementsDto, @@ -40,14 +40,14 @@ export class ListBusinessService { ) {} public async createList( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateListWithExpireDto, ): Promise { this.logger.log('Creating list data type.'); const { keyName } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -60,9 +60,9 @@ export class ListBusinessService { ); } if (dto.expire) { - await this.createListWithExpiration(clientOptions, dto); + await this.createListWithExpiration(clientMetadata, dto); } else { - await this.createSimpleList(clientOptions, dto); + await this.createSimpleList(clientMetadata, dto); } this.logger.log('Succeed to create list data type.'); } catch (error) { @@ -73,14 +73,14 @@ export class ListBusinessService { } public async pushElement( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: PushElementToListDto, ): Promise { this.logger.log('Insert element at the tail/head of the list data type.'); const { keyName, element, destination } = dto; try { const total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, destination === ListElementDestination.Tail ? BrowserToolListCommands.RPushX : BrowserToolListCommands.LPushX, @@ -108,7 +108,7 @@ export class ListBusinessService { } public async getElements( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetListElementsDto, ): Promise { this.logger.log('Getting elements of the list stored at key.'); @@ -116,7 +116,7 @@ export class ListBusinessService { let result: GetListElementsResponse; try { const total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.LLen, [keyName], ); @@ -129,7 +129,7 @@ export class ListBusinessService { ); } const elements = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.Lrange, [keyName, offset, offset + count - 1], ); @@ -148,12 +148,12 @@ export class ListBusinessService { /** * Get List element by index * NotFound exception when redis return null - * @param clientOptions + * @param clientMetadata * @param index * @param dto */ public async getElement( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, index: number, dto: KeyDto, ): Promise { @@ -161,7 +161,7 @@ export class ListBusinessService { const { keyName } = dto; try { const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -173,7 +173,7 @@ export class ListBusinessService { } const value = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.LIndex, [keyName, index], ); @@ -195,14 +195,14 @@ export class ListBusinessService { } public async setElement( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SetListElementDto, ): Promise { this.logger.log('Setting the list element at index'); const { keyName, element, index } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -215,7 +215,7 @@ export class ListBusinessService { ); } await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.LSet, [keyName, index, element], ); @@ -236,11 +236,11 @@ export class ListBusinessService { /** * Delete and return the elements from the tail/head of list stored at key * NotFound exception when redis return null - * @param clientOptions + * @param clientMetadata * @param dto */ public async deleteElements( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteListElementsDto, ): Promise { this.logger.log('Deleting elements from the list stored at key.'); @@ -250,13 +250,13 @@ export class ListBusinessService { let result; if (destination === ListElementDestination.Head) { result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.LPop, execArgs, ); } else { result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.RPop, execArgs, ); @@ -287,27 +287,27 @@ export class ListBusinessService { } public async createSimpleList( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: PushElementToListDto, ): Promise { const { keyName, element } = dto; await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolListCommands.LPush, [keyName, element], ); } public async createListWithExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateListWithExpireDto, ): Promise { const { keyName, element, expire } = dto; const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, [ + ] = await this.browserTool.execMulti(clientMetadata, [ [BrowserToolListCommands.LPush, keyName, element], [BrowserToolKeysCommands.Expire, keyName, expire], ]); diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts index 175276afb0..4694e22322 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts @@ -5,12 +5,11 @@ import { } from '@nestjs/common'; import { when } from 'jest-when'; import { - mockDatabase, + mockBrowserClientMetadata, mockRedisConsumer, mockRedisNoPermError, mockRedisUnknownIndexName, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { RedisearchService } from 'src/modules/browser/services/redisearch/redisearch.service'; import IORedis from 'ioredis'; @@ -29,10 +28,6 @@ clusterClient.nodes = jest.fn(); const keyName1 = Buffer.from('keyName1'); const keyName2 = Buffer.from('keyName2'); -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockCreateRedisearchIndexDto = { index: 'indexName', type: RedisearchIndexKeyType.HASH, @@ -85,7 +80,7 @@ describe('RedisearchService', () => { keyName2.toString('hex'), ]); - const list = await service.list(mockClientOptions); + const list = await service.list(mockBrowserClientMetadata); expect(list).toEqual({ indexes: [ @@ -101,7 +96,7 @@ describe('RedisearchService', () => { keyName2.toString('hex'), ]); - const list = await service.list(mockClientOptions); + const list = await service.list(mockBrowserClientMetadata); expect(list).toEqual({ indexes: [ @@ -114,7 +109,7 @@ describe('RedisearchService', () => { nodeClient.sendCommand.mockRejectedValueOnce(mockRedisNoPermError); try { - await service.list(mockClientOptions); + await service.list(mockBrowserClientMetadata); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -131,7 +126,7 @@ describe('RedisearchService', () => { .calledWith(jasmine.objectContaining({ name: 'FT.CREATE' })) .mockResolvedValue('OK'); - await service.createIndex(mockClientOptions, mockCreateRedisearchIndexDto); + await service.createIndex(mockBrowserClientMetadata, mockCreateRedisearchIndexDto); expect(nodeClient.sendCommand).toHaveBeenCalledTimes(2); expect(nodeClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({ @@ -154,7 +149,7 @@ describe('RedisearchService', () => { .calledWith(jasmine.objectContaining({ name: 'FT.CREATE' })) .mockResolvedValueOnce('OK').mockRejectedValue(new Error('ReplyError: MOVED to somenode')); - await service.createIndex(mockClientOptions, mockCreateRedisearchIndexDto); + await service.createIndex(mockBrowserClientMetadata, mockCreateRedisearchIndexDto); expect(clusterClient.sendCommand).toHaveBeenCalledTimes(1); expect(nodeClient.sendCommand).toHaveBeenCalledTimes(2); @@ -175,7 +170,7 @@ describe('RedisearchService', () => { .mockReturnValue({ any: 'data' }); try { - await service.createIndex(mockClientOptions, mockCreateRedisearchIndexDto); + await service.createIndex(mockBrowserClientMetadata, mockCreateRedisearchIndexDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ConflictException); @@ -187,7 +182,7 @@ describe('RedisearchService', () => { .mockRejectedValue(mockRedisNoPermError); try { - await service.createIndex(mockClientOptions, mockCreateRedisearchIndexDto); + await service.createIndex(mockBrowserClientMetadata, mockCreateRedisearchIndexDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -202,7 +197,7 @@ describe('RedisearchService', () => { .mockRejectedValue(mockRedisNoPermError); try { - await service.createIndex(mockClientOptions, mockCreateRedisearchIndexDto); + await service.createIndex(mockBrowserClientMetadata, mockCreateRedisearchIndexDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -216,7 +211,7 @@ describe('RedisearchService', () => { .calledWith(jasmine.objectContaining({ name: 'FT.SEARCH' })) .mockResolvedValue([100, keyName1, keyName2]); - const res = await service.search(mockClientOptions, mockSearchRedisearchDto); + const res = await service.search(mockBrowserClientMetadata, mockSearchRedisearchDto); expect(res).toEqual({ cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset, @@ -254,7 +249,7 @@ describe('RedisearchService', () => { .calledWith(jasmine.objectContaining({ name: 'FT.SEARCH' })) .mockResolvedValue([100, keyName1, keyName2]); - const res = await service.search(mockClientOptions, mockSearchRedisearchDto); + const res = await service.search(mockBrowserClientMetadata, mockSearchRedisearchDto); expect(res).toEqual({ cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset, @@ -291,7 +286,7 @@ describe('RedisearchService', () => { .mockRejectedValue(mockRedisNoPermError); try { - await service.search(mockClientOptions, mockSearchRedisearchDto); + await service.search(mockBrowserClientMetadata, mockSearchRedisearchDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); diff --git a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts index 9776cb99d3..6e030e4537 100644 --- a/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts +++ b/redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts @@ -9,7 +9,7 @@ import { } from '@nestjs/common'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { catchAclError } from 'src/utils'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { CreateRedisearchIndexDto, ListRedisearchIndexesResponse, @@ -31,13 +31,13 @@ export class RedisearchService { /** * Get list of all available redisearch indexes - * @param clientOptions + * @param clientMetadata */ - public async list(clientOptions: IFindRedisClientInstanceByOptions): Promise { + public async list(clientMetadata: ClientMetadata): Promise { this.logger.log('Getting all redisearch indexes.'); try { - const client = await this.browserTool.getRedisClient(clientOptions); + const client = await this.browserTool.getRedisClient(clientMetadata); const nodes = this.getShards(client); @@ -57,11 +57,11 @@ export class RedisearchService { /** * Creates redisearch index - * @param clientOptions + * @param clientMetadata * @param dto */ public async createIndex( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateRedisearchIndexDto, ): Promise { this.logger.log('Creating redisearch index.'); @@ -71,7 +71,7 @@ export class RedisearchService { index, type, prefixes, fields, } = dto; - const client = await this.browserTool.getRedisClient(clientOptions); + const client = await this.browserTool.getRedisClient(clientMetadata); try { const indexInfo = await client.sendCommand(new Command('FT.INFO', [dto.index], { @@ -131,11 +131,11 @@ export class RedisearchService { /** * Search for key names using RediSearch module * Response is the same as for keys "scan" to have the same behaviour in the browser - * @param clientOptions + * @param clientMetadata * @param dto */ public async search( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SearchRedisearchDto, ): Promise { this.logger.log('Searching keys using redisearch.'); @@ -146,7 +146,7 @@ export class RedisearchService { index, query, offset, limit, } = dto; - const client = await this.browserTool.getRedisClient(clientOptions); + const client = await this.browserTool.getRedisClient(clientMetadata); try { const [[, maxSearchResults]] = await client.sendCommand( diff --git a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts index db94f83617..a6d6349d37 100644 --- a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts @@ -12,7 +12,7 @@ import { mockRedisConsumer, mockRedisNoPermError, mockRedisWrongTypeError, - mockDatabase, + mockDatabase, mockBrowserClientMetadata } from 'src/__mocks__'; import { ReplyError } from 'src/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -21,13 +21,8 @@ import { BrowserToolKeysCommands, BrowserToolRejsonRlCommands, } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { RejsonRlBusinessService } from './rejson-rl-business.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const testKey = Buffer.from('somejson'); const testSerializedObject = JSON.stringify({ some: 'object' }); const testPath = '.'; @@ -62,7 +57,7 @@ describe('JsonBusinessService', () => { ) => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, path, @@ -73,7 +68,7 @@ describe('JsonBusinessService', () => { if (value !== undefined) { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, @@ -87,7 +82,7 @@ describe('JsonBusinessService', () => { case 'array': when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [testKey, path], 'utf8', @@ -97,7 +92,7 @@ describe('JsonBusinessService', () => { case 'object': when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonObjLen, [testKey, path], 'utf8', @@ -112,7 +107,7 @@ describe('JsonBusinessService', () => { beforeEach(() => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, testPath], ) @@ -122,14 +117,14 @@ describe('JsonBusinessService', () => { it('should throw BadRequest error when no key found in the database', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, testPath], ) .mockResolvedValue(null); try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -143,14 +138,14 @@ describe('JsonBusinessService', () => { }); it('should throw BadRequest error when incorrect type of a key', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockResolvedValue(null); try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, forceRetrieve: true, @@ -168,7 +163,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -185,7 +180,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -203,7 +198,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -219,7 +214,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(new Error()); // no message here try { - await service.getJson(mockClientOptions, { + await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -231,13 +226,13 @@ describe('JsonBusinessService', () => { it('should return data (string)', async () => { const testData = 'some string'; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -251,13 +246,13 @@ describe('JsonBusinessService', () => { it('should return data (number)', async () => { const testData = 3.14; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -271,13 +266,13 @@ describe('JsonBusinessService', () => { it('should return data (integer)', async () => { const testData = 123; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -291,13 +286,13 @@ describe('JsonBusinessService', () => { it('should return data (boolean)', async () => { const testData = true; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -311,13 +306,13 @@ describe('JsonBusinessService', () => { it('should return data (null)', async () => { const testData = null; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -339,13 +334,13 @@ describe('JsonBusinessService', () => { { some: 'field' }, ]; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -365,13 +360,13 @@ describe('JsonBusinessService', () => { someInt: 1222, }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -393,20 +388,20 @@ describe('JsonBusinessService', () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, testPath], ) .mockReturnValue(1025); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, forceRetrieve: true, @@ -423,7 +418,7 @@ describe('JsonBusinessService', () => { beforeEach(() => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, testPath], ) @@ -433,14 +428,14 @@ describe('JsonBusinessService', () => { it('should return full string value even if size is above the limit', async () => { const testData = randomBytes(2000).toString('hex'); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonGet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ testKey, testPath, ], 'utf8') .mockReturnValue(JSON.stringify(testData)); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, testPath, @@ -448,7 +443,7 @@ describe('JsonBusinessService', () => { 'utf8', ).mockReturnValue('string'); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -472,7 +467,7 @@ describe('JsonBusinessService', () => { ]; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, @@ -482,7 +477,7 @@ describe('JsonBusinessService', () => { ).mockReturnValue('array'); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [testKey, testPath], 'utf8', @@ -497,7 +492,7 @@ describe('JsonBusinessService', () => { mockRedisCallsForSafeResponse('[5]', 5, 'array', undefined, 3); mockRedisCallsForSafeResponse('[6]', 6, 'object', undefined, 2); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -562,14 +557,14 @@ describe('JsonBusinessService', () => { const testData = [12, 'str']; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, path], ) .mockReturnValue(1025); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, @@ -579,7 +574,7 @@ describe('JsonBusinessService', () => { ).mockReturnValue('array'); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [testKey, path], 'utf8', @@ -598,7 +593,7 @@ describe('JsonBusinessService', () => { testData[1], ); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path, }); @@ -637,14 +632,14 @@ describe('JsonBusinessService', () => { }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, testPath, ], 'utf8') .mockReturnValue('object'); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonObjKeys, [testKey, testPath], 'utf8', @@ -696,7 +691,7 @@ describe('JsonBusinessService', () => { 2, ); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -765,20 +760,20 @@ describe('JsonBusinessService', () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', testKey, path], ) .mockReturnValue(1025); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonType, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonType, [ testKey, path, ], 'utf8') .mockReturnValue('object'); when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonObjKeys, [testKey, path], 'utf8', @@ -798,7 +793,7 @@ describe('JsonBusinessService', () => { testData.fStr, ); - const result = await service.getJson(mockClientOptions, { + const result = await service.getJson(mockBrowserClientMetadata, { keyName: testKey, path, }); @@ -835,7 +830,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockReturnValue(null); try { - await service.create(mockClientOptions, { + await service.create(mockBrowserClientMetadata, { keyName: testKey, data: testSerializedObject, }); @@ -852,7 +847,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.create(mockClientOptions, { + await service.create(mockBrowserClientMetadata, { keyName: testKey, data: testSerializedObject, }); @@ -870,7 +865,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.create(mockClientOptions, { + await service.create(mockBrowserClientMetadata, { keyName: testKey, data: testSerializedObject, }); @@ -887,7 +882,7 @@ describe('JsonBusinessService', () => { ...mockRedisNoPermError, }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolRejsonRlCommands.JsonSet, [ + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonSet, [ testKey, testPath, testSerializedObject, @@ -895,26 +890,26 @@ describe('JsonBusinessService', () => { ]) .mockReturnValue('OK'); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Expire, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Expire, [ testKey, testExpire, ]) .mockRejectedValue(replyError); - await service.create(mockClientOptions, { + await service.create(mockBrowserClientMetadata, { keyName: testKey, data: testSerializedObject, expire: testExpire, }); expect(browserTool.execCommand).lastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Expire, [testKey, testExpire], ); }); it('should successful create key', async () => { - await service.create(mockClientOptions, { + await service.create(mockBrowserClientMetadata, { keyName: testKey, data: testSerializedObject, }); @@ -928,7 +923,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockReturnValue(0); try { - await service.jsonSet(mockClientOptions, { + await service.jsonSet(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: testSerializedObject, @@ -948,7 +943,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.jsonSet(mockClientOptions, { + await service.jsonSet(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: testSerializedObject, @@ -970,7 +965,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.jsonSet(mockClientOptions, { + await service.jsonSet(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: testSerializedObject, @@ -988,7 +983,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.jsonSet(mockClientOptions, { + await service.jsonSet(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: testSerializedObject, @@ -999,7 +994,7 @@ describe('JsonBusinessService', () => { } }); it('should successful modify data', async () => { - await service.jsonSet(mockClientOptions, { + await service.jsonSet(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: testSerializedObject, @@ -1007,12 +1002,12 @@ describe('JsonBusinessService', () => { expect(browserTool.execCommand).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [testKey], ); expect(browserTool.execCommand).lastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonSet, [testKey, testPath, testSerializedObject], ); @@ -1026,7 +1021,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockReturnValue(0); try { - await service.arrAppend(mockClientOptions, { + await service.arrAppend(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: [testSerializedObject], @@ -1046,7 +1041,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.arrAppend(mockClientOptions, { + await service.arrAppend(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: [testSerializedObject], @@ -1066,7 +1061,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.arrAppend(mockClientOptions, { + await service.arrAppend(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: [testSerializedObject], @@ -1077,7 +1072,7 @@ describe('JsonBusinessService', () => { } }); it('should successful modify data', async () => { - await service.arrAppend(mockClientOptions, { + await service.arrAppend(mockBrowserClientMetadata, { keyName: testKey, path: testPath, data: [testSerializedObject, testSerializedObject], @@ -1085,12 +1080,12 @@ describe('JsonBusinessService', () => { expect(browserTool.execCommand).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [testKey], ); expect(browserTool.execCommand).lastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonArrAppend, [testKey, testPath, testSerializedObject, testSerializedObject], ); @@ -1104,7 +1099,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockReturnValue(0); try { - await service.remove(mockClientOptions, { + await service.remove(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -1123,7 +1118,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.remove(mockClientOptions, { + await service.remove(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -1142,7 +1137,7 @@ describe('JsonBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); try { - await service.remove(mockClientOptions, { + await service.remove(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); @@ -1152,19 +1147,19 @@ describe('JsonBusinessService', () => { } }); it('should successful remove path', async () => { - await service.remove(mockClientOptions, { + await service.remove(mockBrowserClientMetadata, { keyName: testKey, path: testPath, }); expect(browserTool.execCommand).toHaveBeenNthCalledWith( 1, - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [testKey], ); expect(browserTool.execCommand).lastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonDel, [testKey, testPath], ); diff --git a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts index 724a6775da..0dcf718d75 100644 --- a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts @@ -9,7 +9,7 @@ import { RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { catchAclError } from 'src/utils'; import config from 'src/utils/config'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { CreateRejsonRlWithExpireDto, GetRejsonRlDto, @@ -36,12 +36,12 @@ export class RejsonRlBusinessService { ) {} private async forceGetJson( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, ): Promise { const data = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonGet, [keyName, path], 'utf8', @@ -57,12 +57,12 @@ export class RejsonRlBusinessService { } private async estimateSize( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, ): Promise { const size = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonDebug, ['MEMORY', keyName, path], ); @@ -77,12 +77,12 @@ export class RejsonRlBusinessService { } private async getObjectKeys( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, ): Promise { return this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonObjKeys, [keyName, path], 'utf8', @@ -90,12 +90,12 @@ export class RejsonRlBusinessService { } private async getJsonDataType( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, ): Promise { return this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonType, [keyName, path], 'utf8', @@ -103,7 +103,7 @@ export class RejsonRlBusinessService { } private async getDetails( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, key: string | number, @@ -115,7 +115,7 @@ export class RejsonRlBusinessService { }; const objectKeyType = await this.getJsonDataType( - clientOptions, + clientMetadata, keyName, path, ); @@ -126,7 +126,7 @@ export class RejsonRlBusinessService { details[ 'cardinality' ] = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonObjLen, [keyName, path], 'utf8', @@ -136,7 +136,7 @@ export class RejsonRlBusinessService { details[ 'cardinality' ] = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [keyName, path], 'utf8', @@ -144,7 +144,7 @@ export class RejsonRlBusinessService { break; default: details['value'] = await this.forceGetJson( - clientOptions, + clientMetadata, keyName, path, ); @@ -155,7 +155,7 @@ export class RejsonRlBusinessService { } private async safeGetJsonByType( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, keyName: RedisString, path: string, type: string, @@ -167,7 +167,7 @@ export class RejsonRlBusinessService { switch (type) { case 'object': objectKeys = await this.getObjectKeys( - clientOptions, + clientMetadata, keyName, path, ); @@ -179,7 +179,7 @@ export class RejsonRlBusinessService { const fullObjectKeyPath = `${rootPath}${childPath}`; result.push( await this.getDetails( - clientOptions, + clientMetadata, keyName, fullObjectKeyPath, objectKey, @@ -190,7 +190,7 @@ export class RejsonRlBusinessService { break; case 'array': arrayLength = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonArrLen, [keyName, path], 'utf8', @@ -199,12 +199,12 @@ export class RejsonRlBusinessService { for (let i = 0; i < arrayLength; i += 1) { const fullObjectKeyPath = `${path === '.' ? '' : path}[${i}]`; result.push( - await this.getDetails(clientOptions, keyName, fullObjectKeyPath, i), + await this.getDetails(clientMetadata, keyName, fullObjectKeyPath, i), ); } break; default: - return this.forceGetJson(clientOptions, keyName, path); + return this.forceGetJson(clientMetadata, keyName, path); } return result; @@ -214,11 +214,11 @@ export class RejsonRlBusinessService { * Method to create REJSON-RL type * Supports key TTL * - * @param clientOptions + * @param clientMetadata * @param dto */ public async create( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateRejsonRlWithExpireDto, ): Promise { this.logger.log('Creating REJSON-RL data type.'); @@ -226,7 +226,7 @@ export class RejsonRlBusinessService { const { keyName, data, expire } = dto; try { const result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonSet, [keyName, '.', data, 'NX'], ); @@ -240,7 +240,7 @@ export class RejsonRlBusinessService { if (expire) { try { await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Expire, [keyName, expire], ); @@ -268,7 +268,7 @@ export class RejsonRlBusinessService { } public async getJson( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetRejsonRlDto, ): Promise { this.logger.log('Getting json by key.'); // todo: investigate logger implementation @@ -283,23 +283,23 @@ export class RejsonRlBusinessService { try { // Get value in the path without any checks if (forceRetrieve) { - result.data = await this.forceGetJson(clientOptions, keyName, path); + result.data = await this.forceGetJson(clientMetadata, keyName, path); return result; } - const jsonSize = await this.estimateSize(clientOptions, keyName, path); + const jsonSize = await this.estimateSize(clientMetadata, keyName, path); if (jsonSize > config.get('modules')['json']['sizeThreshold']) { - const type = await this.getJsonDataType(clientOptions, keyName, path); + const type = await this.getJsonDataType(clientMetadata, keyName, path); result.downloaded = false; result.type = type; result.data = await this.safeGetJsonByType( - clientOptions, + clientMetadata, keyName, path, type, ); } else { - result.data = await this.forceGetJson(clientOptions, keyName, path); + result.data = await this.forceGetJson(clientMetadata, keyName, path); } return result; @@ -327,11 +327,11 @@ export class RejsonRlBusinessService { /** * Method to modify REJSON-RL type using JSON.SET command - * @param clientOptions + * @param clientMetadata * @param dto */ public async jsonSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: ModifyRejsonRlSetDto, ): Promise { this.logger.log('Modifying REJSON-RL data type.'); @@ -339,7 +339,7 @@ export class RejsonRlBusinessService { try { const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -347,9 +347,9 @@ export class RejsonRlBusinessService { if (!exists) { throw new NotFoundException(ERROR_MESSAGES.KEY_NOT_EXIST); } - await this.getJsonDataType(clientOptions, keyName, path); + await this.getJsonDataType(clientMetadata, keyName, path); await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonSet, [keyName, path, data], ); @@ -381,18 +381,18 @@ export class RejsonRlBusinessService { /** * Method to modify REJSON-RL type using JSON.ARRAPPEND command - * @param clientOptions + * @param clientMetadata * @param dto */ public async arrAppend( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: ModifyRejsonRlArrAppendDto, ): Promise { this.logger.log('Modifying REJSON-RL data type.'); const { keyName, path, data } = dto; try { const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -402,7 +402,7 @@ export class RejsonRlBusinessService { } await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonArrAppend, [keyName, path, ...data], ); @@ -426,18 +426,18 @@ export class RejsonRlBusinessService { /** * Method to remove REJSON-RL path using JSON.DEL command - * @param clientOptions + * @param clientMetadata * @param dto */ public async remove( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: RemoveRejsonRlDto, ): Promise { this.logger.log('Removing REJSON-RL data.'); const { keyName, path } = dto; try { const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -447,7 +447,7 @@ export class RejsonRlBusinessService { } const affected = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolRejsonRlCommands.JsonDel, [keyName, path], ); diff --git a/redisinsight/api/src/modules/browser/services/set-business/set-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/set-business/set-business.service.spec.ts index 56251373d5..f4c8481185 100644 --- a/redisinsight/api/src/modules/browser/services/set-business/set-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/set-business/set-business.service.spec.ts @@ -10,9 +10,8 @@ import { mockRedisConsumer, mockRedisNoPermError, mockRedisWrongTypeError, - mockDatabase, + mockBrowserClientMetadata, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { ReplyError } from 'src/models'; import { BrowserToolKeysCommands, @@ -30,10 +29,6 @@ import { } from '../../dto'; import { BrowserToolService } from '../browser-tool/browser-tool.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('SetBusinessService', () => { let service: SetBusinessService; let browserTool; @@ -58,7 +53,7 @@ describe('SetBusinessService', () => { describe('createSet', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersToSetDto.keyName, ]) .mockResolvedValue(false); @@ -68,7 +63,7 @@ describe('SetBusinessService', () => { service.createSetWithExpiration = jest.fn().mockResolvedValue(undefined); await expect( - service.createSet(mockClientOptions, { + service.createSet(mockBrowserClientMetadata, { ...mockAddMembersToSetDto, expire: 1000, }), @@ -77,26 +72,26 @@ describe('SetBusinessService', () => { }); it('create set without expiration', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SAdd, [ mockAddMembersToSetDto.keyName, ...mockAddMembersToSetDto.members, ]) .mockResolvedValue(1); await expect( - service.createSet(mockClientOptions, mockAddMembersToSetDto), + service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto), ).resolves.not.toThrow(); expect(service.createSetWithExpiration).not.toHaveBeenCalled(); }); it('key with this name exist', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersToSetDto.keyName, ]) .mockResolvedValue(true); await expect( - service.createSet(mockClientOptions, mockAddMembersToSetDto), + service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(ConflictException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); expect(browserTool.execMulti).not.toHaveBeenCalled(); @@ -108,14 +103,14 @@ describe('SetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SAdd, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.createSet(mockClientOptions, mockAddMembersToSetDto), + service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for createSet", async () => { @@ -126,7 +121,7 @@ describe('SetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.createSet(mockClientOptions, mockAddMembersToSetDto), + service.createSet(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -138,7 +133,7 @@ describe('SetBusinessService', () => { }; it('succeed to create Set data type with expiration', async () => { when(browserTool.execMulti) - .calledWith(mockClientOptions, [ + .calledWith(mockBrowserClientMetadata, [ [BrowserToolSetCommands.SAdd, dto.keyName, ...dto.members], [BrowserToolKeysCommands.Expire, dto.keyName, dto.expire], ]) @@ -151,7 +146,7 @@ describe('SetBusinessService', () => { ]); const result = await service.createSetWithExpiration( - mockClientOptions, + mockBrowserClientMetadata, dto, ); expect(result).toBe(mockAddMembersToSetDto.members.length); @@ -164,7 +159,7 @@ describe('SetBusinessService', () => { browserTool.execMulti.mockResolvedValue([transactionError, null]); await expect( - service.createSetWithExpiration(mockClientOptions, dto), + service.createSetWithExpiration(mockBrowserClientMetadata, dto), ).rejects.toEqual(transactionError); }); }); @@ -172,7 +167,7 @@ describe('SetBusinessService', () => { describe('getMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SCard, [ mockGetSetMembersDto.keyName, ]) .mockResolvedValue(mockSetMembers.length); @@ -180,20 +175,20 @@ describe('SetBusinessService', () => { it('succeed to get members of the set', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SScan, expect.anything(), ) .mockResolvedValue([Buffer.from('0'), mockSetMembers]); const result = await service.getMembers( - mockClientOptions, + mockBrowserClientMetadata, mockGetSetMembersDto, ); expect(result).toEqual(mockGetSetMembersResponse); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SScan, expect.anything(), ); @@ -204,17 +199,17 @@ describe('SetBusinessService', () => { match: mockSetMembers[0].toString(), }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SIsMember, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SIsMember, [ dto.keyName, dto.match, ]) .mockResolvedValue(1); - const result = await service.getMembers(mockClientOptions, dto); + const result = await service.getMembers(mockBrowserClientMetadata, dto); expect(result).toEqual(mockGetSetMembersResponse); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SScan, expect.anything(), ); @@ -225,13 +220,13 @@ describe('SetBusinessService', () => { match: mockSetMembers[0].toString(), }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SIsMember, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SIsMember, [ dto.keyName, dto.match, ]) .mockResolvedValue(0); - const result = await service.getMembers(mockClientOptions, dto); + const result = await service.getMembers(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockGetSetMembersResponse, members: [] }); }); @@ -241,20 +236,20 @@ describe('SetBusinessService', () => { match: 'm\\[a-e\\]mber', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SIsMember, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SIsMember, [ dto.keyName, 'm[a-e]mber', ]) .mockResolvedValue(1); - const result = await service.getMembers(mockClientOptions, dto); + const result = await service.getMembers(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockGetSetMembersResponse, members: [Buffer.from('m[a-e]mber')], }); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SScan, expect.anything(), ); @@ -271,25 +266,25 @@ describe('SetBusinessService', () => { // ); // when(browserTool.execCommand) // .calledWith( - // mockClientOptions, + // mockBrowserClientMetadata, // BrowserToolSetCommands.SScan, // expect.anything(), // ) // .mockResolvedValue(['200', []]); // - // await service.getMembers(mockClientOptions, dto); + // await service.getMembers(mockBrowserClientMetadata, dto); // // expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1); // }); it('key with this name does not exist for getMembers', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SCard, [ mockGetSetMembersDto.keyName, ]) .mockResolvedValue(0); await expect( - service.getMembers(mockClientOptions, mockGetSetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto), ).rejects.toThrow(NotFoundException); }); it("try to use 'SCARD' command not for list data type", async () => { @@ -300,7 +295,7 @@ describe('SetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getMembers(mockClientOptions, mockGetSetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for getMembers", async () => { @@ -311,7 +306,7 @@ describe('SetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getMembers(mockClientOptions, mockGetSetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetSetMembersDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -319,7 +314,7 @@ describe('SetBusinessService', () => { describe('addMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersToSetDto.keyName, ]) .mockResolvedValue(true); @@ -327,31 +322,31 @@ describe('SetBusinessService', () => { it('succeed to add members to the Set data type', async () => { const { keyName, members } = mockAddMembersToSetDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SAdd, [ keyName, ...members, ]) .mockResolvedValue(1); await expect( - service.addMembers(mockClientOptions, mockAddMembersToSetDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto), ).resolves.not.toThrow(); }); it('key with this name does not exist for addMembers', async () => { const { keyName, members } = mockAddMembersToSetDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersToSetDto.keyName, ]) .mockResolvedValue(false); await expect( - service.addMembers(mockClientOptions, mockAddMembersToSetDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(NotFoundException); expect( browserTool.execCommand, ).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SAdd, [keyName, ...members], ); @@ -363,14 +358,14 @@ describe('SetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SAdd, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.addMembers(mockClientOptions, mockAddMembersToSetDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for addMembers", async () => { @@ -381,7 +376,7 @@ describe('SetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.addMembers(mockClientOptions, mockAddMembersToSetDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersToSetDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -389,7 +384,7 @@ describe('SetBusinessService', () => { describe('deleteMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockDeleteMembersDto.keyName, ]) .mockResolvedValue(true); @@ -397,14 +392,14 @@ describe('SetBusinessService', () => { it('succeeded to delete members from Set data type', async () => { const { members, keyName } = mockDeleteMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolSetCommands.SRem, [ + .calledWith(mockBrowserClientMetadata, BrowserToolSetCommands.SRem, [ keyName, ...members, ]) .mockResolvedValue(members.length); const result = await service.deleteMembers( - mockClientOptions, + mockBrowserClientMetadata, mockDeleteMembersDto, ); @@ -413,18 +408,18 @@ describe('SetBusinessService', () => { it('key with this name does not exist for deleteMembers', async () => { const { members, keyName } = mockDeleteMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ keyName, ]) .mockResolvedValue(false); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(NotFoundException); expect( browserTool.execCommand, ).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SRem, [keyName, ...members], ); @@ -436,14 +431,14 @@ describe('SetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolSetCommands.SRem, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for deleteMembers", async () => { @@ -454,7 +449,7 @@ describe('SetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(ForbiddenException); }); }); diff --git a/redisinsight/api/src/modules/browser/services/set-business/set-business.service.ts b/redisinsight/api/src/modules/browser/services/set-business/set-business.service.ts index da99055226..fb2da61809 100644 --- a/redisinsight/api/src/modules/browser/services/set-business/set-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/set-business/set-business.service.ts @@ -11,7 +11,7 @@ import ERROR_MESSAGES from 'src/constants/error-messages'; import config from 'src/utils/config'; import { catchAclError, catchTransactionError, unescapeGlob } from 'src/utils'; import { ReplyError } from 'src/models'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { BrowserToolKeysCommands, BrowserToolSetCommands, @@ -39,14 +39,14 @@ export class SetBusinessService { ) {} public async createSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateSetWithExpireDto, ): Promise { this.logger.log('Creating Set data type.'); const { keyName } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -59,9 +59,9 @@ export class SetBusinessService { ); } if (dto.expire) { - await this.createSetWithExpiration(clientOptions, dto); + await this.createSetWithExpiration(clientMetadata, dto); } else { - await this.createSimpleSet(clientOptions, dto); + await this.createSimpleSet(clientMetadata, dto); } this.logger.log('Succeed to create Set data type.'); } catch (error) { @@ -75,7 +75,7 @@ export class SetBusinessService { } public async getMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetSetMembersDto, ): Promise { this.logger.log('Getting members of the Set data type stored at key.'); @@ -89,7 +89,7 @@ export class SetBusinessService { try { result.total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SCard, [keyName], ); @@ -105,7 +105,7 @@ export class SetBusinessService { const member = unescapeGlob(dto.match); result.nextCursor = 0; const memberIsExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SIsMember, [keyName, member], ); @@ -113,7 +113,7 @@ export class SetBusinessService { result.members.push(member); } } else { - const scanResult = await this.scanSet(clientOptions, dto); + const scanResult = await this.scanSet(clientMetadata, dto); result = { ...result, ...scanResult }; } this.logger.log('Succeed to get members of the Set data type.'); @@ -128,14 +128,14 @@ export class SetBusinessService { } public async addMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AddMembersToSetDto, ): Promise { this.logger.log('Adding members to the Set data type.'); const { keyName, members } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -148,7 +148,7 @@ export class SetBusinessService { ); } await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SAdd, [keyName, ...members], ); @@ -164,7 +164,7 @@ export class SetBusinessService { } public async deleteMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteMembersFromSetDto, ): Promise { this.logger.log('Deleting members from the Set data type.'); @@ -172,7 +172,7 @@ export class SetBusinessService { let result; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -185,7 +185,7 @@ export class SetBusinessService { ); } result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SRem, [keyName, ...members], ); @@ -201,20 +201,20 @@ export class SetBusinessService { } public async createSimpleSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AddMembersToSetDto, ): Promise { const { keyName, members } = dto; return await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SAdd, [keyName, ...members], ); } public async createSetWithExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateSetWithExpireDto, ): Promise { const { keyName, members, expire } = dto; @@ -222,7 +222,7 @@ export class SetBusinessService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, [ + ] = await this.browserTool.execMulti(clientMetadata, [ [BrowserToolSetCommands.SAdd, keyName, ...members], [BrowserToolKeysCommands.Expire, keyName, expire], ]); @@ -235,7 +235,7 @@ export class SetBusinessService { } public async scanSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetSetMembersDto, ): Promise { const { keyName } = dto; @@ -249,7 +249,7 @@ export class SetBusinessService { while (result.nextCursor !== 0 && result.members.length < count) { const scanResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolSetCommands.SScan, [ keyName, diff --git a/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.spec.ts b/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.spec.ts index a39afa4ffb..0bdde77bf3 100644 --- a/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/stream/consumer-group.service.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { when } from 'jest-when'; -import { mockRedisConsumer, mockDatabase, MockType } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { mockRedisConsumer, MockType, mockBrowserClientMetadata } from 'src/__mocks__'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolKeysCommands, BrowserToolStreamCommands, @@ -15,14 +14,11 @@ import { ConsumerGroupService } from 'src/modules/browser/services/stream/consum import { mockAddStreamEntriesDto, mockConsumerGroup, - mockConsumerGroupsReply, mockCreateConsumerGroupDto, - mockKeyDto + mockConsumerGroupsReply, + mockCreateConsumerGroupDto, + mockKeyDto, } from 'src/modules/browser/__mocks__'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('ConsumerGroupService', () => { let service: ConsumerGroupService; let browserTool: MockType; @@ -42,30 +38,30 @@ describe('ConsumerGroupService', () => { browserTool = module.get(BrowserToolService); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); }); describe('getGroups', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoGroups, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoGroups, expect.anything()) .mockResolvedValue([mockConsumerGroupsReply, mockConsumerGroupsReply]); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockResolvedValue(['s', mockConsumerGroup.smallestPendingId, mockConsumerGroup.greatestPendingId]); }); it('should get consumer groups with info', async () => { - const groups = await service.getGroups(mockClientOptions, mockKeyDto); + const groups = await service.getGroups(mockBrowserClientMetadata, mockKeyDto); expect(groups).toEqual([mockConsumerGroup, mockConsumerGroup]); }); it('should throw error when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.getGroups(mockClientOptions, mockKeyDto); + await service.getGroups(mockBrowserClientMetadata, mockKeyDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -74,11 +70,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.getGroups(mockClientOptions, mockKeyDto); + await service.getGroups(mockBrowserClientMetadata, mockKeyDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -87,11 +83,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Wrong Type error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.getGroups(mockClientOptions, { + await service.getGroups(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -102,11 +98,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockRejectedValueOnce(new Error('oO')); try { - await service.getGroups(mockClientOptions, { + await service.getGroups(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -119,18 +115,18 @@ describe('ConsumerGroupService', () => { describe('createGroups', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); browserTool.execMulti.mockResolvedValue([null, [[null, '123-1']]]); }); it('add groups', async () => { await expect( - service.createGroups(mockClientOptions, { + service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [ BrowserToolStreamCommands.XGroupCreate, mockKeyDto.keyName, mockConsumerGroup.name, mockConsumerGroup.lastDeliveredId, @@ -143,11 +139,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.createGroups(mockClientOptions, { + await service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }); @@ -159,11 +155,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.createGroups(mockClientOptions, { + await service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }); @@ -177,7 +173,7 @@ describe('ConsumerGroupService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.WrongType), [[null, '123-1']]]); try { - await service.createGroups(mockClientOptions, { + await service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }); @@ -194,7 +190,7 @@ describe('ConsumerGroupService', () => { ]); try { - await service.createGroups(mockClientOptions, { + await service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }); @@ -211,7 +207,7 @@ describe('ConsumerGroupService', () => { ]); try { - await service.createGroups(mockClientOptions, { + await service.createGroups(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto, mockCreateConsumerGroupDto], }); @@ -225,29 +221,29 @@ describe('ConsumerGroupService', () => { describe('updateGroup', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XGroupSetId, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XGroupSetId, expect.anything()) .mockResolvedValue('OK'); }); it('update group', async () => { await expect( - service.updateGroup(mockClientOptions, { + service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }), ).resolves.not.toThrow(); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStreamCommands.XGroupSetId, [mockKeyDto.keyName, mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.lastDeliveredId], ); }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.updateGroup(mockClientOptions, { + await service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }); @@ -259,11 +255,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.updateGroup(mockClientOptions, { + await service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }); @@ -277,7 +273,7 @@ describe('ConsumerGroupService', () => { browserTool.execCommand.mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.updateGroup(mockClientOptions, { + await service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }); @@ -291,7 +287,7 @@ describe('ConsumerGroupService', () => { browserTool.execCommand.mockRejectedValueOnce(new Error('NOGROUP no such group')); try { - await service.updateGroup(mockClientOptions, { + await service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }); @@ -305,7 +301,7 @@ describe('ConsumerGroupService', () => { browserTool.execCommand.mockRejectedValueOnce(new Error('oO')); try { - await service.updateGroup(mockClientOptions, { + await service.updateGroup(mockBrowserClientMetadata, { ...mockKeyDto, ...mockCreateConsumerGroupDto, }); @@ -319,29 +315,29 @@ describe('ConsumerGroupService', () => { describe('deleteGroups', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); browserTool.execMulti.mockResolvedValue([null, [[null, '123-1']]]); }); it('add groups', async () => { await expect( - service.deleteGroup(mockClientOptions, { + service.deleteGroup(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.name], }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [BrowserToolStreamCommands.XGroupDestroy, mockKeyDto.keyName, mockConsumerGroup.name], [BrowserToolStreamCommands.XGroupDestroy, mockKeyDto.keyName, mockConsumerGroup.name], ]); }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.deleteGroup(mockClientOptions, { + await service.deleteGroup(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.name], }); @@ -353,11 +349,11 @@ describe('ConsumerGroupService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.deleteGroup(mockClientOptions, { + await service.deleteGroup(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.name], }); @@ -371,7 +367,7 @@ describe('ConsumerGroupService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.WrongType), [[null, '123-1']]]); try { - await service.deleteGroup(mockClientOptions, { + await service.deleteGroup(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.name], }); @@ -388,7 +384,7 @@ describe('ConsumerGroupService', () => { ]); try { - await service.deleteGroup(mockClientOptions, { + await service.deleteGroup(mockBrowserClientMetadata, { ...mockKeyDto, consumerGroups: [mockCreateConsumerGroupDto.name, mockCreateConsumerGroupDto.name], }); 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 80befb2d4e..f12649cfa4 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 @@ -1,7 +1,6 @@ import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { RedisErrorCodes } from 'src/constants'; import { catchAclError, catchTransactionError } from 'src/utils'; import { @@ -19,6 +18,7 @@ import { } from 'src/modules/browser/dto/stream.dto'; import { plainToClass } from 'class-transformer'; import { RedisString } from 'src/common/constants'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class ConsumerGroupService { @@ -30,18 +30,18 @@ export class ConsumerGroupService { * Get consumer groups list for particular stream * In addition fetch pending messages info for each group * !May be slow on huge streams as 'XPENDING' command tagged with as @slow - * @param clientOptions + * @param clientMetadata * @param dto */ async getGroups( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: KeyDto, ): Promise { try { this.logger.log('Getting consumer groups list.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -51,13 +51,13 @@ export class ConsumerGroupService { } const groups = ConsumerGroupService.formatReplyToDto(await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XInfoGroups, [dto.keyName], )); return await Promise.all(groups.map((group) => this.getGroupInfo( - clientOptions, + clientMetadata, dto, group, ))); @@ -76,17 +76,17 @@ export class ConsumerGroupService { /** * Get consumer group pending info using 'XPENDING' command - * @param clientOptions + * @param clientMetadata * @param dto * @param group */ async getGroupInfo( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: KeyDto, group: ConsumerGroupDto, ): Promise { const info = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XPending, [dto.keyName, group.name], ); @@ -100,11 +100,11 @@ export class ConsumerGroupService { /** * Create consumer group(s) - * @param clientOptions + * @param clientMetadata * @param dto */ async createGroups( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateConsumerGroupsDto, ): Promise { try { @@ -112,7 +112,7 @@ export class ConsumerGroupService { const { keyName, consumerGroups } = dto; const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -136,7 +136,7 @@ export class ConsumerGroupService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, toolCommands); + ] = await this.browserTool.execMulti(clientMetadata, toolCommands); catchTransactionError(transactionError, transactionResults); this.logger.log('Stream consumer group(s) created.'); @@ -161,18 +161,18 @@ export class ConsumerGroupService { /** * Updates last delivered id for Consumer Group - * @param clientOptions + * @param clientMetadata * @param dto */ async updateGroup( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: UpdateConsumerGroupDto, ): Promise { try { this.logger.log('Updating consumer group.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -182,7 +182,7 @@ export class ConsumerGroupService { } await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XGroupSetId, [dto.keyName, dto.name, dto.lastDeliveredId], ); @@ -209,18 +209,18 @@ export class ConsumerGroupService { /** * Delete consumer groups in batch - * @param clientOptions + * @param clientMetadata * @param dto */ async deleteGroup( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteConsumerGroupsDto, ): Promise { try { this.logger.log('Deleting consumer group.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -243,7 +243,7 @@ export class ConsumerGroupService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, toolCommands); + ] = await this.browserTool.execMulti(clientMetadata, toolCommands); catchTransactionError(transactionError, transactionResults); this.logger.log('Consumer group(s) successfully deleted.'); diff --git a/redisinsight/api/src/modules/browser/services/stream/consumer.service.spec.ts b/redisinsight/api/src/modules/browser/services/stream/consumer.service.spec.ts index 985fa93d94..8dc5b9ac2a 100644 --- a/redisinsight/api/src/modules/browser/services/stream/consumer.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/stream/consumer.service.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { when } from 'jest-when'; -import { mockRedisConsumer, mockDatabase, MockType } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { mockRedisConsumer, MockType, mockBrowserClientMetadata } from 'src/__mocks__'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolKeysCommands, BrowserToolStreamCommands, @@ -26,10 +25,6 @@ import { mockPendingMessageReply, } from 'src/modules/browser/__mocks__'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('ConsumerService', () => { let service: ConsumerService; let browserTool: MockType; @@ -51,18 +46,18 @@ describe('ConsumerService', () => { browserTool = module.get(BrowserToolService); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); }); describe('getGroups', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) .mockResolvedValue([mockConsumerReply, mockConsumerReply]); }); it('should get consumers list', async () => { - const consumers = await service.getConsumers(mockClientOptions, { + const consumers = await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -70,11 +65,11 @@ describe('ConsumerService', () => { }); it('should throw error when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.getConsumers(mockClientOptions, { + await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -86,11 +81,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.getConsumers(mockClientOptions, { + await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -102,11 +97,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found error when no group', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) .mockRejectedValueOnce(new Error('NOGROUP no such group')); try { - await service.getConsumers(mockClientOptions, { + await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -118,11 +113,11 @@ describe('ConsumerService', () => { }); it('should throw Wrong Type error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.getConsumers(mockClientOptions, { + await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -134,11 +129,11 @@ describe('ConsumerService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoConsumers, expect.anything()) .mockRejectedValueOnce(new Error('oO')); try { - await service.getConsumers(mockClientOptions, { + await service.getConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, }); @@ -152,19 +147,19 @@ describe('ConsumerService', () => { describe('deleteConsumers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); browserTool.execMulti.mockResolvedValue([null, [[null, '123-1']]]); }); it('delete consumers', async () => { await expect( - service.deleteConsumers(mockClientOptions, { + service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [ BrowserToolStreamCommands.XGroupDelConsumer, mockKeyDto.keyName, mockConsumerGroup.name, mockConsumer.name, @@ -177,11 +172,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.deleteConsumers(mockClientOptions, { + await service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], @@ -194,11 +189,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.deleteConsumers(mockClientOptions, { + await service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], @@ -213,7 +208,7 @@ describe('ConsumerService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.NoGroup), [[null, '123-1']]]); try { - await service.deleteConsumers(mockClientOptions, { + await service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], @@ -228,7 +223,7 @@ describe('ConsumerService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.WrongType), [[null, '123-1']]]); try { - await service.deleteConsumers(mockClientOptions, { + await service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], @@ -246,7 +241,7 @@ describe('ConsumerService', () => { ]); try { - await service.deleteConsumers(mockClientOptions, { + await service.deleteConsumers(mockBrowserClientMetadata, { ...mockKeyDto, groupName: mockConsumerGroup.name, consumerNames: [mockConsumer.name, mockConsumer.name], @@ -261,25 +256,25 @@ describe('ConsumerService', () => { describe('getPendingEntries', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockResolvedValue([mockPendingMessageReply, mockPendingMessageReply]); }); it('should get consumers list', async () => { - const consumers = await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + const consumers = await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); expect(consumers).toEqual([mockPendingMessage, mockPendingMessage]); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, Object.values(mockGetPendingMessagesDto), ); }); it('should throw error when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -288,11 +283,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -301,11 +296,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found error when no group', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockRejectedValueOnce(new Error('NOGROUP no such group')); try { - await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -314,11 +309,11 @@ describe('ConsumerService', () => { }); it('should throw Wrong Type error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -327,11 +322,11 @@ describe('ConsumerService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XPending, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XPending, expect.anything()) .mockRejectedValueOnce(new Error('oO')); try { - await service.getPendingEntries(mockClientOptions, mockGetPendingMessagesDto); + await service.getPendingEntries(mockBrowserClientMetadata, mockGetPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -342,15 +337,15 @@ describe('ConsumerService', () => { describe('ackPendingEntries', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); browserTool.execCommand.mockResolvedValue(2); }); it('ack pending entries', async () => { - const result = await service.ackPendingEntries(mockClientOptions, mockAckPendingMessagesDto); + const result = await service.ackPendingEntries(mockBrowserClientMetadata, mockAckPendingMessagesDto); expect(result).toEqual({ affected: 2 }); - expect(browserTool.execCommand).toHaveBeenCalledWith(mockClientOptions, + expect(browserTool.execCommand).toHaveBeenCalledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XAck, [ mockAckPendingMessagesDto.keyName, @@ -360,11 +355,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.ackPendingEntries(mockClientOptions, mockAckPendingMessagesDto); + await service.ackPendingEntries(mockBrowserClientMetadata, mockAckPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -373,11 +368,11 @@ describe('ConsumerService', () => { }); it('should proxy Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XAck, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XAck, expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.ackPendingEntries(mockClientOptions, mockAckPendingMessagesDto); + await service.ackPendingEntries(mockBrowserClientMetadata, mockAckPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -386,11 +381,11 @@ describe('ConsumerService', () => { }); it('should throw Bad Request when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XAck, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XAck, expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.ackPendingEntries(mockClientOptions, mockAckPendingMessagesDto); + await service.ackPendingEntries(mockBrowserClientMetadata, mockAckPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -399,11 +394,11 @@ describe('ConsumerService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XAck, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XAck, expect.anything()) .mockRejectedValueOnce(new Error('oO')); try { - await service.ackPendingEntries(mockClientOptions, mockAckPendingMessagesDto); + await service.ackPendingEntries(mockBrowserClientMetadata, mockAckPendingMessagesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -414,15 +409,15 @@ describe('ConsumerService', () => { describe('claimPendingEntries', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) .mockResolvedValue(mockClaimPendingEntriesReply); }); it('claim pending entries', async () => { - const result = await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + const result = await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); expect(result).toEqual({ affected: mockClaimPendingEntriesReply }); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, [ mockClaimPendingEntriesDto.keyName, @@ -436,14 +431,14 @@ describe('ConsumerService', () => { ); }); it('claim pending entries with additional args', async () => { - const result = await service.claimPendingEntries(mockClientOptions, { + const result = await service.claimPendingEntries(mockBrowserClientMetadata, { ...mockClaimPendingEntriesDto, ...mockAdditionalClaimPendingEntriesDto, }); expect(result).toEqual({ affected: mockClaimPendingEntriesReply }); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, [ mockClaimPendingEntriesDto.keyName, @@ -460,7 +455,7 @@ describe('ConsumerService', () => { ); }); it('claim pending entries with additional args and "idle" instead of "time"', async () => { - const result = await service.claimPendingEntries(mockClientOptions, { + const result = await service.claimPendingEntries(mockBrowserClientMetadata, { ...mockClaimPendingEntriesDto, ...mockAdditionalClaimPendingEntriesDto, idle: 0, @@ -468,7 +463,7 @@ describe('ConsumerService', () => { expect(result).toEqual({ affected: mockClaimPendingEntriesReply }); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, [ mockClaimPendingEntriesDto.keyName, @@ -486,11 +481,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValueOnce(false); try { - await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -499,11 +494,11 @@ describe('ConsumerService', () => { }); it('should proxy Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -512,11 +507,11 @@ describe('ConsumerService', () => { }); it('should throw Bad Request when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -525,11 +520,11 @@ describe('ConsumerService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) .mockRejectedValueOnce(new Error(RedisErrorCodes.NoGroup)); try { - await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -538,11 +533,11 @@ describe('ConsumerService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XClaim, expect.anything(), expect.anything()) .mockRejectedValueOnce(new Error('oO')); try { - await service.claimPendingEntries(mockClientOptions, mockClaimPendingEntriesDto); + await service.claimPendingEntries(mockBrowserClientMetadata, mockClaimPendingEntriesDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); 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 2d4c3d238a..91daaa6d81 100644 --- a/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts +++ b/redisinsight/api/src/modules/browser/services/stream/consumer.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Injectable, Logger, NotFoundException, } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { RedisErrorCodes } from 'src/constants'; import { catchAclError, catchTransactionError } from 'src/utils'; import { @@ -16,6 +15,7 @@ import { GetConsumersDto, GetPendingEntriesDto, PendingEntryDto, } from 'src/modules/browser/dto/stream.dto'; import { plainToClass } from 'class-transformer'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class ConsumerService { @@ -25,18 +25,18 @@ export class ConsumerService { /** * Get consumers list inside particular group - * @param clientOptions + * @param clientMetadata * @param dto */ async getConsumers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetConsumersDto, ): Promise { try { this.logger.log('Getting consumers list.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -46,7 +46,7 @@ export class ConsumerService { } return ConsumerService.formatReplyToDto(await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XInfoConsumers, [dto.keyName, dto.groupName], )); @@ -69,18 +69,18 @@ export class ConsumerService { /** * Get consumers list inside particular group - * @param clientOptions + * @param clientMetadata * @param dto */ async deleteConsumers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteConsumersDto, ): Promise { try { this.logger.log('Deleting consumers from the group.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -104,7 +104,7 @@ export class ConsumerService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, toolCommands); + ] = await this.browserTool.execMulti(clientMetadata, toolCommands); catchTransactionError(transactionError, transactionResults); return undefined; @@ -127,18 +127,18 @@ export class ConsumerService { /** * Get list of pending entries info for particular consumer - * @param clientOptions + * @param clientMetadata * @param dto */ async getPendingEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetPendingEntriesDto, ): Promise { try { this.logger.log('Getting pending entries list.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -148,7 +148,7 @@ export class ConsumerService { } return ConsumerService.formatReplyToPendingEntriesDto(await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XPending, [dto.keyName, dto.groupName, dto.start, dto.end, dto.count, dto.consumerName], )); @@ -171,18 +171,18 @@ export class ConsumerService { /** * Acknowledge pending entries - * @param clientOptions + * @param clientMetadata * @param dto */ async ackPendingEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AckPendingEntriesDto, ): Promise { try { this.logger.log('Acknowledging pending entries.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -192,7 +192,7 @@ export class ConsumerService { } const affected = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XAck, [dto.keyName, dto.groupName, ...dto.entries], ); @@ -217,18 +217,18 @@ export class ConsumerService { /** * Claim pending entries with additional parameters - * @param clientOptions + * @param clientMetadata * @param dto */ async claimPendingEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: ClaimPendingEntryDto, ): Promise { try { this.logger.log('Claiming pending entries.'); const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [dto.keyName], ); @@ -257,7 +257,7 @@ export class ConsumerService { args.push('justid'); const affected = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XClaim, args, 'utf8', diff --git a/redisinsight/api/src/modules/browser/services/stream/stream.service.spec.ts b/redisinsight/api/src/modules/browser/services/stream/stream.service.spec.ts index dff24f5233..774548ed75 100644 --- a/redisinsight/api/src/modules/browser/services/stream/stream.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/stream/stream.service.spec.ts @@ -1,7 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { when } from 'jest-when'; -import { mockRedisConsumer, mockDatabase, MockType } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { mockRedisConsumer, MockType, mockBrowserClientMetadata } from 'src/__mocks__'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolKeysCommands, @@ -20,10 +19,6 @@ import { mockStreamEntry, mockStreamInfo, mockStreamInfoReply, } from 'src/modules/browser/__mocks__'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('StreamService', () => { let service: StreamService; let browserTool: MockType; @@ -46,18 +41,18 @@ describe('StreamService', () => { describe('createStream', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValue(false); browserTool.execMulti.mockResolvedValue([null, [[null, '123-1']]]); }); it('create stream with expiration', async () => { await expect( - service.createStream(mockClientOptions, { + service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, expire: 1000, }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [BrowserToolStreamCommands.XAdd, mockAddStreamEntriesDto.keyName, mockStreamEntry.id, mockStreamEntry.fields[0].name, mockStreamEntry.fields[0].value], [BrowserToolKeysCommands.Expire, mockAddStreamEntriesDto.keyName, 1000], @@ -65,22 +60,22 @@ describe('StreamService', () => { }); it('create stream without expiration', async () => { await expect( - service.createStream(mockClientOptions, { + service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [BrowserToolStreamCommands.XAdd, mockAddStreamEntriesDto.keyName, mockStreamEntry.id, mockStreamEntry.fields[0].name, mockStreamEntry.fields[0].value], ]); }); it('should throw error key exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValueOnce(true); try { - await service.createStream(mockClientOptions, { + await service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -91,11 +86,11 @@ describe('StreamService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.createStream(mockClientOptions, { + await service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -108,7 +103,7 @@ describe('StreamService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.WrongType), [[null, '123-1']]]); try { - await service.createStream(mockClientOptions, { + await service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -124,7 +119,7 @@ describe('StreamService', () => { ]); try { - await service.createStream(mockClientOptions, { + await service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -140,7 +135,7 @@ describe('StreamService', () => { ]); try { - await service.createStream(mockClientOptions, { + await service.createStream(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -153,28 +148,28 @@ describe('StreamService', () => { describe('addEntries', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValue(true); browserTool.execMulti.mockResolvedValue([null, [[null, '123-1']]]); }); it('add entries', async () => { await expect( - service.addEntries(mockClientOptions, { + service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }), ).resolves.not.toThrow(); - expect(browserTool.execMulti).toHaveBeenCalledWith(mockClientOptions, [ + expect(browserTool.execMulti).toHaveBeenCalledWith(mockBrowserClientMetadata, [ [BrowserToolStreamCommands.XAdd, mockAddStreamEntriesDto.keyName, mockStreamEntry.id, mockStreamEntry.fields[0].name, mockStreamEntry.fields[0].value], ]); }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValueOnce(false); try { - await service.addEntries(mockClientOptions, { + await service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -185,11 +180,11 @@ describe('StreamService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.addEntries(mockClientOptions, { + await service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -202,7 +197,7 @@ describe('StreamService', () => { browserTool.execMulti.mockResolvedValue([new Error(RedisErrorCodes.WrongType), [[null, '123-1']]]); try { - await service.addEntries(mockClientOptions, { + await service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -218,7 +213,7 @@ describe('StreamService', () => { ]); try { - await service.addEntries(mockClientOptions, { + await service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -234,7 +229,7 @@ describe('StreamService', () => { ]); try { - await service.addEntries(mockClientOptions, { + await service.addEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -247,17 +242,17 @@ describe('StreamService', () => { describe('get entries from empty stream', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValue(true); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoStream, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, expect.anything()) .mockResolvedValue(mockEmptyStreamInfoReply); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XRevRange, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRevRange, expect.anything()) .mockResolvedValue(mockEmptyStreamEntriesReply); }); it('Should return stream with 0 entries', async () => { - const result = await service.getEntries(mockClientOptions, { + const result = await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, }); expect(result).toEqual({ @@ -269,20 +264,20 @@ describe('StreamService', () => { describe('getEntries', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, expect.anything()) .mockResolvedValue(true); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoStream, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, expect.anything()) .mockResolvedValue(mockStreamInfoReply); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XRevRange, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRevRange, expect.anything()) .mockResolvedValue(mockStreamEntriesReply); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XRange, expect.anything()) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XRange, expect.anything()) .mockResolvedValue(mockStreamEntriesReply); }); it('get entries DESC', async () => { - const result = await service.getEntries(mockClientOptions, { + const result = await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, }); expect(result).toEqual({ @@ -291,7 +286,7 @@ describe('StreamService', () => { }); }); it('get entries ASC', async () => { - const result = await service.getEntries(mockClientOptions, { + const result = await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, sortOrder: SortOrder.Asc, }); @@ -302,11 +297,11 @@ describe('StreamService', () => { }); it('should throw Not Found when key does not exists', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockResolvedValueOnce(false); try { - await service.getEntries(mockClientOptions, { + await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, }); fail(); @@ -317,11 +312,11 @@ describe('StreamService', () => { }); it('should throw Not Found error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [mockAddStreamEntriesDto.keyName]) .mockRejectedValueOnce(new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID)); try { - await service.getEntries(mockClientOptions, { + await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, }); fail(); @@ -332,11 +327,11 @@ describe('StreamService', () => { }); it('should throw Wrong Type error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName]) .mockRejectedValueOnce(new Error(RedisErrorCodes.WrongType)); try { - await service.getEntries(mockClientOptions, { + await service.getEntries(mockBrowserClientMetadata, { ...mockAddStreamEntriesDto, }); fail(); @@ -347,11 +342,11 @@ describe('StreamService', () => { }); it('should throw Internal Server error', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName]) + .calledWith(mockBrowserClientMetadata, BrowserToolStreamCommands.XInfoStream, [mockAddStreamEntriesDto.keyName]) .mockRejectedValueOnce(new Error('oO')); try { - await service.getEntries(mockClientOptions, { + await service.getEntries(mockBrowserClientMetadata, { ...mockGetStreamEntriesDto, }); fail(); diff --git a/redisinsight/api/src/modules/browser/services/stream/stream.service.ts b/redisinsight/api/src/modules/browser/services/stream/stream.service.ts index 2251de263c..8a53171d15 100644 --- a/redisinsight/api/src/modules/browser/services/stream/stream.service.ts +++ b/redisinsight/api/src/modules/browser/services/stream/stream.service.ts @@ -6,7 +6,6 @@ import { } from '@nestjs/common'; import { catchAclError, catchTransactionError } from 'src/utils'; import { SortOrder } from 'src/constants/sort'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolCommands, @@ -25,6 +24,7 @@ import { import ERROR_MESSAGES from 'src/constants/error-messages'; import { RedisErrorCodes } from 'src/constants'; import { plainToClass } from 'class-transformer'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class StreamService { @@ -37,11 +37,11 @@ export class StreamService { * Could be used for lazy loading with "start", "end" and "count" parameters * Could be sorted using "sortOrder" in ASC and DESC order * - * @param clientOptions + * @param clientMetadata * @param dto */ public async getEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetStreamEntriesDto, ): Promise { try { @@ -50,7 +50,7 @@ export class StreamService { const { keyName, sortOrder } = dto; const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -60,16 +60,16 @@ export class StreamService { } const info = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XInfoStream, [keyName], ); let entries = []; if (sortOrder && sortOrder === SortOrder.Asc) { - entries = await this.getRange(clientOptions, dto); + entries = await this.getRange(clientMetadata, dto); } else { - entries = await this.getRevRange(clientOptions, dto); + entries = await this.getRevRange(clientMetadata, dto); } this.logger.log('Succeed to get entries from the stream.'); @@ -100,11 +100,11 @@ export class StreamService { /** * Return specified number of entries in the time range in ASC order * - * @param clientOptions + * @param clientMetadata * @param dto */ public async getRange( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetStreamEntriesDto, ): Promise { const { @@ -112,7 +112,7 @@ export class StreamService { } = dto; const execResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XRange, [keyName, start, end, 'COUNT', count], ); @@ -123,11 +123,11 @@ export class StreamService { /** * Return specified number of entries in the time range in DESC order * - * @param clientOptions + * @param clientMetadata * @param dto */ public async getRevRange( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: GetStreamEntriesDto, ): Promise { const { @@ -135,7 +135,7 @@ export class StreamService { } = dto; const execResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XRevRange, [keyName, end, start, 'COUNT', count], ); @@ -145,11 +145,11 @@ export class StreamService { /** * Create streams with\without expiration time and add multiple entries in a transaction - * @param clientOptions + * @param clientMetadata * @param dto */ public async createStream( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateStreamDto, ): Promise { this.logger.log('Creating stream data type.'); @@ -158,7 +158,7 @@ export class StreamService { const { keyName, entries } = dto; const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -194,7 +194,7 @@ export class StreamService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, toolCommands); + ] = await this.browserTool.execMulti(clientMetadata, toolCommands); catchTransactionError(transactionError, transactionResults); this.logger.log('Succeed to create stream.'); @@ -220,11 +220,11 @@ export class StreamService { /** * Add entries to the existing stream and return entries IDs list - * @param clientOptions + * @param clientMetadata * @param dto */ public async addEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AddStreamEntriesDto, ): Promise { this.logger.log('Adding entries to stream.'); @@ -233,7 +233,7 @@ export class StreamService { const { keyName, entries } = dto; const exists = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -261,7 +261,7 @@ export class StreamService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, toolCommands); + ] = await this.browserTool.execMulti(clientMetadata, toolCommands); catchTransactionError(transactionError, transactionResults); this.logger.log('Succeed to add entries to the stream.'); @@ -290,11 +290,11 @@ export class StreamService { /** * Delete entries from the existing stream and return number of deleted entries - * @param clientOptions + * @param clientMetadata * @param dto */ public async deleteEntries( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteStreamEntriesDto, ): Promise { this.logger.log('Deleting entries from the Stream data type.'); @@ -302,7 +302,7 @@ export class StreamService { let result; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -315,7 +315,7 @@ export class StreamService { ); } result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStreamCommands.XDel, [keyName, ...entries], ); diff --git a/redisinsight/api/src/modules/browser/services/string-business/string-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/string-business/string-business.service.spec.ts index f65e20d1c6..85efb1c785 100644 --- a/redisinsight/api/src/modules/browser/services/string-business/string-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/string-business/string-business.service.spec.ts @@ -8,12 +8,11 @@ import { import { when } from 'jest-when'; import { ReplyError } from 'src/models/redis-client'; import { + mockBrowserClientMetadata, mockRedisConsumer, mockRedisNoPermError, mockRedisWrongTypeError, - mockDatabase, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { SetStringDto, SetStringWithExpireDto, @@ -31,10 +30,6 @@ const mockSetStringDto: SetStringDto = { value: Buffer.from('Lorem ipsum dolor sit amet.'), }; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('StringBusinessService', () => { let service: StringBusinessService; let browserTool; @@ -60,10 +55,10 @@ describe('StringBusinessService', () => { const dto: SetStringWithExpireDto = { ...mockSetStringDto, expire: 1000 }; await expect( - service.setString(mockClientOptions, dto), + service.setString(mockBrowserClientMetadata, dto), ).resolves.not.toThrow(); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStringCommands.Set, [dto.keyName, dto.value, 'EX', `${dto.expire}`, 'NX'], ); @@ -73,10 +68,10 @@ describe('StringBusinessService', () => { const dto: SetStringDto = { ...mockSetStringDto }; await expect( - service.setString(mockClientOptions, dto), + service.setString(mockBrowserClientMetadata, dto), ).resolves.not.toThrow(); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStringCommands.Set, [dto.keyName, dto.value, 'NX'], ); @@ -85,7 +80,7 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockResolvedValue(null); await expect( - service.setString(mockClientOptions, mockSetStringDto), + service.setString(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(ConflictException); }); it("user don't have required permissions for setString", async () => { @@ -96,14 +91,14 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.setString(mockClientOptions, mockSetStringDto), + service.setString(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(ForbiddenException); }); it('Should proxy EncryptionService errors', async () => { browserTool.execCommand.mockRejectedValueOnce(new KeytarUnavailableException()); await expect( - service.setString(mockClientOptions, mockSetStringDto), + service.setString(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(KeytarUnavailableException); }); }); @@ -113,12 +108,12 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockResolvedValue(mockSetStringDto.value); const result = await service.getStringValue( - mockClientOptions, - mockSetStringDto.keyName, + mockBrowserClientMetadata, + mockSetStringDto, ); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStringCommands.Get, [mockSetStringDto.keyName], ); @@ -135,14 +130,14 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getStringValue(mockClientOptions, mockSetStringDto.keyName), + service.getStringValue(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(BadRequestException); }); it('key not found', async () => { browserTool.execCommand.mockResolvedValue(null); await expect( - service.getStringValue(mockClientOptions, mockSetStringDto.keyName), + service.getStringValue(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for getStringValue", async () => { @@ -153,7 +148,7 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getStringValue(mockClientOptions, mockSetStringDto.keyName), + service.getStringValue(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -162,13 +157,13 @@ describe('StringBusinessService', () => { it('succeed to update string without expiration', async () => { const dto: SetStringDto = mockSetStringDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [ dto.keyName, ]) .mockResolvedValue(-1); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStringCommands.Set, [ + .calledWith(mockBrowserClientMetadata, BrowserToolStringCommands.Set, [ dto.keyName, dto.value, 'XX', @@ -176,12 +171,12 @@ describe('StringBusinessService', () => { .mockResolvedValue('OK'); await expect( - service.updateStringValue(mockClientOptions, dto), + service.updateStringValue(mockBrowserClientMetadata, dto), ).resolves.not.toThrow(); expect( browserTool.execCommand, ).toHaveBeenLastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStringCommands.Set, [dto.keyName, dto.value, 'XX'], ); @@ -190,12 +185,12 @@ describe('StringBusinessService', () => { const dto: SetStringDto = mockSetStringDto; const currentTtl = 1000; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Ttl, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Ttl, [ dto.keyName, ]) .mockResolvedValue(currentTtl); when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolStringCommands.Set, [ + .calledWith(mockBrowserClientMetadata, BrowserToolStringCommands.Set, [ dto.keyName, dto.value, 'XX', @@ -203,17 +198,17 @@ describe('StringBusinessService', () => { .mockResolvedValue('OK'); await expect( - service.updateStringValue(mockClientOptions, dto), + service.updateStringValue(mockBrowserClientMetadata, dto), ).resolves.not.toThrow(); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolStringCommands.Set, [dto.keyName, dto.value, 'XX'], ); expect( browserTool.execCommand, ).toHaveBeenLastCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolKeysCommands.Expire, [dto.keyName, currentTtl], ); @@ -222,7 +217,7 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockResolvedValue(null); await expect( - service.updateStringValue(mockClientOptions, mockSetStringDto), + service.updateStringValue(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(NotFoundException); }); it("user don't have required permissions for updateStringValue", async () => { @@ -233,7 +228,7 @@ describe('StringBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.updateStringValue(mockClientOptions, mockSetStringDto), + service.updateStringValue(mockBrowserClientMetadata, mockSetStringDto), ).rejects.toThrow(ForbiddenException); }); }); diff --git a/redisinsight/api/src/modules/browser/services/string-business/string-business.service.ts b/redisinsight/api/src/modules/browser/services/string-business/string-business.service.ts index d2459a35a3..de535fe1d0 100644 --- a/redisinsight/api/src/modules/browser/services/string-business/string-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/string-business/string-business.service.ts @@ -8,7 +8,6 @@ import { import { RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { catchAclError } from 'src/utils'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { GetStringValueResponse, SetStringDto, @@ -20,7 +19,8 @@ import { BrowserToolStringCommands, } from 'src/modules/browser/constants/browser-tool-commands'; import { plainToClass } from 'class-transformer'; -import { RedisString } from 'src/common/constants'; +import { GetKeyInfoDto } from 'src/modules/browser/dto'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class StringBusinessService { @@ -31,7 +31,7 @@ export class StringBusinessService { ) {} public async setString( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SetStringWithExpireDto, ): Promise { this.logger.log('Setting string key type.'); @@ -40,13 +40,13 @@ export class StringBusinessService { try { if (expire) { result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStringCommands.Set, [keyName, value, 'EX', `${expire}`, 'NX'], ); } else { result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStringCommands.Set, [keyName, value, 'NX'], ); @@ -65,14 +65,17 @@ export class StringBusinessService { } public async getStringValue( - clientOptions: IFindRedisClientInstanceByOptions, - keyName: RedisString, + clientMetadata: ClientMetadata, + dto: GetKeyInfoDto, ): Promise { this.logger.log('Getting string value.'); + + const { keyName } = dto; let result: GetStringValueResponse; + try { const value = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStringCommands.Get, [keyName], ); @@ -96,7 +99,7 @@ export class StringBusinessService { } public async updateStringValue( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SetStringDto, ): Promise { this.logger.log('Updating string value.'); @@ -104,18 +107,18 @@ export class StringBusinessService { let result; try { const ttl = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Ttl, [keyName], ); result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolStringCommands.Set, [keyName, value, 'XX'], ); if (result && ttl > 0) { await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Expire, [keyName, ttl], ); diff --git a/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.spec.ts index 4fb989b47e..05b73ecc96 100644 --- a/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.spec.ts @@ -9,7 +9,7 @@ import { when } from 'jest-when'; import { SortOrder } from 'src/constants/sort'; import { ReplyError } from 'src/models'; import { - mockDatabase, + mockBrowserClientMetadata, mockRedisConsumer, mockRedisNoPermError, mockRedisWrongTypeError, @@ -22,7 +22,6 @@ import { BrowserToolKeysCommands, BrowserToolZSetCommands, } from 'src/modules/browser/constants/browser-tool-commands'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { getZSetMembersInAscResponse, getZSetMembersInDescResponse, mockAddMembersDto, mockDeleteMembersDto, @@ -32,10 +31,6 @@ import { import { ZSetBusinessService } from './z-set-business.service'; import { BrowserToolService } from '../browser-tool/browser-tool.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - describe('ZSetBusinessService', () => { let service: ZSetBusinessService; let browserTool; @@ -58,7 +53,7 @@ describe('ZSetBusinessService', () => { describe('createZSet', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersDto.keyName, ]) .mockResolvedValue(0); @@ -70,7 +65,7 @@ describe('ZSetBusinessService', () => { .mockResolvedValue(mockAddMembersDto.members.length); await expect( - service.createZSet(mockClientOptions, { + service.createZSet(mockBrowserClientMetadata, { ...mockAddMembersDto, expire: 1000, }), @@ -80,26 +75,26 @@ describe('ZSetBusinessService', () => { it('create zset without expiration', async () => { const { keyName } = mockAddMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, [ keyName, ...mockMembersForZAddCommand, ]) .mockResolvedValue(mockAddMembersDto.members.length); await expect( - service.createZSet(mockClientOptions, mockAddMembersDto), + service.createZSet(mockBrowserClientMetadata, mockAddMembersDto), ).resolves.not.toThrow(); expect(service.createZSetWithExpiration).not.toHaveBeenCalled(); }); it('key with this name exist', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersDto.keyName, ]) .mockResolvedValue(1); await expect( - service.createZSet(mockClientOptions, mockAddMembersDto), + service.createZSet(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(ConflictException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); expect(browserTool.execMulti).not.toHaveBeenCalled(); @@ -111,14 +106,14 @@ describe('ZSetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.createZSet(mockClientOptions, mockAddMembersDto), + service.createZSet(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for createZSet", async () => { @@ -129,7 +124,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.createZSet(mockClientOptions, mockAddMembersDto), + service.createZSet(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -141,7 +136,7 @@ describe('ZSetBusinessService', () => { }; it('succeed to create ZSet data type with expiration', async () => { when(browserTool.execMulti) - .calledWith(mockClientOptions, expect.anything()) + .calledWith(mockBrowserClientMetadata, expect.anything()) .mockResolvedValue([ null, [ @@ -151,7 +146,7 @@ describe('ZSetBusinessService', () => { ]); const result = await service.createZSetWithExpiration( - mockClientOptions, + mockBrowserClientMetadata, dto, ); expect(result).toBe(mockAddMembersDto.members.length); @@ -164,7 +159,7 @@ describe('ZSetBusinessService', () => { browserTool.execMulti.mockResolvedValue([transactionError, null]); await expect( - service.createZSetWithExpiration(mockClientOptions, dto), + service.createZSetWithExpiration(mockBrowserClientMetadata, dto), ).rejects.toEqual(transactionError); }); }); @@ -172,7 +167,7 @@ describe('ZSetBusinessService', () => { describe('getMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZCard, [ mockGetMembersDto.keyName, ]) .mockResolvedValue(mockAddMembersDto.members.length); @@ -180,14 +175,14 @@ describe('ZSetBusinessService', () => { it('get members sorted in asc', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZRange, expect.anything(), ) .mockResolvedValue(['member1', '-inf', 'member2', '0', 'member3', '2', 'member4', 'inf']); const result = await service.getMembers( - mockClientOptions, + mockBrowserClientMetadata, mockGetMembersDto, ); await expect(result).toEqual(getZSetMembersInAscResponse); @@ -195,13 +190,13 @@ describe('ZSetBusinessService', () => { it('get members sorted in desc', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZRevRange, expect.anything(), ) .mockResolvedValue(['member4', 'inf', 'member3', '2', 'member2', '0', 'member1', '-inf']); - const result = await service.getMembers(mockClientOptions, { + const result = await service.getMembers(mockBrowserClientMetadata, { ...mockGetMembersDto, sortOrder: SortOrder.Desc, }); @@ -209,13 +204,13 @@ describe('ZSetBusinessService', () => { }); it('key with this name does not exist for getMembers', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZCard, [ mockGetMembersDto.keyName, ]) .mockResolvedValue(0); await expect( - service.getMembers(mockClientOptions, mockGetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetMembersDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); }); @@ -227,7 +222,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getMembers(mockClientOptions, mockGetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for getMembers", async () => { @@ -238,7 +233,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.getMembers(mockClientOptions, mockGetMembersDto), + service.getMembers(mockBrowserClientMetadata, mockGetMembersDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -246,7 +241,7 @@ describe('ZSetBusinessService', () => { describe('addMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersDto.keyName, ]) .mockResolvedValue(1); @@ -254,29 +249,29 @@ describe('ZSetBusinessService', () => { it('succeed to add members to the ZSet data type', async () => { const { keyName } = mockAddMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, [ keyName, ...mockMembersForZAddCommand, ]) .mockResolvedValue(mockAddMembersDto.members.length); await expect( - service.addMembers(mockClientOptions, mockAddMembersDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersDto), ).resolves.not.toThrow(); }); it('key with this name does not exist for addMembers', async () => { const { keyName } = mockAddMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ keyName, ]) .mockResolvedValue(0); await expect( - service.addMembers(mockClientOptions, mockAddMembersDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, expect.anything(), ); @@ -288,14 +283,14 @@ describe('ZSetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.addMembers(mockClientOptions, mockAddMembersDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for addMembers", async () => { @@ -306,14 +301,14 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.addMembers(mockClientOptions, mockAddMembersDto), + service.addMembers(mockBrowserClientMetadata, mockAddMembersDto), ).rejects.toThrow(ForbiddenException); }); }); describe('updateMember', () => { beforeEach(() => when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockAddMembersDto.keyName, ]) .mockResolvedValue(1)); @@ -321,7 +316,7 @@ describe('ZSetBusinessService', () => { it('succeed to update member in key', async () => { const { keyName, member } = mockUpdateMemberDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, [ keyName, 'XX', 'CH', @@ -331,22 +326,22 @@ describe('ZSetBusinessService', () => { .mockResolvedValue(1); await expect( - service.updateMember(mockClientOptions, mockUpdateMemberDto), + service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto), ).resolves.not.toThrow(); }); it('key with this name does not exist for updateMember', async () => { const { keyName } = mockUpdateMemberDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ keyName, ]) .mockResolvedValue(0); await expect( - service.updateMember(mockClientOptions, mockUpdateMemberDto), + service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, expect.anything(), ); @@ -354,7 +349,7 @@ describe('ZSetBusinessService', () => { it('member does not exist in key', async () => { const { keyName, member } = mockUpdateMemberDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZAdd, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, [ keyName, 'XX', 'CH', @@ -364,7 +359,7 @@ describe('ZSetBusinessService', () => { .mockResolvedValue(0); await expect( - service.updateMember(mockClientOptions, mockUpdateMemberDto), + service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto), ).rejects.toThrow(NotFoundException); }); it("try to use 'ZADD' command not for zset data type for updateMember", async () => { @@ -374,14 +369,14 @@ describe('ZSetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZAdd, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.updateMember(mockClientOptions, mockUpdateMemberDto), + service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for updateMember", async () => { @@ -392,7 +387,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.updateMember(mockClientOptions, mockUpdateMemberDto), + service.updateMember(mockBrowserClientMetadata, mockUpdateMemberDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -400,7 +395,7 @@ describe('ZSetBusinessService', () => { describe('deleteMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ mockDeleteMembersDto.keyName, ]) .mockResolvedValue(1); @@ -408,14 +403,14 @@ describe('ZSetBusinessService', () => { it('succeeded to delete members from ZSet data type', async () => { const { members, keyName } = mockDeleteMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZRem, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZRem, [ keyName, ...members, ]) .mockResolvedValue(members.length); const result = await service.deleteMembers( - mockClientOptions, + mockBrowserClientMetadata, mockDeleteMembersDto, ); @@ -424,18 +419,18 @@ describe('ZSetBusinessService', () => { it('key with this name does not exist for deleteMembers', async () => { const { members, keyName } = mockDeleteMembersDto; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolKeysCommands.Exists, [ + .calledWith(mockBrowserClientMetadata, BrowserToolKeysCommands.Exists, [ keyName, ]) .mockResolvedValue(0); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(NotFoundException); expect( browserTool.execCommand, ).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZRem, [keyName, ...members], ); @@ -447,14 +442,14 @@ describe('ZSetBusinessService', () => { }; when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZRem, expect.anything(), ) .mockRejectedValue(replyError); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for deleteMembers", async () => { @@ -465,7 +460,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.deleteMembers(mockClientOptions, mockDeleteMembersDto), + service.deleteMembers(mockBrowserClientMetadata, mockDeleteMembersDto), ).rejects.toThrow(ForbiddenException); }); }); @@ -473,7 +468,7 @@ describe('ZSetBusinessService', () => { describe('searchMembers', () => { beforeEach(() => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZCard, [ mockSearchMembersDto.keyName, ]) .mockResolvedValue(mockAddMembersDto.members.length); @@ -481,19 +476,19 @@ describe('ZSetBusinessService', () => { it('succeeded to search members in ZSet data type', async () => { when(browserTool.execCommand) .calledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZScan, expect.anything(), ) .mockResolvedValue([0, ['member1', '-inf', 'member2', '0', 'member3', '2', 'member4', 'inf']]); const result = await service.searchMembers( - mockClientOptions, + mockBrowserClientMetadata, mockSearchMembersDto, ); await expect(result).toEqual(mockSearchZSetMembersResponse); expect(browserTool.execCommand).toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZScan, expect.anything(), ); @@ -505,20 +500,20 @@ describe('ZSetBusinessService', () => { match: item.name.toString(), }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZScore, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZScore, [ dto.keyName, dto.match, ]) .mockResolvedValue(item.score); - const result = await service.searchMembers(mockClientOptions, dto); + const result = await service.searchMembers(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockSearchZSetMembersResponse, members: [item], }); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZScan, expect.anything(), ); @@ -529,13 +524,13 @@ describe('ZSetBusinessService', () => { match: 'member', }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZScore, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZScore, [ dto.keyName, dto.match, ]) .mockResolvedValue(null); - const result = await service.searchMembers(mockClientOptions, dto); + const result = await service.searchMembers(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockSearchZSetMembersResponse, members: [] }); }); @@ -547,20 +542,20 @@ describe('ZSetBusinessService', () => { match: mockMatch, }; when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZScore, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZScore, [ dto.keyName, mockSpecialMember.toString(), ]) .mockResolvedValue(1); - const result = await service.searchMembers(mockClientOptions, dto); + const result = await service.searchMembers(mockBrowserClientMetadata, dto); expect(result).toEqual({ ...mockSearchZSetMembersResponse, members: [{ name: mockSpecialMember, score: 1 }], }); expect(browserTool.execCommand).not.toHaveBeenCalledWith( - mockClientOptions, + mockBrowserClientMetadata, BrowserToolZSetCommands.ZScan, expect.anything(), ); @@ -577,25 +572,25 @@ describe('ZSetBusinessService', () => { // ); // when(browserTool.execCommand) // .calledWith( - // mockClientOptions, + // mockBrowserClientMetadata, // BrowserToolZSetCommands.ZScan, // expect.anything(), // ) // .mockResolvedValue(['200', []]); // - // await service.searchMembers(mockClientOptions, dto); + // await service.searchMembers(mockBrowserClientMetadata, dto); // // expect(browserTool.execCommand).toHaveBeenCalledTimes(maxScanCalls + 1); // }); it('key with this name does not exist for searchMembers', async () => { when(browserTool.execCommand) - .calledWith(mockClientOptions, BrowserToolZSetCommands.ZCard, [ + .calledWith(mockBrowserClientMetadata, BrowserToolZSetCommands.ZCard, [ mockSearchMembersDto.keyName, ]) .mockResolvedValue(0); await expect( - service.searchMembers(mockClientOptions, mockSearchMembersDto), + service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto), ).rejects.toThrow(NotFoundException); expect(browserTool.execCommand).toHaveBeenCalledTimes(1); }); @@ -607,7 +602,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.searchMembers(mockClientOptions, mockSearchMembersDto), + service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto), ).rejects.toThrow(BadRequestException); }); it("user don't have required permissions for searchMembers", async () => { @@ -618,7 +613,7 @@ describe('ZSetBusinessService', () => { browserTool.execCommand.mockRejectedValue(replyError); await expect( - service.searchMembers(mockClientOptions, mockSearchMembersDto), + service.searchMembers(mockBrowserClientMetadata, mockSearchMembersDto), ).rejects.toThrow(ForbiddenException); }); }); diff --git a/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.ts b/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.ts index 5cf1772f98..d4ef9d4fd2 100644 --- a/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/z-set-business/z-set-business.service.ts @@ -26,13 +26,13 @@ import { SortOrder } from 'src/constants/sort'; import { RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { ReplyError } from 'src/models'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { BrowserToolKeysCommands, BrowserToolZSetCommands, } from 'src/modules/browser/constants/browser-tool-commands'; import { plainToClass } from 'class-transformer'; +import { ClientMetadata } from 'src/common/models'; const REDIS_SCAN_CONFIG = config.get('redis_scan'); @@ -45,14 +45,14 @@ export class ZSetBusinessService { ) {} public async createZSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateZSetWithExpireDto, ): Promise { this.logger.log('Creating ZSet data type.'); const { keyName } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -65,9 +65,9 @@ export class ZSetBusinessService { ); } if (dto.expire) { - await this.createZSetWithExpiration(clientOptions, dto); + await this.createZSetWithExpiration(clientMetadata, dto); } else { - await this.createSimpleZSet(clientOptions, dto); + await this.createSimpleZSet(clientMetadata, dto); } this.logger.log('Succeed to create ZSet data type.'); } catch (error) { @@ -81,7 +81,7 @@ export class ZSetBusinessService { } public async getMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, getZSetDto: GetZSetMembersDto, ): Promise { this.logger.log('Getting members of the ZSet data type stored at key.'); @@ -89,7 +89,7 @@ export class ZSetBusinessService { let result: GetZSetResponse; try { const total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZCard, [keyName], ); @@ -104,9 +104,9 @@ export class ZSetBusinessService { let members: ZSetMemberDto[] = []; if (sortOrder && sortOrder === SortOrder.Asc) { - members = await this.getZRange(clientOptions, getZSetDto); + members = await this.getZRange(clientMetadata, getZSetDto); } else { - members = await this.getZRevRange(clientOptions, getZSetDto); + members = await this.getZRevRange(clientMetadata, getZSetDto); } this.logger.log('Succeed to get members of the ZSet data type.'); @@ -126,14 +126,14 @@ export class ZSetBusinessService { } public async addMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: AddMembersToZSetDto, ): Promise { this.logger.log('Adding members to the ZSet data type.'); const { keyName, members } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -147,7 +147,7 @@ export class ZSetBusinessService { } const args = this.formatMembersDtoToCommandArgs(members); await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZAdd, [keyName, ...args], ); @@ -163,14 +163,14 @@ export class ZSetBusinessService { } public async updateMember( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: UpdateMemberInZSetDto, ): Promise { this.logger.log('Updating member in ZSet data type.'); const { keyName, member } = dto; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -183,7 +183,7 @@ export class ZSetBusinessService { ); } const result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZAdd, [keyName, 'XX', 'CH', `${member.score}`, member.name], ); @@ -207,7 +207,7 @@ export class ZSetBusinessService { } public async deleteMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: DeleteMembersFromZSetDto, ): Promise { this.logger.log('Deleting members from the ZSet data type.'); @@ -215,7 +215,7 @@ export class ZSetBusinessService { let result; try { const isExist = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolKeysCommands.Exists, [keyName], ); @@ -228,7 +228,7 @@ export class ZSetBusinessService { ); } result = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZRem, [keyName, ...members], ); @@ -244,7 +244,7 @@ export class ZSetBusinessService { } public async searchMembers( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SearchZSetMembersDto, ): Promise { this.logger.log('Search members of the ZSet data type stored at key.'); @@ -257,7 +257,7 @@ export class ZSetBusinessService { }; try { result.total = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZCard, [keyName], ); @@ -273,7 +273,7 @@ export class ZSetBusinessService { const member = unescapeGlob(dto.match); result.nextCursor = 0; const score = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZScore, [keyName, member], ); @@ -283,7 +283,7 @@ export class ZSetBusinessService { result.members.push(plainToClass(ZSetMemberDto, { name: member, score: formattedScore })); } } else { - const scanResult = await this.scanZSet(clientOptions, dto); + const scanResult = await this.scanZSet(clientMetadata, dto); result = { ...result, ...scanResult }; } this.logger.log('Succeed to search members of the ZSet data type.'); @@ -300,13 +300,13 @@ export class ZSetBusinessService { } public async getZRange( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, getZSetDto: GetZSetMembersDto, ): Promise { const { keyName, offset, count } = getZSetDto; const execResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZRange, [keyName, offset, offset + count - 1, 'WITHSCORES'], ); @@ -315,13 +315,13 @@ export class ZSetBusinessService { } public async getZRevRange( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, getZSetDto: GetZSetMembersDto, ): Promise { const { keyName, offset, count } = getZSetDto; const execResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZRevRange, [keyName, offset, offset + count - 1, 'WITHSCORES'], ); @@ -330,21 +330,21 @@ export class ZSetBusinessService { } public async createSimpleZSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateZSetWithExpireDto, ): Promise { const { keyName, members } = dto; const args = this.formatMembersDtoToCommandArgs(members); return await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZAdd, [keyName, ...args], ); } public async createZSetWithExpiration( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateZSetWithExpireDto, ): Promise { const { keyName, members, expire } = dto; @@ -353,7 +353,7 @@ export class ZSetBusinessService { const [ transactionError, transactionResults, - ] = await this.browserTool.execMulti(clientOptions, [ + ] = await this.browserTool.execMulti(clientMetadata, [ [BrowserToolZSetCommands.ZAdd, keyName, ...args], [BrowserToolKeysCommands.Expire, keyName, expire], ]); @@ -366,7 +366,7 @@ export class ZSetBusinessService { } public async scanZSet( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SearchZSetMembersDto, ): Promise { const { keyName } = dto; @@ -379,7 +379,7 @@ export class ZSetBusinessService { }; while (result.nextCursor !== 0 && result.members.length < count) { const scanResult = await this.browserTool.execCommand( - clientOptions, + clientMetadata, BrowserToolZSetCommands.ZScan, [ keyName, diff --git a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts index f64b55c849..a0e2b32cf5 100644 --- a/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts +++ b/redisinsight/api/src/modules/bulk-actions/providers/bulk-actions.provider.ts @@ -3,11 +3,11 @@ import { } from '@nestjs/common'; import { BulkAction } from 'src/modules/bulk-actions/models/bulk-action'; import { CreateBulkActionDto } from 'src/modules/bulk-actions/dto/create-bulk-action.dto'; -import { AppTool } from 'src/models'; import { Socket } from 'socket.io'; import { BulkActionStatus, BulkActionType } from 'src/modules/bulk-actions/constants'; import { DeleteBulkActionSimpleRunner } from 'src/modules/bulk-actions/models/runners/simple/delete.bulk-action.simple.runner'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { ClientContext } from 'src/common/models'; @Injectable() export class BulkActionsProvider { @@ -33,9 +33,11 @@ export class BulkActionsProvider { this.bulkActions.set(dto.id, bulkAction); + // todo: add multi user support const client = await this.databaseConnectionService.getOrCreateClient({ + session: undefined, databaseId: dto.databaseId, - namespace: AppTool.Common, + context: ClientContext.Common, }); await bulkAction.prepare(client, BulkActionsProvider.getSimpleRunnerClass(dto)); diff --git a/redisinsight/api/src/modules/cli/cli.module.ts b/redisinsight/api/src/modules/cli/cli.module.ts index 3b79cd7fd1..145399adac 100644 --- a/redisinsight/api/src/modules/cli/cli.module.ts +++ b/redisinsight/api/src/modules/cli/cli.module.ts @@ -6,7 +6,7 @@ import { RedisToolFactory } from 'src/modules/redis/redis-tool.factory'; import { CommandsModule } from 'src/modules/commands/commands.module'; import { CommandsService } from 'src/modules/commands/commands.service'; import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider'; -import { AppTool } from 'src/models'; +import { ClientContext } from 'src/common/models'; import config from 'src/utils/config'; import { CliController } from './controllers/cli.controller'; import { CliBusinessService } from './services/cli-business/cli-business.service'; @@ -23,7 +23,7 @@ const COMMANDS_CONFIGS = config.get('commands'); { provide: RedisToolService, useFactory: (redisToolFactory: RedisToolFactory) => redisToolFactory.createRedisTool( - AppTool.CLI, + ClientContext.CLI, { enableAutoConnection: false }, ), inject: [RedisToolFactory], diff --git a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts index d99f6feeb0..1631343145 100644 --- a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts +++ b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts @@ -21,6 +21,8 @@ import { import { CliBusinessService } from 'src/modules/cli/services/cli-business/cli-business.service'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ApiCLIParams } from 'src/modules/cli/decorators/api-cli-params.decorator'; +import { CliClientMetadata, ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('CLI') @Controller('cli') @@ -42,10 +44,9 @@ export class CliController { ], }) async getClient( - @Param('dbInstance') dbInstance: string, - @Body() dto: CreateCliClientDto, + @ClientMetadataFromRequest() clientMetadata: ClientMetadata, ): Promise { - return this.service.getClient(dbInstance, dto.namespace); + return this.service.getClient(clientMetadata); } @Post('/:uuid/send-command') @@ -62,17 +63,10 @@ export class CliController { ], }) async sendCommand( - @Param('dbInstance') dbInstance: string, - @Param('uuid') uuid: string, + @CliClientMetadata() clientMetadata: ClientMetadata, @Body() dto: SendCommandDto, ): Promise { - return this.service.sendCommand( - { - instanceId: dbInstance, - uuid, - }, - dto, - ); + return this.service.sendCommand(clientMetadata, dto); } @Post('/:uuid/send-cluster-command') @@ -90,17 +84,10 @@ export class CliController { ], }) async sendClusterCommand( - @Param('dbInstance') dbInstance: string, - @Param('uuid') uuid: string, + @CliClientMetadata() clientMetadata: ClientMetadata, @Body() dto: SendClusterCommandDto, ): Promise { - return this.service.sendClusterCommand( - { - instanceId: dbInstance, - uuid, - }, - dto, - ); + return this.service.sendClusterCommand(clientMetadata, dto); } @Delete('/:uuid') @@ -130,10 +117,8 @@ export class CliController { statusCode: 200, }) async reCreateClient( - @Param('dbInstance') dbInstance: string, - @Param('uuid') uuid: string, - @Body() dto: CreateCliClientDto, + @CliClientMetadata() clientMetadata: ClientMetadata, ): Promise { - return this.service.reCreateClient(dbInstance, uuid, dto.namespace); + return this.service.reCreateClient(clientMetadata); } } diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts index 078f193ae1..eafa4be94c 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts @@ -10,6 +10,7 @@ import { mockDatabase, mockCliAnalyticsService, mockRedisMovedError, MockType, + mockCliClientMetadata, } from 'src/__mocks__'; import { ClusterNodeRole, @@ -19,7 +20,6 @@ import { SendCommandDto, SendCommandResponse, } from 'src/modules/cli/dto/cli.dto'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { ReplyError } from 'src/models'; import { CliToolUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands'; import { @@ -37,9 +37,6 @@ import { OutputFormatterManager } from './output-formatter/output-formatter-mana import { CliOutputFormatterTypes, IOutputFormatterStrategy } from './output-formatter/output-formatter.interface'; import { CliBusinessService } from './cli-business.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; const mockClientUuid = uuidv4(); const mockNode = { host: '127.0.0.1', @@ -112,11 +109,11 @@ describe('CliBusinessService', () => { it('should successfully create new redis client', async () => { cliTool.createNewToolClient.mockResolvedValue(mockClientUuid); - const result = await service.getClient(mockDatabase.id); + const result = await service.getClient(mockCliClientMetadata); expect(result).toEqual({ uuid: mockClientUuid }); expect(analyticsService.sendClientCreatedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, ); }); @@ -126,12 +123,12 @@ describe('CliBusinessService', () => { ); try { - await service.getClient(mockDatabase.id); + await service.getClient(mockCliClientMetadata); fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new InternalServerErrorException(mockENotFoundMessage), ); } @@ -141,12 +138,12 @@ describe('CliBusinessService', () => { cliTool.createNewToolClient.mockRejectedValue(new KeytarUnavailableException()); try { - await service.getClient(mockDatabase.id); + await service.getClient(mockCliClientMetadata); fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new KeytarUnavailableException(), ); } @@ -157,14 +154,11 @@ describe('CliBusinessService', () => { it('should successfully create new redis client', async () => { cliTool.reCreateToolClient.mockResolvedValue(mockClientUuid); - const result = await service.reCreateClient( - mockDatabase.id, - mockClientUuid, - ); + const result = await service.reCreateClient(mockCliClientMetadata); expect(result).toEqual({ uuid: mockClientUuid }); expect(analyticsService.sendClientRecreatedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, ); }); @@ -174,15 +168,12 @@ describe('CliBusinessService', () => { ); try { - await service.reCreateClient( - mockDatabase.id, - mockClientUuid, - ); + await service.reCreateClient(mockCliClientMetadata); fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new InternalServerErrorException(mockENotFoundMessage), ); } @@ -192,15 +183,12 @@ describe('CliBusinessService', () => { cliTool.reCreateToolClient.mockRejectedValue(new KeytarUnavailableException()); try { - await service.reCreateClient( - mockDatabase.id, - mockClientUuid, - ); + await service.reCreateClient(mockCliClientMetadata); fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); expect(analyticsService.sendClientCreationFailedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new KeytarUnavailableException(), ); } @@ -219,7 +207,7 @@ describe('CliBusinessService', () => { expect(result).toEqual({ affected: 1 }); expect(analyticsService.sendClientDeletedEvent).toHaveBeenCalledWith( 1, - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, ); }); @@ -248,15 +236,15 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Success, }; when(cliTool.execCommand) - .calledWith(mockClientOptions, 'memory', ['usage', 'key'], undefined) + .calledWith(mockCliClientMetadata, 'memory', ['usage', 'key'], undefined) .mockReturnValue(5); - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(formatSpy).toHaveBeenCalled(); expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { command: 'memory', outputFormat: CliOutputFormatterTypes.Raw, @@ -274,15 +262,15 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Success, }; when(cliTool.execCommand) - .calledWith(mockClientOptions, 'memory', ['usage', 'key'], undefined) + .calledWith(mockCliClientMetadata, 'memory', ['usage', 'key'], undefined) .mockReturnValue(5); - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(formatSpy).toHaveBeenCalled(); expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { command: 'memory', outputFormat: CliOutputFormatterTypes.Raw, @@ -299,11 +287,11 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Fail, }; - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandNotSupportedError( ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED(command.toUpperCase()), ), @@ -322,11 +310,11 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Fail, }; - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandParsingError( ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(), ), @@ -350,11 +338,11 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Fail, }; - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, replyError, { command: 'get', @@ -368,12 +356,12 @@ describe('CliBusinessService', () => { cliTool.execCommand.mockRejectedValue(new Error(mockENotFoundMessage)); try { - await service.sendCommand(mockClientOptions, dto); + await service.sendCommand(mockCliClientMetadata, dto); fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new Error(mockENotFoundMessage), { command: 'get', @@ -388,12 +376,12 @@ describe('CliBusinessService', () => { cliTool.execCommand.mockRejectedValue(new KeytarUnavailableException()); try { - await service.sendCommand(mockClientOptions, dto); + await service.sendCommand(mockCliClientMetadata, dto); fail(); } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new KeytarUnavailableException(), { command: 'get', @@ -409,14 +397,14 @@ describe('CliBusinessService', () => { status: CommandExecutionStatus.Success, }; when(cliTool.execCommand) - .calledWith(mockClientOptions, 'info', ['server'], 'utf8') + .calledWith(mockCliClientMetadata, 'info', ['server'], 'utf8') .mockReturnValue(mockRedisServerInfoResponse); - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockCliClientMetadata, dto); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { command: 'info', outputFormat: CliOutputFormatterTypes.Raw, @@ -436,7 +424,7 @@ describe('CliBusinessService', () => { role: ClusterNodeRole.Master, }; - await service.sendClusterCommand(mockClientOptions, dto); + await service.sendClusterCommand(mockCliClientMetadata, dto); expect(service.sendCommandForNodes).toHaveBeenCalled(); }); @@ -447,7 +435,7 @@ describe('CliBusinessService', () => { nodeOptions: { ...mockNode, enableRedirection: true }, }; - await service.sendClusterCommand(mockClientOptions, dto); + await service.sendClusterCommand(mockCliClientMetadata, dto); expect(service.sendCommandForSingleNode).toHaveBeenCalled(); }); @@ -460,7 +448,7 @@ describe('CliBusinessService', () => { }; service.sendCommandForSingleNode = jest.fn().mockRejectedValue(new KeytarUnavailableException()); - await expect(service.sendClusterCommand(mockClientOptions, dto)).rejects.toThrow(KeytarUnavailableException); + await expect(service.sendClusterCommand(mockCliClientMetadata, dto)).rejects.toThrow(KeytarUnavailableException); }); }); @@ -479,14 +467,14 @@ describe('CliBusinessService', () => { ]); const result = await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); expect(result).toEqual(mockResult); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: mockIntegerResponse, status: CommandExecutionStatus.Success, @@ -516,21 +504,21 @@ describe('CliBusinessService', () => { ]); const result = await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, mockServerInfoCommand, ClusterNodeRole.Master, ); expect(result).toEqual(mockResult); expect(cliTool.execCommandForNodes).toHaveBeenCalledWith( - mockClientOptions, + mockCliClientMetadata, 'info', ['server'], ClusterNodeRole.Master, 'utf8', ); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: mockRedisServerInfoResponse, status: CommandExecutionStatus.Success, @@ -555,14 +543,14 @@ describe('CliBusinessService', () => { ]; const result = await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandNotSupportedError(ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED( command.toUpperCase(), )), @@ -583,14 +571,14 @@ describe('CliBusinessService', () => { ]; const result = await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()), { command: unknownCommand, @@ -606,7 +594,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); @@ -615,7 +603,7 @@ describe('CliBusinessService', () => { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual(ERROR_MESSAGES.WRONG_DATABASE_TYPE); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new WrongDatabaseTypeError(ERROR_MESSAGES.WRONG_DATABASE_TYPE), { command: 'memory', @@ -630,7 +618,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); @@ -638,7 +626,7 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new Error(mockENotFoundMessage), { command: 'memory', @@ -653,7 +641,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForNodes( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.Master, ); @@ -661,7 +649,7 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new KeytarUnavailableException(), { command: 'memory', @@ -688,14 +676,14 @@ describe('CliBusinessService', () => { }); const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, ); expect(result).toEqual(mockResult); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: mockIntegerResponse, ...mockNode, @@ -721,14 +709,14 @@ describe('CliBusinessService', () => { }); const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, mockServerInfoCommand, ClusterNodeRole.All, nodeOptions, ); expect(result).toEqual(mockResult); expect(cliTool.execCommandForNode).toHaveBeenCalledWith( - mockClientOptions, + mockCliClientMetadata, 'info', ['server'], ClusterNodeRole.All, @@ -736,7 +724,7 @@ describe('CliBusinessService', () => { 'utf8', ); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: mockRedisServerInfoResponse, ...mockNode, @@ -770,7 +758,7 @@ describe('CliBusinessService', () => { }); const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -780,7 +768,7 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); expect(result).toEqual(mockResult); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: 'OK', ...mockNode, @@ -815,7 +803,7 @@ describe('CliBusinessService', () => { }); const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -825,7 +813,7 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(2); expect(result).toEqual(mockResult); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { response: mockResult.response, ...mockNode, @@ -854,7 +842,7 @@ describe('CliBusinessService', () => { }); const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, { ...nodeOptions, enableRedirection: false }, @@ -863,7 +851,7 @@ describe('CliBusinessService', () => { expect(cliTool.execCommandForNode).toHaveBeenCalledTimes(1); expect(result).toEqual(mockResult); expect(analyticsService.sendClusterCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, { error: mockRedisMovedError, response: mockRedisMovedError.message, @@ -886,7 +874,7 @@ describe('CliBusinessService', () => { }; const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -894,7 +882,7 @@ describe('CliBusinessService', () => { expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandNotSupportedError(ERROR_MESSAGES.CLI_COMMAND_NOT_SUPPORTED( command.toUpperCase(), )), @@ -912,7 +900,7 @@ describe('CliBusinessService', () => { }; const result = await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -920,7 +908,7 @@ describe('CliBusinessService', () => { expect(result).toEqual(mockResult); expect(analyticsService.sendCommandErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()), { command: unknownCommand, @@ -936,7 +924,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -946,7 +934,7 @@ describe('CliBusinessService', () => { expect(err).toBeInstanceOf(BadRequestException); expect(err.message).toEqual(ERROR_MESSAGES.WRONG_DATABASE_TYPE); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new WrongDatabaseTypeError(ERROR_MESSAGES.WRONG_DATABASE_TYPE), { command: 'get', @@ -965,7 +953,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -974,7 +962,7 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(BadRequestException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new ClusterNodeNotFoundError( ERROR_MESSAGES.CLUSTER_NODE_NOT_FOUND('127.0.0.1:7002'), ), @@ -991,7 +979,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -1000,7 +988,7 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new Error(mockENotFoundMessage), { command: 'get', @@ -1015,7 +1003,7 @@ describe('CliBusinessService', () => { try { await service.sendCommandForSingleNode( - mockClientOptions, + mockCliClientMetadata, command, ClusterNodeRole.All, nodeOptions, @@ -1024,7 +1012,7 @@ describe('CliBusinessService', () => { } catch (err) { expect(err).toBeInstanceOf(KeytarUnavailableException); expect(analyticsService.sendConnectionErrorEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockCliClientMetadata.databaseId, new KeytarUnavailableException(), { command: 'get', diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts index 4ebf383e9f..3fe7b11910 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException, Logger, } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { unknownCommand } from 'src/constants'; import { CommandsService } from 'src/modules/commands/commands.service'; import { @@ -33,7 +33,6 @@ import { } from 'src/modules/cli/constants/errors'; import { CliAnalyticsService } from 'src/modules/cli/services/cli-analytics/cli-analytics.service'; import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions'; -import { AppTool } from 'src/models'; import { RedisToolService } from 'src/modules/redis/redis-tool.service'; import { getUnsupportedCommands } from 'src/modules/cli/utils/getUnsupportedCommands'; import { ClientNotFoundErrorException } from 'src/modules/redis/exceptions/client-not-found-error.exception'; @@ -66,46 +65,36 @@ export class CliBusinessService { /** * Method to create new redis client and return uuid - * @param instanceId - * @param namespace + * @param clientMetadata */ - public async getClient( - instanceId: string, - namespace: string = AppTool.CLI, - ): Promise { + public async getClient(clientMetadata: ClientMetadata): Promise { this.logger.log('Create Redis client for CLI.'); try { - const uuid = await this.cliTool.createNewToolClient(instanceId, namespace); + const uuid = await this.cliTool.createNewToolClient(clientMetadata); this.logger.log('Succeed to create Redis client for CLI.'); - this.cliAnalyticsService.sendClientCreatedEvent(instanceId); + this.cliAnalyticsService.sendClientCreatedEvent(clientMetadata.databaseId); return { uuid }; } catch (error) { this.logger.error('Failed to create redis client for CLI.', error); - this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(clientMetadata.databaseId, error); throw error; } } /** * Method to close exist client and create a new one - * @param instanceId - * @param uuid - * @param namespace + * @param clientMetadata */ - public async reCreateClient( - instanceId: string, - uuid: string, - namespace: string = AppTool.CLI, - ): Promise { + public async reCreateClient(clientMetadata: ClientMetadata): Promise { this.logger.log('re-create Redis client for CLI.'); try { - const clientUuid = await this.cliTool.reCreateToolClient(instanceId, uuid, namespace); + const clientUuid = await this.cliTool.reCreateToolClient(clientMetadata); this.logger.log('Succeed to re-create Redis client for CLI.'); - this.cliAnalyticsService.sendClientRecreatedEvent(instanceId); + this.cliAnalyticsService.sendClientRecreatedEvent(clientMetadata.databaseId); return { uuid: clientUuid }; } catch (error) { this.logger.error('Failed to re-create redis client for CLI.', error); - this.cliAnalyticsService.sendClientCreationFailedEvent(instanceId, error); + this.cliAnalyticsService.sendClientCreationFailedEvent(clientMetadata.databaseId, error); throw error; } } @@ -135,11 +124,11 @@ export class CliBusinessService { /** * Method to execute cli command for redis client and return result - * @param clientOptions + * @param clientMetadata * @param dto */ public async sendCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SendCommandDto, ): Promise { this.logger.log('Executing redis CLI command.'); @@ -154,12 +143,12 @@ export class CliBusinessService { const replyEncoding = checkHumanReadableCommands(`${command} ${args[0]}`) ? 'utf8' : undefined; this.checkUnsupportedCommands(`${command} ${args[0]}`); - const reply = await this.cliTool.execCommand(clientOptions, command, args, replyEncoding); + const reply = await this.cliTool.execCommand(clientMetadata, command, args, replyEncoding); this.logger.log('Succeed to execute redis CLI command.'); this.cliAnalyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, + clientMetadata.databaseId, { command, outputFormat, @@ -177,14 +166,14 @@ export class CliBusinessService { || error instanceof CommandNotSupportedError || error?.name === 'ReplyError' ) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendCommandErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendConnectionErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); @@ -203,7 +192,7 @@ export class CliBusinessService { * @param dto */ public async sendClusterCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: SendClusterCommandDto, ): Promise { this.logger.log('Executing redis.cluster CLI command.'); @@ -212,7 +201,7 @@ export class CliBusinessService { } = dto; if (nodeOptions) { const result = await this.sendCommandForSingleNode( - clientOptions, + clientMetadata, command, role, nodeOptions, @@ -220,11 +209,11 @@ export class CliBusinessService { ); return [result]; } - return this.sendCommandForNodes(clientOptions, command, role, outputFormat); + return this.sendCommandForNodes(clientMetadata, command, role, outputFormat); } public async sendCommandForNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, commandLine: string, role: ClusterNodeRole, outputFormat: CliOutputFormatterTypes = CliOutputFormatterTypes.Raw, @@ -240,7 +229,7 @@ export class CliBusinessService { this.checkUnsupportedCommands(`${command} ${args[0]}`); const result = await this.cliTool.execCommandForNodes( - clientOptions, + clientMetadata, command, args, role, @@ -249,7 +238,7 @@ export class CliBusinessService { return result.map((nodeExecReply) => { this.cliAnalyticsService.sendClusterCommandExecutedEvent( - clientOptions.instanceId, + clientMetadata.databaseId, nodeExecReply, { command, outputFormat }, ); @@ -266,7 +255,7 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendCommandErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); @@ -275,7 +264,7 @@ export class CliBusinessService { ]; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendConnectionErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); @@ -292,7 +281,7 @@ export class CliBusinessService { } public async sendCommandForSingleNode( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, commandLine: string, role: ClusterNodeRole, nodeOptions: ClusterSingleNodeOptions, @@ -309,7 +298,7 @@ export class CliBusinessService { this.checkUnsupportedCommands(`${command} ${args[0]}`); const nodeAddress = `${nodeOptions.host}:${nodeOptions.port}`; let result = await this.cliTool.execCommandForNode( - clientOptions, + clientMetadata, command, args, role, @@ -319,7 +308,7 @@ export class CliBusinessService { if (result?.error && checkRedirectionError(result.error) && nodeOptions.enableRedirection) { const { slot, address } = parseRedirectionError(result.error); result = await this.cliTool.execCommandForNode( - clientOptions, + clientMetadata, command, args, role, @@ -332,7 +321,7 @@ export class CliBusinessService { result.response = formatter.format(result.response); } this.cliAnalyticsService.sendClusterCommandExecutedEvent( - clientOptions.instanceId, + clientMetadata.databaseId, result, { command, outputFormat }, ); @@ -344,14 +333,14 @@ export class CliBusinessService { this.logger.error('Failed to execute redis.cluster CLI command.', error); if (error instanceof CommandParsingError || error instanceof CommandNotSupportedError) { - this.cliAnalyticsService.sendCommandErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendCommandErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); return { response: error.message, status: CommandExecutionStatus.Fail }; } - this.cliAnalyticsService.sendConnectionErrorEvent(clientOptions.instanceId, error, { + this.cliAnalyticsService.sendConnectionErrorEvent(clientMetadata.databaseId, error, { command, outputFormat, }); diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts index cf170a0037..a634e2bec3 100644 --- a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts @@ -1,9 +1,10 @@ -import { Controller, Get, Param } from '@nestjs/common'; +import { Controller, Get } from '@nestjs/common'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service'; -import { AppTool } from 'src/models'; import { ApiTags } from '@nestjs/swagger'; import { ClusterDetails } from 'src/modules/cluster-monitor/models'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Cluster Monitor') @Controller('/cluster-details') @@ -22,11 +23,8 @@ export class ClusterMonitorController { }) @Get() async getClusterDetails( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata: ClientMetadata, ): Promise { - return this.clusterMonitorService.getClusterDetails({ - instanceId, - tool: AppTool.Common, - }); + return this.clusterMonitorService.getClusterDetails(clientMetadata); } } diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts index f232f6c14f..0a98303056 100644 --- a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.service.ts @@ -4,12 +4,12 @@ import { BadRequestException, HttpException, Injectable, Logger, } from '@nestjs/common'; import { catchAclError, convertRedisInfoReplyToObject } from 'src/utils'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { IClusterInfo } from 'src/modules/cluster-monitor/strategies/cluster.info.interface'; import { ClusterNodesInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-nodes.info.strategy'; import { ClusterShardsInfoStrategy } from 'src/modules/cluster-monitor/strategies/cluster-shards.info.strategy'; import { ClusterDetails } from 'src/modules/cluster-monitor/models'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { ClientMetadata } from 'src/common/models'; export enum ClusterInfoStrategies { CLUSTER_NODES = 'CLUSTER_NODES', @@ -31,14 +31,11 @@ export class ClusterMonitorService { /** * Get cluster details and details for all nodes - * @param clientOptions + * @param clientMetadata */ - public async getClusterDetails(clientOptions: IFindRedisClientInstanceByOptions): Promise { + public async getClusterDetails(clientMetadata: ClientMetadata): Promise { try { - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); if (!(client instanceof IORedis.Cluster)) { return Promise.reject(new BadRequestException('Current database is not in a cluster mode')); diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts index d6972c0b4a..52f62e46a5 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts @@ -7,8 +7,9 @@ import { ApiTags } from '@nestjs/swagger'; import { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; import { BrowserSerializeInterceptor } from 'src/common/interceptors'; -import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, ClientMetadataFromRequest } from 'src/common/decorators'; import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; +import { ClientMetadata } from 'src/common/models'; @UseInterceptors(BrowserSerializeInterceptor) @UsePipes(new ValidationPipe({ transform: true })) @@ -30,10 +31,10 @@ export class DatabaseAnalysisController { @Post() @ApiQueryRedisStringEncoding() async create( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata: ClientMetadata, @Body() dto: CreateDatabaseAnalysisDto, ): Promise { - return this.service.create({ instanceId }, dto); + return this.service.create(clientMetadata, dto); } @ApiEndpoint({ diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index b1add19ae0..28db2bc7d8 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -1,5 +1,4 @@ import { HttpException, Injectable, Logger } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { catchAclError } from 'src/utils'; import { DatabaseAnalyzer } from 'src/modules/database-analysis/providers/database-analyzer'; import { plainToClass } from 'class-transformer'; @@ -8,7 +7,7 @@ import { DatabaseAnalysisProvider } from 'src/modules/database-analysis/provider import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; import { KeysScanner } from 'src/modules/database-analysis/scanner/keys-scanner'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { AppTool } from 'src/models'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class DatabaseAnalysisService { @@ -23,18 +22,15 @@ export class DatabaseAnalysisService { /** * Get cluster details and details for all nodes - * @param clientOptions + * @param clientMetadata * @param dto */ public async create( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateDatabaseAnalysisDto, ): Promise { try { - const client = await this.databaseConnectionService.createClient({ - databaseId: clientOptions.instanceId, - namespace: AppTool.Common, - }); + const client = await this.databaseConnectionService.createClient(clientMetadata); const scanResults = await this.scanner.scan(client, { filter: dto.filter, @@ -53,7 +49,7 @@ export class DatabaseAnalysisService { }); const analysis = plainToClass(DatabaseAnalysis, await this.analyzer.analyze({ - databaseId: clientOptions.instanceId, + databaseId: clientMetadata.databaseId, ...dto, progress, }, [].concat(...scanResults.map((nodeResult) => nodeResult.keys)))); diff --git a/redisinsight/api/src/modules/database/database-connection.service.spec.ts b/redisinsight/api/src/modules/database/database-connection.service.spec.ts index 029fd625ff..6dee9f498a 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.spec.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.spec.ts @@ -1,7 +1,7 @@ import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { - mockClientMetadata, + mockClientMetadata, mockCommonClientMetadata, mockDatabase, mockDatabaseAnalytics, mockDatabaseInfoProvider, @@ -10,7 +10,7 @@ import { mockIORedisClient, mockRedisNoAuthError, mockRedisService, - MockType, + MockType } from 'src/__mocks__'; import { DatabaseAnalytics } from 'src/modules/database/database.analytics'; import { DatabaseService } from 'src/modules/database/database.service'; @@ -62,7 +62,7 @@ describe('DatabaseConnectionService', () => { describe('connect', () => { it('should connect to database', async () => { - expect(await service.connect(mockDatabase.id, AppTool.Common)).toEqual(undefined); + expect(await service.connect(mockCommonClientMetadata)).toEqual(undefined); expect(redisService.connectToDatabaseInstance).not.toHaveBeenCalled(); }); }); diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index 0c9be37a1d..5e69d845fc 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -1,13 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; -import { AppTool } from 'src/models'; import * as IORedis from 'ioredis'; import { generateRedisConnectionName, getRedisConnectionException } from 'src/utils'; import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; import { DatabaseAnalytics } from 'src/modules/database/database.analytics'; import { RedisService } from 'src/modules/redis/redis.service'; -import { ClientMetadata } from 'src/modules/redis/models/client-metadata'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class DatabaseConnectionService { @@ -23,27 +22,20 @@ export class DatabaseConnectionService { /** * Connects to database and updates modules list and last connected time - * @param databaseId - * @param namespace + * @param clientMetadata */ - async connect( - databaseId: string, - namespace = AppTool.Common, - ): Promise { - const client = await this.getOrCreateClient({ - databaseId, - namespace, - }); + async connect(clientMetadata: ClientMetadata): Promise { + const client = await this.getOrCreateClient(clientMetadata); // refresh modules list and last connected time // will be refreshed after user navigate to particular database from the databases list // Note: move to a different place in case if we need to update such info more often - await this.repository.update(databaseId, { + await this.repository.update(clientMetadata.databaseId, { lastConnection: new Date(), modules: await this.databaseInfoProvider.determineDatabaseModules(client), }); - this.logger.log(`Succeed to connect to database ${databaseId}`); + this.logger.log(`Succeed to connect to database ${clientMetadata.databaseId}`); } /** @@ -56,12 +48,7 @@ export class DatabaseConnectionService { async getOrCreateClient(clientMetadata: ClientMetadata) { this.logger.log('Getting database client.'); - let client = (await this.redisService.getClientInstance({ - // todo: change RedisService logic to match new metadata interface - instanceId: clientMetadata.databaseId, - tool: clientMetadata.namespace, - uuid: clientMetadata.uuid, - }))?.client; + let client = (await this.redisService.getClientInstance(clientMetadata))?.client; if (client && this.redisService.isClientConnected(client)) { return client; @@ -69,14 +56,7 @@ export class DatabaseConnectionService { client = await this.createClient(clientMetadata); - this.redisService.setClientInstance( - { - instanceId: clientMetadata.databaseId, - tool: clientMetadata.namespace, - uuid: clientMetadata.uuid, - }, - client, - ); + this.redisService.setClientInstance(clientMetadata, client); return client; } @@ -92,12 +72,12 @@ export class DatabaseConnectionService { async createClient(clientMetadata: ClientMetadata): Promise { this.logger.log('Creating database client.'); const database = await this.databaseService.get(clientMetadata.databaseId); - const connectionName = generateRedisConnectionName(clientMetadata.namespace, clientMetadata.databaseId); + const connectionName = generateRedisConnectionName(clientMetadata.context, clientMetadata.databaseId); try { return await this.redisService.connectToDatabaseInstance( database, - clientMetadata.namespace, + clientMetadata.context, connectionName, ); } catch (error) { diff --git a/redisinsight/api/src/modules/database/database-info.controller.ts b/redisinsight/api/src/modules/database/database-info.controller.ts index f0c1ceac09..e5f4c3ade8 100644 --- a/redisinsight/api/src/modules/database/database-info.controller.ts +++ b/redisinsight/api/src/modules/database/database-info.controller.ts @@ -1,13 +1,14 @@ import { ApiTags } from '@nestjs/swagger'; import { - Controller, Get, Param, UseInterceptors, + Controller, Get, UseInterceptors, } from '@nestjs/common'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor'; -import { AppTool } from 'src/models'; import { DatabaseInfoService } from 'src/modules/database/database-info.service'; import { DatabaseOverview } from 'src/modules/database/models/database-overview'; import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Database Instances') @Controller('databases') @@ -30,12 +31,11 @@ export class DatabaseInfoController { ], }) async getInfo( - @Param('id') id: string, + @ClientMetadataFromRequest({ + paramPath: 'id', + }) clientMetadata: ClientMetadata, ): Promise { - return this.databaseInfoService.getInfo( - id, - AppTool.Common, - ); + return this.databaseInfoService.getInfo(clientMetadata); } @Get(':id/overview') @@ -52,8 +52,10 @@ export class DatabaseInfoController { ], }) async getDatabaseOverview( - @Param('id') id: string, + @ClientMetadataFromRequest({ + paramPath: 'id', + }) clientMetadata: ClientMetadata, ): Promise { - return this.databaseInfoService.getOverview(id); + return this.databaseInfoService.getOverview(clientMetadata); } } diff --git a/redisinsight/api/src/modules/database/database-info.service.spec.ts b/redisinsight/api/src/modules/database/database-info.service.spec.ts index 7b5c1ae930..5a0cda8ec5 100644 --- a/redisinsight/api/src/modules/database/database-info.service.spec.ts +++ b/redisinsight/api/src/modules/database/database-info.service.spec.ts @@ -1,13 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { - mockDatabase, + mockCommonClientMetadata, mockDatabaseConnectionService, mockDatabaseInfoProvider, mockDatabaseOverview, mockDatabaseOverviewProvider, mockRedisGeneralInfo, } from 'src/__mocks__'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { AppTool } from 'src/models'; import { DatabaseInfoService } from 'src/modules/database/database-info.service'; import { DatabaseOverviewProvider } from 'src/modules/database/providers/database-overview.provider'; @@ -40,13 +39,13 @@ describe('DatabaseConnectionService', () => { describe('getInfo', () => { it('should create client and get general info', async () => { - expect(await service.getInfo(mockDatabase.id, AppTool.Common)).toEqual(mockRedisGeneralInfo); + expect(await service.getInfo(mockCommonClientMetadata)).toEqual(mockRedisGeneralInfo); }); }); describe('getOverview', () => { it('should create client and get overview', async () => { - expect(await service.getOverview(mockDatabase.id)).toEqual(mockDatabaseOverview); + expect(await service.getOverview(mockCommonClientMetadata)).toEqual(mockDatabaseOverview); }); }); }); diff --git a/redisinsight/api/src/modules/database/database-info.service.ts b/redisinsight/api/src/modules/database/database-info.service.ts index add770da8a..28fc3d1792 100644 --- a/redisinsight/api/src/modules/database/database-info.service.ts +++ b/redisinsight/api/src/modules/database/database-info.service.ts @@ -5,6 +5,7 @@ import { DatabaseOverviewProvider } from 'src/modules/database/providers/databas import { DatabaseOverview } from 'src/modules/database/models/database-overview'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class DatabaseInfoService { @@ -18,19 +19,12 @@ export class DatabaseInfoService { /** * Get database general info - * @param databaseId - * @param namespace + * @param clientMetadata */ - public async getInfo( - databaseId: string, - namespace = AppTool.Common, - ): Promise { - this.logger.log(`Getting database info for: ${databaseId}`); + public async getInfo(clientMetadata: ClientMetadata): Promise { + this.logger.log(`Getting database info for: ${clientMetadata.databaseId}`); - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId, - namespace, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); return this.databaseInfoProvider.getRedisGeneralInfo(client); } @@ -38,16 +32,13 @@ export class DatabaseInfoService { /** * Get redis database overview * - * @param databaseId + * @param clientMetadata */ - public async getOverview(databaseId: string): Promise { - this.logger.log(`Getting database overview for: ${databaseId}`); + public async getOverview(clientMetadata: ClientMetadata): Promise { + this.logger.log(`Getting database overview for: ${clientMetadata.databaseId}`); - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId, - namespace: AppTool.Common, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); - return this.databaseOverviewProvider.getOverview(databaseId, client); + return this.databaseOverviewProvider.getOverview(clientMetadata.databaseId, client); } } diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index af19aa4f0f..db97bee87f 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -8,13 +8,13 @@ import { Database } from 'src/modules/database/models/database'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor'; -import { AppTool } from 'src/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; import { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto'; import { BuildType } from 'src/modules/server/models/server'; import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dto'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; @ApiTags('Database Instances') @Controller('databases') @@ -160,11 +160,10 @@ export class DatabaseController { }) @UsePipes(new ValidationPipe({ transform: true })) async connect( - @Param('id') id: string, + @ClientMetadataFromRequest({ + paramPath: 'id', + }) clientMetadata, ): Promise { - await this.connectionService.connect( - id, - AppTool.Common, - ); + await this.connectionService.connect(clientMetadata); } } diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 12a43577c5..660479599a 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -17,7 +17,7 @@ import { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto' import { AppRedisInstanceEvents } from 'src/constants'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; -import { AppTool } from 'src/models'; +import { ClientContext } from 'src/common/models'; @Injectable() export class DatabaseService { @@ -89,7 +89,7 @@ export class DatabaseService { // todo: clarify if we need this and if yes - rethink implementation try { - const client = await this.redisService.connectToDatabaseInstance(database, AppTool.Common); + const client = await this.redisService.connectToDatabaseInstance(database, ClientContext.Common); const redisInfo = await this.databaseInfoProvider.getRedisGeneralInfo(client); this.analytics.sendInstanceAddedEvent(database, redisInfo); await client.disconnect(); @@ -130,7 +130,7 @@ export class DatabaseService { database = await this.repository.update(id, database); // todo: rethink - this.redisService.removeClientInstance({ instanceId: id }); + this.redisService.removeClientInstance({ databaseId: id }); this.analytics.sendInstanceEditedEvent( oldDatabase, database, @@ -156,7 +156,7 @@ export class DatabaseService { try { await this.repository.delete(id); // todo: rethink - this.redisService.removeClientInstance({ instanceId: id }); + this.redisService.removeClientInstance({ databaseId: id }); this.logger.log('Succeed to delete database instance.'); this.analytics.sendInstanceDeletedEvent(database); diff --git a/redisinsight/api/src/modules/database/providers/database.factory.ts b/redisinsight/api/src/modules/database/providers/database.factory.ts index a8623558cf..c79e489393 100644 --- a/redisinsight/api/src/modules/database/providers/database.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.factory.ts @@ -3,7 +3,7 @@ import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { catchRedisConnectionError, getHostingProvider } from 'src/utils'; import { Database } from 'src/modules/database/models/database'; import * as IORedis from 'ioredis'; -import { AppTool } from 'src/models'; +import { ClientContext } from 'src/common/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { RedisService } from 'src/modules/redis/redis.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; @@ -31,7 +31,7 @@ export class DatabaseFactory { const client = await this.redisService.createStandaloneClient( database, - AppTool.Common, + ClientContext.Common, false, ); @@ -140,7 +140,7 @@ export class DatabaseFactory { const sentinelClient = await this.redisService.createSentinelClient( model, selectedMaster.nodes, - AppTool.Common, + ClientContext.Common, ); model.connectionType = ConnectionType.SENTINEL; diff --git a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts index 72ac5b58b9..e08928579c 100644 --- a/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts +++ b/redisinsight/api/src/modules/profiler/providers/redis-observer.provider.ts @@ -2,11 +2,11 @@ import * as IORedis from 'ioredis'; import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common'; import { RedisObserver } from 'src/modules/profiler/models/redis.observer'; import { RedisObserverStatus } from 'src/modules/profiler/constants'; -import { AppTool } from 'src/models'; import { withTimeout } from 'src/utils/promise-with-timeout'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import ERROR_MESSAGES from 'src/constants/error-messages'; import config from 'src/utils/config'; +import { ClientContext, ClientMetadata } from 'src/common/models'; const serverConfig = config.get('server'); @@ -35,8 +35,13 @@ export class RedisObserverProvider { redisObserver = new RedisObserver(); this.redisObservers.set(instanceId, redisObserver); + // todo: add multi user support // initialize redis observer - redisObserver.init(this.getRedisClientFn(instanceId)).catch(); + redisObserver.init(this.getRedisClientFn({ + session: undefined, + databaseId: instanceId, + context: ClientContext.Common, + })).catch(); } else { switch (redisObserver.status) { case RedisObserverStatus.Ready: @@ -46,8 +51,13 @@ export class RedisObserverProvider { case RedisObserverStatus.End: case RedisObserverStatus.Error: this.logger.debug(`Trying to reconnect. Current status: ${redisObserver.status}`); + // todo: add multiuser support // try to reconnect - redisObserver.init(this.getRedisClientFn(instanceId)).catch(); + redisObserver.init(this.getRedisClientFn({ + session: undefined, + databaseId: instanceId, + context: ClientContext.Common, + })).catch(); break; case RedisObserverStatus.Initializing: case RedisObserverStatus.Wait: @@ -90,15 +100,12 @@ export class RedisObserverProvider { /** * Get Redis existing common IORedis client for instance or create a new common connection - * @param databaseId + * @param clientMetadata * @private */ - private getRedisClientFn(databaseId: string): () => Promise { + private getRedisClientFn(clientMetadata: ClientMetadata): () => Promise { return async () => withTimeout( - this.databaseConnectionService.getOrCreateClient({ - databaseId, - namespace: AppTool.Common, - }), + this.databaseConnectionService.getOrCreateClient(clientMetadata), serverConfig.requestTimeout, new ServiceUnavailableException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB), ); diff --git a/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.spec.ts b/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.spec.ts index d6dbfb358f..d8afc2eebd 100644 --- a/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.spec.ts @@ -1,13 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { mockDatabaseConnectionService, mockSocket } from 'src/__mocks__'; -import { UserClient } from 'src/modules/pub-sub/model/user-client'; +import { mockCommonClientMetadata, mockDatabaseConnectionService } from 'src/__mocks__'; import { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider'; import { RedisService } from 'src/modules/redis/redis.service'; import { RedisClient } from 'src/modules/pub-sub/model/redis-client'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -const mockUserClient = new UserClient('socketId', mockSocket, 'databaseId'); - describe('RedisClientProvider', () => { let service: RedisClientProvider; @@ -33,7 +30,7 @@ describe('RedisClientProvider', () => { describe('createClient', () => { it('should create redis client', async () => { - const redisClient = service.createClient(mockUserClient.getId()); + const redisClient = service.createClient(mockCommonClientMetadata); expect(redisClient).toBeInstanceOf(RedisClient); }); }); diff --git a/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.ts b/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.ts index 03d32b2dd2..838b55066e 100644 --- a/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.ts +++ b/redisinsight/api/src/modules/pub-sub/providers/redis-client.provider.ts @@ -1,10 +1,10 @@ import { Injectable, ServiceUnavailableException } from '@nestjs/common'; -import { AppTool } from 'src/models'; import { withTimeout } from 'src/utils/promise-with-timeout'; import ERROR_MESSAGES from 'src/constants/error-messages'; import config from 'src/utils/config'; import { RedisClient } from 'src/modules/pub-sub/model/redis-client'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { ClientMetadata } from 'src/common/models'; const serverConfig = config.get('server'); @@ -14,16 +14,13 @@ export class RedisClientProvider { private databaseConnectionService: DatabaseConnectionService, ) {} - createClient(databaseId: string): RedisClient { - return new RedisClient(databaseId, this.getConnectFn(databaseId)); + createClient(clientMetadata: ClientMetadata): RedisClient { + return new RedisClient(clientMetadata.databaseId, this.getConnectFn(clientMetadata)); } - private getConnectFn(databaseId: string) { + private getConnectFn(clientMetadata: ClientMetadata) { return () => withTimeout( - this.databaseConnectionService.createClient({ - databaseId, - namespace: AppTool.Common, - }), + this.databaseConnectionService.createClient(clientMetadata), serverConfig.requestTimeout, new ServiceUnavailableException(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB), ); diff --git a/redisinsight/api/src/modules/pub-sub/providers/user-session.provider.ts b/redisinsight/api/src/modules/pub-sub/providers/user-session.provider.ts index 8abb03ad76..b068953d14 100644 --- a/redisinsight/api/src/modules/pub-sub/providers/user-session.provider.ts +++ b/redisinsight/api/src/modules/pub-sub/providers/user-session.provider.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { UserSession } from 'src/modules/pub-sub/model/user-session'; import { UserClient } from 'src/modules/pub-sub/model/user-client'; import { RedisClientProvider } from 'src/modules/pub-sub/providers/redis-client.provider'; +import { ClientContext } from 'src/common/models'; @Injectable() export class UserSessionProvider { @@ -17,7 +18,12 @@ export class UserSessionProvider { if (!session) { session = new UserSession( userClient, - this.redisClientProvider.createClient(userClient.getDatabaseId()), + // todo: add multi user support + this.redisClientProvider.createClient({ + session: undefined, + databaseId: userClient.getDatabaseId(), + context: ClientContext.Common, + }), ); this.sessions.set(session.getId(), session); this.logger.debug(`New session was added ${this}`); diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts index 8ab37fda43..fbfa87c67f 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts @@ -8,6 +8,8 @@ import { PubSubService } from 'src/modules/pub-sub/pub-sub.service'; import { AppTool } from 'src/models'; import { PublishDto } from 'src/modules/pub-sub/dto/publish.dto'; import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Pub/Sub') @Controller('pub-sub') @@ -28,12 +30,9 @@ export class PubSubController { ], }) async publish( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata: ClientMetadata, @Body() dto: PublishDto, ): Promise { - return this.service.publish({ - instanceId, - tool: AppTool.Common, - }, dto); + return this.service.publish(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts index a35e4ed310..875bff1a83 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.spec.ts @@ -1,13 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { mockSocket, - mockDatabase, MockType, mockPubSubAnalyticsService, mockDatabaseConnectionService, mockIORedisClient, + mockCommonClientMetadata, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { PubSubService } from 'src/modules/pub-sub/pub-sub.service'; import { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session.provider'; import { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider'; @@ -41,10 +40,6 @@ mockUserSession['subscribe'] = mockSubscribe; mockUserSession['unsubscribe'] = mockUnsubscribe; mockUserSession['destroy'] = jest.fn(); -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockPublishDto = { message: 'message-a', channel: 'channel-a', @@ -154,14 +149,14 @@ describe('PubSubService', () => { describe('publish', () => { it('should publish using existing client', async () => { - const res = await service.publish(mockClientOptions, mockPublishDto); + const res = await service.publish(mockCommonClientMetadata, mockPublishDto); expect(res).toEqual({ affected: 2 }); }); it('should throw an error when client not found during publishing', async () => { databaseConnectionService.getOrCreateClient.mockRejectedValueOnce(new NotFoundException('Not Found')); try { - await service.publish(mockClientOptions, mockPublishDto); + await service.publish(mockCommonClientMetadata, mockPublishDto); fail(); } catch (e) { expect(e).toBeInstanceOf(NotFoundException); @@ -171,7 +166,7 @@ describe('PubSubService', () => { databaseConnectionService.getOrCreateClient.mockRejectedValueOnce(new Error('NOPERM')); try { - await service.publish(mockClientOptions, mockPublishDto); + await service.publish(mockCommonClientMetadata, mockPublishDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts index f2605551c2..b1d7ec0316 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.service.ts @@ -3,12 +3,12 @@ import { UserSessionProvider } from 'src/modules/pub-sub/providers/user-session. import { UserClient } from 'src/modules/pub-sub/model/user-client'; import { SubscribeDto } from 'src/modules/pub-sub/dto'; import { SubscriptionProvider } from 'src/modules/pub-sub/providers/subscription.provider'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; import { PublishDto } from 'src/modules/pub-sub/dto/publish.dto'; import { PubSubAnalyticsService } from 'src/modules/pub-sub/pub-sub.analytics.service'; import { catchAclError } from 'src/utils'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class PubSubService { @@ -73,24 +73,18 @@ export class PubSubService { /** * Publish a message to a particular channel - * @param clientOptions + * @param clientMetadata * @param dto */ - async publish( - clientOptions: IFindRedisClientInstanceByOptions, - dto: PublishDto, - ): Promise { + async publish(clientMetadata: ClientMetadata, dto: PublishDto): Promise { try { this.logger.log('Publishing message.'); - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const affected = await client.publish(dto.channel, dto.message); - this.analyticsService.sendMessagePublishedEvent(clientOptions.instanceId, affected); + this.analyticsService.sendMessagePublishedEvent(clientMetadata.databaseId, affected); return { affected, diff --git a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts index 540d5a6292..8807e324ff 100644 --- a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts +++ b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts @@ -4,9 +4,8 @@ import { import { CreateSentinelDatabaseResponse } from 'src/modules/redis-sentinel/dto/create.sentinel.database.response'; import { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto'; import { RedisService } from 'src/modules/redis/redis.service'; -import { AppTool } from 'src/models'; import { Database } from 'src/modules/database/models/database'; -import { ActionStatus } from 'src/common/models'; +import { ActionStatus, ClientContext } from 'src/common/models'; import { DatabaseService } from 'src/modules/database/database.service'; import { getRedisConnectionException } from 'src/utils'; import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master'; @@ -113,7 +112,7 @@ export class RedisSentinelService { this.logger.log('Connection and getting sentinel masters.'); let result: SentinelMaster[]; try { - const client = await this.redisService.createStandaloneClient(dto, AppTool.Common, false); + const client = await this.redisService.createStandaloneClient(dto, ClientContext.Common, false); result = await this.databaseInfoProvider.determineSentinelMasterGroups(client); this.redisSentinelAnalytics.sendGetSentinelMastersSucceedEvent(result); diff --git a/redisinsight/api/src/modules/redis/models/client-metadata.ts b/redisinsight/api/src/modules/redis/models/client-metadata.ts deleted file mode 100644 index b0d487a9f5..0000000000 --- a/redisinsight/api/src/modules/redis/models/client-metadata.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { AppTool } from 'src/models'; - -export class ClientMetadata { - databaseId: string; - - userId?: string; - - sessionId?: string; - - uuid?: string; - - namespace: AppTool = AppTool.Common; -} diff --git a/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.spec.ts b/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.spec.ts index 033e66f35f..5eee95297f 100644 --- a/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.spec.ts +++ b/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.spec.ts @@ -3,26 +3,23 @@ import { BadRequestException } from '@nestjs/common'; import * as Redis from 'ioredis-mock'; import { v4 as uuidv4 } from 'uuid'; import { mockDatabase, mockDatabaseService } from 'src/__mocks__'; -import { AppTool } from 'src/models'; -import { - IFindRedisClientInstanceByOptions, - IRedisClientInstance, - RedisService, -} from 'src/modules/redis/redis.service'; +import { IRedisClientInstance, RedisService } from 'src/modules/redis/redis.service'; import { BrowserToolService } from 'src/modules/browser/services/browser-tool/browser-tool.service'; import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; import { RedisConsumerAbstractService } from 'src/modules/redis/redis-consumer.abstract.service'; import { ClientNotFoundErrorException } from 'src/modules/redis/exceptions/client-not-found-error.exception'; import { DatabaseService } from 'src/modules/database/database.service'; +import { ClientContext, ClientMetadata } from 'src/common/models'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, +const mockClientMetadata: ClientMetadata = { + session: undefined, + databaseId: mockDatabase.id, + context: ClientContext.Browser, }; export const mockRedisClientInstance: IRedisClientInstance = { - uuid: uuidv4(), - tool: AppTool.Browser, - instanceId: mockClientOptions.instanceId, + ...mockClientMetadata, + uniqueId: uuidv4(), client: new Redis(), lastTimeUsed: 1619791508019, }; @@ -68,7 +65,7 @@ describe('RedisConsumerAbstractService', () => { mockRedisClientInstance.client, ); - const result = await consumerInstance.getRedisClient(mockClientOptions); + const result = await consumerInstance.getRedisClient(mockClientMetadata); expect(result).toEqual(mockRedisClientInstance.client); expect(consumerInstance.createNewClient).toHaveBeenCalled(); @@ -77,7 +74,7 @@ describe('RedisConsumerAbstractService', () => { redisService.getClientInstance.mockReturnValue(mockRedisClientInstance); redisService.isClientConnected.mockReturnValue(true); - const result = await consumerInstance.getRedisClient(mockClientOptions); + const result = await consumerInstance.getRedisClient(mockClientMetadata); expect(result).toEqual(mockRedisClientInstance.client); expect(consumerInstance.createNewClient).not.toHaveBeenCalled(); @@ -90,7 +87,7 @@ describe('RedisConsumerAbstractService', () => { mockRedisClientInstance.client, ); - const result = await consumerInstance.getRedisClient(mockClientOptions); + const result = await consumerInstance.getRedisClient(mockClientMetadata); expect(result).toEqual(mockRedisClientInstance.client); expect(consumerInstance.createNewClient).toHaveBeenCalled(); @@ -101,7 +98,7 @@ describe('RedisConsumerAbstractService', () => { await expect( consumerInstance.getRedisClient({ - ...mockClientOptions, + ...mockClientMetadata, }), ).resolves.not.toThrow(); @@ -116,7 +113,7 @@ describe('RedisConsumerAbstractService', () => { await expect( consumerInstance.getRedisClient({ - ...mockClientOptions, + ...mockClientMetadata, dbNumber: 1, }), ).rejects.toThrow(error); @@ -126,11 +123,11 @@ describe('RedisConsumerAbstractService', () => { // @ts-ignore class Tool extends RedisConsumerAbstractService { constructor() { - super(AppTool.CLI, redisService, instancesBusinessService, { enableAutoConnection: false }); + super(ClientContext.CLI, redisService, instancesBusinessService, { enableAutoConnection: false }); } } - await expect(new Tool().getRedisClient(mockClientOptions)) + await expect(new Tool().getRedisClient(mockClientMetadata)) .rejects.toThrow(new ClientNotFoundErrorException()); }); }); @@ -142,7 +139,7 @@ describe('RedisConsumerAbstractService', () => { ); const result = await consumerInstance.createNewClient( - mockRedisClientInstance.instanceId, + mockRedisClientInstance.databaseId, ); expect(result).toEqual(mockRedisClientInstance.client); @@ -154,7 +151,7 @@ describe('RedisConsumerAbstractService', () => { redisService.connectToDatabaseInstance.mockRejectedValue(error); await expect( - consumerInstance.createNewClient(mockRedisClientInstance.instanceId), + consumerInstance.createNewClient(mockRedisClientInstance.databaseId), ).rejects.toThrow(error); }); }); diff --git a/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.ts b/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.ts index d434954b88..7ca4a39f6c 100644 --- a/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.ts +++ b/redisinsight/api/src/modules/redis/redis-consumer.abstract.service.ts @@ -1,30 +1,30 @@ import * as IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; -import { AppTool, ReplyError, IRedisConsumer } from 'src/models'; +import { ReplyError, IRedisConsumer } from 'src/models'; import { catchRedisConnectionError, generateRedisConnectionName, getConnectionNamespace, } from 'src/utils'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; import { ClientNotFoundErrorException } from 'src/modules/redis/exceptions/client-not-found-error.exception'; import { IRedisToolOptions, DEFAULT_REDIS_TOOL_OPTIONS } from 'src/modules/redis/redis-tool-options'; import { DatabaseService } from 'src/modules/database/database.service'; +import { ClientContext, ClientMetadata } from 'src/common/models'; export abstract class RedisConsumerAbstractService implements IRedisConsumer { protected redisService: RedisService; protected databaseService: DatabaseService; - protected consumer: AppTool; + protected consumer: ClientContext; private readonly options: IRedisToolOptions = DEFAULT_REDIS_TOOL_OPTIONS; protected constructor( - consumer: AppTool, + consumer: ClientContext, redisService: RedisService, databaseService: DatabaseService, options: IRedisToolOptions = {}, @@ -36,13 +36,13 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { } abstract execCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: any, args: Array, ): any; abstract execPipeline( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommands: Array<[toolCommand: any, ...args: Array]>, ): Promise<[ReplyError | null, any]>; @@ -94,34 +94,29 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { }); } - async getRedisClient( - options: IFindRedisClientInstanceByOptions, - ): Promise { + async getRedisClient(clientMetadata: ClientMetadata): Promise { const redisClientInstance = this.redisService.getClientInstance({ - ...options, - tool: this.consumer, + ...clientMetadata, + context: this.consumer, }); if (!redisClientInstance || !this.redisService.isClientConnected(redisClientInstance.client)) { this.redisService.removeClientInstance({ - instanceId: redisClientInstance?.instanceId, - tool: this.consumer, + databaseId: redisClientInstance?.databaseId, + context: this.consumer, }); if (!this.options.enableAutoConnection) throw new ClientNotFoundErrorException(); - return await this.createNewClient( - options.instanceId, - options.uuid, - ); + return await this.createNewClient(clientMetadata); } return redisClientInstance.client; } - getRedisClientNamespace(options: IFindRedisClientInstanceByOptions): string { + getRedisClientNamespace(clientMetadata: ClientMetadata): string { try { const clientInstance = this.redisService.getClientInstance({ - ...options, - tool: this.consumer, + ...clientMetadata, + context: this.consumer, }); return clientInstance?.client ? getConnectionNamespace(clientInstance.client) : ''; } catch (e) { @@ -129,13 +124,14 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { } } - protected async createNewClient( - instanceId: string, - uuid = uuidv4(), - namespace?: string, - ): Promise { - const instanceDto = await this.databaseService.get(instanceId); - const connectionName = generateRedisConnectionName(namespace || this.consumer, uuid); + protected async createNewClient(clientMetadata: ClientMetadata): Promise { + const instanceDto = await this.databaseService.get(clientMetadata.databaseId); + const uniqueId = clientMetadata.uniqueId || uuidv4(); + const connectionName = generateRedisConnectionName( + clientMetadata.context || this.consumer, + uniqueId, + ); + try { const client = await this.redisService.connectToDatabaseInstance( instanceDto, @@ -144,9 +140,9 @@ export abstract class RedisConsumerAbstractService implements IRedisConsumer { ); this.redisService.setClientInstance( { - uuid, - instanceId, - tool: this.consumer, + ...clientMetadata, + uniqueId, + context: clientMetadata.context || this.consumer, }, client, ); diff --git a/redisinsight/api/src/modules/redis/redis-tool.factory.ts b/redisinsight/api/src/modules/redis/redis-tool.factory.ts index 0db5cf7b09..8a4f1f795e 100644 --- a/redisinsight/api/src/modules/redis/redis-tool.factory.ts +++ b/redisinsight/api/src/modules/redis/redis-tool.factory.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { AppTool } from 'src/models'; +import { ClientContext } from 'src/common/models'; import { RedisService } from 'src/modules/redis/redis.service'; import { RedisToolService } from 'src/modules/redis/redis-tool.service'; import { IRedisToolOptions } from 'src/modules/redis/redis-tool-options'; @@ -12,7 +12,7 @@ export class RedisToolFactory { protected databaseService: DatabaseService, ) {} - createRedisTool(appTool: AppTool, options: IRedisToolOptions = {}) { - return new RedisToolService(appTool, this.redisService, this.databaseService, options); + createRedisTool(clientContext: ClientContext, options: IRedisToolOptions = {}) { + return new RedisToolService(clientContext, this.redisService, this.databaseService, options); } } diff --git a/redisinsight/api/src/modules/redis/redis-tool.service.ts b/redisinsight/api/src/modules/redis/redis-tool.service.ts index 4daa5431c8..44e93825d8 100644 --- a/redisinsight/api/src/modules/redis/redis-tool.service.ts +++ b/redisinsight/api/src/modules/redis/redis-tool.service.ts @@ -2,10 +2,10 @@ import { Logger } from '@nestjs/common'; import * as Redis from 'ioredis'; import * as IORedis from 'ioredis'; import { v4 as uuidv4 } from 'uuid'; -import { AppTool, ReplyError } from 'src/models'; +import { ReplyError } from 'src/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; +import { ClientContext, ClientMetadata } from 'src/common/models'; import { - IFindRedisClientInstanceByOptions, RedisService, } from 'src/modules/redis/redis.service'; import { RedisConsumerAbstractService } from 'src/modules/redis/redis-consumer.abstract.service'; @@ -34,7 +34,7 @@ export class RedisToolService extends RedisConsumerAbstractService { private logger: Logger; constructor( - private appTool: AppTool, + private appTool: ClientContext, protected redisService: RedisService, protected databaseService: DatabaseService, options: IRedisToolOptions = {}, @@ -44,12 +44,12 @@ export class RedisToolService extends RedisConsumerAbstractService { } async execCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: string, args: Array, replyEncoding?: BufferEncoding, ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); this.logger.log(`Execute command '${toolCommand}', connectionName: ${getConnectionName(client)}`); const [command, ...commandArgs] = toolCommand.split(' '); return client.sendCommand( @@ -60,7 +60,7 @@ export class RedisToolService extends RedisConsumerAbstractService { } async execCommandForNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: string, args: Array, nodeRole: ClusterNodeRole, @@ -68,7 +68,7 @@ export class RedisToolService extends RedisConsumerAbstractService { ): Promise { const [command, ...commandArgs] = toolCommand.split(' '); const nodes: IORedis.Redis[] = await this.getClusterNodes( - clientOptions, + clientMetadata, nodeRole, ); return await Promise.all( @@ -103,7 +103,7 @@ export class RedisToolService extends RedisConsumerAbstractService { } async execCommandForNode( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, toolCommand: string, args: Array, nodeRole: ClusterNodeRole, @@ -112,7 +112,7 @@ export class RedisToolService extends RedisConsumerAbstractService { ): Promise { const [command, ...commandArgs] = toolCommand.split(' '); const nodes: IORedis.Redis[] = await this.getClusterNodes( - clientOptions, + clientMetadata, nodeRole, ); let node: any = nodes.find((item: IORedis.Redis) => { @@ -156,37 +156,36 @@ export class RedisToolService extends RedisConsumerAbstractService { throw new Error('CLI ERROR: Pipeline not supported'); } - async createNewToolClient(instanceId: string, namespace: string): Promise { - const uuid = uuidv4(); - await this.createNewClient(instanceId, uuid, namespace); + async createNewToolClient(clientMetadata: ClientMetadata): Promise { + const uniqueId = uuidv4(); + await this.createNewClient({ + ...clientMetadata, + uniqueId, + }); - return uuid; + return uniqueId; } - async reCreateToolClient(instanceId: string, uuid: string, namespace: string): Promise { - this.redisService.removeClientInstance({ - instanceId, - uuid, - tool: this.consumer, - }); - await this.createNewClient(instanceId, uuid, namespace); + async reCreateToolClient(clientMetadata: ClientMetadata): Promise { + this.redisService.removeClientInstance(clientMetadata); + await this.createNewClient(clientMetadata); - return uuid; + return clientMetadata.uniqueId; } - async deleteToolClient(instanceId: string, uuid: string): Promise { + async deleteToolClient(databaseId: string, uniqueId: string): Promise { return this.redisService.removeClientInstance({ - instanceId, - uuid, - tool: this.consumer, + databaseId, + uniqueId, + context: this.consumer, }); } private async getClusterNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, role: ClusterNodeRole, ): Promise { - const client = await this.getRedisClient(clientOptions); + const client = await this.getRedisClient(clientMetadata); if (!(client instanceof IORedis.Cluster)) { throw new WrongDatabaseTypeError(ERROR_MESSAGES.WRONG_DATABASE_TYPE); } diff --git a/redisinsight/api/src/modules/redis/redis.service.spec.ts b/redisinsight/api/src/modules/redis/redis.service.spec.ts index 8ae0dafc78..720a173ecd 100644 --- a/redisinsight/api/src/modules/redis/redis.service.spec.ts +++ b/redisinsight/api/src/modules/redis/redis.service.spec.ts @@ -1,430 +1,405 @@ -xdescribe('', () => { it('', () => {}); }); -// import { Test, TestingModule } from '@nestjs/testing'; -// import * as Redis from 'ioredis-mock'; -// import { v4 as uuidv4 } from 'uuid'; -// import { ConnectionOptions } from 'tls'; -// import { -// mockCaCertDto, -// mockCaCertEntity, -// mockCaCertificatesService, -// mockClientCertDto, -// mockClientCertEntity, -// mockClientCertificatesService, -// mockOSSClusterDatabaseEntity, -// mockSentinelDatabaseEntity, -// mockStandaloneDatabaseEntity, -// MockType, -// } from 'src/__mocks__'; -// import { AppTool, ReplyError } from 'src/models'; -// import { convertEntityToDto } from 'src/modules/shared/utils/database-entity-converter'; -// import { mockRedisClientInstance } from 'src/modules/redis/redis-consumer.abstract.service.spec'; -// import { IFindRedisClientInstanceByOptions, RedisService } from './redis.service'; -// import { CaCertBusinessService } from '../certificates/ca-cert-business/ca-cert-business.service'; -// import { ClientCertBusinessService } from '../certificates/client-cert-business/client-cert-business.service'; -// -// jest.mock('ioredis'); -// -// const mockTlsConfigResult: ConnectionOptions = { -// rejectUnauthorized: true, -// servername: mockStandaloneDatabaseEntity.tlsServername, -// checkServerIdentity: () => undefined, -// ca: [mockCaCertDto.cert], -// key: mockClientCertDto.key, -// cert: mockClientCertDto.cert, -// }; -// -// const removeNullsFromDto = (dto): any => { -// const result = dto; -// Object.keys(dto).forEach((key: string) => { -// if (result[key] === null) { -// delete result[key]; -// } -// }); -// -// return result; -// }; -// -// describe('RedisService', () => { -// let service; -// let caCertBusinessService: MockType; -// let clientCertBusinessService: MockType; -// -// beforeEach(async () => { -// const module: TestingModule = await Test.createTestingModule({ -// providers: [ -// RedisService, -// { -// provide: CaCertBusinessService, -// useFactory: mockCaCertificatesService, -// }, -// { -// provide: ClientCertBusinessService, -// useFactory: mockClientCertificatesService, -// }, -// ], -// }).compile(); -// -// service = await module.get(RedisService); -// caCertBusinessService = module.get(CaCertBusinessService); -// clientCertBusinessService = module.get(ClientCertBusinessService); -// }); -// -// it('should be defined', () => { -// expect(service.clients).toEqual([]); -// }); -// -// describe('connectToDatabaseInstance', () => { -// beforeEach(async () => { -// service.clients = []; -// }); -// it('should create standalone client', async () => { -// const mockClient = new Redis(); -// const dto = convertEntityToDto(mockStandaloneDatabaseEntity); -// service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); -// -// const result = await service.connectToDatabaseInstance(dto); -// -// expect(result).toEqual(mockClient); -// expect(service.createStandaloneClient).toHaveBeenCalledWith(dto, AppTool.Common, true, undefined); -// }); -// it('should create cluster client', async () => { -// const mockClient = new Redis.Cluster([ -// 'redis://localhost:7001', -// 'redis://localhost:7002', -// ]); -// const dto = removeNullsFromDto(convertEntityToDto(mockOSSClusterDatabaseEntity)); -// -// const { endpoints, connectionType, ...options } = dto; -// service.createClusterClient = jest.fn().mockResolvedValue(mockClient); -// -// const result = await service.connectToDatabaseInstance(dto); -// -// expect(result).toEqual(mockClient); -// expect(service.createClusterClient).toHaveBeenCalledWith( -// options, -// endpoints, -// true, -// undefined, -// ); -// }); -// it('should create sentinel client', async () => { -// const mockClient = new Redis(); -// const dto = removeNullsFromDto(convertEntityToDto(mockSentinelDatabaseEntity)); -// Object.keys(dto).forEach((key: string) => { -// if (dto[key] === null) { -// delete dto[key]; -// } -// }); -// const { endpoints, connectionType, ...options } = dto; -// service.createSentinelClient = jest.fn().mockResolvedValue(mockClient); -// -// const result = await service.connectToDatabaseInstance(dto); -// -// expect(result).toEqual(mockClient); -// expect(service.createSentinelClient).toHaveBeenCalledWith( -// options, -// endpoints, -// AppTool.Common, -// true, -// undefined, -// ); -// }); -// it('should select redis database by number', async () => { -// const mockClient = new Redis(); -// mockClient.call = jest.fn(); -// const dto = convertEntityToDto(mockStandaloneDatabaseEntity); -// service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); -// -// await service.connectToDatabaseInstance(dto, AppTool.Common); -// -// expect(service.createStandaloneClient).toHaveBeenCalledWith(dto, AppTool.Common, true, undefined); -// }); -// it('should throw error db index is out of range', async () => { -// const replyError: ReplyError = { -// name: 'ReplyError', -// message: '(error) DB index is out of range', -// command: 'SELECT', -// }; -// service.createStandaloneClient = jest.fn().mockRejectedValue(replyError); -// -// try { -// await service.connectToDatabaseInstance( -// convertEntityToDto(mockStandaloneDatabaseEntity), -// ); -// fail('Should throw an error'); -// } catch (err) { -// expect(err).toEqual(replyError); -// } -// expect(service.clients.length).toEqual(0); -// }); -// it('connection error [Connection details are incorrect]', async () => { -// service.createStandaloneClient = jest -// .fn() -// .mockRejectedValue(new Error('ENOTFOUND some message')); -// -// try { -// await service.connectToDatabaseInstance( -// convertEntityToDto(mockStandaloneDatabaseEntity), -// 0, -// ); -// fail('Should throw an error'); -// } catch (err) { -// expect(err.message).toEqual('ENOTFOUND some message'); -// expect(service.clients.length).toEqual(0); -// } -// }); -// }); -// -// describe('getClientInstance', () => { -// beforeEach(() => { -// service.clients = [ -// { -// ...mockRedisClientInstance, tool: AppTool.Common, -// }, -// { -// ...mockRedisClientInstance, tool: AppTool.Browser, -// }, -// { -// ...mockRedisClientInstance, tool: AppTool.CLI, -// }, -// ]; -// }); -// it('should correctly find client instance for App.Common by instance id', () => { -// const newClient = { ...service.clients[0], tool: AppTool.Browser }; -// service.clients.push(newClient); -// const options = { -// instanceId: newClient.instanceId, -// }; -// -// const result = service.getClientInstance(options); -// -// expect(result).toEqual(service.clients[0]); -// }); -// it('should correctly find client instance by instance id and tool', () => { -// const options: IFindRedisClientInstanceByOptions = { -// instanceId: service.clients[0].instanceId, -// tool: AppTool.CLI, -// }; -// -// const result = service.getClientInstance(options); -// -// expect(result).toEqual(service.clients[2]); -// }); -// it('should correctly find client instance by instance id, tool and uuid', () => { -// const newClient = { ...mockRedisClientInstance, uuid: uuidv4(), tool: AppTool.CLI }; -// service.clients.push(newClient); -// const options: IFindRedisClientInstanceByOptions = { -// instanceId: newClient.instanceId, -// uuid: newClient.uuid, -// tool: newClient.tool, -// }; -// -// const result = service.getClientInstance(options); -// -// expect(result).toEqual(newClient); -// }); -// it('should return undefined', () => { -// const options: IFindRedisClientInstanceByOptions = { -// instanceId: 'invalid-instance-id', -// }; -// -// const result = service.getClientInstance(options); -// -// expect(result).toBeUndefined(); -// }); -// }); -// -// describe('removeClientInstance', () => { -// beforeEach(() => { -// service.clients = [ -// { -// ...mockRedisClientInstance, -// tool: AppTool.Common, -// }, -// { -// ...mockRedisClientInstance, -// tool: AppTool.Browser, -// }, -// ]; -// }); -// it('should remove only client for browser tool', () => { -// const options: IFindRedisClientInstanceByOptions = { -// instanceId: mockRedisClientInstance.instanceId, -// tool: AppTool.Browser, -// }; -// -// const result = service.removeClientInstance(options); -// -// expect(result).toEqual(1); -// expect(service.clients.length).toEqual(1); -// }); -// it('should remove all clients by instance id', () => { -// const options: IFindRedisClientInstanceByOptions = { -// instanceId: mockRedisClientInstance.instanceId, -// }; -// -// const result = service.removeClientInstance(options); -// -// expect(result).toEqual(2); -// expect(service.clients.length).toEqual(0); -// }); -// }); -// -// describe('setClientInstance', () => { -// beforeEach(() => { -// service.clients = [{ ...mockRedisClientInstance }]; -// }); -// it('should add new client', () => { -// const initialClientsCount = service.clients.length; -// const newClientInstance = { -// ...mockRedisClientInstance, -// instanceId: uuidv4(), -// }; -// -// const result = service.setClientInstance(newClientInstance); -// -// expect(result).toBe(1); -// expect(service.clients.length).toBe(initialClientsCount + 1); -// }); -// it('should replace exist client', () => { -// const initialClientsCount = service.clients.length; -// -// const result = service.setClientInstance(mockRedisClientInstance); -// -// expect(result).toBe(0); -// expect(service.clients.length).toBe(initialClientsCount); -// }); -// }); -// -// describe('isClientConnected', () => { -// const mockClient = new Redis(); -// it('should return true', async () => { -// mockClient.status = 'ready'; -// -// const result = service.isClientConnected(mockClient); -// -// expect(result).toEqual(true); -// }); -// it('should return false', async () => { -// mockClient.status = 'end'; -// -// const result = service.isClientConnected(mockClient); -// -// expect(result).toEqual(false); -// }); -// }); -// -// describe('getRedisConnectionConfig', () => { -// it('should return config with tls', async () => { -// service.getTLSConfig = jest.fn().mockResolvedValue(mockTlsConfigResult); -// const dto = convertEntityToDto(mockStandaloneDatabaseEntity); -// const { -// host, port, password, username, db, -// } = dto; -// -// const expectedResult = { -// host, port, username, password, db, tls: mockTlsConfigResult, -// }; -// -// const result = await service.getRedisConnectionConfig(dto); -// -// expect(JSON.stringify(result)).toEqual(JSON.stringify(expectedResult)); -// }); -// it('should return without tls', async () => { -// const dto = convertEntityToDto(mockStandaloneDatabaseEntity); -// delete dto.tls; -// const { -// host, port, password, username, db, -// } = dto; -// -// const expectedResult = { -// host, port, username, password, db, -// }; -// -// const result = await service.getRedisConnectionConfig(dto); -// -// expect(result).toEqual(expectedResult); -// }); -// }); -// -// describe('getTLSConfig', () => { -// it('should return tls config', async () => { -// service.getCaCertConfig = jest -// .fn() -// .mockResolvedValue({ ca: [mockCaCertDto.cert] }); -// service.getClientCertConfig = jest.fn().mockResolvedValue({ -// key: mockClientCertDto.key, -// cert: mockClientCertDto.cert, -// }); -// const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); -// -// const result = await service.getTLSConfig(tls); -// -// expect(JSON.stringify(result)).toEqual( -// JSON.stringify(mockTlsConfigResult), -// ); -// }); -// }); -// -// describe('getCaCertConfig', () => { -// it('should load exist cert', async () => { -// caCertBusinessService.getOneById = jest -// .fn() -// .mockResolvedValue(mockCaCertEntity); -// const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); -// -// const result = await service.getCaCertConfig(tls); -// -// expect(result).toEqual({ ca: [mockCaCertDto.cert] }); -// expect(caCertBusinessService.getOneById).toHaveBeenCalledWith( -// tls.caCertId, -// ); -// }); -// it('should return new cert', async () => { -// const result = await service.getCaCertConfig({ -// newCaCert: mockCaCertDto, -// }); -// -// expect(result).toEqual({ ca: [mockCaCertDto.cert] }); -// expect(caCertBusinessService.getOneById).not.toHaveBeenCalled(); -// }); -// it('should return null', async () => { -// const result = await service.getCaCertConfig({}); -// -// expect(result).toBeNull(); -// }); -// }); -// -// describe('getClientCertConfig', () => { -// const mockResult = { -// key: mockClientCertDto.key, -// cert: mockClientCertDto.cert, -// }; -// it('should load exist cert', async () => { -// clientCertBusinessService.getOneById = jest -// .fn() -// .mockResolvedValue(mockClientCertEntity); -// const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); -// -// const result = await service.getClientCertConfig(tls); -// -// expect(result).toEqual(mockResult); -// expect(clientCertBusinessService.getOneById).toHaveBeenCalledWith( -// tls.clientCertPairId, -// ); -// }); -// it('should return new cert', async () => { -// const result = await service.getClientCertConfig({ -// newClientCertPair: mockClientCertDto, -// }); -// -// expect(result).toEqual(mockResult); -// expect(clientCertBusinessService.getOneById).not.toHaveBeenCalled(); -// }); -// it('should return null', async () => { -// const result = await service.getClientCertConfig({}); -// -// expect(result).toBeNull(); -// }); -// }); -// }); +import { Test, TestingModule } from '@nestjs/testing'; +import * as Redis from 'ioredis-mock'; +import { v4 as uuidv4 } from 'uuid'; +import { ConnectionOptions } from 'tls'; +import { + mockCaCertificate, mockClientCertificate, mockClusterDatabaseWithTlsAuth, mockDatabase, + mockDatabaseEntity, mockSentinelDatabaseWithTlsAuth, + MockType +} from 'src/__mocks__'; +import { AppTool, ReplyError } from 'src/models'; +import { mockRedisClientInstance } from 'src/modules/redis/redis-consumer.abstract.service.spec'; +import { RedisService } from './redis.service'; + +jest.mock('ioredis'); + +const mockTlsConfigResult: ConnectionOptions = { + rejectUnauthorized: true, + servername: mockDatabaseEntity.tlsServername, + checkServerIdentity: () => undefined, + ca: [mockCaCertificate.certificate], + key: mockClientCertificate.key, + cert: mockClientCertificate.certificate, +}; + +const removeNullsFromDto = (dto): any => { + const result = dto; + Object.keys(dto).forEach((key: string) => { + if (result[key] === null) { + delete result[key]; + } + }); + + return result; +}; + +describe('RedisService', () => { + let service: RedisService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + RedisService, + ], + }).compile(); + + service = await module.get(RedisService); + }); + + it('should be defined', () => { + expect(service.clients).toEqual([]); + }); + + describe('connectToDatabaseInstance', () => { + beforeEach(async () => { + service.clients = []; + }); + it('should create standalone client', async () => { + const mockClient = new Redis(); + service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); + + const result = await service.connectToDatabaseInstance(mockDatabase); + + expect(result).toEqual(mockClient); + expect(service.createStandaloneClient).toHaveBeenCalledWith(mockDatabase, AppTool.Common, true, undefined); + }); + it('should create cluster client', async () => { + const mockClient = new Redis.Cluster([ + 'redis://localhost:7001', + 'redis://localhost:7002', + ]); + + const { nodes, connectionType, ...options } = mockClusterDatabaseWithTlsAuth; + service.createClusterClient = jest.fn().mockResolvedValue(mockClient); + + const result = await service.connectToDatabaseInstance(mockClusterDatabaseWithTlsAuth); + + expect(result).toEqual(mockClient); + expect(service.createClusterClient).toHaveBeenCalledWith( + options, + nodes, + true, + undefined, + ); + }); + it('should create sentinel client', async () => { + const mockClient = new Redis(); + const dto = removeNullsFromDto(mockSentinelDatabaseWithTlsAuth); + Object.keys(dto).forEach((key: string) => { + if (dto[key] === null) { + delete dto[key]; + } + }); + const { nodes, connectionType, ...options } = dto; + service.createSentinelClient = jest.fn().mockResolvedValue(mockClient); + + const result = await service.connectToDatabaseInstance(dto); + + expect(result).toEqual(mockClient); + expect(service.createSentinelClient).toHaveBeenCalledWith( + options, + nodes, + AppTool.Common, + true, + undefined, + ); + }); + // it('should select redis database by number', async () => { + // const mockClient = new Redis(); + // mockClient.call = jest.fn(); + // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); + // service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); + // + // await service.connectToDatabaseInstance(dto, AppTool.Common); + // + // expect(service.createStandaloneClient).toHaveBeenCalledWith(dto, AppTool.Common, true, undefined); + // }); + // it('should throw error db index is out of range', async () => { + // const replyError: ReplyError = { + // name: 'ReplyError', + // message: '(error) DB index is out of range', + // command: 'SELECT', + // }; + // service.createStandaloneClient = jest.fn().mockRejectedValue(replyError); + // + // try { + // await service.connectToDatabaseInstance( + // convertEntityToDto(mockStandaloneDatabaseEntity), + // ); + // fail('Should throw an error'); + // } catch (err) { + // expect(err).toEqual(replyError); + // } + // expect(service.clients.length).toEqual(0); + // }); + // it('connection error [Connection details are incorrect]', async () => { + // service.createStandaloneClient = jest + // .fn() + // .mockRejectedValue(new Error('ENOTFOUND some message')); + // + // try { + // await service.connectToDatabaseInstance( + // convertEntityToDto(mockStandaloneDatabaseEntity), + // 0, + // ); + // fail('Should throw an error'); + // } catch (err) { + // expect(err.message).toEqual('ENOTFOUND some message'); + // expect(service.clients.length).toEqual(0); + // } + // }); + }); + + // describe('getClientInstance', () => { + // beforeEach(() => { + // service.clients = [ + // { + // ...mockRedisClientInstance, tool: AppTool.Common, + // }, + // { + // ...mockRedisClientInstance, tool: AppTool.Browser, + // }, + // { + // ...mockRedisClientInstance, tool: AppTool.CLI, + // }, + // ]; + // }); + // it('should correctly find client instance for App.Common by instance id', () => { + // const newClient = { ...service.clients[0], tool: AppTool.Browser }; + // service.clients.push(newClient); + // const options = { + // instanceId: newClient.instanceId, + // }; + // + // const result = service.getClientInstance(options); + // + // expect(result).toEqual(service.clients[0]); + // }); + // it('should correctly find client instance by instance id and tool', () => { + // const options: IFindRedisClientInstanceByOptions = { + // instanceId: service.clients[0].instanceId, + // tool: AppTool.CLI, + // }; + // + // const result = service.getClientInstance(options); + // + // expect(result).toEqual(service.clients[2]); + // }); + // it('should correctly find client instance by instance id, tool and uuid', () => { + // const newClient = { ...mockRedisClientInstance, uuid: uuidv4(), tool: AppTool.CLI }; + // service.clients.push(newClient); + // const options: IFindRedisClientInstanceByOptions = { + // instanceId: newClient.instanceId, + // uuid: newClient.uuid, + // tool: newClient.tool, + // }; + // + // const result = service.getClientInstance(options); + // + // expect(result).toEqual(newClient); + // }); + // it('should return undefined', () => { + // const options: IFindRedisClientInstanceByOptions = { + // instanceId: 'invalid-instance-id', + // }; + // + // const result = service.getClientInstance(options); + // + // expect(result).toBeUndefined(); + // }); + // }); + + // describe('removeClientInstance', () => { + // beforeEach(() => { + // service.clients = [ + // { + // ...mockRedisClientInstance, + // tool: AppTool.Common, + // }, + // { + // ...mockRedisClientInstance, + // tool: AppTool.Browser, + // }, + // ]; + // }); + // it('should remove only client for browser tool', () => { + // const options: IFindRedisClientInstanceByOptions = { + // instanceId: mockRedisClientInstance.instanceId, + // tool: AppTool.Browser, + // }; + // + // const result = service.removeClientInstance(options); + // + // expect(result).toEqual(1); + // expect(service.clients.length).toEqual(1); + // }); + // it('should remove all clients by instance id', () => { + // const options: IFindRedisClientInstanceByOptions = { + // instanceId: mockRedisClientInstance.instanceId, + // }; + // + // const result = service.removeClientInstance(options); + // + // expect(result).toEqual(2); + // expect(service.clients.length).toEqual(0); + // }); + // }); + + // describe('setClientInstance', () => { + // beforeEach(() => { + // service.clients = [{ ...mockRedisClientInstance }]; + // }); + // it('should add new client', () => { + // const initialClientsCount = service.clients.length; + // const newClientInstance = { + // ...mockRedisClientInstance, + // instanceId: uuidv4(), + // }; + // + // const result = service.setClientInstance(newClientInstance); + // + // expect(result).toBe(1); + // expect(service.clients.length).toBe(initialClientsCount + 1); + // }); + // it('should replace exist client', () => { + // const initialClientsCount = service.clients.length; + // + // const result = service.setClientInstance(mockRedisClientInstance); + // + // expect(result).toBe(0); + // expect(service.clients.length).toBe(initialClientsCount); + // }); + // }); + + // describe('isClientConnected', () => { + // const mockClient = new Redis(); + // it('should return true', async () => { + // mockClient.status = 'ready'; + // + // const result = service.isClientConnected(mockClient); + // + // expect(result).toEqual(true); + // }); + // it('should return false', async () => { + // mockClient.status = 'end'; + // + // const result = service.isClientConnected(mockClient); + // + // expect(result).toEqual(false); + // }); + // }); + + // describe('getRedisConnectionConfig', () => { + // it('should return config with tls', async () => { + // service.getTLSConfig = jest.fn().mockResolvedValue(mockTlsConfigResult); + // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); + // const { + // host, port, password, username, db, + // } = dto; + // + // const expectedResult = { + // host, port, username, password, db, tls: mockTlsConfigResult, + // }; + // + // const result = await service.getRedisConnectionConfig(dto); + // + // expect(JSON.stringify(result)).toEqual(JSON.stringify(expectedResult)); + // }); + // it('should return without tls', async () => { + // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); + // delete dto.tls; + // const { + // host, port, password, username, db, + // } = dto; + // + // const expectedResult = { + // host, port, username, password, db, + // }; + // + // const result = await service.getRedisConnectionConfig(dto); + // + // expect(result).toEqual(expectedResult); + // }); + // }); + + // describe('getTLSConfig', () => { + // it('should return tls config', async () => { + // service.getCaCertConfig = jest + // .fn() + // .mockResolvedValue({ ca: [mockCaCertDto.cert] }); + // service.getClientCertConfig = jest.fn().mockResolvedValue({ + // key: mockClientCertDto.key, + // cert: mockClientCertDto.cert, + // }); + // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); + // + // const result = await service.getTLSConfig(tls); + // + // expect(JSON.stringify(result)).toEqual( + // JSON.stringify(mockTlsConfigResult), + // ); + // }); + // }); + + // describe('getCaCertConfig', () => { + // it('should load exist cert', async () => { + // caCertBusinessService.getOneById = jest + // .fn() + // .mockResolvedValue(mockCaCertEntity); + // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); + // + // const result = await service.getCaCertConfig(tls); + // + // expect(result).toEqual({ ca: [mockCaCertDto.cert] }); + // expect(caCertBusinessService.getOneById).toHaveBeenCalledWith( + // tls.caCertId, + // ); + // }); + // it('should return new cert', async () => { + // const result = await service.getCaCertConfig({ + // newCaCert: mockCaCertDto, + // }); + // + // expect(result).toEqual({ ca: [mockCaCertDto.cert] }); + // expect(caCertBusinessService.getOneById).not.toHaveBeenCalled(); + // }); + // it('should return null', async () => { + // const result = await service.getCaCertConfig({}); + // + // expect(result).toBeNull(); + // }); + // }); + + // describe('getClientCertConfig', () => { + // const mockResult = { + // key: mockClientCertDto.key, + // cert: mockClientCertDto.cert, + // }; + // it('should load exist cert', async () => { + // clientCertBusinessService.getOneById = jest + // .fn() + // .mockResolvedValue(mockClientCertEntity); + // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); + // + // const result = await service.getClientCertConfig(tls); + // + // expect(result).toEqual(mockResult); + // expect(clientCertBusinessService.getOneById).toHaveBeenCalledWith( + // tls.clientCertPairId, + // ); + // }); + // it('should return new cert', async () => { + // const result = await service.getClientCertConfig({ + // newClientCertPair: mockClientCertDto, + // }); + // + // expect(result).toEqual(mockResult); + // expect(clientCertBusinessService.getOneById).not.toHaveBeenCalled(); + // }); + // it('should return null', async () => { + // const result = await service.getClientCertConfig({}); + // + // expect(result).toBeNull(); + // }); + // }); +}); diff --git a/redisinsight/api/src/modules/redis/redis.service.ts b/redisinsight/api/src/modules/redis/redis.service.ts index 39436bdb21..c3ce2f8901 100644 --- a/redisinsight/api/src/modules/redis/redis.service.ts +++ b/redisinsight/api/src/modules/redis/redis.service.ts @@ -5,36 +5,24 @@ import { find, findIndex, isEmpty, isNil, omitBy, remove, } from 'lodash'; import { v4 as uuidv4 } from 'uuid'; -import { AppTool } from 'src/models'; import apiConfig from 'src/utils/config'; import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; import { IRedisClusterNodeAddress } from 'src/models/redis-cluster'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { Database } from 'src/modules/database/models/database'; import { cloneClassInstance } from 'src/utils'; +import { ClientContext, ClientMetadata } from 'src/common/models'; const REDIS_CLIENTS_CONFIG = apiConfig.get('redis_clients'); -export interface ISetClientInstanceOptions { - instanceId: string; - tool: AppTool; - uuid: string; -} - export interface IRedisClientInstance { - instanceId: string; - tool: AppTool; + databaseId: string; + context: ClientContext; + uniqueId: string; client: any; - uuid: string; lastTimeUsed: number; } -export interface IFindRedisClientInstanceByOptions { - instanceId: string; - tool?: AppTool; - uuid?: string; -} - @Injectable() export class RedisService { private logger = new Logger('RedisService'); @@ -49,7 +37,7 @@ export class RedisService { public async createStandaloneClient( database: Database, - appTool: AppTool, + appTool: ClientContext, useRetry: boolean, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, ): Promise { @@ -117,7 +105,7 @@ export class RedisService { public async createSentinelClient( database: Database, sentinels: Array<{ host: string; port: number }>, - appTool: AppTool, + appTool: ClientContext, useRetry: boolean = false, connectionName: string = CONNECTION_NAME_GLOBAL_PREFIX, ): Promise { @@ -168,7 +156,7 @@ export class RedisService { public async connectToDatabaseInstance( databaseDto: Database, - tool = AppTool.Common, + tool = ClientContext.Common, connectionName?, ): Promise { const database = cloneClassInstance(databaseDto); @@ -206,10 +194,8 @@ export class RedisService { } } - public getClientInstance( - options: IFindRedisClientInstanceByOptions, - ): IRedisClientInstance { - const found = this.findClientInstance(options.instanceId, options.tool, options.uuid); + public getClientInstance(clientMetadata: ClientMetadata): IRedisClientInstance { + const found = this.findClientInstance(clientMetadata.databaseId, clientMetadata.context, clientMetadata.uniqueId); if (found) { found.lastTimeUsed = Date.now(); } @@ -217,12 +203,10 @@ export class RedisService { return found; } - public removeClientInstance( - options: IFindRedisClientInstanceByOptions, - ): number { + public removeClientInstance(clientMetadata: Partial): number { const removed: IRedisClientInstance[] = remove( this.clients, - options, + clientMetadata, ); removed.forEach((clientInstance) => { clientInstance.client.disconnect(); @@ -230,10 +214,15 @@ export class RedisService { return removed.length; } - public setClientInstance(options: ISetClientInstanceOptions, client): 0 | 1 { - const found = this.findClientInstance(options.instanceId, options.tool, options.uuid); + public setClientInstance(clientMetadata: ClientMetadata, client): 0 | 1 { + const found = this.findClientInstance( + clientMetadata.databaseId, + clientMetadata.context, + clientMetadata.uniqueId, + ); + if (found) { - const index = findIndex(this.clients, { uuid: found.uuid }); + const index = findIndex(this.clients, { uniqueId: found.uniqueId }); this.clients[index].client.disconnect(); this.clients[index] = { ...this.clients[index], @@ -242,9 +231,10 @@ export class RedisService { }; return 0; } + const clientInstance: IRedisClientInstance = { - ...options, - uuid: options.uuid || uuidv4(), + ...clientMetadata, + uniqueId: clientMetadata.uniqueId || uuidv4(), lastTimeUsed: Date.now(), client, }; @@ -315,11 +305,11 @@ export class RedisService { } private findClientInstance( - instanceId: string, - tool: AppTool = AppTool.Common, - uuid: string = undefined, + databaseId: string, + context: ClientContext = ClientContext.Common, + uniqueId: string = undefined, ): IRedisClientInstance { - const options = omitBy({ instanceId, uuid, tool }, isNil); + const options = omitBy({ databaseId, uniqueId, context }, isNil); return find(this.clients, options); } } diff --git a/redisinsight/api/src/modules/slow-log/slow-log.controller.ts b/redisinsight/api/src/modules/slow-log/slow-log.controller.ts index c66e15006c..91568858d8 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.controller.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.controller.ts @@ -1,14 +1,13 @@ import { - Body, - Controller, Delete, Get, Param, Patch, Query, UsePipes, ValidationPipe, + Body, Controller, Delete, Get, Patch, Query, UsePipes, ValidationPipe, } from '@nestjs/common'; import { SlowLogService } from 'src/modules/slow-log/slow-log.service'; import { ApiTags } from '@nestjs/swagger'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models'; -import { AppTool } from 'src/models'; import { UpdateSlowLogConfigDto } from 'src/modules/slow-log/dto/update-slow-log-config.dto'; import { GetSlowLogsDto } from 'src/modules/slow-log/dto/get-slow-logs.dto'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; @ApiTags('Slow Logs') @Controller('slow-logs') @@ -31,13 +30,10 @@ export class SlowLogController { }) @Get('') async getSlowLogs( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata, @Query() getSlowLogsDto: GetSlowLogsDto, ): Promise { - return this.service.getSlowLogs({ - instanceId, - tool: AppTool.Common, - }, getSlowLogsDto); + return this.service.getSlowLogs(clientMetadata, getSlowLogsDto); } @ApiEndpoint({ @@ -46,12 +42,9 @@ export class SlowLogController { }) @Delete('') async resetSlowLogs( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata, ): Promise { - return this.service.reset({ - instanceId, - tool: AppTool.Common, - }); + return this.service.reset(clientMetadata); } @ApiEndpoint({ @@ -66,12 +59,9 @@ export class SlowLogController { }) @Get('config') async getConfig( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata, ): Promise { - return this.service.getConfig({ - instanceId, - tool: AppTool.Common, - }); + return this.service.getConfig(clientMetadata); } @ApiEndpoint({ @@ -86,12 +76,9 @@ export class SlowLogController { }) @Patch('config') async updateConfig( - @Param('dbInstance') instanceId: string, + @ClientMetadataFromRequest() clientMetadata, @Body() dto: UpdateSlowLogConfigDto, ): Promise { - return this.service.updateConfig({ - instanceId, - tool: AppTool.Common, - }, dto); + return this.service.updateConfig(clientMetadata, dto); } } diff --git a/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts b/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts index 108010ba6d..5c596a8be7 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.service.spec.ts @@ -1,28 +1,21 @@ import { Test, TestingModule } from '@nestjs/testing'; import { mockRedisNoPermError, - mockDatabase, MockType, mockDatabaseConnectionService, mockIORedisClient, mockIORedisCluster, mockIOClusterNode1, mockIOClusterNode2, + mockCommonClientMetadata, } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { SlowLogService } from 'src/modules/slow-log/slow-log.service'; -import { AppTool } from 'src/models'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { SlowLogArguments, SlowLogCommands } from 'src/modules/slow-log/constants/commands'; import { SlowLogAnalyticsService } from 'src/modules/slow-log/slow-log-analytics.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, - tool: AppTool.Common, -}; - const getSlowLogDto = { count: 100 }; const mockSlowLog = { id: 1, @@ -85,16 +78,16 @@ describe('SlowLogService', () => { describe('getSlowLogs', () => { it('should return slowlogs for standalone', async () => { - const res = await service.getSlowLogs(mockClientOptions, getSlowLogDto); + const res = await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto); expect(res).toEqual([mockSlowLog, mockSlowLog]); }); it('should return slowlogs for standalone without active connection', async () => { - const res = await service.getSlowLogs(mockClientOptions, getSlowLogDto); + const res = await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto); expect(res).toEqual([mockSlowLog, mockSlowLog]); }); it('should return slowlogs cluster', async () => { databaseConnectionService.getOrCreateClient.mockResolvedValueOnce(mockIORedisCluster); - const res = await service.getSlowLogs(mockClientOptions, getSlowLogDto); + const res = await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto); expect(res).toEqual([mockSlowLog, mockSlowLog, mockSlowLog, mockSlowLog]); }); it('should proxy HttpException', async () => { @@ -102,7 +95,7 @@ describe('SlowLogService', () => { throw new BadRequestException('error'); }); try { - await service.getSlowLogs(mockClientOptions, getSlowLogDto); + await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -114,7 +107,7 @@ describe('SlowLogService', () => { }); try { - await service.getSlowLogs(mockClientOptions, getSlowLogDto); + await service.getSlowLogs(mockCommonClientMetadata, getSlowLogDto); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -124,12 +117,12 @@ describe('SlowLogService', () => { describe('reset', () => { it('should reset slowlogs for standalone', async () => { - await service.reset(mockClientOptions); + await service.reset(mockCommonClientMetadata); expect(mockIORedisClient.call).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); }); it('should reset slowlogs cluster', async () => { databaseConnectionService.getOrCreateClient.mockResolvedValueOnce(mockIORedisCluster); - await service.reset(mockClientOptions); + await service.reset(mockCommonClientMetadata); expect(mockIOClusterNode1.call).toHaveBeenCalledWith(SlowLogCommands.SlowLog, SlowLogArguments.Reset); }); it('should proxy HttpException', async () => { @@ -138,7 +131,7 @@ describe('SlowLogService', () => { }); try { - await service.reset(mockClientOptions); + await service.reset(mockCommonClientMetadata); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -150,7 +143,7 @@ describe('SlowLogService', () => { }); try { - await service.reset(mockClientOptions); + await service.reset(mockCommonClientMetadata); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -162,7 +155,7 @@ describe('SlowLogService', () => { it('should get slowlogs config', async () => { mockIORedisClient.call.mockResolvedValueOnce(mockSlowlogConfigReply); - const res = await service.getConfig(mockClientOptions); + const res = await service.getConfig(mockCommonClientMetadata); expect(res).toEqual(mockSlowLogConfig); }); it('should get ONLY supported slowlogs config even if there some extra fields in resp', async () => { @@ -172,7 +165,7 @@ describe('SlowLogService', () => { 12, ]); - const res = await service.getConfig(mockClientOptions); + const res = await service.getConfig(mockCommonClientMetadata); expect(res).toEqual(mockSlowLogConfig); }); it('should proxy HttpException', async () => { @@ -181,7 +174,7 @@ describe('SlowLogService', () => { }); try { - await service.getConfig(mockClientOptions); + await service.getConfig(mockCommonClientMetadata); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -193,7 +186,7 @@ describe('SlowLogService', () => { }); try { - await service.getConfig(mockClientOptions); + await service.getConfig(mockCommonClientMetadata); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); @@ -206,7 +199,7 @@ describe('SlowLogService', () => { mockIORedisClient.call.mockResolvedValueOnce(mockSlowlogConfigReply); mockIORedisClient.call.mockResolvedValueOnce('OK'); - const res = await service.updateConfig(mockClientOptions, { slowlogMaxLen: 128 }); + const res = await service.updateConfig(mockCommonClientMetadata, { slowlogMaxLen: 128 }); expect(res).toEqual(mockSlowLogConfig); expect(mockIORedisClient.call).toHaveBeenCalledTimes(2); }); @@ -216,7 +209,7 @@ describe('SlowLogService', () => { .mockResolvedValueOnce('OK') .mockResolvedValueOnce('OK'); - const res = await service.updateConfig(mockClientOptions, { slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); + const res = await service.updateConfig(mockCommonClientMetadata, { slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); expect(res).toEqual({ slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); expect(mockIORedisClient.call).toHaveBeenCalledTimes(3); }); @@ -226,7 +219,7 @@ describe('SlowLogService', () => { mockIORedisCluster.call.mockResolvedValueOnce(mockSlowlogConfigReply); try { - await service.updateConfig(mockClientOptions, { slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); + await service.updateConfig(mockCommonClientMetadata, { slowlogMaxLen: 128, slowlogLogSlowerThan: 1 }); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -238,7 +231,7 @@ describe('SlowLogService', () => { }); try { - await service.updateConfig(mockClientOptions, { slowlogMaxLen: 1 }); + await service.updateConfig(mockCommonClientMetadata, { slowlogMaxLen: 1 }); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -250,7 +243,7 @@ describe('SlowLogService', () => { }); try { - await service.updateConfig(mockClientOptions, { slowlogMaxLen: 1 }); + await service.updateConfig(mockCommonClientMetadata, { slowlogMaxLen: 1 }); fail(); } catch (e) { expect(e).toBeInstanceOf(ForbiddenException); diff --git a/redisinsight/api/src/modules/slow-log/slow-log.service.ts b/redisinsight/api/src/modules/slow-log/slow-log.service.ts index 360fdcbe41..2c8ba97109 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.service.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.service.ts @@ -3,7 +3,6 @@ import { concat } from 'lodash'; import { BadRequestException, HttpException, Injectable, Logger, } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models'; import { SlowLogArguments, SlowLogCommands } from 'src/modules/slow-log/constants/commands'; @@ -11,6 +10,7 @@ import { catchAclError, convertStringsArrayToObject } from 'src/utils'; import { UpdateSlowLogConfigDto } from 'src/modules/slow-log/dto/update-slow-log-config.dto'; import { GetSlowLogsDto } from 'src/modules/slow-log/dto/get-slow-logs.dto'; import { SlowLogAnalyticsService } from 'src/modules/slow-log/slow-log-analytics.service'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class SlowLogService { @@ -23,20 +23,14 @@ export class SlowLogService { /** * Get slow logs for each node and return concatenated result - * @param clientOptions + * @param clientMetadata * @param dto */ - async getSlowLogs( - clientOptions: IFindRedisClientInstanceByOptions, - dto: GetSlowLogsDto, - ) { + async getSlowLogs(clientMetadata: ClientMetadata, dto: GetSlowLogsDto) { try { this.logger.log('Getting slow logs'); - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const nodes = await this.getNodes(client); return concat(...(await Promise.all(nodes.map((node) => this.getNodeSlowLogs(node, dto))))); @@ -74,18 +68,13 @@ export class SlowLogService { /** * Clear slow logs in all nodes - * @param clientOptions + * @param clientMetadata */ - async reset( - clientOptions: IFindRedisClientInstanceByOptions, - ): Promise { + async reset(clientMetadata: ClientMetadata): Promise { try { this.logger.log('Resetting slow logs'); - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const nodes = await this.getNodes(client); await Promise.all(nodes.map((node) => node.call(SlowLogCommands.SlowLog, SlowLogArguments.Reset))); @@ -100,16 +89,11 @@ export class SlowLogService { /** * Get current slowlog config to show for user - * @param clientOptions + * @param clientMetadata */ - async getConfig( - clientOptions: IFindRedisClientInstanceByOptions, - ): Promise { + async getConfig(clientMetadata: ClientMetadata): Promise { try { - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); const resp = convertStringsArrayToObject( await client.call(SlowLogCommands.Config, [SlowLogArguments.Get, 'slowlog*']), ); @@ -129,16 +113,13 @@ export class SlowLogService { /** * Update slowlog config - * @param clientOptions + * @param clientMetadata * @param dto */ - async updateConfig( - clientOptions: IFindRedisClientInstanceByOptions, - dto: UpdateSlowLogConfigDto, - ): Promise { + async updateConfig(clientMetadata: ClientMetadata, dto: UpdateSlowLogConfigDto): Promise { try { const commands = []; - const config = await this.getConfig(clientOptions); + const config = await this.getConfig(clientMetadata); const { slowlogLogSlowerThan, slowlogMaxLen } = config; if (dto.slowlogLogSlowerThan !== undefined) { @@ -146,7 +127,7 @@ export class SlowLogService { command: SlowLogCommands.Config, args: [SlowLogArguments.Set, 'slowlog-log-slower-than', dto.slowlogLogSlowerThan], analytics: () => this.analyticsService.slowlogLogSlowerThanUpdated( - clientOptions.instanceId, + clientMetadata.databaseId, slowlogLogSlowerThan, dto.slowlogLogSlowerThan, ), @@ -160,7 +141,7 @@ export class SlowLogService { command: SlowLogCommands.Config, args: [SlowLogArguments.Set, 'slowlog-max-len', dto.slowlogMaxLen], analytics: () => this.analyticsService.slowlogMaxLenUpdated( - clientOptions.instanceId, + clientMetadata.databaseId, slowlogMaxLen, dto.slowlogMaxLen, ), @@ -170,10 +151,7 @@ export class SlowLogService { } if (commands.length) { - const client = await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: clientOptions.tool, - }); + const client = await this.databaseConnectionService.getOrCreateClient(clientMetadata); if (client.isCluster) { return Promise.reject(new BadRequestException('Configuration slowlog for cluster is deprecated')); diff --git a/redisinsight/api/src/modules/workbench/plugins.controller.ts b/redisinsight/api/src/modules/workbench/plugins.controller.ts index 50a24d652d..055608054a 100644 --- a/redisinsight/api/src/modules/workbench/plugins.controller.ts +++ b/redisinsight/api/src/modules/workbench/plugins.controller.ts @@ -1,15 +1,24 @@ import { - Body, ClassSerializerInterceptor, Controller, Get, Param, Post, UseInterceptors, UsePipes, ValidationPipe, + Body, + ClassSerializerInterceptor, + Controller, + Get, + Param, + Post, + UseInterceptors, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; -import { AppTool } from 'src/models'; import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; import { PluginsService } from 'src/modules/workbench/plugins.service'; import { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution'; import { CreatePluginStateDto } from 'src/modules/workbench/dto/create-plugin-state.dto'; import { PluginState } from 'src/modules/workbench/models/plugin-state'; +import { ClientContext, ClientMetadata } from 'src/common/models'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; @ApiTags('Plugins') @UsePipes(new ValidationPipe({ transform: true })) @@ -31,16 +40,10 @@ export class PluginsController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async sendCommand( - @Param('dbInstance') dbInstance: string, + @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata: ClientMetadata, @Body() dto: CreateCommandExecutionDto, ): Promise { - return this.service.sendCommand( - { - instanceId: dbInstance, - tool: AppTool.Workbench, - }, - dto, - ); + return this.service.sendCommand(clientMetadata, dto); } @ApiEndpoint({ @@ -58,9 +61,9 @@ export class PluginsController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async getPluginCommands( - @Param('dbInstance') databaseId: string, + @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata: ClientMetadata, ): Promise { - return this.service.getWhitelistCommands(databaseId); + return this.service.getWhitelistCommands(clientMetadata); } @ApiEndpoint({ diff --git a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts index e18c5b623c..f97a5da332 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.spec.ts @@ -1,13 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { mockDatabase, mockWhitelistCommandsResponse } from 'src/__mocks__'; +import { mockDatabase, mockWhitelistCommandsResponse, mockWorkbenchClientMetadata } from 'src/__mocks__'; import { v4 as uuidv4 } from 'uuid'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { ClusterNodeRole, CreateCommandExecutionDto, - RunQueryMode, ResultsMode, + RunQueryMode, } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CommandExecutionResult } from 'src/modules/workbench/models/command-execution-result'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; @@ -22,10 +21,6 @@ import config from 'src/utils/config'; const PLUGINS_CONFIG = config.get('plugins'); -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { command: 'get foo', nodeOptions: { @@ -116,7 +111,7 @@ describe('PluginsService', () => { workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(mockWhitelistCommandsResponse); - const result = await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + const result = await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); expect(result).toEqual(mockPluginCommandExecution); expect(workbenchCommandsExecutor.sendCommand).toHaveBeenCalled(); @@ -129,11 +124,11 @@ describe('PluginsService', () => { pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(mockWhitelistCommandsResponse); - const result = await service.sendCommand(mockClientOptions, dto); + const result = await service.sendCommand(mockWorkbenchClientMetadata, dto); expect(result).toEqual(new PluginCommandExecution({ ...dto, - databaseId: mockClientOptions.instanceId, + databaseId: mockWorkbenchClientMetadata.databaseId, result: [new CommandExecutionResult({ response: ERROR_MESSAGES.PLUGIN_COMMAND_NOT_SUPPORTED('subscribe'.toUpperCase()), status: CommandExecutionStatus.Fail, @@ -152,7 +147,7 @@ describe('PluginsService', () => { }; try { - await service.sendCommand(mockClientOptions, dto); + await service.sendCommand(mockWorkbenchClientMetadata, dto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -164,7 +159,7 @@ describe('PluginsService', () => { workbenchCommandsExecutor.sendCommand.mockResolvedValueOnce(mockCommandExecutionResults); pluginsCommandsWhitelistProvider.getWhitelistCommands.mockResolvedValueOnce(mockWhitelistCommandsResponse); - const result = await service.getWhitelistCommands(mockClientOptions.instanceId); + const result = await service.getWhitelistCommands(mockWorkbenchClientMetadata); expect(result).toEqual(mockWhitelistCommandsResponse); }); @@ -197,27 +192,12 @@ describe('PluginsService', () => { }); }); describe('getState', () => { - it('should successfully save state', async () => { + it('should successfully get state', async () => { pluginStateProvider.getOne.mockResolvedValueOnce(mockPluginState); const result = await service.getState(mockVisualizationId, mockCommandExecutionId); expect(result).toEqual(mockPluginState); }); - it('should throw an error when state too large', async () => { - pluginStateProvider.upsert.mockResolvedValueOnce(mockPluginState); - - try { - const dto = { - state: Buffer.alloc(PLUGINS_CONFIG.stateMaxSize + 1, 0), - }; - await service.saveState(mockVisualizationId, mockCommandExecutionId, dto); - fail(); - } catch (e) { - expect(e).toBeInstanceOf(BadRequestException); - expect(e.message).toEqual(ERROR_MESSAGES.PLUGIN_STATE_MAX_SIZE(PLUGINS_CONFIG.stateMaxSize)); - } - expect(pluginStateProvider.upsert).not.toHaveBeenCalled(); - }); }); }); diff --git a/redisinsight/api/src/modules/workbench/plugins.service.ts b/redisinsight/api/src/modules/workbench/plugins.service.ts index e8e1559b2e..c22d5d786b 100644 --- a/redisinsight/api/src/modules/workbench/plugins.service.ts +++ b/redisinsight/api/src/modules/workbench/plugins.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CreateCommandExecutionDto } from 'src/modules/workbench/dto/create-command-execution.dto'; import { CommandNotSupportedError } from 'src/modules/cli/constants/errors'; @@ -13,6 +12,7 @@ import { CreatePluginStateDto } from 'src/modules/workbench/dto/create-plugin-st import { PluginStateProvider } from 'src/modules/workbench/providers/plugin-state.provider'; import { PluginState } from 'src/modules/workbench/models/plugin-state'; import config from 'src/utils/config'; +import { ClientMetadata } from 'src/common/models'; const PLUGINS_CONFIG = config.get('plugins'); @@ -27,28 +27,28 @@ export class PluginsService { /** * Send redis command from workbench and save history * - * @param clientOptions + * @param clientMetadata * @param dto */ async sendCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateCommandExecutionDto, ): Promise { try { - await this.checkWhitelistedCommands(clientOptions.instanceId, dto.command); + await this.checkWhitelistedCommands(clientMetadata, dto.command); - const result = await this.commandsExecutor.sendCommand(clientOptions, dto); + const result = await this.commandsExecutor.sendCommand(clientMetadata, dto); return plainToClass(PluginCommandExecution, { ...dto, - databaseId: clientOptions.instanceId, + databaseId: clientMetadata.databaseId, result, }); } catch (error) { if (error instanceof CommandNotSupportedError) { return new PluginCommandExecution({ ...dto, - databaseId: clientOptions.instanceId, + databaseId: clientMetadata.databaseId, result: [new CommandExecutionResult({ response: error.message, status: CommandExecutionStatus.Fail, @@ -62,10 +62,10 @@ export class PluginsService { /** * Get database white listed commands for plugins - * @param instanceId + * @param clientMetadata */ - async getWhitelistCommands(instanceId: string): Promise { - return await this.whitelistProvider.getWhitelistCommands(instanceId); + async getWhitelistCommands(clientMetadata: ClientMetadata): Promise { + return await this.whitelistProvider.getWhitelistCommands(clientMetadata); } /** @@ -99,14 +99,14 @@ export class PluginsService { /** * Check if command outside workbench commands black list - * @param databaseId + * @param clientMetadata * @param commandLine * @private */ - private async checkWhitelistedCommands(databaseId: string, commandLine: string) { + private async checkWhitelistedCommands(clientMetadata: ClientMetadata, commandLine: string) { const targetCommand = commandLine.toLowerCase(); - const whitelist = await this.getWhitelistCommands(databaseId); + const whitelist = await this.getWhitelistCommands(clientMetadata); if (!whitelist.find((command) => targetCommand.startsWith(command))) { throw new CommandNotSupportedError( diff --git a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts index e95b9f0eee..cb7ba4e7cf 100644 --- a/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts +++ b/redisinsight/api/src/modules/workbench/providers/plugin-commands-whitelist.provider.ts @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common'; import { RedisToolService } from 'src/modules/redis/redis-tool.service'; import { filter, get, map } from 'lodash'; import { pluginBlockingCommands, pluginUnsupportedCommands } from 'src/constants'; +import { ClientMetadata } from 'src/common/models'; @Injectable() export class PluginCommandsWhitelistProvider { @@ -13,24 +14,23 @@ export class PluginCommandsWhitelistProvider { /** * Get cached commands list or determine it and cache - * @param instanceId + * @param clientMetadata */ - async getWhitelistCommands( - instanceId: string, - ): Promise { - return this.databasesCommands.get(instanceId) || this.determineWhitelistCommandsForDatabase(instanceId); + async getWhitelistCommands(clientMetadata: ClientMetadata): Promise { + return this.databasesCommands.get(clientMetadata.databaseId) + || this.determineWhitelistCommandsForDatabase(clientMetadata); } /** * Get or create Workbench redis client, fetch commands and cache them - * @param instanceId + * @param clientMetadata */ - async determineWhitelistCommandsForDatabase(instanceId: string): Promise { + async determineWhitelistCommandsForDatabase(clientMetadata: ClientMetadata): Promise { // no need to define AppTool since it was set on RedisTool creation. todo: do not forget after refactoring - const client = await this.redisTool.getRedisClient({ instanceId }); + const client = await this.redisTool.getRedisClient(clientMetadata); const commands = await this.calculateWhiteListCommands(client); - this.databasesCommands.set(instanceId, commands); + this.databasesCommands.set(clientMetadata.databaseId, commands); return commands; } diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts index 0edbe8c0a8..c10e915a07 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.spec.ts @@ -2,12 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { get } from 'lodash'; import { mockRedisMovedError, - mockDatabase, mockWorkbenchAnalyticsService, + mockWorkbenchClientMetadata, } from 'src/__mocks__'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { unknownCommand } from 'src/constants'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { ClusterNodeRole, @@ -29,10 +28,6 @@ import { WorkbenchAnalyticsService } from '../services/workbench-analytics/workb const MOCK_ERROR_MESSAGE = 'Some error'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockCliTool = () => ({ execCommand: jest.fn(), execCommandForNodes: jest.fn(), @@ -116,7 +111,7 @@ describe('WorkbenchCommandsExecutor', () => { it('should successfully send command for standalone', async () => { cliTool.execCommand.mockResolvedValueOnce(mockCommandExecutionResult.response); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, }); @@ -127,7 +122,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { response: mockCommandExecutionResult.response, @@ -143,7 +138,7 @@ describe('WorkbenchCommandsExecutor', () => { it('should return fail status in case of unsupported command error', async () => { cliTool.execCommand.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, }); @@ -154,7 +149,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), @@ -174,7 +169,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommand.mockRejectedValueOnce(replyError); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, }); @@ -185,7 +180,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: replyError, @@ -202,7 +197,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommand.mockResolvedValueOnce(mockCommandExecutionResult.response); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, }); @@ -214,7 +209,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(formatSpy).toHaveBeenCalled(); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { response: mockCommandExecutionResult.response, @@ -232,7 +227,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommand.mockResolvedValueOnce(mockCommandExecutionResult.response); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.Raw, }); @@ -244,7 +239,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(formatSpy).toHaveBeenCalled(); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { response: mockCommandExecutionResult.response, @@ -261,7 +256,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommand.mockRejectedValueOnce(new ServiceUnavailableException(MOCK_ERROR_MESSAGE)); try { - await service.sendCommand(mockClientOptions, { + await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, mode: RunQueryMode.ASCII, }); @@ -271,7 +266,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(e.message).toEqual(MOCK_ERROR_MESSAGE); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), @@ -289,14 +284,14 @@ describe('WorkbenchCommandsExecutor', () => { it('should successfully send command for standalone', async () => { cliTool.execCommandForNode.mockResolvedValueOnce(mockCliNodeResponse); - const result = await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + const result = await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); expect(result).toEqual([{ ...mockCommandExecutionResult, }]); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { ...mockCommandExecutionResult, @@ -314,7 +309,7 @@ describe('WorkbenchCommandsExecutor', () => { error: mockRedisMovedError, }); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { ...mockCreateCommandExecutionDto, nodeOptions: { ...mockCreateCommandExecutionDto.nodeOptions, @@ -327,7 +322,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { ...mockCommandExecutionResult, @@ -346,7 +341,7 @@ describe('WorkbenchCommandsExecutor', () => { }); cliTool.execCommandForNode.mockResolvedValueOnce(mockCliNodeResponse); - const result = await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + const result = await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); expect(result).toEqual([{ ...mockCommandExecutionResult, @@ -357,7 +352,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ { ...mockCommandExecutionResult, @@ -376,7 +371,7 @@ describe('WorkbenchCommandsExecutor', () => { it('should return fail status when command is not supported', async () => { cliTool.execCommandForNode.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); - const result = await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + const result = await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); expect(result).toEqual([{ response: MOCK_ERROR_MESSAGE, @@ -384,7 +379,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), @@ -400,14 +395,14 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommandForNode.mockRejectedValueOnce(new ClusterNodeNotFoundError(MOCK_ERROR_MESSAGE)); try { - await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new ClusterNodeNotFoundError(MOCK_ERROR_MESSAGE), @@ -424,14 +419,14 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommandForNode.mockRejectedValueOnce(new ServiceUnavailableException(MOCK_ERROR_MESSAGE)); try { - await service.sendCommand(mockClientOptions, mockCreateCommandExecutionDto); + await service.sendCommand(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); expect(e.message).toEqual(MOCK_ERROR_MESSAGE); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), @@ -455,7 +450,7 @@ describe('WorkbenchCommandsExecutor', () => { }, ]); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, role: mockCreateCommandExecutionDto.role, mode: RunQueryMode.ASCII, @@ -470,7 +465,7 @@ describe('WorkbenchCommandsExecutor', () => { ]); expect(mockAnalyticsService.sendCommandExecutedEvents).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, [ mockCommandExecutionResult, { @@ -487,7 +482,7 @@ describe('WorkbenchCommandsExecutor', () => { it('should return fail status when command is not supported', async () => { cliTool.execCommandForNodes.mockRejectedValueOnce(new CommandNotSupportedError(MOCK_ERROR_MESSAGE)); - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, role: mockCreateCommandExecutionDto.role, mode: RunQueryMode.ASCII, @@ -499,7 +494,7 @@ describe('WorkbenchCommandsExecutor', () => { }]); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new CommandNotSupportedError(MOCK_ERROR_MESSAGE), @@ -515,7 +510,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommandForNodes.mockRejectedValueOnce(new WrongDatabaseTypeError(MOCK_ERROR_MESSAGE)); try { - await service.sendCommand(mockClientOptions, { + await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, role: mockCreateCommandExecutionDto.role, mode: RunQueryMode.ASCII, @@ -526,7 +521,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(e.message).toEqual(MOCK_ERROR_MESSAGE); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new WrongDatabaseTypeError(MOCK_ERROR_MESSAGE), @@ -543,7 +538,7 @@ describe('WorkbenchCommandsExecutor', () => { cliTool.execCommandForNodes.mockRejectedValueOnce(new ServiceUnavailableException(MOCK_ERROR_MESSAGE)); try { - await service.sendCommand(mockClientOptions, { + await service.sendCommand(mockWorkbenchClientMetadata, { command: mockCreateCommandExecutionDto.command, role: mockCreateCommandExecutionDto.role, mode: RunQueryMode.ASCII, @@ -554,7 +549,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(e.message).toEqual(MOCK_ERROR_MESSAGE); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: MOCK_ERROR_MESSAGE, error: new ServiceUnavailableException(MOCK_ERROR_MESSAGE), @@ -577,7 +572,7 @@ describe('WorkbenchCommandsExecutor', () => { }, ]; - const result = await service.sendCommand(mockClientOptions, { + const result = await service.sendCommand(mockWorkbenchClientMetadata, { command: mockGetEscapedKeyCommand, role: mockCreateCommandExecutionDto.role, mode: RunQueryMode.ASCII, @@ -585,7 +580,7 @@ describe('WorkbenchCommandsExecutor', () => { expect(result).toEqual(mockResult); expect(mockAnalyticsService.sendCommandExecutedEvent).toHaveBeenCalledWith( - mockClientOptions.instanceId, + mockWorkbenchClientMetadata.databaseId, { response: ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES(), error: new CommandParsingError(ERROR_MESSAGES.CLI_UNTERMINATED_QUOTES()), diff --git a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts index 53ba13104a..1cfbef9c24 100644 --- a/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts +++ b/redisinsight/api/src/modules/workbench/providers/workbench-commands.executor.ts @@ -1,7 +1,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, Logger, } from '@nestjs/common'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { ClusterNodeRole, ClusterSingleNodeOptions, CommandExecutionStatus, @@ -55,11 +55,11 @@ export class WorkbenchCommandsExecutor { * Entrypoint for any CommandExecution * Will determine type of a command (standalone, per node(s)) and format, and execute it * Also sis a single place of analytics events invocation - * @param clientOptions + * @param clientMetadata * @param dto */ public async sendCommand( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateCommandExecutionDto, ): Promise { let result; @@ -77,7 +77,7 @@ export class WorkbenchCommandsExecutor { if (nodeOptions) { result = [await this.sendCommandForSingleNode( - clientOptions, + clientMetadata, command, commandArgs, role, @@ -86,7 +86,7 @@ export class WorkbenchCommandsExecutor { )]; } else if (role) { result = await this.sendCommandForNodes( - clientOptions, + clientMetadata, command, commandArgs, role, @@ -94,7 +94,7 @@ export class WorkbenchCommandsExecutor { ); } else { result = [await this.sendCommandForStandalone( - clientOptions, + clientMetadata, command, commandArgs, mode, @@ -102,7 +102,7 @@ export class WorkbenchCommandsExecutor { } this.analyticsService.sendCommandExecutedEvents( - clientOptions.instanceId, + clientMetadata.databaseId, result, { command, rawMode: mode === RunQueryMode.Raw }, ); @@ -113,7 +113,7 @@ export class WorkbenchCommandsExecutor { const errorResult = { response: error.message, status: CommandExecutionStatus.Fail }; this.analyticsService.sendCommandExecutedEvent( - clientOptions.instanceId, + clientMetadata.databaseId, { ...errorResult, error }, { command, rawMode: dto.mode === RunQueryMode.Raw }, ); @@ -136,14 +136,14 @@ export class WorkbenchCommandsExecutor { /** * Sends command for standalone instances - * @param clientOptions + * @param clientMetadata * @param command * @param commandArgs * @param mode * @private */ private async sendCommandForStandalone( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, command: string, commandArgs: string[], mode: RunQueryMode, @@ -155,7 +155,7 @@ export class WorkbenchCommandsExecutor { const replyEncoding = checkHumanReadableCommands(`${command} ${commandArgs[0]}`) ? 'utf8' : undefined; const response = formatter.format( - await this.redisTool.execCommand(clientOptions, command, commandArgs, replyEncoding), + await this.redisTool.execCommand(clientMetadata, command, commandArgs, replyEncoding), ); this.logger.log('Succeed to execute workbench command.'); @@ -165,7 +165,7 @@ export class WorkbenchCommandsExecutor { /** * Sends command for a single node in cluster by host and port (nodeOptions) - * @param clientOptions + * @param clientMetadata * @param command * @param commandArgs * @param role @@ -174,7 +174,7 @@ export class WorkbenchCommandsExecutor { * @private */ private async sendCommandForSingleNode( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, command: string, commandArgs: string[], role: ClusterNodeRole = ClusterNodeRole.All, @@ -189,7 +189,7 @@ export class WorkbenchCommandsExecutor { const nodeAddress = `${nodeOptions.host}:${nodeOptions.port}`; let result = await this.redisTool.execCommandForNode( - clientOptions, + clientMetadata, command, commandArgs, role, @@ -199,7 +199,7 @@ export class WorkbenchCommandsExecutor { if (result.error && checkRedirectionError(result.error) && nodeOptions.enableRedirection) { const { slot, address } = parseRedirectionError(result.error); result = await this.redisTool.execCommandForNode( - clientOptions, + clientMetadata, command, commandArgs, role, @@ -222,7 +222,7 @@ export class WorkbenchCommandsExecutor { /** * Sends commands for multiple nodes in cluster based on their role - * @param clientOptions + * @param clientMetadata * @param command * @param commandArgs * @param role @@ -230,7 +230,7 @@ export class WorkbenchCommandsExecutor { * @private */ private async sendCommandForNodes( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, command: string, commandArgs: string[], role: ClusterNodeRole, @@ -243,7 +243,7 @@ export class WorkbenchCommandsExecutor { const replyEncoding = checkHumanReadableCommands(`${command} ${commandArgs[0]}`) ? 'utf8' : undefined; return ( - await this.redisTool.execCommandForNodes(clientOptions, command, commandArgs, role, replyEncoding) + await this.redisTool.execCommandForNodes(clientMetadata, command, commandArgs, role, replyEncoding) ).map((nodeExecReply) => { const { response, status, host, port, diff --git a/redisinsight/api/src/modules/workbench/workbench.controller.ts b/redisinsight/api/src/modules/workbench/workbench.controller.ts index 6784417999..36456c1475 100644 --- a/redisinsight/api/src/modules/workbench/workbench.controller.ts +++ b/redisinsight/api/src/modules/workbench/workbench.controller.ts @@ -1,14 +1,24 @@ import { - Body, ClassSerializerInterceptor, Controller, Delete, Get, Param, Post, UseInterceptors, UsePipes, ValidationPipe, + Body, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + Param, + Post, + UseInterceptors, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { WorkbenchService } from 'src/modules/workbench/workbench.service'; -import { AppTool } from 'src/models'; import { CommandExecution } from 'src/modules/workbench/models/command-execution'; import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; +import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientContext } from 'src/common/models'; @ApiTags('Workbench') @UsePipes(new ValidationPipe({ transform: true })) @@ -30,16 +40,10 @@ export class WorkbenchController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async sendCommands( - @Param('dbInstance') dbInstance: string, + @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata, @Body() dto: CreateCommandExecutionsDto, ): Promise { - return this.service.createCommandExecutions( - { - instanceId: dbInstance, - tool: AppTool.Workbench, - }, - dto, - ); + return this.service.createCommandExecutions(clientMetadata, dto); } @ApiEndpoint({ diff --git a/redisinsight/api/src/modules/workbench/workbench.module.ts b/redisinsight/api/src/modules/workbench/workbench.module.ts index e16470427d..52b34c9b4a 100644 --- a/redisinsight/api/src/modules/workbench/workbench.module.ts +++ b/redisinsight/api/src/modules/workbench/workbench.module.ts @@ -10,7 +10,7 @@ import { CommandsService } from 'src/modules/commands/commands.service'; import { CommandsJsonProvider } from 'src/modules/commands/commands-json.provider'; import { RedisToolService } from 'src/modules/redis/redis-tool.service'; import { RedisToolFactory } from 'src/modules/redis/redis-tool.factory'; -import { AppTool } from 'src/models'; +import { ClientContext } from 'src/common/models'; import { PluginsService } from 'src/modules/workbench/plugins.service'; import { PluginCommandsWhitelistProvider } from 'src/modules/workbench/providers/plugin-commands-whitelist.provider'; import { PluginsController } from 'src/modules/workbench/plugins.controller'; @@ -34,7 +34,7 @@ const COMMANDS_CONFIGS = config.get('commands'); CommandExecutionProvider, { provide: RedisToolService, - useFactory: (redisToolFactory: RedisToolFactory) => redisToolFactory.createRedisTool(AppTool.Workbench), + useFactory: (redisToolFactory: RedisToolFactory) => redisToolFactory.createRedisTool(ClientContext.Workbench), inject: [RedisToolFactory], }, { diff --git a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts index 0a7d3a5944..273a989275 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.spec.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.spec.ts @@ -1,8 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { v4 as uuidv4 } from 'uuid'; import { when } from 'jest-when'; -import { mockDatabase, mockDatabaseConnectionService, mockWorkbenchAnalyticsService } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { + mockDatabase, + mockDatabaseConnectionService, + mockWorkbenchAnalyticsService, + mockWorkbenchClientMetadata, +} from 'src/__mocks__'; import { WorkbenchService } from 'src/modules/workbench/workbench.service'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CommandExecutionProvider } from 'src/modules/workbench/providers/command-execution.provider'; @@ -21,10 +25,6 @@ import { DatabaseConnectionService } from 'src/modules/database/database-connect import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { command: 'set foo bar', nodeOptions: { @@ -37,7 +37,7 @@ const mockCreateCommandExecutionDto: CreateCommandExecutionDto = { resultsMode: ResultsMode.Default, }; -const mockCommands = ["set 1 1", "get 1"]; +const mockCommands = ['set 1 1', 'get 1']; const mockCreateCommandExecutionDtoWithGroupMode: CreateCommandExecutionsDto = { commands: mockCommands, @@ -82,20 +82,24 @@ const mockCommandExecution: CommandExecution = new CommandExecution({ result: mockCommandExecutionResults, }); -const mockSendCommandResultSuccess = { response: "1", status: "success" }; -const mockSendCommandResultFail = { response: "error", status: "fail" }; +const mockSendCommandResultSuccess = { response: '1', status: 'success' }; +const mockSendCommandResultFail = { response: 'error', status: 'fail' }; const mockCommandExecutionWithGroupMode = { - mode: "ASCII", + mode: 'ASCII', commands: mockCommands, - resultsMode: "GROUP_MODE", - databaseId: "d05043d0 - 0d12- 4ce1-9ca3 - 30c6d7e391ea", - summary: { "total": 2, "success": 1, "fail": 1 }, - command: "set 1 1\r\nget 1", + resultsMode: 'GROUP_MODE', + databaseId: 'd05043d0 - 0d12- 4ce1-9ca3 - 30c6d7e391ea', + summary: { total: 2, success: 1, fail: 1 }, + command: 'set 1 1\r\nget 1', result: [{ - "status": "success", "response": [{ "response": "OK", "status": "success", "command": "set 1 1" }, { "response": "error", "status": "fail", "command": "get 1" }] - }] -} + status: 'success', + response: [ + { response: 'OK', status: 'success', command: 'set 1 1' }, + { response: 'error', status: 'fail', command: 'get 1' }, + ], + }], +}; const mockCommandExecutionProvider = () => ({ createMany: jest.fn(), @@ -141,7 +145,7 @@ describe('WorkbenchService', () => { describe('createCommandExecution', () => { it('should successfully execute command and save it', async () => { - const result = await service.createCommandExecution(mockClientOptions, mockCreateCommandExecutionDto); + const result = await service.createCommandExecution(mockWorkbenchClientMetadata, mockCreateCommandExecutionDto); // can't predict execution time expect(result).toMatchObject(mockCommandExecutionToRun); expect(result.executionTime).toBeGreaterThan(0); @@ -155,9 +159,9 @@ describe('WorkbenchService', () => { mode: RunQueryMode.ASCII, }; - expect(await service.createCommandExecution(mockClientOptions, dto)).toEqual({ + expect(await service.createCommandExecution(mockWorkbenchClientMetadata, dto)).toEqual({ ...dto, - databaseId: mockClientOptions.instanceId, + databaseId: mockWorkbenchClientMetadata.databaseId, result: [ { response: ERROR_MESSAGES.WORKBENCH_COMMAND_NOT_SUPPORTED(dto.command.toUpperCase()), @@ -176,7 +180,7 @@ describe('WorkbenchService', () => { }; try { - await service.createCommandExecution(mockClientOptions, dto); + await service.createCommandExecution(mockWorkbenchClientMetadata, dto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -191,20 +195,20 @@ describe('WorkbenchService', () => { ); commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); - const result = await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); + const result = await service.createCommandExecutions(mockWorkbenchClientMetadata, mockCreateCommandExecutionsDto); expect(result).toEqual([mockCommandExecution, mockCommandExecution]); }); it('should successfully execute commands and save in group mode view', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(mockClientOptions, expect.anything()) + .calledWith(mockWorkbenchClientMetadata, expect.anything()) .mockResolvedValue([mockSendCommandResultSuccess]); commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); const result = await service.createCommandExecutions( - mockClientOptions, + mockWorkbenchClientMetadata, mockCreateCommandExecutionDtoWithGroupMode, ); @@ -213,17 +217,23 @@ describe('WorkbenchService', () => { it('should successfully execute commands with error and save summary', async () => { when(workbenchCommandsExecutor.sendCommand) - .calledWith(mockClientOptions, {...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[0]}) + .calledWith(mockWorkbenchClientMetadata, { + ...mockCreateCommandExecutionDtoWithGroupMode, + command: mockCommands[0], + }) .mockResolvedValue([mockSendCommandResultSuccess]); when(workbenchCommandsExecutor.sendCommand) - .calledWith(mockClientOptions, {...mockCreateCommandExecutionDtoWithGroupMode, command: mockCommands[1]}) + .calledWith(mockWorkbenchClientMetadata, { + ...mockCreateCommandExecutionDtoWithGroupMode, + command: mockCommands[1], + }) .mockResolvedValue([mockSendCommandResultFail]); commandExecutionProvider.createMany.mockResolvedValueOnce([mockCommandExecutionWithGroupMode]); const result = await service.createCommandExecutions( - mockClientOptions, + mockWorkbenchClientMetadata, mockCreateCommandExecutionDtoWithGroupMode, ); @@ -234,7 +244,7 @@ describe('WorkbenchService', () => { workbenchCommandsExecutor.sendCommand.mockRejectedValueOnce(new BadRequestException('error')); try { - await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); + await service.createCommandExecutions(mockWorkbenchClientMetadata, mockCreateCommandExecutionsDto); fail(); } catch (e) { expect(e).toBeInstanceOf(BadRequestException); @@ -245,7 +255,7 @@ describe('WorkbenchService', () => { commandExecutionProvider.createMany.mockRejectedValueOnce(new InternalServerErrorException('db error')); try { - await service.createCommandExecutions(mockClientOptions, mockCreateCommandExecutionsDto); + await service.createCommandExecutions(mockWorkbenchClientMetadata, mockCreateCommandExecutionsDto); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -257,7 +267,7 @@ describe('WorkbenchService', () => { it('should return list of command executions', async () => { commandExecutionProvider.getList.mockResolvedValueOnce([mockCommandExecution, mockCommandExecution]); - const result = await service.listCommandExecutions(mockClientOptions.instanceId); + const result = await service.listCommandExecutions(mockWorkbenchClientMetadata.databaseId); expect(result).toEqual([mockCommandExecution, mockCommandExecution]); }); @@ -265,7 +275,7 @@ describe('WorkbenchService', () => { commandExecutionProvider.getList.mockRejectedValueOnce(new InternalServerErrorException()); try { - await service.listCommandExecutions(mockClientOptions.instanceId); + await service.listCommandExecutions(mockWorkbenchClientMetadata.databaseId); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -276,7 +286,7 @@ describe('WorkbenchService', () => { it('should return full command executions', async () => { commandExecutionProvider.getOne.mockResolvedValueOnce(mockCommandExecution); - const result = await service.getCommandExecution(mockClientOptions.instanceId, mockCommandExecution.id); + const result = await service.getCommandExecution(mockWorkbenchClientMetadata.databaseId, mockCommandExecution.id); expect(result).toEqual(mockCommandExecution); }); @@ -284,7 +294,7 @@ describe('WorkbenchService', () => { commandExecutionProvider.getOne.mockRejectedValueOnce(new InternalServerErrorException()); try { - await service.getCommandExecution(mockClientOptions.instanceId, mockCommandExecution.id); + await service.getCommandExecution(mockWorkbenchClientMetadata.databaseId, mockCommandExecution.id); fail(); } catch (e) { expect(e).toBeInstanceOf(InternalServerErrorException); @@ -295,7 +305,7 @@ describe('WorkbenchService', () => { it('should not return anything on delete', async () => { commandExecutionProvider.delete.mockResolvedValueOnce('some response'); - const result = await service.deleteCommandExecution(mockClientOptions.instanceId, mockCommandExecution.id); + const result = await service.deleteCommandExecution(mockWorkbenchClientMetadata.databaseId, mockCommandExecution.id); expect(result).toEqual(undefined); }); diff --git a/redisinsight/api/src/modules/workbench/workbench.service.ts b/redisinsight/api/src/modules/workbench/workbench.service.ts index 8623c353a7..fb600f64a2 100644 --- a/redisinsight/api/src/modules/workbench/workbench.service.ts +++ b/redisinsight/api/src/modules/workbench/workbench.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { omit } from 'lodash'; -import { IFindRedisClientInstanceByOptions } from 'src/modules/redis/redis.service'; +import { ClientMetadata } from 'src/common/models'; import { WorkbenchCommandsExecutor } from 'src/modules/workbench/providers/workbench-commands.executor'; import { CommandExecutionProvider } from 'src/modules/workbench/providers/command-execution.provider'; import { CommandExecution } from 'src/modules/workbench/models/command-execution'; @@ -11,7 +11,6 @@ import ERROR_MESSAGES from 'src/constants/error-messages'; import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; import { CommandExecutionStatus } from 'src/modules/cli/dto/cli.dto'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; -import { AppTool } from 'src/models'; import { getUnsupportedCommands } from './utils/getUnsupportedCommands'; import { WorkbenchAnalyticsService } from './services/workbench-analytics/workbench-analytics.service'; @@ -27,16 +26,16 @@ export class WorkbenchService { /** * Send redis command from workbench and save history * - * @param clientOptions + * @param clientMetadata * @param dto */ async createCommandExecution( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateCommandExecutionDto, ): Promise> { const commandExecution: Partial = { ...omit(dto, 'commands'), - databaseId: clientOptions.instanceId, + databaseId: clientMetadata.databaseId, }; const command = multilineCommandToOneLine(dto.command); @@ -50,7 +49,7 @@ export class WorkbenchService { ]; } else { const startCommandExecutionTime = process.hrtime.bigint(); - commandExecution.result = await this.commandsExecutor.sendCommand(clientOptions, { ...dto, command }); + commandExecution.result = await this.commandsExecutor.sendCommand(clientMetadata, { ...dto, command }); const endCommandExecutionTime = process.hrtime.bigint(); commandExecution.executionTime = Math.round((Number(endCommandExecutionTime - startCommandExecutionTime) / 1000)); } @@ -61,17 +60,18 @@ export class WorkbenchService { /** * Send redis command from workbench and save history * - * @param clientOptions + * @param clientMetadata * @param dto + * @param commands */ async createCommandsExecution( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: Partial, commands: string[], ): Promise> { const commandExecution: Partial = { ...dto, - databaseId: clientOptions.instanceId, + databaseId: clientMetadata.databaseId, }; let executionTimeInNanoseconds = BigInt(0); @@ -87,7 +87,7 @@ export class WorkbenchService { status: CommandExecutionStatus.Fail, }); } - const result = await this.commandsExecutor.sendCommand(clientOptions, { ...dto, command }); + const result = await this.commandsExecutor.sendCommand(clientMetadata, { ...dto, command }); const endCommandExecutionTime = process.hrtime.bigint(); executionTimeInNanoseconds += (endCommandExecutionTime - startCommandExecutionTime); @@ -123,29 +123,26 @@ export class WorkbenchService { /** * Send redis command from workbench and save history * - * @param clientOptions + * @param clientMetadata * @param dto */ async createCommandExecutions( - clientOptions: IFindRedisClientInstanceByOptions, + clientMetadata: ClientMetadata, dto: CreateCommandExecutionsDto, ): Promise { // todo: handle concurrent client creation on RedisModule side // temporary workaround. Just create client before any command execution precess - await this.databaseConnectionService.getOrCreateClient({ - databaseId: clientOptions.instanceId, - namespace: AppTool.Workbench, - }); + await this.databaseConnectionService.getOrCreateClient(clientMetadata); if (dto.resultsMode === ResultsMode.GroupMode) { return this.commandExecutionProvider.createMany( - [await this.createCommandsExecution(clientOptions, dto, dto.commands)], + [await this.createCommandsExecution(clientMetadata, dto, dto.commands)], ); } // todo: rework to support pipeline // prepare and execute commands const commandExecutions = await Promise.all( - dto.commands.map(async (command) => await this.createCommandExecution(clientOptions, { ...dto, command })), + dto.commands.map(async (command) => await this.createCommandExecution(clientMetadata, { ...dto, command })), ); // save history From 48ca7b42fd55c24c071e0a6e81f68969a46f1a82 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 29 Nov 2022 11:35:00 +0000 Subject: [PATCH 028/107] Add verifyKeysIsNotDisplayedInTheList --- tests/e2e/helpers/keys.ts | 39 ++++-- tests/e2e/pageObjects/browser-page.ts | 1 + .../browser/search-capabilities.e2e.ts | 128 ++++++++++++++++-- 3 files changed, 140 insertions(+), 28 deletions(-) diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index d277db769e..9245673c78 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -84,14 +84,14 @@ export async function populateDBWithHashes(host: string, port: string, keyArgume const dbConf = { host, port: Number(port) }; const client = createClient(dbConf); - await client.on('error', async function(error: string) { + await client.on('error', async function (error: string) { throw new Error(error); }); - await client.on('connect', async function() { + await client.on('connect', async function () { if (keyArguments.keysCount != undefined) { for (let i = 0; i < keyArguments.keysCount; i++) { const keyName = `${keyArguments.keyNameStartWith}${common.generateWord(20)}`; - await client.hset([keyName, 'field1', 'Hello'], async(error: string) => { + await client.hset([keyName, 'field1', 'Hello'], async (error: string) => { if (error) { throw error; } @@ -113,10 +113,10 @@ export async function populateHashWithFields(host: string, port: string, keyArgu const client = createClient(dbConf); const fields: string[] = []; - await client.on('error', async function(error: string) { + await client.on('error', async function (error: string) { throw new Error(error); }); - await client.on('connect', async function() { + await client.on('connect', async function () { if (keyArguments.fieldsCount != undefined) { for (let i = 0; i < keyArguments.fieldsCount; i++) { const field = `${keyArguments.fieldStartWith}${common.generateWord(10)}`; @@ -124,7 +124,7 @@ export async function populateHashWithFields(host: string, port: string, keyArgu fields.push(field, fieldValue); } } - await client.hset(keyArguments.keyName, fields, async(error: string) => { + await client.hset(keyArguments.keyName, fields, async (error: string) => { if (error) { throw error; } @@ -144,17 +144,17 @@ export async function populateListWithElements(host: string, port: string, keyAr const client = createClient(dbConf); const elements: string[] = []; - await client.on('error', async function(error: string) { + await client.on('error', async function (error: string) { throw new Error(error); }); - await client.on('connect', async function() { + await client.on('connect', async function () { if (keyArguments.elementsCount != undefined) { for (let i = 0; i < keyArguments.elementsCount; i++) { const element = `${keyArguments.elementStartWith}${common.generateWord(10)}`; elements.push(element); } } - await client.lpush(keyArguments.keyName, elements, async(error: string) => { + await client.lpush(keyArguments.keyName, elements, async (error: string) => { if (error) { throw error; } @@ -174,17 +174,17 @@ export async function populateSetWithMembers(host: string, port: string, keyArgu const client = createClient(dbConf); const members: string[] = []; - await client.on('error', async function(error: string) { + await client.on('error', async function (error: string) { throw new Error(error); }); - await client.on('connect', async function() { + await client.on('connect', async function () { if (keyArguments.membersCount != undefined) { for (let i = 0; i < keyArguments.membersCount; i++) { const member = `${keyArguments.memberStartWith}${common.generateWord(10)}`; members.push(member); } } - await client.sadd(keyArguments.keyName, members, async(error: string) => { + await client.sadd(keyArguments.keyName, members, async (error: string) => { if (error) { throw error; } @@ -202,10 +202,10 @@ export async function deleteAllKeysFromDB(host: string, port: string): Promise { if (error) { throw error; @@ -223,4 +223,15 @@ export async function verifyKeysDisplayedInTheList(keyNames: string[]): Promise< for (const keyName of keyNames) { await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok(`The key ${keyName} not found`); } +} + +/** +* Verifying if the Keys are not in the List of keys +* @param keyNames The names of the keys +*/ + +export async function verifyKeysIsNotDisplayedInTheList(keyNames: string[]): Promise { + for (const keyName of keyNames) { + await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); + } } \ No newline at end of file diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index a8c7cb7ff8..0a3134f5c5 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,6 +22,7 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS + myRedisDbIcon = Selector('[data-testid = my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); editKeyTTLButton = Selector('[data-testid=edit-ttl-btn]'); diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 37abc490df..1ff7476439 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -8,9 +8,9 @@ import { ossStandaloneV5Config } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; -import { verifyKeysDisplayedInTheList } from '../../../helpers/keys'; +import { verifyKeysDisplayedInTheList, verifyKeysIsNotDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const common = new Common(); @@ -24,6 +24,12 @@ const searchPerValue = '(@name:"Hall School") | (@students:[500, 1000])'; let keyName = common.generateWord(10); let keyNames: string[]; let indexName = common.generateWord(5); +const databasesForAdding = [ + ossStandaloneConfig +]; + +const simpleDbName = ossStandaloneConfig.databaseName +const bigDbName = ossStandaloneBigConfig.databaseName async function verifyContext(): Promise { await t .expect(browserPage.selectIndexDdn.withText(indexName).exists).ok('Index selection not saved') @@ -31,16 +37,16 @@ async function verifyContext(): Promise { .expect(browserPage.keyNameFormDetails.withExactText(keyName).exists).ok('Key details not opened'); } -fixture `Search capabilities in Browser` +fixture`Search capabilities in Browser` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl); test - .before(async() => { + .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); keyName = common.generateWord(10); await browserPage.addHashKey(keyName); }) - .after(async() => { + .after(async () => { // Clear and delete database await browserPage.deleteKeyByName(keyName); await cliPage.sendCommandsInCli([`DEL ${keyNames.join(' ')}`, `FT.DROPINDEX ${indexName}`]); @@ -102,11 +108,11 @@ test await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).ok('Database not scanned after returning to Pattern search mode'); }); test - .before(async() => { + .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); }) - .after(async() => { - // Clear and delete database + .after(async () => { + // Clear and delete database await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexName}`); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Search by index keys scanned for JSON', async t => { @@ -129,10 +135,10 @@ test await t.expect(keysNumberOfResults).contains('10 000', 'Number of results is not 10 000'); }); test - .before(async() => { + .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneV5Config, ossStandaloneV5Config.databaseName); }) - .after(async() => { + .after(async () => { await deleteStandaloneDatabaseApi(ossStandaloneV5Config); })('No RediSearch module message', async t => { const noRedisearchMessage = 'RediSearch module is not loaded. Create a free Redis database(opens in a new tab or window) with module support on Redis Cloud.'; @@ -147,10 +153,10 @@ test await t.switchToParentWindow(); }); test - .before(async() => { + .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); }) - .after(async() => { + .after(async () => { await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexName}`); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Index creation', async t => { @@ -195,10 +201,10 @@ test await browserPage.selectIndexByName(indexName); }); test - .before(async() => { + .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .after(async() => { + .after(async () => { // Clear and delete database await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexName}`); await deleteStandaloneDatabaseApi(ossStandaloneConfig); @@ -233,3 +239,97 @@ test await common.reloadPage(); await t.expect(browserPage.keyListTable.textContent).contains(notSelectedIndexText, 'Search by Values of Keys section not opened'); }); + +test + .only + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); + await addNewStandaloneDatabasesApi(databasesForAdding); + }) + .after(async () => { + // Clear and delete database + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { + /* + + Presetup: + + Create several hash/json keys in Standalone DB + + Create Index for created keys + + Create Index for hash/json for Standalone Big DB + + Steps to cover: + + Open DB (Standalone DB) + + Select Search mode + + Select created index from 2nd pre-setup step + + Validate displayed keys + + Open another DB (Standalone Big) → Search mode is displayed (saved as context) + + Validate that keys belong to the Standalone Big database + + */ + const keyNameSimpleDb = common.generateWord(10); + const keyNameBigDb = common.generateWord(10); + + const indexNameSimpleDb = `idx:${keyNameSimpleDb}`; // index in the standalone database + const indexNameBigDb = `idx:${keyNameBigDb}`; // index in the big standalone database + + // key names to validate in the standalone database + keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`]; + + /* + create index as name ${indexNameBigDb} + in the big standalone database + with the help of CLI + */ + const commandsForBigStandalone = [ + `FT.CREATE ${indexNameBigDb} ON hash PREFIX 1 mobile SCHEMA k0 text` + ]; + + await cliPage.sendCommandsInCli(commandsForBigStandalone); + + await t.click(browserPage.treeViewButton); // switch to tree view + + await t.click(browserPage.redisearchModeBtn); + + await t.click(browserPage.myRedisDbIcon); // go back to database selection page + + await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database + + const commandsForStandalone = [ + `HSET ${keyNames[0]} "name" "Hall School" "description" " Spanning 10 states" "class" "independent" "type" "traditional" "address_city" "London" "address_street" "Manor Street" "students" 342 "location" "51.445417, -0.258352"`, + `HSET ${keyNames[1]} "name" "Garden School" "description" "Garden School is a new outdoor" "class" "state" "type" "forest; montessori;" "address_city" "London" "address_street" "Gordon Street" "students" 1452 "location" "51.402926, -0.321523"`, + `HSET ${keyNames[2]} "name" "Gillford School" "description" "Gillford School is a centre" "class" "private" "type" "democratic; waldorf" "address_city" "Goudhurst" "address_street" "Goudhurst" "students" 721 "location" "51.112685, 0.451076"`, + `HSET ${keyNames[3]} "name" "Box School" "description" "Top School is a new outdoor" "class" "state" "type" "forest; montessori;" "address_city" "London" "address_street" "Gordon Street" "students" 1452 "location" "51.402926, -0.321523"`, + `HSET ${keyNames[4]} "name" "Bill School" "description" "Billing School is a centre" "class" "private" "type" "democratic; waldorf" "address_city" "Goudhurst" "address_street" "Goudhurst" "students" 721 "location" "51.112685, 0.451076"`, + `FT.CREATE ${indexNameSimpleDb} ON HASH PREFIX 1 "${keyNameSimpleDb}:" SCHEMA name TEXT NOSTEM description TEXT class TAG type TAG SEPARATOR ";" address_city AS city TAG address_street AS address TEXT NOSTEM students NUMERIC SORTABLE location GEO` + ]; + // Create 5 keys and index + await cliPage.sendCommandsInCli(commandsForStandalone); + + await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view + + await t.eval(() => location.reload()); + + await t.click(browserPage.redisearchModeBtn); // click redisearch button + + await browserPage.selectIndexByName(indexNameSimpleDb); // select pre-created index in the standalone database + + await verifyKeysDisplayedInTheList(keyNames); // verify created keys are visible + + await t.click(browserPage.myRedisDbIcon); // go back to database selection page + + await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName + + await verifyKeysIsNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are not visible + + + }); From ba6f5cfd21dc81d82abd9300fcce3c15c485b4c1 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 29 Nov 2022 11:37:46 +0000 Subject: [PATCH 029/107] Add test for Verify that indexed keys from previous DB are NOT displayed --- .../browser/search-capabilities.e2e.ts | 39 +++++++------------ 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 1ff7476439..6daaa80196 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -1,4 +1,4 @@ -import { t } from 'testcafe'; +import { Selector, t } from 'testcafe'; import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; import { BrowserPage, CliPage, MyRedisDatabasePage } from '../../../pageObjects'; import { @@ -250,30 +250,10 @@ test // Clear and delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + // delete index and keys })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { /* - - Presetup: - - Create several hash/json keys in Standalone DB - - Create Index for created keys - - Create Index for hash/json for Standalone Big DB - - Steps to cover: - - Open DB (Standalone DB) - - Select Search mode - - Select created index from 2nd pre-setup step - - Validate displayed keys - - Open another DB (Standalone Big) → Search mode is displayed (saved as context) - - Validate that keys belong to the Standalone Big database + link add */ const keyNameSimpleDb = common.generateWord(10); @@ -282,6 +262,10 @@ test const indexNameSimpleDb = `idx:${keyNameSimpleDb}`; // index in the standalone database const indexNameBigDb = `idx:${keyNameBigDb}`; // index in the big standalone database + console.log("indexNameSimpleDb--- " + indexNameSimpleDb); + console.log("indexNameBigDb--- " + indexNameBigDb); + + // key names to validate in the standalone database keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`]; @@ -315,9 +299,11 @@ test // Create 5 keys and index await cliPage.sendCommandsInCli(commandsForStandalone); - await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view + await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily + + await t.debug() - await t.eval(() => location.reload()); + await t.eval(() => location.reload()); // replace with existing await t.click(browserPage.redisearchModeBtn); // click redisearch button @@ -329,7 +315,8 @@ test await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName - await verifyKeysIsNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are not visible + await verifyKeysIsNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible + await t.expect(Selector('span').withText('Select Index').exists).ok(); // Verify index is NOT selected }); From 16b9b6bc4887bc48ddc44f39f6305fd8461b70d1 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 29 Nov 2022 15:20:13 +0000 Subject: [PATCH 030/107] Delete t.debug() --- .../e2e/tests/critical-path/browser/search-capabilities.e2e.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 6daaa80196..01dc7d3d12 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -301,8 +301,6 @@ test await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily - await t.debug() - await t.eval(() => location.reload()); // replace with existing await t.click(browserPage.redisearchModeBtn); // click redisearch button From e861de3c241ae2cc295933e28b615def234232fa Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 29 Nov 2022 18:12:53 +0000 Subject: [PATCH 031/107] After hook - delete keys and index Code cleaning --- .../browser/search-capabilities.e2e.ts | 34 ++++++++++--------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 01dc7d3d12..35af38672d 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -8,7 +8,7 @@ import { ossStandaloneV5Config } from '../../../helpers/conf'; import { rte } from '../../../helpers/constants'; -import { addNewStandaloneDatabasesApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; import { verifyKeysDisplayedInTheList, verifyKeysIsNotDisplayedInTheList } from '../../../helpers/keys'; @@ -28,6 +28,12 @@ const databasesForAdding = [ ossStandaloneConfig ]; +const keyNameSimpleDb = common.generateWord(10); +const keyNameBigDb = common.generateWord(10); + +let indexNameSimpleDb = `idx:${keyNameSimpleDb}`; // index in the standalone database +let indexNameBigDb = `idx:${keyNameBigDb}`; // index in the big standalone database + const simpleDbName = ossStandaloneConfig.databaseName const bigDbName = ossStandaloneBigConfig.databaseName async function verifyContext(): Promise { @@ -241,30 +247,26 @@ test }); test - .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); - await addNewStandaloneDatabasesApi(databasesForAdding); + await addNewStandaloneDatabaseApi(ossStandaloneConfig); }) .after(async () => { + await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameBigDb}`); + + await t.click(browserPage.myRedisDbIcon); // go back to database selection page + + await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database + + await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameSimpleDb}`); // Clear and delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); // delete index and keys })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { /* - link add - + Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 */ - const keyNameSimpleDb = common.generateWord(10); - const keyNameBigDb = common.generateWord(10); - - const indexNameSimpleDb = `idx:${keyNameSimpleDb}`; // index in the standalone database - const indexNameBigDb = `idx:${keyNameBigDb}`; // index in the big standalone database - - console.log("indexNameSimpleDb--- " + indexNameSimpleDb); - console.log("indexNameBigDb--- " + indexNameBigDb); - // key names to validate in the standalone database keyNames = [`${keyNameSimpleDb}:1`, `${keyNameSimpleDb}:2`, `${keyNameSimpleDb}:3`, `${keyNameSimpleDb}:4`, `${keyNameSimpleDb}:5`]; @@ -301,7 +303,7 @@ test await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily - await t.eval(() => location.reload()); // replace with existing + await common.reloadPage(); // reload page await t.click(browserPage.redisearchModeBtn); // click redisearch button @@ -315,6 +317,6 @@ test await verifyKeysIsNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible - await t.expect(Selector('span').withText('Select Index').exists).ok(); // Verify index is NOT selected + await t.expect(Selector('span').withText('Select Index').exists).ok('verify index is not selected'); // Verify index is NOT selected }); From 9250f19f188dba4cee11c35a4fd5b62fa5f38474 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 29 Nov 2022 18:26:10 +0000 Subject: [PATCH 032/107] Code cleaning --- tests/e2e/helpers/keys.ts | 2 +- tests/e2e/pageObjects/browser-page.ts | 16 ++++++++-------- .../browser/search-capabilities.e2e.ts | 7 +++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/tests/e2e/helpers/keys.ts b/tests/e2e/helpers/keys.ts index 9245673c78..f2b1fa8007 100644 --- a/tests/e2e/helpers/keys.ts +++ b/tests/e2e/helpers/keys.ts @@ -230,7 +230,7 @@ export async function verifyKeysDisplayedInTheList(keyNames: string[]): Promise< * @param keyNames The names of the keys */ -export async function verifyKeysIsNotDisplayedInTheList(keyNames: string[]): Promise { +export async function verifyKeysNotDisplayedInTheList(keyNames: string[]): Promise { for (const keyName of keyNames) { await t.expect(await browserPage.isKeyIsDisplayedInTheList(keyName)).notOk(`The key ${keyName} found`); } diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 657fb6b3aa..4aba59ac9c 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,7 +22,7 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS - myRedisDbIcon = Selector('[data-testid = my-redis-db-icon]'); + myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); editKeyTTLButton = Selector('[data-testid=edit-ttl-btn]'); @@ -99,8 +99,8 @@ export class BrowserPage { workbenchLinkButton = Selector('[data-test-subj=workbench-page-btn]'); cancelStreamGroupBtn = Selector('[data-testid=cancel-stream-groups-btn]'); submitTooltipBtn = Selector('[data-testid=submit-tooltip-btn]'); - patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]'); - redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]'); + patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]'); + redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]'); //CONTAINERS streamGroupsContainer = Selector('[data-testid=stream-groups-container]'); streamConsumersContainer = Selector('[data-testid=stream-consumers-container]'); @@ -137,7 +137,7 @@ export class BrowserPage { createIndexBtn = Selector('[data-testid=create-index-btn]'); cancelIndexCreationBtn = Selector('[data-testid=create-index-cancel-btn]'); confirmIndexCreationBtn = Selector('[data-testid=create-index-btn]'); - resizeTrigger = Selector('[data-testid^=resize-trigger-]'); + resizeTrigger = Selector('[data-testid^=resize-trigger-]'); //TABS streamTabGroups = Selector('[data-testid=stream-tab-Groups]'); streamTabConsumers = Selector('[data-testid=stream-tab-Consumers]'); @@ -446,7 +446,7 @@ export class BrowserPage { async addEntryToStream(field: string, value: string, entryId?: string): Promise { await t .click(this.addNewStreamEntry) - // Specify field, value and add new entry + // Specify field, value and add new entry .typeText(this.streamField, field, { replace: true, paste: true }) .typeText(this.streamValue, value, { replace: true, paste: true }); if (entryId !== undefined) { @@ -454,7 +454,7 @@ export class BrowserPage { } await t .click(this.saveElementButton) - // Validate that new entry is added + // Validate that new entry is added .expect(this.streamEntriesContainer.textContent).contains(field, 'Field parameter not correct') .expect(this.streamEntriesContainer.textContent).contains(value, 'Value parameter not correct'); } @@ -567,8 +567,8 @@ export class BrowserPage { * Delete keys by their Names * @param keyNames The names of the key array */ - async deleteKeysByNames(keyNames: string[]): Promise { - for(const name of keyNames) { + async deleteKeysByNames(keyNames: string[]): Promise { + for (const name of keyNames) { await this.deleteKeyByName(name); } } diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 35af38672d..4c3cf5e250 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -10,7 +10,7 @@ import { import { rte } from '../../../helpers/constants'; import { addNewStandaloneDatabaseApi, deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; import { Common } from '../../../helpers/common'; -import { verifyKeysDisplayedInTheList, verifyKeysIsNotDisplayedInTheList } from '../../../helpers/keys'; +import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const common = new Common(); @@ -315,8 +315,7 @@ test await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName - await verifyKeysIsNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible - - await t.expect(Selector('span').withText('Select Index').exists).ok('verify index is not selected'); // Verify index is NOT selected + await verifyKeysNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible + await t.expect(Selector('span').withText('Select Index').exists).ok('verify index is not selected'); }); From 848d7d8aa6fbfb0769242756bcd244b10cf4cdda Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 30 Nov 2022 00:19:18 +0200 Subject: [PATCH 033/107] #RI-3775 rework ClientMetadata decorators, fix tests --- redisinsight/api/src/__mocks__/databases.ts | 7 - .../client-metadata.decorator.ts | 78 +-- .../decorators/session/session.decorator.ts | 17 +- .../api/src/common/models/client-metadata.ts | 12 + redisinsight/api/src/common/models/session.ts | 8 + .../controllers/hash/hash.controller.ts | 3 +- .../controllers/keys/keys.controller.ts | 3 +- .../controllers/list/list.controller.ts | 3 +- .../redisearch/redisearch.controller.ts | 3 +- .../rejson-rl/rejson-rl.controller.ts | 2 +- .../browser/controllers/set/set.controller.ts | 3 +- .../stream/consumer-group.controller.ts | 3 +- .../controllers/stream/consumer.controller.ts | 3 +- .../controllers/stream/stream.controller.ts | 3 +- .../controllers/string/string.controller.ts | 3 +- .../controllers/z-set/z-set.controller.ts | 3 +- .../browser-client-metadata.decorator.ts | 11 + .../modules/cli/controllers/cli.controller.ts | 10 +- .../cli-client-metadata.decorator.ts | 13 + .../cli-business/cli-business.service.spec.ts | 22 +- .../cli-business/cli-business.service.ts | 10 +- .../cluster-monitor.controller.ts | 4 +- .../database-analysis.controller.ts | 4 +- .../database-connection.service.spec.ts | 13 +- .../database/database-info.controller.ts | 10 +- .../modules/database/database.controller.ts | 9 +- .../src/modules/database/database.service.ts | 5 + .../src/modules/pub-sub/pub-sub.controller.ts | 7 +- .../modules/redis/redis-tool.service.spec.ts | 12 +- .../src/modules/redis/redis-tool.service.ts | 8 +- .../src/modules/redis/redis.service.spec.ts | 541 +++++++----------- .../modules/slow-log/slow-log.controller.ts | 11 +- .../workbench-client-metadata.decorator.ts | 11 + .../modules/workbench/plugins.controller.ts | 8 +- .../modules/workbench/workbench.controller.ts | 6 +- 35 files changed, 404 insertions(+), 465 deletions(-) create mode 100644 redisinsight/api/src/modules/browser/decorators/browser-client-metadata.decorator.ts create mode 100644 redisinsight/api/src/modules/cli/decorators/cli-client-metadata.decorator.ts create mode 100644 redisinsight/api/src/modules/workbench/decorators/workbench-client-metadata.decorator.ts diff --git a/redisinsight/api/src/__mocks__/databases.ts b/redisinsight/api/src/__mocks__/databases.ts index 580c710427..409a9a06ee 100644 --- a/redisinsight/api/src/__mocks__/databases.ts +++ b/redisinsight/api/src/__mocks__/databases.ts @@ -7,8 +7,6 @@ import { mockIORedisClient } from 'src/__mocks__/redis'; import { mockSentinelMasterDto } from 'src/__mocks__/redis-sentinel'; import { pick } from 'lodash'; import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto'; -import { ClientMetadata } from 'src/modules/redis/models/client-metadata'; -import { AppTool } from 'src/models'; import { DatabaseOverview } from 'src/modules/database/models/database-overview'; export const mockDatabaseId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id'; @@ -115,11 +113,6 @@ export const mockClusterDatabaseWithTlsAuthEntity = Object.assign(new DatabaseEn nodes: JSON.stringify(mockClusterNodes), }); -export const mockClientMetadata: ClientMetadata = { - databaseId: mockDatabase.id, - namespace: AppTool.Common, -}; - export const mockDatabaseOverview: DatabaseOverview = { version: '6.2.4', usedMemory: 1, diff --git a/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts b/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts index 504c293a59..783d36c4b9 100644 --- a/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts +++ b/redisinsight/api/src/common/decorators/client-metadata/client-metadata.decorator.ts @@ -1,72 +1,56 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; import { ClientContext, ClientMetadata } from 'src/common/models'; import { sessionFromRequestFactory } from 'src/common/decorators'; -import { API_PARAM_CLI_CLIENT_ID, API_PARAM_DATABASE_ID } from 'src/common/constants'; +import { Validator } from 'class-validator'; +import { API_PARAM_DATABASE_ID } from 'src/common/constants'; -export interface IClientMetadataDecoratorOptions { - paramPath?: string, - queryPath?: string, - bodyPath?: string, +const validator = new Validator(); + +export interface IClientMetadataParamOptions { + databaseIdParam?: string, + uniqueIdParam?: string, context?: ClientContext, - uniqueId?: string, } -export const clientMetadataFromRequestFactory = (options: IClientMetadataDecoratorOptions, ctx: ExecutionContext) => { - const opts: IClientMetadataDecoratorOptions = { +export const clientMetadataParamFactory = ( + options: IClientMetadataParamOptions, + ctx: ExecutionContext, +): ClientMetadata => { + const opts: IClientMetadataParamOptions = { context: ClientContext.Common, + databaseIdParam: API_PARAM_DATABASE_ID, ...options, }; - const request = ctx.switchToHttp().getRequest(); + const req = ctx.switchToHttp().getRequest(); let databaseId; - if (opts.paramPath) { - databaseId = request.params?.[opts.paramPath]; - } else if (opts.queryPath) { - // TBD - } else if (opts.bodyPath) { - // TBD + if (opts?.databaseIdParam) { + databaseId = req.params?.[opts.databaseIdParam]; } - // todo: add validation - if (!databaseId) { - // todo: define proper error - throw new Error('No databaseId found'); + let uniqueId; + if (opts?.uniqueIdParam) { + uniqueId = req.params?.[opts.uniqueIdParam]; } - return plainToClass(ClientMetadata, { + const clientMetadata = plainToClass(ClientMetadata, { session: sessionFromRequestFactory(undefined, ctx), databaseId, - context: opts.context, - uniqueId: opts.uniqueId, + uniqueId, + context: opts?.context || ClientContext.Common, }); -}; - -export const ClientMetadataFromRequest = createParamDecorator(clientMetadataFromRequestFactory); -export const browserClientMetadataFactory = ( - param = API_PARAM_DATABASE_ID, - ctx: ExecutionContext, -): ClientMetadata => clientMetadataFromRequestFactory({ - paramPath: param, - context: ClientContext.Browser, -}, ctx); - -export const BrowserClientMetadata = createParamDecorator(browserClientMetadataFactory); + const errors = validator.validateSync(clientMetadata, { + whitelist: false, // we need this to allow additional fields if needed for flexibility + }); -export const cliClientMetadataFactory = ( - options = { databaseParam: API_PARAM_DATABASE_ID, uuidParam: API_PARAM_CLI_CLIENT_ID }, - ctx: ExecutionContext, -): ClientMetadata => { - const request = ctx.switchToHttp().getRequest(); + if (errors?.length) { + throw new BadRequestException(Object.values(errors[0].constraints) || 'Bad request'); + } - // todo: add validation - return clientMetadataFromRequestFactory({ - paramPath: options.databaseParam, - context: ClientContext.CLI, - uniqueId: request.params?.[options.uuidParam], - }, ctx); + return clientMetadata; }; -export const CliClientMetadata = createParamDecorator(cliClientMetadataFactory); +export const ClientMetadataParam = createParamDecorator(clientMetadataParamFactory); diff --git a/redisinsight/api/src/common/decorators/session/session.decorator.ts b/redisinsight/api/src/common/decorators/session/session.decorator.ts index de8e502c5d..976a921075 100644 --- a/redisinsight/api/src/common/decorators/session/session.decorator.ts +++ b/redisinsight/api/src/common/decorators/session/session.decorator.ts @@ -1,11 +1,24 @@ -import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { Validator } from 'class-validator'; import { plainToClass } from 'class-transformer'; import { Session } from 'src/common/models'; +const validator = new Validator(); + export const sessionFromRequestFactory = (data: unknown, ctx: ExecutionContext): Session => { const request = ctx.switchToHttp().getRequest(); - return plainToClass(Session, request.session); + const session = plainToClass(Session, request.session); + + const errors = validator.validateSync(session, { + whitelist: false, // we need this to allow additional fields if needed for flexibility + }); + + if (errors?.length) { + throw new BadRequestException(Object.values(errors[0].constraints) || 'Bad request'); + } + + return session; }; export const SessionFromRequest = createParamDecorator(sessionFromRequestFactory); diff --git a/redisinsight/api/src/common/models/client-metadata.ts b/redisinsight/api/src/common/models/client-metadata.ts index 4841b0ac98..db9c561929 100644 --- a/redisinsight/api/src/common/models/client-metadata.ts +++ b/redisinsight/api/src/common/models/client-metadata.ts @@ -1,4 +1,8 @@ import { Session } from 'src/common/models/session'; +import { Type } from 'class-transformer'; +import { + IsEnum, IsNotEmpty, IsOptional, IsString, +} from 'class-validator'; export enum ClientContext { Common = 'Common', @@ -8,11 +12,19 @@ export enum ClientContext { } export class ClientMetadata { + @IsNotEmpty() + @Type(() => Session) session: Session; + @IsNotEmpty() + @IsString() databaseId: string; + @IsNotEmpty() + @IsEnum(ClientContext) context: ClientContext; + @IsOptional() + @IsString() uniqueId?: string; } diff --git a/redisinsight/api/src/common/models/session.ts b/redisinsight/api/src/common/models/session.ts index 39ef6a3903..b5ca9680e5 100644 --- a/redisinsight/api/src/common/models/session.ts +++ b/redisinsight/api/src/common/models/session.ts @@ -1,3 +1,5 @@ +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + export interface ISession { userId: string; sessionId: string; @@ -5,9 +7,15 @@ export interface ISession { } export class Session implements ISession { + @IsNotEmpty() + @IsString() userId: string; + @IsNotEmpty() + @IsString() sessionId: string; + @IsOptional() + @IsString() uniqueId?: string; } diff --git a/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts b/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts index ee1711537d..2f6d2c78bc 100644 --- a/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/hash/hash.controller.ts @@ -11,7 +11,8 @@ import { } from '@nestjs/swagger'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; import { AddFieldsToHashDto, diff --git a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts index be54e66324..9397a8df96 100644 --- a/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/keys/keys.controller.ts @@ -13,7 +13,8 @@ import { KeysBusinessService } from 'src/modules/browser/services/keys-business/ import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { RedisService } from 'src/modules/redis/redis.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; import { DeleteKeysDto, diff --git a/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts b/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts index 23e0d14a35..ab9b71ed47 100644 --- a/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/list/list.controller.ts @@ -30,7 +30,8 @@ import { DeleteListElementsResponse, PushListElementsResponse, } from 'src/modules/browser/dto'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; import { ClientMetadata } from 'src/common/models'; import { ListBusinessService } from '../../services/list-business/list-business.service'; diff --git a/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts b/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts index aef0db0e8f..8e7ca17b6d 100644 --- a/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/redisearch/redisearch.controller.ts @@ -11,7 +11,8 @@ import { ApiTags, } from '@nestjs/swagger'; import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; import { CreateRedisearchIndexDto, diff --git a/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts b/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts index 3b3fd148da..3d72bea380 100644 --- a/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/rejson-rl/rejson-rl.controller.ts @@ -19,7 +19,7 @@ import { } from 'src/modules/browser/dto'; import { RejsonRlBusinessService } from 'src/modules/browser/services/rejson-rl-business/rejson-rl-business.service'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; -import { BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; import { ClientMetadata } from 'src/common/models'; @ApiTags('REJSON-RL') diff --git a/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts b/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts index 3e45af8125..b8eaa66e93 100644 --- a/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/set/set.controller.ts @@ -12,7 +12,8 @@ import { import { ApiRedisParams } from 'src/decorators/api-redis-params.decorator'; import { ClientMetadata } from 'src/common/models'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { AddMembersToSetDto, CreateSetWithExpireDto, diff --git a/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts index 4e04635767..1a8a314656 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/consumer-group.controller.ts @@ -16,7 +16,8 @@ import { import { ConsumerGroupService } from 'src/modules/browser/services/stream/consumer-group.service'; import { KeyDto } from 'src/modules/browser/dto'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') diff --git a/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts index a3bae2edee..db7cec5ddd 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/consumer.controller.ts @@ -13,7 +13,8 @@ import { } from 'src/modules/browser/dto/stream.dto'; import { ConsumerService } from 'src/modules/browser/services/stream/consumer.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') diff --git a/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts b/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts index 7681564f5d..c159aa41b0 100644 --- a/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/stream/stream.controller.ts @@ -16,7 +16,8 @@ import { } from 'src/modules/browser/dto/stream.dto'; import { StreamService } from 'src/modules/browser/services/stream/stream.service'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; @ApiTags('Streams') diff --git a/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts b/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts index 1252e376b8..ae90902eab 100644 --- a/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/string/string.controller.ts @@ -16,7 +16,8 @@ import { } from 'src/modules/browser/dto/string.dto'; import { GetKeyInfoDto } from 'src/modules/browser/dto'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; import { StringBusinessService } from '../../services/string-business/string-business.service'; diff --git a/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts b/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts index 1d80221b39..d1223392da 100644 --- a/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts +++ b/redisinsight/api/src/modules/browser/controllers/z-set/z-set.controller.ts @@ -9,7 +9,8 @@ import { import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { BaseController } from 'src/modules/browser/controllers/base.controller'; -import { ApiQueryRedisStringEncoding, BrowserClientMetadata } from 'src/common/decorators'; +import { BrowserClientMetadata } from 'src/modules/browser/decorators/browser-client-metadata.decorator'; +import { ApiQueryRedisStringEncoding } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; import { AddMembersToZSetDto, diff --git a/redisinsight/api/src/modules/browser/decorators/browser-client-metadata.decorator.ts b/redisinsight/api/src/modules/browser/decorators/browser-client-metadata.decorator.ts new file mode 100644 index 0000000000..999925d736 --- /dev/null +++ b/redisinsight/api/src/modules/browser/decorators/browser-client-metadata.decorator.ts @@ -0,0 +1,11 @@ +import { API_PARAM_DATABASE_ID } from 'src/common/constants'; +import { createParamDecorator } from '@nestjs/common'; +import { ClientContext } from 'src/common/models'; +import { clientMetadataParamFactory } from 'src/common/decorators'; + +export const BrowserClientMetadata = ( + databaseIdParam = API_PARAM_DATABASE_ID, +) => createParamDecorator(clientMetadataParamFactory)({ + context: ClientContext.Browser, + databaseIdParam, +}); diff --git a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts index 1631343145..0c84c02f9d 100644 --- a/redisinsight/api/src/modules/cli/controllers/cli.controller.ts +++ b/redisinsight/api/src/modules/cli/controllers/cli.controller.ts @@ -16,12 +16,11 @@ import { SendClusterCommandDto, SendCommandDto, SendCommandResponse, - CreateCliClientDto, } from 'src/modules/cli/dto/cli.dto'; import { CliBusinessService } from 'src/modules/cli/services/cli-business/cli-business.service'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ApiCLIParams } from 'src/modules/cli/decorators/api-cli-params.decorator'; -import { CliClientMetadata, ClientMetadataFromRequest } from 'src/common/decorators'; +import { CliClientMetadata } from 'src/modules/cli/decorators/cli-client-metadata.decorator'; import { ClientMetadata } from 'src/common/models'; @ApiTags('CLI') @@ -44,7 +43,7 @@ export class CliController { ], }) async getClient( - @ClientMetadataFromRequest() clientMetadata: ClientMetadata, + @CliClientMetadata() clientMetadata: ClientMetadata, ): Promise { return this.service.getClient(clientMetadata); } @@ -104,10 +103,9 @@ export class CliController { ], }) async deleteClient( - @Param('dbInstance') dbInstance: string, - @Param('uuid') uuid: string, + @CliClientMetadata() clientMetadata: ClientMetadata, ): Promise { - return this.service.deleteClient(dbInstance, uuid); + return this.service.deleteClient(clientMetadata); } @Patch('/:uuid') diff --git a/redisinsight/api/src/modules/cli/decorators/cli-client-metadata.decorator.ts b/redisinsight/api/src/modules/cli/decorators/cli-client-metadata.decorator.ts new file mode 100644 index 0000000000..d8d586cd52 --- /dev/null +++ b/redisinsight/api/src/modules/cli/decorators/cli-client-metadata.decorator.ts @@ -0,0 +1,13 @@ +import { API_PARAM_CLI_CLIENT_ID, API_PARAM_DATABASE_ID } from 'src/common/constants'; +import { createParamDecorator } from '@nestjs/common'; +import { ClientContext } from 'src/common/models'; +import { clientMetadataParamFactory } from 'src/common/decorators'; + +export const CliClientMetadata = ( + databaseIdParam = API_PARAM_DATABASE_ID, + uniqueIdParam = API_PARAM_CLI_CLIENT_ID, +) => createParamDecorator(clientMetadataParamFactory)({ + context: ClientContext.CLI, + databaseIdParam, + uniqueIdParam, +}); diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts index eafa4be94c..c38cf6df7d 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.spec.ts @@ -1,13 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; import { get } from 'lodash'; -import { v4 as uuidv4 } from 'uuid'; + import { when } from 'jest-when'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { mockRedisServerInfoResponse, mockRedisWrongTypeError, - mockDatabase, mockCliAnalyticsService, mockRedisMovedError, MockType, mockCliClientMetadata, @@ -37,7 +36,6 @@ import { OutputFormatterManager } from './output-formatter/output-formatter-mana import { CliOutputFormatterTypes, IOutputFormatterStrategy } from './output-formatter/output-formatter.interface'; import { CliBusinessService } from './cli-business.service'; -const mockClientUuid = uuidv4(); const mockNode = { host: '127.0.0.1', port: 7002, @@ -107,11 +105,11 @@ describe('CliBusinessService', () => { describe('getClient', () => { it('should successfully create new redis client', async () => { - cliTool.createNewToolClient.mockResolvedValue(mockClientUuid); + cliTool.createNewToolClient.mockResolvedValue(mockCliClientMetadata.uniqueId); const result = await service.getClient(mockCliClientMetadata); - expect(result).toEqual({ uuid: mockClientUuid }); + expect(result).toEqual({ uuid: mockCliClientMetadata.uniqueId }); expect(analyticsService.sendClientCreatedEvent).toHaveBeenCalledWith( mockCliClientMetadata.databaseId, ); @@ -152,11 +150,11 @@ describe('CliBusinessService', () => { describe('reCreateClient', () => { it('should successfully create new redis client', async () => { - cliTool.reCreateToolClient.mockResolvedValue(mockClientUuid); + cliTool.reCreateToolClient.mockResolvedValue(mockCliClientMetadata.uniqueId); const result = await service.reCreateClient(mockCliClientMetadata); - expect(result).toEqual({ uuid: mockClientUuid }); + expect(result).toEqual({ uuid: mockCliClientMetadata.uniqueId }); expect(analyticsService.sendClientRecreatedEvent).toHaveBeenCalledWith( mockCliClientMetadata.databaseId, ); @@ -199,10 +197,7 @@ describe('CliBusinessService', () => { it('should successfully close redis client', async () => { cliTool.deleteToolClient.mockResolvedValue(1); - const result = await service.deleteClient( - mockDatabase.id, - mockClientUuid, - ); + const result = await service.deleteClient(mockCliClientMetadata); expect(result).toEqual({ affected: 1 }); expect(analyticsService.sendClientDeletedEvent).toHaveBeenCalledWith( @@ -215,10 +210,7 @@ describe('CliBusinessService', () => { cliTool.deleteToolClient.mockRejectedValue(new Error(mockENotFoundMessage)); try { - await service.deleteClient( - mockDatabase.id, - mockClientUuid, - ); + await service.deleteClient(mockCliClientMetadata); fail(); } catch (err) { expect(err).toBeInstanceOf(InternalServerErrorException); diff --git a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts index 3fe7b11910..5132d5efe5 100644 --- a/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts +++ b/redisinsight/api/src/modules/cli/services/cli-business/cli-business.service.ts @@ -101,19 +101,17 @@ export class CliBusinessService { /** * Method to close exist redis client - * @param instanceId - * @param uuid + * @param clientMetadata */ public async deleteClient( - instanceId: string, - uuid: string, + clientMetadata: ClientMetadata, ): Promise { this.logger.log('Deleting Redis client for CLI.'); try { - const affected = await this.cliTool.deleteToolClient(instanceId, uuid); + const affected = await this.cliTool.deleteToolClient(clientMetadata); this.logger.log('Succeed to delete Redis client for CLI.'); if (affected) { - this.cliAnalyticsService.sendClientDeletedEvent(affected, instanceId); + this.cliAnalyticsService.sendClientDeletedEvent(affected, clientMetadata.databaseId); } return { affected }; } catch (error) { diff --git a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts index a634e2bec3..22defd4bc7 100644 --- a/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts +++ b/redisinsight/api/src/modules/cluster-monitor/cluster-monitor.controller.ts @@ -3,8 +3,8 @@ import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { ClusterMonitorService } from 'src/modules/cluster-monitor/cluster-monitor.service'; import { ApiTags } from '@nestjs/swagger'; import { ClusterDetails } from 'src/modules/cluster-monitor/models'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; +import { ClientMetadataParam } from 'src/common/decorators'; @ApiTags('Cluster Monitor') @Controller('/cluster-details') @@ -23,7 +23,7 @@ export class ClusterMonitorController { }) @Get() async getClusterDetails( - @ClientMetadataFromRequest() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, ): Promise { return this.clusterMonitorService.getClusterDetails(clientMetadata); } diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts index 52f62e46a5..fa9e8d675d 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.controller.ts @@ -7,7 +7,7 @@ import { ApiTags } from '@nestjs/swagger'; import { DatabaseAnalysisService } from 'src/modules/database-analysis/database-analysis.service'; import { DatabaseAnalysis, ShortDatabaseAnalysis } from 'src/modules/database-analysis/models'; import { BrowserSerializeInterceptor } from 'src/common/interceptors'; -import { ApiQueryRedisStringEncoding, ClientMetadataFromRequest } from 'src/common/decorators'; +import { ApiQueryRedisStringEncoding, ClientMetadataParam } from 'src/common/decorators'; import { CreateDatabaseAnalysisDto } from 'src/modules/database-analysis/dto'; import { ClientMetadata } from 'src/common/models'; @@ -31,7 +31,7 @@ export class DatabaseAnalysisController { @Post() @ApiQueryRedisStringEncoding() async create( - @ClientMetadataFromRequest() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: CreateDatabaseAnalysisDto, ): Promise { return this.service.create(clientMetadata, dto); diff --git a/redisinsight/api/src/modules/database/database-connection.service.spec.ts b/redisinsight/api/src/modules/database/database-connection.service.spec.ts index 6dee9f498a..dbc4801819 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.spec.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.spec.ts @@ -1,7 +1,7 @@ import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { - mockClientMetadata, mockCommonClientMetadata, + mockCommonClientMetadata, mockDatabase, mockDatabaseAnalytics, mockDatabaseInfoProvider, @@ -10,7 +10,7 @@ import { mockIORedisClient, mockRedisNoAuthError, mockRedisService, - MockType + MockType, } from 'src/__mocks__'; import { DatabaseAnalytics } from 'src/modules/database/database.analytics'; import { DatabaseService } from 'src/modules/database/database.service'; @@ -19,7 +19,6 @@ import { RedisService } from 'src/modules/redis/redis.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { DatabaseConnectionService } from 'src/modules/database/database-connection.service'; import ERROR_MESSAGES from 'src/constants/error-messages'; -import { AppTool } from 'src/models'; describe('DatabaseConnectionService', () => { let service: DatabaseConnectionService; @@ -69,13 +68,13 @@ describe('DatabaseConnectionService', () => { describe('getOrCreateClient', () => { it('should get existing client', async () => { - expect(await service.getOrCreateClient(mockClientMetadata)).toEqual(mockIORedisClient); + expect(await service.getOrCreateClient(mockCommonClientMetadata)).toEqual(mockIORedisClient); expect(redisService.connectToDatabaseInstance).not.toHaveBeenCalled(); }); it('should create new and save it client', async () => { redisService.getClientInstance.mockResolvedValue(null); - expect(await service.getOrCreateClient(mockClientMetadata)).toEqual(mockIORedisClient); + expect(await service.getOrCreateClient(mockCommonClientMetadata)).toEqual(mockIORedisClient); expect(redisService.connectToDatabaseInstance).toHaveBeenCalled(); expect(redisService.setClientInstance).toHaveBeenCalled(); }); @@ -83,11 +82,11 @@ describe('DatabaseConnectionService', () => { describe('createClient', () => { it('should create client for standalone datbaase', async () => { - expect(await service.createClient(mockClientMetadata)).toEqual(mockIORedisClient); + expect(await service.createClient(mockCommonClientMetadata)).toEqual(mockIORedisClient); }); it('should throw Unauthorized error in case of NOAUTH', async () => { redisService.connectToDatabaseInstance.mockRejectedValueOnce(mockRedisNoAuthError); - await expect(service.createClient(mockClientMetadata)).rejects.toThrow(UnauthorizedException); + await expect(service.createClient(mockCommonClientMetadata)).rejects.toThrow(UnauthorizedException); expect(analytics.sendConnectionFailedEvent).toHaveBeenCalledWith( mockDatabase, new UnauthorizedException(ERROR_MESSAGES.AUTHENTICATION_FAILED()), diff --git a/redisinsight/api/src/modules/database/database-info.controller.ts b/redisinsight/api/src/modules/database/database-info.controller.ts index e5f4c3ade8..0ae9625d4f 100644 --- a/redisinsight/api/src/modules/database/database-info.controller.ts +++ b/redisinsight/api/src/modules/database/database-info.controller.ts @@ -7,8 +7,8 @@ import { TimeoutInterceptor } from 'src/common/interceptors/timeout.interceptor' import { DatabaseInfoService } from 'src/modules/database/database-info.service'; import { DatabaseOverview } from 'src/modules/database/models/database-overview'; import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; +import { ClientMetadataParam } from 'src/common/decorators'; @ApiTags('Database Instances') @Controller('databases') @@ -31,8 +31,8 @@ export class DatabaseInfoController { ], }) async getInfo( - @ClientMetadataFromRequest({ - paramPath: 'id', + @ClientMetadataParam({ + databaseIdParam: 'id', }) clientMetadata: ClientMetadata, ): Promise { return this.databaseInfoService.getInfo(clientMetadata); @@ -52,8 +52,8 @@ export class DatabaseInfoController { ], }) async getDatabaseOverview( - @ClientMetadataFromRequest({ - paramPath: 'id', + @ClientMetadataParam({ + databaseIdParam: 'id', }) clientMetadata: ClientMetadata, ): Promise { return this.databaseInfoService.getOverview(clientMetadata); diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index db97bee87f..99cbf7a2e0 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -14,7 +14,8 @@ import { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto' import { BuildType } from 'src/modules/server/models/server'; import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dto'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadataParam } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Database Instances') @Controller('databases') @@ -160,9 +161,9 @@ export class DatabaseController { }) @UsePipes(new ValidationPipe({ transform: true })) async connect( - @ClientMetadataFromRequest({ - paramPath: 'id', - }) clientMetadata, + @ClientMetadataParam({ + databaseIdParam: 'id', + }) clientMetadata: ClientMetadata, ): Promise { await this.connectionService.connect(clientMetadata); } diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 660479599a..8c29e28050 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -65,6 +65,11 @@ export class DatabaseService { async get(id: string, ignoreEncryptionErrors = false): Promise { this.logger.log(`Getting database ${id}`); + if (!id) { + this.logger.error('Database id was not provided'); + throw new NotFoundException(ERROR_MESSAGES.INVALID_DATABASE_INSTANCE_ID); + } + const model = await this.repository.get(id, ignoreEncryptionErrors); if (!model) { diff --git a/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts b/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts index fbfa87c67f..9e2320c64e 100644 --- a/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts +++ b/redisinsight/api/src/modules/pub-sub/pub-sub.controller.ts @@ -1,15 +1,14 @@ import { Body, - Controller, Param, Post, UsePipes, ValidationPipe + Controller, Post, UsePipes, ValidationPipe, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { ApiRedisInstanceOperation } from 'src/decorators/api-redis-instance-operation.decorator'; import { PubSubService } from 'src/modules/pub-sub/pub-sub.service'; -import { AppTool } from 'src/models'; import { PublishDto } from 'src/modules/pub-sub/dto/publish.dto'; import { PublishResponse } from 'src/modules/pub-sub/dto/publish.response'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; +import { ClientMetadataParam } from 'src/common/decorators'; @ApiTags('Pub/Sub') @Controller('pub-sub') @@ -30,7 +29,7 @@ export class PubSubController { ], }) async publish( - @ClientMetadataFromRequest() clientMetadata: ClientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: PublishDto, ): Promise { return this.service.publish(clientMetadata, dto); diff --git a/redisinsight/api/src/modules/redis/redis-tool.service.spec.ts b/redisinsight/api/src/modules/redis/redis-tool.service.spec.ts index dfd123d9fc..a07bfd8ecd 100644 --- a/redisinsight/api/src/modules/redis/redis-tool.service.spec.ts +++ b/redisinsight/api/src/modules/redis/redis-tool.service.spec.ts @@ -1,16 +1,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as Redis from 'ioredis-mock'; -import { mockDatabase, mockDatabaseService } from 'src/__mocks__'; -import { IFindRedisClientInstanceByOptions, RedisService } from 'src/modules/redis/redis.service'; +import { mockCommonClientMetadata, mockDatabaseService } from 'src/__mocks__'; +import { RedisService } from 'src/modules/redis/redis.service'; import { BrowserToolKeysCommands } from 'src/modules/browser/constants/browser-tool-commands'; import { InternalServerErrorException } from '@nestjs/common'; import { RedisToolService } from 'src/modules/redis/redis-tool.service'; import { DatabaseService } from 'src/modules/database/database.service'; -const mockClientOptions: IFindRedisClientInstanceByOptions = { - instanceId: mockDatabase.id, -}; - const mockClient = new Redis(); describe('CliToolService', () => { @@ -43,7 +39,7 @@ describe('CliToolService', () => { getRedisClient.mockResolvedValue(mockClient); await service.execCommand( - mockClientOptions, + mockCommonClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ); @@ -60,7 +56,7 @@ describe('CliToolService', () => { await expect( service.execCommand( - mockClientOptions, + mockCommonClientMetadata, BrowserToolKeysCommands.MemoryUsage, [keyName], ), diff --git a/redisinsight/api/src/modules/redis/redis-tool.service.ts b/redisinsight/api/src/modules/redis/redis-tool.service.ts index 44e93825d8..cd186a0ec5 100644 --- a/redisinsight/api/src/modules/redis/redis-tool.service.ts +++ b/redisinsight/api/src/modules/redis/redis-tool.service.ts @@ -173,12 +173,8 @@ export class RedisToolService extends RedisConsumerAbstractService { return clientMetadata.uniqueId; } - async deleteToolClient(databaseId: string, uniqueId: string): Promise { - return this.redisService.removeClientInstance({ - databaseId, - uniqueId, - context: this.consumer, - }); + async deleteToolClient(clientMetadata: ClientMetadata): Promise { + return this.redisService.removeClientInstance(clientMetadata); } private async getClusterNodes( diff --git a/redisinsight/api/src/modules/redis/redis.service.spec.ts b/redisinsight/api/src/modules/redis/redis.service.spec.ts index 720a173ecd..80d980134b 100644 --- a/redisinsight/api/src/modules/redis/redis.service.spec.ts +++ b/redisinsight/api/src/modules/redis/redis.service.spec.ts @@ -1,17 +1,23 @@ import { Test, TestingModule } from '@nestjs/testing'; -import * as Redis from 'ioredis-mock'; import { v4 as uuidv4 } from 'uuid'; import { ConnectionOptions } from 'tls'; import { mockCaCertificate, mockClientCertificate, mockClusterDatabaseWithTlsAuth, mockDatabase, - mockDatabaseEntity, mockSentinelDatabaseWithTlsAuth, - MockType + mockDatabaseEntity, mockIORedisClient, mockIORedisCluster, mockIORedisSentinel, mockSentinelDatabaseWithTlsAuth, } from 'src/__mocks__'; -import { AppTool, ReplyError } from 'src/models'; -import { mockRedisClientInstance } from 'src/modules/redis/redis-consumer.abstract.service.spec'; +import { AppTool } from 'src/models'; +import { ClientContext, ClientMetadata } from 'src/common/models'; +import { Database } from 'src/modules/database/models/database'; import { RedisService } from './redis.service'; -jest.mock('ioredis'); +const mockRedisClientInstance = { + session: undefined, + databaseId: mockDatabase.id, + context: ClientContext.Common, + uniqueId: undefined, + client: mockIORedisClient, + lastTimeUsed: Date.now(), +}; const mockTlsConfigResult: ConnectionOptions = { rejectUnauthorized: true, @@ -55,351 +61,244 @@ describe('RedisService', () => { service.clients = []; }); it('should create standalone client', async () => { - const mockClient = new Redis(); - service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); + service.createStandaloneClient = jest.fn().mockResolvedValue(mockIORedisClient); const result = await service.connectToDatabaseInstance(mockDatabase); - expect(result).toEqual(mockClient); + expect(result).toEqual(mockIORedisClient); expect(service.createStandaloneClient).toHaveBeenCalledWith(mockDatabase, AppTool.Common, true, undefined); }); - it('should create cluster client', async () => { - const mockClient = new Redis.Cluster([ - 'redis://localhost:7001', - 'redis://localhost:7002', - ]); + it('should create standalone client (by default)', async () => { + service.createStandaloneClient = jest.fn().mockResolvedValue(mockIORedisClient); + const mockDatabaseWithoutConnectionType = Object.assign(new Database(), { + ...mockDatabase, + connectionType: null, + }); + + const result = await service.connectToDatabaseInstance(mockDatabaseWithoutConnectionType); - const { nodes, connectionType, ...options } = mockClusterDatabaseWithTlsAuth; - service.createClusterClient = jest.fn().mockResolvedValue(mockClient); + expect(result).toEqual(mockIORedisClient); + expect(service.createStandaloneClient) + .toHaveBeenCalledWith({ + ...mockDatabaseWithoutConnectionType, + connectionType: undefined, + }, AppTool.Common, true, undefined); + }); + it('should create cluster client', async () => { + service.createClusterClient = jest.fn().mockResolvedValue(mockIORedisCluster); const result = await service.connectToDatabaseInstance(mockClusterDatabaseWithTlsAuth); - expect(result).toEqual(mockClient); + expect(result).toEqual(mockIORedisCluster); expect(service.createClusterClient).toHaveBeenCalledWith( - options, - nodes, + mockClusterDatabaseWithTlsAuth, + mockClusterDatabaseWithTlsAuth.nodes, true, undefined, ); }); it('should create sentinel client', async () => { - const mockClient = new Redis(); const dto = removeNullsFromDto(mockSentinelDatabaseWithTlsAuth); Object.keys(dto).forEach((key: string) => { if (dto[key] === null) { delete dto[key]; } }); - const { nodes, connectionType, ...options } = dto; - service.createSentinelClient = jest.fn().mockResolvedValue(mockClient); + const { nodes } = dto; + service.createSentinelClient = jest.fn().mockResolvedValue(mockIORedisSentinel); const result = await service.connectToDatabaseInstance(dto); - expect(result).toEqual(mockClient); + expect(result).toEqual(mockIORedisSentinel); expect(service.createSentinelClient).toHaveBeenCalledWith( - options, + mockSentinelDatabaseWithTlsAuth, nodes, - AppTool.Common, + ClientContext.Common, true, undefined, ); }); - // it('should select redis database by number', async () => { - // const mockClient = new Redis(); - // mockClient.call = jest.fn(); - // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); - // service.createStandaloneClient = jest.fn().mockResolvedValue(mockClient); - // - // await service.connectToDatabaseInstance(dto, AppTool.Common); - // - // expect(service.createStandaloneClient).toHaveBeenCalledWith(dto, AppTool.Common, true, undefined); - // }); - // it('should throw error db index is out of range', async () => { - // const replyError: ReplyError = { - // name: 'ReplyError', - // message: '(error) DB index is out of range', - // command: 'SELECT', - // }; - // service.createStandaloneClient = jest.fn().mockRejectedValue(replyError); - // - // try { - // await service.connectToDatabaseInstance( - // convertEntityToDto(mockStandaloneDatabaseEntity), - // ); - // fail('Should throw an error'); - // } catch (err) { - // expect(err).toEqual(replyError); - // } - // expect(service.clients.length).toEqual(0); - // }); - // it('connection error [Connection details are incorrect]', async () => { - // service.createStandaloneClient = jest - // .fn() - // .mockRejectedValue(new Error('ENOTFOUND some message')); - // - // try { - // await service.connectToDatabaseInstance( - // convertEntityToDto(mockStandaloneDatabaseEntity), - // 0, - // ); - // fail('Should throw an error'); - // } catch (err) { - // expect(err.message).toEqual('ENOTFOUND some message'); - // expect(service.clients.length).toEqual(0); - // } - // }); }); - // describe('getClientInstance', () => { - // beforeEach(() => { - // service.clients = [ - // { - // ...mockRedisClientInstance, tool: AppTool.Common, - // }, - // { - // ...mockRedisClientInstance, tool: AppTool.Browser, - // }, - // { - // ...mockRedisClientInstance, tool: AppTool.CLI, - // }, - // ]; - // }); - // it('should correctly find client instance for App.Common by instance id', () => { - // const newClient = { ...service.clients[0], tool: AppTool.Browser }; - // service.clients.push(newClient); - // const options = { - // instanceId: newClient.instanceId, - // }; - // - // const result = service.getClientInstance(options); - // - // expect(result).toEqual(service.clients[0]); - // }); - // it('should correctly find client instance by instance id and tool', () => { - // const options: IFindRedisClientInstanceByOptions = { - // instanceId: service.clients[0].instanceId, - // tool: AppTool.CLI, - // }; - // - // const result = service.getClientInstance(options); - // - // expect(result).toEqual(service.clients[2]); - // }); - // it('should correctly find client instance by instance id, tool and uuid', () => { - // const newClient = { ...mockRedisClientInstance, uuid: uuidv4(), tool: AppTool.CLI }; - // service.clients.push(newClient); - // const options: IFindRedisClientInstanceByOptions = { - // instanceId: newClient.instanceId, - // uuid: newClient.uuid, - // tool: newClient.tool, - // }; - // - // const result = service.getClientInstance(options); - // - // expect(result).toEqual(newClient); - // }); - // it('should return undefined', () => { - // const options: IFindRedisClientInstanceByOptions = { - // instanceId: 'invalid-instance-id', - // }; - // - // const result = service.getClientInstance(options); - // - // expect(result).toBeUndefined(); - // }); - // }); - - // describe('removeClientInstance', () => { - // beforeEach(() => { - // service.clients = [ - // { - // ...mockRedisClientInstance, - // tool: AppTool.Common, - // }, - // { - // ...mockRedisClientInstance, - // tool: AppTool.Browser, - // }, - // ]; - // }); - // it('should remove only client for browser tool', () => { - // const options: IFindRedisClientInstanceByOptions = { - // instanceId: mockRedisClientInstance.instanceId, - // tool: AppTool.Browser, - // }; - // - // const result = service.removeClientInstance(options); - // - // expect(result).toEqual(1); - // expect(service.clients.length).toEqual(1); - // }); - // it('should remove all clients by instance id', () => { - // const options: IFindRedisClientInstanceByOptions = { - // instanceId: mockRedisClientInstance.instanceId, - // }; - // - // const result = service.removeClientInstance(options); - // - // expect(result).toEqual(2); - // expect(service.clients.length).toEqual(0); - // }); - // }); - - // describe('setClientInstance', () => { - // beforeEach(() => { - // service.clients = [{ ...mockRedisClientInstance }]; - // }); - // it('should add new client', () => { - // const initialClientsCount = service.clients.length; - // const newClientInstance = { - // ...mockRedisClientInstance, - // instanceId: uuidv4(), - // }; - // - // const result = service.setClientInstance(newClientInstance); - // - // expect(result).toBe(1); - // expect(service.clients.length).toBe(initialClientsCount + 1); - // }); - // it('should replace exist client', () => { - // const initialClientsCount = service.clients.length; - // - // const result = service.setClientInstance(mockRedisClientInstance); - // - // expect(result).toBe(0); - // expect(service.clients.length).toBe(initialClientsCount); - // }); - // }); - - // describe('isClientConnected', () => { - // const mockClient = new Redis(); - // it('should return true', async () => { - // mockClient.status = 'ready'; - // - // const result = service.isClientConnected(mockClient); - // - // expect(result).toEqual(true); - // }); - // it('should return false', async () => { - // mockClient.status = 'end'; - // - // const result = service.isClientConnected(mockClient); - // - // expect(result).toEqual(false); - // }); - // }); - - // describe('getRedisConnectionConfig', () => { - // it('should return config with tls', async () => { - // service.getTLSConfig = jest.fn().mockResolvedValue(mockTlsConfigResult); - // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); - // const { - // host, port, password, username, db, - // } = dto; - // - // const expectedResult = { - // host, port, username, password, db, tls: mockTlsConfigResult, - // }; - // - // const result = await service.getRedisConnectionConfig(dto); - // - // expect(JSON.stringify(result)).toEqual(JSON.stringify(expectedResult)); - // }); - // it('should return without tls', async () => { - // const dto = convertEntityToDto(mockStandaloneDatabaseEntity); - // delete dto.tls; - // const { - // host, port, password, username, db, - // } = dto; - // - // const expectedResult = { - // host, port, username, password, db, - // }; - // - // const result = await service.getRedisConnectionConfig(dto); - // - // expect(result).toEqual(expectedResult); - // }); - // }); - - // describe('getTLSConfig', () => { - // it('should return tls config', async () => { - // service.getCaCertConfig = jest - // .fn() - // .mockResolvedValue({ ca: [mockCaCertDto.cert] }); - // service.getClientCertConfig = jest.fn().mockResolvedValue({ - // key: mockClientCertDto.key, - // cert: mockClientCertDto.cert, - // }); - // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); - // - // const result = await service.getTLSConfig(tls); - // - // expect(JSON.stringify(result)).toEqual( - // JSON.stringify(mockTlsConfigResult), - // ); - // }); - // }); - - // describe('getCaCertConfig', () => { - // it('should load exist cert', async () => { - // caCertBusinessService.getOneById = jest - // .fn() - // .mockResolvedValue(mockCaCertEntity); - // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); - // - // const result = await service.getCaCertConfig(tls); - // - // expect(result).toEqual({ ca: [mockCaCertDto.cert] }); - // expect(caCertBusinessService.getOneById).toHaveBeenCalledWith( - // tls.caCertId, - // ); - // }); - // it('should return new cert', async () => { - // const result = await service.getCaCertConfig({ - // newCaCert: mockCaCertDto, - // }); - // - // expect(result).toEqual({ ca: [mockCaCertDto.cert] }); - // expect(caCertBusinessService.getOneById).not.toHaveBeenCalled(); - // }); - // it('should return null', async () => { - // const result = await service.getCaCertConfig({}); - // - // expect(result).toBeNull(); - // }); - // }); - - // describe('getClientCertConfig', () => { - // const mockResult = { - // key: mockClientCertDto.key, - // cert: mockClientCertDto.cert, - // }; - // it('should load exist cert', async () => { - // clientCertBusinessService.getOneById = jest - // .fn() - // .mockResolvedValue(mockClientCertEntity); - // const { tls } = convertEntityToDto(mockStandaloneDatabaseEntity); - // - // const result = await service.getClientCertConfig(tls); - // - // expect(result).toEqual(mockResult); - // expect(clientCertBusinessService.getOneById).toHaveBeenCalledWith( - // tls.clientCertPairId, - // ); - // }); - // it('should return new cert', async () => { - // const result = await service.getClientCertConfig({ - // newClientCertPair: mockClientCertDto, - // }); - // - // expect(result).toEqual(mockResult); - // expect(clientCertBusinessService.getOneById).not.toHaveBeenCalled(); - // }); - // it('should return null', async () => { - // const result = await service.getClientCertConfig({}); - // - // expect(result).toBeNull(); - // }); - // }); + describe('getClientInstance', () => { + beforeEach(() => { + service.clients = [ + { + ...mockRedisClientInstance, + }, + { + ...mockRedisClientInstance, context: ClientContext.Browser, + }, + { + ...mockRedisClientInstance, context: ClientContext.CLI, + }, + ]; + }); + it('should correctly find client instance for App.Common by instance id', () => { + const newClient = { ...service.clients[0], context: ClientContext.Browser }; + service.clients.push(newClient); + const options = { + session: undefined, + databaseId: newClient.databaseId, + context: ClientContext.Common, + }; + + const result = service.getClientInstance(options); + + expect(result).toEqual(service.clients[0]); + }); + it('should correctly find client instance by instance id and tool', () => { + const options = { + session: undefined, + databaseId: service.clients[0].databaseId, + context: ClientContext.CLI, + }; + + const result = service.getClientInstance(options); + + expect(result).toEqual(service.clients[2]); + }); + it('should correctly find client instance by instance id, tool and uuid', () => { + const newClient = { ...mockRedisClientInstance, uniqueId: uuidv4(), context: ClientContext.CLI }; + service.clients.push(newClient); + + const options = { + session: undefined, + databaseId: newClient.databaseId, + uniqueId: newClient.uniqueId, + context: newClient.context, + }; + + const result = service.getClientInstance(options); + + expect(result).toEqual(newClient); + }); + it('should return undefined', () => { + const options = { + session: undefined, + databaseId: 'invalid-instance-id', + context: ClientContext.Common, + }; + + const result = service.getClientInstance(options); + + expect(result).toBeUndefined(); + }); + }); + + describe('removeClientInstance', () => { + beforeEach(() => { + service.clients = [ + { + ...mockRedisClientInstance, + }, + { + ...mockRedisClientInstance, context: ClientContext.Browser, + }, + ]; + }); + it('should remove only client for browser tool', () => { + const options = { + databaseId: mockRedisClientInstance.databaseId, + context: ClientContext.Browser, + }; + + const result = service.removeClientInstance(options); + + expect(result).toEqual(1); + expect(service.clients.length).toEqual(1); + }); + it('should remove all clients by instance id', () => { + const options = { + databaseId: mockRedisClientInstance.databaseId, + }; + + const result = service.removeClientInstance(options); + + expect(result).toEqual(2); + expect(service.clients.length).toEqual(0); + }); + }); + + describe('setClientInstance', () => { + beforeEach(() => { + service.clients = [{ ...mockRedisClientInstance }]; + }); + it('should add new client', () => { + const initialClientsCount = service.clients.length; + const newClientInstance: ClientMetadata = { + ...mockRedisClientInstance, + databaseId: uuidv4(), + }; + + const result = service.setClientInstance(newClientInstance, mockIORedisClient); + + expect(result).toBe(1); + expect(service.clients.length).toBe(initialClientsCount + 1); + }); + it('should replace exist client', () => { + const initialClientsCount = service.clients.length; + + const result = service.setClientInstance(mockRedisClientInstance, mockIORedisClient); + + expect(result).toBe(0); + expect(service.clients.length).toBe(initialClientsCount); + }); + }); + + describe('isClientConnected', () => { + it('should return true', async () => { + const result = service.isClientConnected(mockIORedisClient); + + expect(result).toEqual(true); + }); + it('should return false', async () => { + const mockClient = { ...mockIORedisClient }; + mockClient.status = 'end'; + + const result = service.isClientConnected(mockClient); + + expect(result).toEqual(false); + }); + }); + + describe('getRedisConnectionConfig', () => { + it('should return config with tls', async () => { + service['getTLSConfig'] = jest.fn().mockResolvedValue(mockTlsConfigResult); + const { + host, port, password, username, db, + } = mockClusterDatabaseWithTlsAuth; + + const expectedResult = { + host, port, username, password, db, tls: mockTlsConfigResult, + }; + + const result = await service['getRedisConnectionConfig'](mockClusterDatabaseWithTlsAuth); + + expect(JSON.stringify(result)).toEqual(JSON.stringify(expectedResult)); + }); + it('should return without tls', async () => { + const { + host, port, password, username, db, + } = mockDatabase; + + const expectedResult = { + host, port, username, password, db, + }; + + const result = await service['getRedisConnectionConfig'](mockDatabase); + + expect(result).toEqual(expectedResult); + }); + }); + + xdescribe('getTLSConfig', () => { + it('should return tls config', async () => { + const result = await service['getTLSConfig'](mockClusterDatabaseWithTlsAuth); + + expect(JSON.stringify(result)).toEqual( + JSON.stringify(mockTlsConfigResult), + ); + }); + }); }); diff --git a/redisinsight/api/src/modules/slow-log/slow-log.controller.ts b/redisinsight/api/src/modules/slow-log/slow-log.controller.ts index 91568858d8..a3e92f0554 100644 --- a/redisinsight/api/src/modules/slow-log/slow-log.controller.ts +++ b/redisinsight/api/src/modules/slow-log/slow-log.controller.ts @@ -7,7 +7,8 @@ import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { SlowLog, SlowLogConfig } from 'src/modules/slow-log/models'; import { UpdateSlowLogConfigDto } from 'src/modules/slow-log/dto/update-slow-log-config.dto'; import { GetSlowLogsDto } from 'src/modules/slow-log/dto/get-slow-logs.dto'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadataParam } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; @ApiTags('Slow Logs') @Controller('slow-logs') @@ -30,7 +31,7 @@ export class SlowLogController { }) @Get('') async getSlowLogs( - @ClientMetadataFromRequest() clientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Query() getSlowLogsDto: GetSlowLogsDto, ): Promise { return this.service.getSlowLogs(clientMetadata, getSlowLogsDto); @@ -42,7 +43,7 @@ export class SlowLogController { }) @Delete('') async resetSlowLogs( - @ClientMetadataFromRequest() clientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, ): Promise { return this.service.reset(clientMetadata); } @@ -59,7 +60,7 @@ export class SlowLogController { }) @Get('config') async getConfig( - @ClientMetadataFromRequest() clientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, ): Promise { return this.service.getConfig(clientMetadata); } @@ -76,7 +77,7 @@ export class SlowLogController { }) @Patch('config') async updateConfig( - @ClientMetadataFromRequest() clientMetadata, + @ClientMetadataParam() clientMetadata: ClientMetadata, @Body() dto: UpdateSlowLogConfigDto, ): Promise { return this.service.updateConfig(clientMetadata, dto); diff --git a/redisinsight/api/src/modules/workbench/decorators/workbench-client-metadata.decorator.ts b/redisinsight/api/src/modules/workbench/decorators/workbench-client-metadata.decorator.ts new file mode 100644 index 0000000000..3709ad8dd5 --- /dev/null +++ b/redisinsight/api/src/modules/workbench/decorators/workbench-client-metadata.decorator.ts @@ -0,0 +1,11 @@ +import { API_PARAM_CLI_CLIENT_ID, API_PARAM_DATABASE_ID } from 'src/common/constants'; +import { createParamDecorator } from '@nestjs/common'; +import { ClientContext } from 'src/common/models'; +import { clientMetadataParamFactory } from 'src/common/decorators'; + +export const WorkbenchClientMetadata = ( + databaseIdParam = API_PARAM_DATABASE_ID, +) => createParamDecorator(clientMetadataParamFactory)({ + context: ClientContext.Workbench, + databaseIdParam, +}); diff --git a/redisinsight/api/src/modules/workbench/plugins.controller.ts b/redisinsight/api/src/modules/workbench/plugins.controller.ts index 055608054a..34f0582a74 100644 --- a/redisinsight/api/src/modules/workbench/plugins.controller.ts +++ b/redisinsight/api/src/modules/workbench/plugins.controller.ts @@ -17,8 +17,8 @@ import { PluginsService } from 'src/modules/workbench/plugins.service'; import { PluginCommandExecution } from 'src/modules/workbench/models/plugin-command-execution'; import { CreatePluginStateDto } from 'src/modules/workbench/dto/create-plugin-state.dto'; import { PluginState } from 'src/modules/workbench/models/plugin-state'; -import { ClientContext, ClientMetadata } from 'src/common/models'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; +import { ClientMetadata } from 'src/common/models'; +import { WorkbenchClientMetadata } from 'src/modules/workbench/decorators/workbench-client-metadata.decorator'; @ApiTags('Plugins') @UsePipes(new ValidationPipe({ transform: true })) @@ -40,7 +40,7 @@ export class PluginsController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async sendCommand( - @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata: ClientMetadata, + @WorkbenchClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateCommandExecutionDto, ): Promise { return this.service.sendCommand(clientMetadata, dto); @@ -61,7 +61,7 @@ export class PluginsController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async getPluginCommands( - @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata: ClientMetadata, + @WorkbenchClientMetadata() clientMetadata: ClientMetadata, ): Promise { return this.service.getWhitelistCommands(clientMetadata); } diff --git a/redisinsight/api/src/modules/workbench/workbench.controller.ts b/redisinsight/api/src/modules/workbench/workbench.controller.ts index 36456c1475..37d59ec02b 100644 --- a/redisinsight/api/src/modules/workbench/workbench.controller.ts +++ b/redisinsight/api/src/modules/workbench/workbench.controller.ts @@ -17,8 +17,8 @@ import { WorkbenchService } from 'src/modules/workbench/workbench.service'; import { CommandExecution } from 'src/modules/workbench/models/command-execution'; import { CreateCommandExecutionsDto } from 'src/modules/workbench/dto/create-command-executions.dto'; import { ShortCommandExecution } from 'src/modules/workbench/models/short-command-execution'; -import { ClientMetadataFromRequest } from 'src/common/decorators'; -import { ClientContext } from 'src/common/models'; +import { ClientMetadata } from 'src/common/models'; +import { WorkbenchClientMetadata } from 'src/modules/workbench/decorators/workbench-client-metadata.decorator'; @ApiTags('Workbench') @UsePipes(new ValidationPipe({ transform: true })) @@ -40,7 +40,7 @@ export class WorkbenchController { @UseInterceptors(ClassSerializerInterceptor) @ApiRedisParams() async sendCommands( - @ClientMetadataFromRequest({ context: ClientContext.Workbench }) clientMetadata, + @WorkbenchClientMetadata() clientMetadata: ClientMetadata, @Body() dto: CreateCommandExecutionsDto, ): Promise { return this.service.createCommandExecutions(clientMetadata, dto); From bdefecbc066d8ea30fb3a991864f1457af90f31b Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Wed, 30 Nov 2022 11:30:09 +0100 Subject: [PATCH 034/107] #RI-3355 - Tree view navigation improvements --- .../components/virtual-tree/VirtualTree.tsx | 39 +++++++++++++++++-- .../filter-key-type/FilterKeyType.spec.tsx | 2 - .../filter-key-type/FilterKeyType.tsx | 4 -- .../browser/components/key-tree/KeyTree.tsx | 22 ++++++----- .../KeyTreeDelimiter.spec.tsx | 4 +- .../KeyTreeDelimiter/KeyTreeDelimiter.tsx | 4 +- .../components/keys-header/KeysHeader.tsx | 4 ++ .../search-key-list/SearchKeyList.spec.tsx | 3 +- .../search-key-list/SearchKeyList.tsx | 2 - 9 files changed, 60 insertions(+), 24 deletions(-) diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx index 8c67a37e13..c1a99e8313 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx @@ -7,6 +7,7 @@ import { FixedSizeTree as Tree, } from 'react-vtree' import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui' +import { useDispatch } from 'react-redux' import { getTreeLeafField, Maybe } from 'uiSrc/utils' import { useDisposableWebworker } from 'uiSrc/services' @@ -15,6 +16,7 @@ import { ThemeContext } from 'uiSrc/contexts/themeContext' import { DEFAULT_DELIMITER, Theme } from 'uiSrc/constants' import KeyLightSVG from 'uiSrc/assets/img/sidebar/browser.svg' import KeyDarkSVG from 'uiSrc/assets/img/sidebar/browser_active.svg' +import { resetBrowserTree } from 'uiSrc/slices/app/context' import { Node } from './components/Node' import { NodeMeta, TreeData, TreeNode } from './interfaces' @@ -43,6 +45,8 @@ export interface Props { setConstructingTree: (status: boolean) => void } +export const KEYS = 'keys' + const VirtualTree = (props: Props) => { const { items, @@ -64,6 +68,8 @@ const VirtualTree = (props: Props) => { const [nodes, setNodes] = useState([]) const { result, run: runWebworker } = useDisposableWebworker(webworkerFn) + const dispatch = useDispatch() + useEffect(() => () => setNodes([]), []) @@ -85,13 +91,40 @@ const VirtualTree = (props: Props) => { } if (isArray(nodes) && isEmpty(statusSelected)) { - const rootLeaf: Maybe = nodes?.find(({ children = [] }) => children.length === 0) + let selectedLeaf: Maybe = nodes?.find(({ children = [] }) => children.length === 0) + + // if Keys folder not exists - first folder should be opened + if (!selectedLeaf && nodes.length) { + selectedLeaf = nodes?.[0] + + onStatusOpen?.(selectedLeaf?.fullName ?? '', true) + onStatusSelected?.( + `${selectedLeaf?.fullName + KEYS + delimiter + KEYS + delimiter}` ?? '', + selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys + ) + } else { + // if Keys folder exist - open it + onStatusSelected?.(selectedLeaf?.fullName ?? '', selectedLeaf?.keys) + } + disableSelectDefaultLeaf?.() - onStatusSelected?.(rootLeaf?.fullName ?? '', rootLeaf?.keys) - onSelectLeaf?.(rootLeaf?.keys ?? []) + onSelectLeaf?.(selectedLeaf?.keys ?? selectedLeaf?.children?.[0]?.keys ?? []) } }, [nodes, loading, selectDefaultLeaf]) + useEffect(() => { + if (isEmpty(statusSelected) || !nodes.length) { + return + } + + // if selected Keys folder is not exists (after a new search) needs reset Browser state + const selectedLeafExists = !!nodes.find((node) => Object.keys(statusSelected)?.[0]?.startsWith(node.fullName)) + + if (!selectedLeafExists) { + dispatch(resetBrowserTree()) + } + }, [nodes]) + useEffect(() => { if (!items?.length) { setNodes([]) diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.spec.tsx b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.spec.tsx index 9bdec81fd2..16b30da469 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.spec.tsx @@ -11,7 +11,6 @@ import { import { loadKeys, setFilter } from 'uiSrc/slices/browser/keys' import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instances' import { KeyTypes } from 'uiSrc/constants' -import { resetBrowserTree } from 'uiSrc/slices/app/context' import FilterKeyType from './FilterKeyType' let store: typeof mockedStore @@ -64,7 +63,6 @@ describe('FilterKeyType', () => { const expectedActions = [ setFilter(KeyTypes.Hash), loadKeys(), - resetBrowserTree() ] expect(clearStoreActions(store.getActions())).toEqual( clearStoreActions(expectedActions) diff --git a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx index 9e311f716e..e445f7d23c 100644 --- a/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx +++ b/redisinsight/ui/src/pages/browser/components/filter-key-type/FilterKeyType.tsx @@ -15,7 +15,6 @@ import { connectedInstanceOverviewSelector } from 'uiSrc/slices/instances/instan import { fetchKeys, keysSelector, setFilter } from 'uiSrc/slices/browser/keys' import { isVersionHigherOrEquals } from 'uiSrc/utils' import HelpTexts from 'uiSrc/constants/help-texts' -import { resetBrowserTree } from 'uiSrc/slices/app/context' import { KeyViewType } from 'uiSrc/slices/interfaces/keys' import { FILTER_KEY_TYPE_OPTIONS } from './constants' @@ -82,9 +81,6 @@ const FilterKeyType = () => { '0', viewType === KeyViewType.Browser ? SCAN_COUNT_DEFAULT : SCAN_TREE_COUNT_DEFAULT, )) - - // reset browser tree context - dispatch(resetBrowserTree()) } const UnsupportedInfo = () => ( diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index df26b7f1fb..36a52c814a 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -1,10 +1,12 @@ -import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState, useTransition } from 'react' +import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState, useTransition } from 'react' import cx from 'classnames' import { EuiResizableContainer } from '@elastic/eui' import { useDispatch, useSelector } from 'react-redux' +import { isEmpty } from 'lodash' import { appContextBrowserTree, + resetBrowserTree, setBrowserTreeNodesOpen, setBrowserTreeSelectedLeaf } from 'uiSrc/slices/app/context' @@ -48,7 +50,7 @@ const KeyTree = forwardRef((props: Props, ref) => { const [sizes, setSizes] = useState(panelSizes) const [keyListState, setKeyListState] = useState(keysState) const [constructingTree, setConstructingTree] = useState(false) - const [selectDefaultLeaf, setSelectDefaultLeaf] = useState(true) + const [selectDefaultLeaf, setSelectDefaultLeaf] = useState(isEmpty(selectedLeaf)) const [items, setItems] = useState(keysState.keys ?? []) const dispatch = useDispatch() @@ -68,21 +70,22 @@ const KeyTree = forwardRef((props: Props, ref) => { }, [openNodes]) useEffect(() => { - if (selectedLeaf) { - setStatusSelected(selectedLeaf) - updateKeysList(Object.values(selectedLeaf)[0]) - } + setStatusSelected(selectedLeaf) + updateKeysList(Object.values(selectedLeaf)?.[0]) + + setSelectDefaultLeaf(isEmpty(selectedLeaf)) }, [selectedLeaf]) useEffect(() => { setItems(parseKeyNames(keysState.keys)) + if (keysState.keys?.length === 0) { - dispatch(setBrowserTreeSelectedLeaf({})) + updateSelectedKeys() } }, [keysState.keys]) useEffect(() => { - updateSelectedKeys() + setItems(parseKeyNames(keysState.keys)) }, [delimiter, keysState.lastRefreshTime]) const onLoadMoreItems = (props: { startIndex: number, stopIndex: number }) => { @@ -92,7 +95,8 @@ const KeyTree = forwardRef((props: Props, ref) => { // select default leaf "Keys" after each change delimiter, filter or search const updateSelectedKeys = () => { - setItems(parseKeyNames(keysState.keys)) + dispatch(resetBrowserTree()) + setTimeout(() => { startTransition(() => { setStatusSelected({}) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx index 8140e17b4a..4ff7030c69 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.spec.tsx @@ -2,7 +2,7 @@ import { cloneDeep } from 'lodash' import React from 'react' import { instance, mock } from 'ts-mockito' import { DEFAULT_DELIMITER } from 'uiSrc/constants' -import { setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' +import { resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' import { localStorageService } from 'uiSrc/services' import { cleanup, @@ -104,6 +104,7 @@ describe('KeyTreeDelimiter', () => { const expectedActions = [ setBrowserTreeDelimiter(value), + resetBrowserTree(), ] expect(clearStoreActions(store.getActions())).toEqual( @@ -127,6 +128,7 @@ describe('KeyTreeDelimiter', () => { const expectedActions = [ setBrowserTreeDelimiter(DEFAULT_DELIMITER), + resetBrowserTree(), ] expect(clearStoreActions(store.getActions())).toEqual( diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx index 2e0d6edf95..3f08816358 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTreeDelimiter/KeyTreeDelimiter.tsx @@ -9,7 +9,7 @@ import { localStorageService } from 'uiSrc/services' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import InlineItemEditor from 'uiSrc/components/inline-item-editor' import { BrowserStorageItem, DEFAULT_DELIMITER } from 'uiSrc/constants' -import { appContextBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' +import { appContextBrowserTree, resetBrowserTree, setBrowserTreeDelimiter } from 'uiSrc/slices/app/context' import styles from './styles.module.scss' @@ -62,6 +62,8 @@ const KeyTreeDelimiter = ({ loading }: Props) => { }) closePopover() dispatch(setBrowserTreeDelimiter(value || DEFAULT_DELIMITER)) + + dispatch(resetBrowserTree()) } return ( diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index dc4c1ac28d..ccaf51efd6 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -259,6 +259,10 @@ const KeysHeader = (props: Props) => { dispatch(changeSearchMode(mode)) + if (viewType === KeyViewType.Tree) { + dispatch(resetBrowserTree()) + } + localStorageService.set(BrowserStorageItem.browserSearchMode, mode) } diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx index eb7c12970e..32bbe62418 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.spec.tsx @@ -10,7 +10,6 @@ import { screen, } from 'uiSrc/utils/test-utils' import { loadKeys, setPatternSearchMatch } from 'uiSrc/slices/browser/keys' -import { resetBrowserTree } from 'uiSrc/slices/app/context' import SearchKeyList from './SearchKeyList' let store: typeof mockedStore @@ -38,7 +37,7 @@ describe('SearchKeyList', () => { fireEvent.keyDown(screen.getByTestId('search-key'), { key: keys.ENTER }) - const expectedActions = [setPatternSearchMatch(searchTerm), resetBrowserTree(), loadKeys()] + const expectedActions = [setPatternSearchMatch(searchTerm), loadKeys()] expect(clearStoreActions(store.getActions())).toEqual( clearStoreActions(expectedActions) diff --git a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx index ec2bb61d66..03481b78b0 100644 --- a/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx +++ b/redisinsight/ui/src/pages/browser/components/search-key-list/SearchKeyList.tsx @@ -36,8 +36,6 @@ const SearchKeyList = () => { const handleApply = (match = value) => { dispatch(setSearchMatch(match, searchMode)) - // reset browser tree context - dispatch(resetBrowserTree()) dispatch(fetchKeys( searchMode, From a024fce0d0afac6734021019343daf0e0a0e0e7e Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Thu, 1 Dec 2022 12:34:28 +0100 Subject: [PATCH 035/107] #RI-3894 - [FE] Keys from previously selected folder are still displayed when this subfolder not found after filter by type #RI-3895 - [FE] Deleted key from non-existing folder is displayed after refresh --- .../components/virtual-tree/VirtualTree.tsx | 4 +- redisinsight/ui/src/utils/tests/nodes.json | 446 ++++++++++++++++++ redisinsight/ui/src/utils/tests/tree.spec.ts | 23 +- redisinsight/ui/src/utils/tree.ts | 25 + 4 files changed, 495 insertions(+), 3 deletions(-) create mode 100644 redisinsight/ui/src/utils/tests/nodes.json diff --git a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx index c1a99e8313..e9f2ddbab1 100644 --- a/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx +++ b/redisinsight/ui/src/components/virtual-tree/VirtualTree.tsx @@ -9,7 +9,7 @@ import { import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui' import { useDispatch } from 'react-redux' -import { getTreeLeafField, Maybe } from 'uiSrc/utils' +import { findTreeNode, getTreeLeafField, Maybe } from 'uiSrc/utils' import { useDisposableWebworker } from 'uiSrc/services' import { IKeyPropTypes } from 'uiSrc/constants/prop-types/keys' import { ThemeContext } from 'uiSrc/contexts/themeContext' @@ -118,7 +118,7 @@ const VirtualTree = (props: Props) => { } // if selected Keys folder is not exists (after a new search) needs reset Browser state - const selectedLeafExists = !!nodes.find((node) => Object.keys(statusSelected)?.[0]?.startsWith(node.fullName)) + const selectedLeafExists = !!findTreeNode(nodes, Object.keys(statusSelected)?.[0], 'fullName') if (!selectedLeafExists) { dispatch(resetBrowserTree()) diff --git a/redisinsight/ui/src/utils/tests/nodes.json b/redisinsight/ui/src/utils/tests/nodes.json new file mode 100644 index 0000000000..d6230b8885 --- /dev/null +++ b/redisinsight/ui/src/utils/tests/nodes.json @@ -0,0 +1,446 @@ +[ + { + "name":"hash", + "children":[ + { + "name":"keys:keys", + "children":[ + + ], + "keys":{ + "hash:3":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 51 + ] + }, + "nameString":"hash:3" + }, + "hash:153":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 49, + 53, + 51 + ] + }, + "nameString":"hash:153" + }, + "hash:2":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 50 + ] + }, + "nameString":"hash:2" + }, + "hash:21":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 50, + 49 + ] + }, + "nameString":"hash:21" + }, + "hash:151":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 49, + 53, + 49 + ] + }, + "nameString":"hash:151" + }, + "hash:11":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 49, + 49 + ] + }, + "nameString":"hash:11" + }, + "hash:15":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 49, + 53 + ] + }, + "nameString":"hash:15" + }, + "hash:152":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 49, + 53, + 50 + ] + }, + "nameString":"hash:152" + }, + "hash:5":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 53 + ] + }, + "nameString":"hash:5" + } + }, + "keyCount":9, + "fullName":"hash:keys:keys:", + "keyApproximate":50, + "id":"0.g9y9ox4nau" + }, + { + "name":"string", + "children":[ + { + "name":"keys:keys", + "children":[ + + ], + "keys":{ + "hash:string:2":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 50 + ] + }, + "nameString":"hash:string:2" + }, + "hash:string:1":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49 + ] + }, + "nameString":"hash:string:1" + } + }, + "keyCount":2, + "fullName":"hash:string:keys:keys:", + "keyApproximate":11.11111111111111, + "id":"0.baqi0l0e9j6" + }, + { + "name":"1", + "children":[ + { + "name":"1", + "children":[ + { + "name":"1", + "children":[ + { + "name":"keys:keys", + "children":[ + + ], + "keys":{ + "hash:string:1:1:1:3":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49, + 58, + 49, + 58, + 49, + 58, + 51 + ] + }, + "nameString":"hash:string:1:1:1:3" + }, + "hash:string:1:1:1:5":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49, + 58, + 49, + 58, + 49, + 58, + 53 + ] + }, + "nameString":"hash:string:1:1:1:5" + }, + "hash:string:1:1:1:1":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49, + 58, + 49, + 58, + 49, + 58, + 49 + ] + }, + "nameString":"hash:string:1:1:1:1" + }, + "hash:string:1:1:1:2":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49, + 58, + 49, + 58, + 49, + 58, + 50 + ] + }, + "nameString":"hash:string:1:1:1:2" + }, + "hash:string:1:1:1:4":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 58, + 115, + 116, + 114, + 105, + 110, + 103, + 58, + 49, + 58, + 49, + 58, + 49, + 58, + 52 + ] + }, + "nameString":"hash:string:1:1:1:4" + } + }, + "keyCount":5, + "fullName":"hash:string:1:1:1:keys:keys:", + "keyApproximate":27.77777777777778, + "id":"0.6uk30kg4cll" + } + ], + "keyCount":5, + "fullName":"hash:string:1:1:1:", + "keyApproximate":27.77777777777778, + "id":"0.mdmi066idef" + } + ], + "keyCount":5, + "fullName":"hash:string:1:1:", + "keyApproximate":27.77777777777778, + "id":"0.i4phiogdhog" + } + ], + "keyCount":5, + "fullName":"hash:string:1:", + "keyApproximate":27.77777777777778, + "id":"0.fll9zclury" + } + ], + "keyCount":7, + "fullName":"hash:string:", + "keyApproximate":38.88888888888889, + "id":"0.0nrbpnjpg77i" + } + ], + "keyCount":16, + "fullName":"hash:", + "keyApproximate":88.88888888888889, + "id":"0.75h78cd80yk" + }, + { + "name":"hash2", + "children":[ + { + "name":"keys:keys", + "children":[ + + ], + "keys":{ + "hash2:2":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 50, + 58, + 50 + ] + }, + "nameString":"hash2:2" + }, + "hash2:1":{ + "name":{ + "type":"Buffer", + "data":[ + 104, + 97, + 115, + 104, + 50, + 58, + 49 + ] + }, + "nameString":"hash2:1" + } + }, + "keyCount":2, + "fullName":"hash2:keys:keys:", + "keyApproximate":11.11111111111111, + "id":"0.g1rrlj3k0d" + } + ], + "keyCount":2, + "fullName":"hash2:", + "keyApproximate":11.11111111111111, + "id":"0.tge74eh7psa" + } +] diff --git a/redisinsight/ui/src/utils/tests/tree.spec.ts b/redisinsight/ui/src/utils/tests/tree.spec.ts index 4c04963a49..9eb64953d9 100644 --- a/redisinsight/ui/src/utils/tests/tree.spec.ts +++ b/redisinsight/ui/src/utils/tests/tree.spec.ts @@ -1,4 +1,5 @@ -import { getTreeLeafField } from 'uiSrc/utils' +import { findTreeNode, getTreeLeafField } from 'uiSrc/utils' +import nodes from './nodes.json' const getTreeLeafFieldTests: any[] = [ [':', 'keys:keys'], @@ -18,3 +19,23 @@ describe('getTreeLeafField', () => { expect(result).toBe(expected) }) }) + +const findTreeNodeTests: any[] = [ + ['hash2:keys:keys:', 'id', null], + ['hash2:keys:keys:', 'fullName', nodes[1]?.children[0]], + ['hash:string:', 'fullName', nodes[0]?.children[1]], + ['hash:string:keys:keys:', 'fullName', nodes[0]?.children[1]?.children[0]], + ['0.g9y9ox4nau', 'id', nodes[0]?.children[0]], + ['hash2:keys:keys:', 'id', null], + ['uoeuoeuoe', 'id', null], + ['uoeuoeuoe', 'fullName', null], + ['hash2:', 'fullName', nodes[1]], +] + +describe('findTreeNode', () => { + it.each(findTreeNodeTests)('for input: %s (reply), should be output: %s', + (reply, key, expected) => { + const result = findTreeNode(nodes, reply, key) + expect(result).toBe(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/tree.ts b/redisinsight/ui/src/utils/tree.ts index fc235737a7..817cd5d600 100644 --- a/redisinsight/ui/src/utils/tree.ts +++ b/redisinsight/ui/src/utils/tree.ts @@ -1 +1,26 @@ +import { TreeNode } from 'uiSrc/components/virtual-tree' +import { Nullable } from './types' + export const getTreeLeafField = (delimiter = '') => `keys${delimiter}keys` + +export const findTreeNode = ( + data: TreeNode[], + value: string, + key = 'id', + tempObj: { found?: TreeNode } = {}, +): Nullable => { + if (value && data) { + // eslint-disable-next-line sonarjs/no-ignored-return + data.find((node) => { + if (node[key] === value) { + tempObj.found = node + return node + } + return findTreeNode(node.children, value, key, tempObj) + }) + if (tempObj.found) { + return tempObj.found + } + } + return null +} From efbc120147661ca6274153e847fa721bdb7be1ee Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 1 Dec 2022 13:53:53 +0100 Subject: [PATCH 036/107] #RI-3893 - Wrong key is displayed sometimes when switching between browser/tree view a several times --- .../src/pages/browser/components/key-tree/KeyTree.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx index 0976609953..89f20fd70c 100644 --- a/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx +++ b/redisinsight/ui/src/pages/browser/components/key-tree/KeyTree.tsx @@ -36,6 +36,10 @@ export interface Props { export const firstPanelId = 'tree' export const secondPanelId = 'keys' +const parseKeyNames = (keys: GetKeyInfoResponse[]) => + keys.map((item) => + ({ ...item, nameString: item.nameString ?? bufferToString(item.name) })) + const KeyTree = forwardRef((props: Props, ref) => { const { selectKey, loadMoreItems, loading, keysState } = props @@ -53,7 +57,7 @@ const KeyTree = forwardRef((props: Props, ref) => { const [keyListState, setKeyListState] = useState(keysState) const [constructingTree, setConstructingTree] = useState(false) const [selectDefaultLeaf, setSelectDefaultLeaf] = useState(isEmpty(selectedLeaf)) - const [items, setItems] = useState(keysState.keys ?? []) + const [items, setItems] = useState(parseKeyNames(keysState.keys ?? [])) const dispatch = useDispatch() @@ -107,10 +111,6 @@ const KeyTree = forwardRef((props: Props, ref) => { }, 0) } - const parseKeyNames = (keys: GetKeyInfoResponse[]) => - keys.map((item) => - ({ ...item, nameString: item.nameString ?? bufferToString(item.name) })) - const updateKeysList = (items:any = {}) => { startTransition(() => { const newState:KeysStoreData = { From 738745604a450bb0d51f8c69fa7688325c32fa93 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Thu, 1 Dec 2022 18:01:54 +0400 Subject: [PATCH 037/107] #RI-3401 - add unit tests --- jest.config.js | 8 +- redisinsight/__mocks__/monacoMock.js | 48 +++- .../QueryCardCliPlugin.spec.tsx | 40 ++++ .../ui/src/components/query/Query/Query.tsx | 14 +- .../virtual-grid/VirtualGrid.spec.tsx | 29 +++ .../MessageAckPopover/MessageAckPopover.tsx | 2 +- .../SearchDatabasesList.spec.tsx | 5 +- .../RedisCloudSubscriptionsPage.spec.tsx | 30 +++ .../protected-route/ProtectedRoute.spec.tsx | 23 ++ .../utils/tests/HtmlToJsxString.spec.tsx | 11 + .../utils/tests/MarkdownToJsxString.spec.ts | 19 ++ .../ui/src/services/hooks/useWebworkers.ts | 27 +-- .../ui/src/services/tests/PluguinApi.spec.ts | 24 ++ .../ui/src/services/tests/routing.spec.tsx | 23 ++ redisinsight/ui/src/slices/app/plugins.ts | 30 +-- .../ui/src/slices/browser/bulkActions.ts | 2 + .../ui/src/slices/tests/app/plugins.spec.ts | 181 ++++++++++++++- .../slices/tests/browser/bulkActions.spec.ts | 209 ++++++++++++++++++ .../ui/src/slices/tests/pubsub/pubsub.spec.ts | 204 ++++++++++++++++- .../monaco/monacoRedisCompletionProvider.ts | 4 +- .../monacoRedisSignatureHelpProvider.ts | 2 +- .../getDiffKeysOfObjectValues.spec.ts | 17 ++ .../monacoRedisCompletionProvider.spec.ts | 29 +++ 23 files changed, 919 insertions(+), 62 deletions(-) create mode 100644 redisinsight/ui/src/pages/redisStack/components/protected-route/ProtectedRoute.spec.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/HtmlToJsxString.spec.tsx create mode 100644 redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/MarkdownToJsxString.spec.ts create mode 100644 redisinsight/ui/src/services/tests/PluguinApi.spec.ts create mode 100644 redisinsight/ui/src/services/tests/routing.spec.tsx create mode 100644 redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts create mode 100644 redisinsight/ui/src/utils/tests/comparisons/getDiffKeysOfObjectValues.spec.ts create mode 100644 redisinsight/ui/src/utils/tests/monaco/monacoRedisCompletionProvider.spec.ts diff --git a/jest.config.js b/jest.config.js index 1ed96682be..d657c1e87f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -54,10 +54,10 @@ module.exports = { ], coverageThreshold: { global: { - statements: 70, - branches: 50, - functions: 60, - lines: 72, + statements: 77, + branches: 55, + functions: 65, + lines: 75, }, // './redisinsight/ui/src/slices/**/*.ts': { // statements: 90, diff --git a/redisinsight/__mocks__/monacoMock.js b/redisinsight/__mocks__/monacoMock.js index af886b4467..a3922e5b95 100644 --- a/redisinsight/__mocks__/monacoMock.js +++ b/redisinsight/__mocks__/monacoMock.js @@ -1,5 +1,49 @@ -import * as React from 'react'; +import React, { useEffect } from 'react'; export default function MonacoEditor(props) { - return
; + useEffect(() => { + props.editorDidMount && props.editorDidMount( + // editor + { + addCommand: jest.fn(), + getContribution: jest.fn(), + onKeyDown: jest.fn(), + onMouseDown: jest.fn(), + addAction: jest.fn(), + getAction: jest.fn(), + deltaDecorations: jest.fn(), + createContextKey: jest.fn(), + focus: jest.fn(), + onDidChangeCursorPosition: jest.fn(), + executeEdits: jest.fn() + }, + // monaco + { + Range: jest.fn().mockImplementation(() => { return {} }), + languages: { + getLanguages: jest.fn(), + register: jest.fn(), + registerCompletionItemProvider: jest.fn().mockReturnValue({ + dispose: jest.fn() + }), + registerSignatureHelpProvider: jest.fn().mockReturnValue({ + dispose: jest.fn() + }), + setLanguageConfiguration: jest.fn(), + setMonarchTokensProvider: jest.fn(), + }, + KeyMod: {}, + KeyCode: {} + }) + }, []) + return ; +} + +export const languages = { + CompletionItemKind: { + Function: 1 + }, + CompletionItemInsertTextRule: { + InsertAsSnippet: 4 + } } diff --git a/redisinsight/ui/src/components/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx b/redisinsight/ui/src/components/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx index 018b1c18d3..499a5d8895 100644 --- a/redisinsight/ui/src/components/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx +++ b/redisinsight/ui/src/components/query-card/QueryCardCliPlugin/QueryCardCliPlugin.spec.tsx @@ -1,6 +1,8 @@ import { cloneDeep } from 'lodash' import React from 'react' import { instance, mock } from 'ts-mockito' +import { PluginEvents } from 'uiSrc/plugins/pluginEvents' +import { pluginApi } from 'uiSrc/services/PluginAPI' import { cleanup, mockedStore, render } from 'uiSrc/utils/test-utils' import QueryCardCliPlugin, { Props } from './QueryCardCliPlugin' @@ -13,6 +15,28 @@ beforeEach(() => { store.clearActions() }) +jest.mock('uiSrc/services/PluginAPI', () => ({ + pluginApi: { + onEvent: jest.fn() + } +})) + +jest.mock('uiSrc/slices/app/plugins', () => ({ + ...jest.requireActual('uiSrc/slices/app/plugins'), + appPluginsSelector: jest.fn().mockReturnValue({ + visualizations: [ + { + id: '1', + uniqId: '1', + name: 'test', + plugin: '', + activationMethod: 'render', + matchCommands: ['*'], + } + ] + }), +})) + jest.mock('uiSrc/services', () => ({ ...jest.requireActual('uiSrc/services'), sessionStorageService: { @@ -25,4 +49,20 @@ describe('QueryCardCliPlugin', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should subscribes on events', () => { + const onEventMock = jest.fn(); + + (pluginApi.onEvent as jest.Mock).mockImplementation(onEventMock) + + render() + + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.heightChanged, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.loaded, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.error, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.setHeaderText, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.executeRedisCommand, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.getState, expect.any(Function)) + expect(onEventMock).toBeCalledWith(expect.any(String), PluginEvents.setState, expect.any(Function)) + }) }) diff --git a/redisinsight/ui/src/components/query/Query/Query.tsx b/redisinsight/ui/src/components/query/Query/Query.tsx index 5f8e98b963..fa30157df1 100644 --- a/redisinsight/ui/src/components/query/Query/Query.tsx +++ b/redisinsight/ui/src/components/query/Query/Query.tsx @@ -73,13 +73,13 @@ const Query = (props: Props) => { query = '', activeMode, resultsMode, - setQuery, - onKeyDown, - onSubmit, - setQueryEl, - setIsCodeBtnDisabled = () => { }, - onQueryChangeMode, - onChangeGroupMode + setQuery = () => {}, + onKeyDown = () => {}, + onSubmit = () => {}, + setQueryEl = () => {}, + setIsCodeBtnDisabled = () => {}, + onQueryChangeMode = () => {}, + onChangeGroupMode = () => {} } = props let contribution: Nullable = null const [isDedicatedEditorOpen, setIsDedicatedEditorOpen] = useState(false) diff --git a/redisinsight/ui/src/components/virtual-grid/VirtualGrid.spec.tsx b/redisinsight/ui/src/components/virtual-grid/VirtualGrid.spec.tsx index 460514be6f..3eed00cfea 100644 --- a/redisinsight/ui/src/components/virtual-grid/VirtualGrid.spec.tsx +++ b/redisinsight/ui/src/components/virtual-grid/VirtualGrid.spec.tsx @@ -1,15 +1,44 @@ import React from 'react' import { instance, mock } from 'ts-mockito' +import { ITableColumn } from 'uiSrc/components/virtual-table/interfaces' import { render } from 'uiSrc/utils/test-utils' import VirtualGrid from './VirtualGrid' import { IProps } from './interfaces' const mockedProps = mock() +const columns: ITableColumn[] = [ + { + id: 'name', + label: 'Member', + staySearchAlwaysOpen: true, + initialSearchValue: '', + truncateText: true, + minWidth: 50 + }, +] + +const items = ['member1', 'member2'] + describe('VirtualGrid', () => { it('should render', () => { expect( render() ).toBeTruthy() }) + + it('should render rows', () => { + expect( + render( + + ) + ).toBeTruthy() + }) }) diff --git a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.tsx b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.tsx index 0b50b938bd..c991aa73b3 100644 --- a/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.tsx +++ b/redisinsight/ui/src/pages/browser/components/stream-details/messages-view/MessageAckPopover/MessageAckPopover.tsx @@ -12,7 +12,7 @@ export interface Props { } const AckPopover = (props: Props) => { - const { id, isOpen, closePopover, showPopover, acknowledge } = props + const { id, isOpen, closePopover = () => {}, showPopover = () => {}, acknowledge = () => {} } = props return ( { expect(render()).toBeTruthy() }) - it.skip('should call loadInstancesSuccess with after typing', async () => { + it('should call loadInstancesSuccess with after typing', async () => { const state: RootState = store.getState(); (useSelector as jest.Mock).mockImplementation((callback: (arg0: RootState) => RootState) => callback({ ...state, @@ -79,9 +79,10 @@ describe('SearchDatabasesList', () => { ) }) + newInstancesMock[0].visible = false newInstancesMock[1].visible = false const expectedActions = [loadInstancesSuccess(newInstancesMock)] - expect(storeMock.getActions()).toEqual(expect.arrayContaining(expectedActions)) + expect(storeMock.getActions()).toEqual(expectedActions) }) }) diff --git a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.spec.tsx b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.spec.tsx index 47e70a09d8..5f987ecb85 100644 --- a/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.spec.tsx +++ b/redisinsight/ui/src/pages/redisCloudSubscriptions/RedisCloudSubscriptionsPage.spec.tsx @@ -1,8 +1,16 @@ import React from 'react' +import { cloudSelector } from 'uiSrc/slices/instances/cloud' import { render } from 'uiSrc/utils/test-utils' import RedisCloudSubscriptionsPage from './RedisCloudSubscriptionsPage' +jest.mock('uiSrc/slices/instances/cloud', () => ({ + ...jest.requireActual('uiSrc/slices/instances/cloud'), + cloudSelector: jest.fn().mockReturnValue({ + ...jest.requireActual('uiSrc/slices/instances/cloud').initialState + }) +})) + /** * RedisCloudSubscriptionsPage tests * @@ -12,4 +20,26 @@ describe('RedisCloudSubscriptionsPage', () => { it('should render', () => { expect(render()).toBeTruthy() }) + + it('should render with subscriptions', () => { + const cloudSelectorMock = jest.fn().mockReturnValue({ + credentials: null, + loading: false, + error: '', + loaded: { instances: false }, + account: { error: '', data: [] }, + subscriptions: [ + { + id: 123, + name: 'name', + numberOfDatabases: 123, + provider: 'provider', + region: 'region', + status: 'active', + }, + ] + }) + cloudSelector.mockImplementation(cloudSelectorMock) + expect(render()).toBeTruthy() + }) }) diff --git a/redisinsight/ui/src/pages/redisStack/components/protected-route/ProtectedRoute.spec.tsx b/redisinsight/ui/src/pages/redisStack/components/protected-route/ProtectedRoute.spec.tsx new file mode 100644 index 0000000000..558cdee468 --- /dev/null +++ b/redisinsight/ui/src/pages/redisStack/components/protected-route/ProtectedRoute.spec.tsx @@ -0,0 +1,23 @@ +import React from 'react' +import Router from 'uiSrc/Router' +import { render } from 'uiSrc/utils/test-utils' +import RouteWithSubRoutes from 'uiSrc/utils/routerWithSubRoutes' +import ROUTES from 'uiSrc/components/main-router/constants/commonRoutes' + +import ProtectedRoute from './ProtectedRoute' + +describe('ProtectedRoute', () => { + it('should render', () => { + expect(render( + + + + + + )) + .toBeTruthy() + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/HtmlToJsxString.spec.tsx b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/HtmlToJsxString.spec.tsx new file mode 100644 index 0000000000..d84552f938 --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/HtmlToJsxString.spec.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import HtmlToJsxString from '../formatter/HtmlToJsxString' + +describe('HtmlToJsxString', () => { + it('should return proper string', async () => { + const div = (
) + const formatter = new HtmlToJsxString() + const result = await formatter.format(div) + expect(result).toEqual(String(div)) + }) +}) diff --git a/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/MarkdownToJsxString.spec.ts b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/MarkdownToJsxString.spec.ts new file mode 100644 index 0000000000..c2b2cad42f --- /dev/null +++ b/redisinsight/ui/src/pages/workbench/components/enablement-area/EnablementArea/utils/tests/MarkdownToJsxString.spec.ts @@ -0,0 +1,19 @@ +import { unified } from 'unified' +import MarkdownToJsxString from '../formatter/MarkdownToJsxString' + +jest.mock('unified') +describe('MarkdownToJsxString', () => { + it('should call process', async () => { + // mock implementation + const useProcessMock = jest.fn().mockImplementation(() => Promise.resolve()) + function useMock() { + return { use: useMock, process: useProcessMock } + } + (unified as jest.Mock).mockImplementation(() => ({ + use: useMock + })) + + await (new MarkdownToJsxString()).format('') + expect(useProcessMock).toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/services/hooks/useWebworkers.ts b/redisinsight/ui/src/services/hooks/useWebworkers.ts index d4726a6a7c..7de5d85697 100644 --- a/redisinsight/ui/src/services/hooks/useWebworkers.ts +++ b/redisinsight/ui/src/services/hooks/useWebworkers.ts @@ -1,4 +1,4 @@ -import { useEffect, useState, useRef } from 'react' +import { useState } from 'react' import { Nullable } from 'uiSrc/utils' const workerHandler = (fn: (...args: any) => any) => { @@ -7,31 +7,6 @@ const workerHandler = (fn: (...args: any) => any) => { } } -export const useWebworker = (fn: (...args: any) => any) => { - const [result, setResult] = useState>(null) - const [error, setError] = useState>(null) - - const workerRef = useRef>(null) - - useEffect(() => { - const worker = new Worker( - URL.createObjectURL(new Blob([`(${workerHandler})(${fn})`])) - ) - workerRef.current = worker - worker.onmessage = (event) => setResult(event.data) - worker.onerror = (error: ErrorEvent) => setError(error) - return () => { - worker.terminate() - } - }, [fn]) - - return { - result, - error, - run: (value:any) => workerRef.current?.postMessage(value), - } -} - export const useDisposableWebworker = (fn: (...args: any) => any) => { const [result, setResult] = useState>(null) const [error, setError] = useState>(null) diff --git a/redisinsight/ui/src/services/tests/PluguinApi.spec.ts b/redisinsight/ui/src/services/tests/PluguinApi.spec.ts new file mode 100644 index 0000000000..2c7083c0c6 --- /dev/null +++ b/redisinsight/ui/src/services/tests/PluguinApi.spec.ts @@ -0,0 +1,24 @@ +import { pluginApi } from 'uiSrc/services/PluginAPI' + +describe('PluginApi', () => { + it('should subscribe on event and receive data after emit', () => { + const mockCallback = jest.fn() + const data = { data: 'some data' } + + pluginApi.onEvent('id1', 'someEvent', mockCallback) + pluginApi.sendEvent('id1', 'someEvent', data) + + expect(mockCallback).toBeCalledWith(data) + }) + + it('should subscribe on event and not receive data after unregister all subscriptions and emit', () => { + const mockCallback = jest.fn() + const data = { data: 'some data' } + + pluginApi.onEvent('id1', 'someEvent', mockCallback) + pluginApi.unregisterSubscriptions() + pluginApi.sendEvent('id1', 'someEvent', data) + + expect(mockCallback).not.toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/services/tests/routing.spec.tsx b/redisinsight/ui/src/services/tests/routing.spec.tsx new file mode 100644 index 0000000000..edb468e8e1 --- /dev/null +++ b/redisinsight/ui/src/services/tests/routing.spec.tsx @@ -0,0 +1,23 @@ +import { EuiLink } from '@elastic/eui' +import React from 'react' +import { Pages } from 'uiSrc/constants' +import { getRouterLinkProps } from 'uiSrc/services' +import { render, fireEvent, screen } from 'uiSrc/utils/test-utils' + +describe('getRouterLinkProps', () => { + it('should call click callback', () => { + const mockOnClick = jest.fn() + + render( + + Text + + ) + fireEvent.click(screen.getByTestId('link')) + + expect(mockOnClick).toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/slices/app/plugins.ts b/redisinsight/ui/src/slices/app/plugins.ts index 6b572a68af..77c248067c 100644 --- a/redisinsight/ui/src/slices/app/plugins.ts +++ b/redisinsight/ui/src/slices/app/plugins.ts @@ -96,11 +96,11 @@ export function loadPluginsAction() { // Asynchronous thunk action export function sendPluginCommandAction({ command = '', onSuccessAction, onFailAction }: { - command: string; - onSuccessAction?: (responseData: any) => void; - onFailAction?: (error: any) => void; + command: string + onSuccessAction?: (responseData: any) => void + onFailAction?: (error: any) => void }) { - return async (dispatch: AppDispatch, stateInit: () => RootState) => { + return async (_dispatch: AppDispatch, stateInit: () => RootState) => { try { const state = stateInit() const { id = '' } = state.connections.instances.connectedInstance @@ -126,12 +126,12 @@ export function sendPluginCommandAction({ command = '', onSuccessAction, onFailA } export function getPluginStateAction({ visualizationId = '', commandId = '', onSuccessAction, onFailAction }: { - visualizationId: string; - commandId: string; - onSuccessAction?: (responseData: any) => void; - onFailAction?: (error: any) => void; + visualizationId: string + commandId: string + onSuccessAction?: (responseData: any) => void + onFailAction?: (error: any) => void }) { - return async (dispatch: AppDispatch, stateInit: () => RootState) => { + return async (_dispatch: AppDispatch, stateInit: () => RootState) => { try { const state = stateInit() const { id = '' } = state.connections.instances.connectedInstance @@ -157,13 +157,13 @@ export function getPluginStateAction({ visualizationId = '', commandId = '', onS } export function setPluginStateAction({ visualizationId = '', commandId = '', pluginState, onSuccessAction, onFailAction }: { - visualizationId: string; - commandId: string; - pluginState: any; - onSuccessAction?: (responseData: any) => void; - onFailAction?: (error: any) => void; + visualizationId: string + commandId: string + pluginState: any + onSuccessAction?: (responseData: any) => void + onFailAction?: (error: any) => void }) { - return async (dispatch: AppDispatch, stateInit: () => RootState) => { + return async (_dispatch: AppDispatch, stateInit: () => RootState) => { try { const state = stateInit() const { id = '' } = state.connections.instances.connectedInstance diff --git a/redisinsight/ui/src/slices/browser/bulkActions.ts b/redisinsight/ui/src/slices/browser/bulkActions.ts index bf9e4687eb..c69c39ccad 100644 --- a/redisinsight/ui/src/slices/browser/bulkActions.ts +++ b/redisinsight/ui/src/slices/browser/bulkActions.ts @@ -77,10 +77,12 @@ export const { setLoading, setBulkActionType, setBulkActionConnected, + toggleBulkActions, disconnectBulkAction, toggleBulkActionTriggered, setOverview, setBulkActionsInitialState, + bulkDeleteSuccess } = bulkActionsSlice.actions // Selectors diff --git a/redisinsight/ui/src/slices/tests/app/plugins.spec.ts b/redisinsight/ui/src/slices/tests/app/plugins.spec.ts index 6fc643b633..020bd20a83 100644 --- a/redisinsight/ui/src/slices/tests/app/plugins.spec.ts +++ b/redisinsight/ui/src/slices/tests/app/plugins.spec.ts @@ -1,10 +1,17 @@ import { cloneDeep, flatMap, isEmpty, reject } from 'lodash' +import { apiService } from 'uiSrc/services' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' import { IPlugin, PluginsResponse } from 'uiSrc/slices/interfaces' import reducer, { appPluginsSelector, - getAllPlugins, getAllPluginsFailure, getAllPluginsSuccess, + getAllPlugins, + getAllPluginsFailure, + getAllPluginsSuccess, + getPluginStateAction, initialState, + loadPluginsAction, + sendPluginCommandAction, + setPluginStateAction, } from '../../app/plugins' let store: typeof mockedStore @@ -134,4 +141,176 @@ describe('slices', () => { expect(appPluginsSelector(rootState)).toEqual(state) }) }) + + // thunks + + describe('loadPluginsAction', () => { + it('succeed to fetch plugins', async () => { + // Arrange + const data = MOCK_PLUGINS_RESPONSE + const responsePayload = { status: 200, data } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(loadPluginsAction()) + + // Assert + const expectedActions = [ + getAllPlugins(), + getAllPluginsSuccess(data), + ] + + expect(mockedStore.getActions()).toEqual(expectedActions) + }) + + it('failed to fetch plugins', async () => { + // Arrange + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(loadPluginsAction()) + + // Assert + const expectedActions = [ + getAllPlugins(), + getAllPluginsFailure(errorMessage), + ] + + expect(mockedStore.getActions()).toEqual(expectedActions) + }) + }) + + describe('sendPluginCommandAction', () => { + it('succeed to send command', async () => { + // Arrange + const data = 'response' + const onSuccess = jest.fn() + const responsePayload = { status: 200, data } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(sendPluginCommandAction({ + command: 'info', + onSuccessAction: onSuccess + })) + + expect(onSuccess).toBeCalledWith(data) + }) + + it('failed to send command', async () => { + // Arrange + const onFailed = jest.fn() + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(sendPluginCommandAction({ + command: 'info', + onFailAction: onFailed + })) + + expect(onFailed).toBeCalledWith(responsePayload) + }) + }) + + describe('getPluginStateAction', () => { + it('succeed to get plugin state ', async () => { + // Arrange + const data = 'response' + const onSuccess = jest.fn() + const responsePayload = { status: 200, data } + + apiService.get = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(getPluginStateAction({ + visualizationId: '1', + commandId: '1', + onSuccessAction: onSuccess + })) + + expect(onSuccess).toBeCalledWith(data) + }) + + it('failed to get plugin state', async () => { + // Arrange + const onFailed = jest.fn() + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + apiService.get = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(getPluginStateAction({ + visualizationId: '1', + commandId: '1', + onFailAction: onFailed + })) + + expect(onFailed).toBeCalledWith(responsePayload) + }) + }) + + describe('setPluginStateAction', () => { + it('succeed to set plugin state ', async () => { + // Arrange + const data = 'response' + const onSuccess = jest.fn() + const responsePayload = { status: 200, data } + + apiService.post = jest.fn().mockResolvedValue(responsePayload) + + // Act + await store.dispatch(setPluginStateAction({ + visualizationId: '1', + commandId: '1', + pluginState: { info: 'smth' }, + onSuccessAction: onSuccess + })) + + expect(onSuccess).toBeCalledWith(data) + }) + + it('failed to set plugin state', async () => { + // Arrange + const onFailed = jest.fn() + const errorMessage = 'Something was wrong!' + const responsePayload = { + response: { + status: 500, + data: { message: errorMessage }, + }, + } + apiService.post = jest.fn().mockRejectedValue(responsePayload) + + // Act + await store.dispatch(setPluginStateAction({ + visualizationId: '1', + commandId: '1', + pluginState: { info: 'smth' }, + onFailAction: onFailed + })) + + expect(onFailed).toBeCalledWith(responsePayload) + }) + }) }) diff --git a/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts b/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts new file mode 100644 index 0000000000..19921876b9 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/browser/bulkActions.spec.ts @@ -0,0 +1,209 @@ +import { cloneDeep } from 'lodash' +import { BulkActionsType } from 'uiSrc/constants' +import reducer, { + bulkActionsSelector, + initialState, + toggleBulkActionTriggered, + toggleBulkActions, + setBulkActionConnected, + setLoading, + setBulkActionType, + setOverview, + overviewBulkActionsSelector, + disconnectBulkAction, + bulkDeleteSuccess, +} from 'uiSrc/slices/browser/bulkActions' +import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' + +let store: typeof mockedStore + +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +describe('bulkActions slice', () => { + describe('reducer, actions and selectors', () => { + it('should return the initial state on first run', () => { + // Arrange + const nextState = initialState + + // Act + const result = reducer(undefined, {}) + + // Assert + expect(result).toEqual(nextState) + }) + + describe('toggleBulkActions', () => { + it('should properly set state', () => { + const currentState = { + ...initialState, + isShowBulkActions: true + } + + // Arrange + const state = { + ...initialState, + isShowBulkActions: false + } + + // Act + const nextState = reducer(currentState, toggleBulkActions()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setBulkActionConnected', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + isConnected: true + } + + // Act + const nextState = reducer(initialState, setBulkActionConnected(true)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setLoading', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + loading: true + } + + // Act + const nextState = reducer(initialState, setLoading(true)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setBulkActionType', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + selectedBulkAction: { + ...initialState.selectedBulkAction, + type: BulkActionsType.Delete + } + } + + // Act + const nextState = reducer(initialState, setBulkActionType(BulkActionsType.Delete)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(state) + }) + }) + + describe('toggleBulkActionTriggered', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + isActionTriggered: true + } + + // Act + const nextState = reducer(initialState, toggleBulkActionTriggered()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(state) + }) + }) + + describe('setOverview', () => { + it('should properly set state', () => { + // Arrange + const data = { + id: 1, + databaseId: '1', + duration: 300, + status: 'completed', + type: BulkActionsType.Delete, + summary: { processed: 1, succeed: 1, failed: 0, errors: [] }, + } + + const overview = { + ...data + } + + // Act + const nextState = reducer(initialState, setOverview(data)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(overviewBulkActionsSelector(rootState)).toEqual(overview) + }) + }) + + describe('disconnectBulkAction', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + loading: true, + isActionTriggered: true, + isConnected: true, + } + + // Act + const nextState = reducer(currentState, disconnectBulkAction()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(initialState) + }) + }) + + describe('bulkDeleteSuccess', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + loading: true + } + + // Act + const nextState = reducer(currentState, bulkDeleteSuccess()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + browser: { bulkActions: nextState }, + }) + expect(bulkActionsSelector(rootState)).toEqual(initialState) + }) + }) + }) +}) diff --git a/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts b/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts index 725a6d7afd..0853c5367d 100644 --- a/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts +++ b/redisinsight/ui/src/slices/tests/pubsub/pubsub.spec.ts @@ -3,13 +3,20 @@ import { cloneDeep } from 'lodash' import { apiService } from 'uiSrc/services' import { addErrorNotification } from 'uiSrc/slices/app/notifications' import reducer, { + clearPubSubMessages, concatPubSubMessages, + disconnectPubSub, initialState, PUB_SUB_ITEMS_MAX_COUNT, publishMessage, publishMessageAction, publishMessageError, - publishMessageSuccess, pubSubSelector + publishMessageSuccess, + pubSubSelector, + setIsPubSubUnSubscribed, + setLoading, + setPubSubConnected, + toggleSubscribeTriggerPubSub } from 'uiSrc/slices/pubsub/pubsub' import { cleanup, initialStateDefault, mockedStore } from 'uiSrc/utils/test-utils' @@ -34,6 +41,72 @@ describe('pubsub slice', () => { expect(result).toEqual(nextState) }) + describe('setPubSubConnected', () => { + it('should properly set state', () => { + const isConnected = true + + // Arrange + const state = { + ...initialState, + isConnected + } + + // Act + const nextState = reducer(initialState, setPubSubConnected(isConnected)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('toggleSubscribeTriggerPubSub', () => { + it('should properly set state', () => { + const subscriptions = [{ channel: '1', type: 'ss' }] + + // Arrange + const state = { + ...initialState, + isSubscribeTriggered: !initialState.isSubscribeTriggered, + subscriptions + } + + // Act + const nextState = reducer(initialState, toggleSubscribeTriggerPubSub(subscriptions)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('setIsPubSubUnSubscribed', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + isSubscribed: true + } + const state = { + ...currentState, + isSubscribed: false + } + + // Act + const nextState = reducer(currentState, setIsPubSubUnSubscribed()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + describe('concatPubSubMessages', () => { it('should properly set payload to items', () => { const payload = { @@ -92,6 +165,135 @@ describe('pubsub slice', () => { expect(pubSubSelector(rootState)).toEqual(state) }) }) + + describe('clearPubSubMessages', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + messages: ['a', 'b', 'c'], + count: 3 + } + + const state = { + ...currentState, + messages: [], + count: 0 + } + + // Act + const nextState = reducer(currentState, clearPubSubMessages()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('setLoading', () => { + it('should properly set state', () => { + // Arrange + + const state = { + ...initialState, + loading: true + } + + // Act + const nextState = reducer(state, setLoading(true)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('disconnectPubSub', () => { + it('should properly set state', () => { + // Arrange + const currentState = { + ...initialState, + loading: true, + isSubscribed: true, + isSubscribeTriggered: true, + isConnected: true + } + + const state = { + ...initialState, + } + + // Act + const nextState = reducer(currentState, disconnectPubSub()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('publishMessage', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState, + publishing: true + } + + // Act + const nextState = reducer(initialState, publishMessage()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('publishMessageSuccess', () => { + it('should properly set state', () => { + // Arrange + const state = { + ...initialState + } + + // Act + const nextState = reducer(initialState, publishMessageSuccess()) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) + + describe('publishMessageError', () => { + it('should properly set state', () => { + // Arrange + const error = 'Some error' + const state = { + ...initialState, + error + } + + // Act + const nextState = reducer(initialState, publishMessageError(error)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + pubsub: nextState, + }) + expect(pubSubSelector(rootState)).toEqual(state) + }) + }) }) // thunks diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisCompletionProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisCompletionProvider.ts index 6c9cba284a..f58ffa2193 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisCompletionProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisCompletionProvider.ts @@ -7,7 +7,7 @@ type DependencyProposals = { [key: string]: monacoEditor.languages.CompletionItem } -const getCommandMarkdown = (commandName = '', command: ICommand): string => { +export const getCommandMarkdown = (commandName = '', command: ICommand): string => { const docUrl = getDocUrlForCommand(commandName) const linkMore = ` [Read more](${docUrl})` const lines: string[] = [command?.summary + linkMore] @@ -24,7 +24,7 @@ const getCommandMarkdown = (commandName = '', command: ICommand): string => { return lines.join('\n'.repeat(2)) } -const createDependencyProposals = (commandsSpec: ICommands): DependencyProposals => { +export const createDependencyProposals = (commandsSpec: ICommands): DependencyProposals => { const result: DependencyProposals = {} const commandsArr = Object.keys(commandsSpec).sort() commandsArr.forEach((command: string) => { diff --git a/redisinsight/ui/src/utils/monaco/monacoRedisSignatureHelpProvider.ts b/redisinsight/ui/src/utils/monaco/monacoRedisSignatureHelpProvider.ts index 8e58a86c24..8c06c71a42 100644 --- a/redisinsight/ui/src/utils/monaco/monacoRedisSignatureHelpProvider.ts +++ b/redisinsight/ui/src/utils/monaco/monacoRedisSignatureHelpProvider.ts @@ -2,8 +2,8 @@ import { MutableRefObject } from 'react' import * as monacoEditor from 'monaco-editor' import { isNull } from 'lodash' import { ICommands } from 'uiSrc/constants' +import { findCommandEarlier } from 'uiSrc/utils' import { generateArgsNames } from 'uiSrc/utils/commands' -import { findCommandEarlier } from './monacoUtils' export const getRedisSignatureHelpProvider = ( commandsSpec: ICommands, diff --git a/redisinsight/ui/src/utils/tests/comparisons/getDiffKeysOfObjectValues.spec.ts b/redisinsight/ui/src/utils/tests/comparisons/getDiffKeysOfObjectValues.spec.ts new file mode 100644 index 0000000000..7f923186aa --- /dev/null +++ b/redisinsight/ui/src/utils/tests/comparisons/getDiffKeysOfObjectValues.spec.ts @@ -0,0 +1,17 @@ +import { getDiffKeysOfObjectValues } from 'uiSrc/utils' + +const getDiffKeysOfObjectValuesTests: any[] = [ + [{}, {}, []], + [{ key1: '1' }, { key1: '2' }, ['key1']], + [{ key1: '1' }, { key2: 2 }, ['key1']], + [{}, { key2: '1' }, []], + [{ key1: 1 }, { }, ['key1']], +] + +describe('getDiffKeysOfObjectValues', () => { + it.each(getDiffKeysOfObjectValuesTests)('for input: %s, %s should be output: %s', + (obj1, obj2, expected) => { + const result = getDiffKeysOfObjectValues(obj1, obj2) + expect(result).toEqual(expected) + }) +}) diff --git a/redisinsight/ui/src/utils/tests/monaco/monacoRedisCompletionProvider.spec.ts b/redisinsight/ui/src/utils/tests/monaco/monacoRedisCompletionProvider.spec.ts new file mode 100644 index 0000000000..d32537b28c --- /dev/null +++ b/redisinsight/ui/src/utils/tests/monaco/monacoRedisCompletionProvider.spec.ts @@ -0,0 +1,29 @@ +import { MOCK_COMMANDS_SPEC } from 'uiSrc/constants' +import { createDependencyProposals, getCommandMarkdown } from 'uiSrc/utils' + +const spec = { GET: MOCK_COMMANDS_SPEC.GET } + +describe('createDependencyProposals', () => { + it('should prepare completion', () => { + const result = createDependencyProposals(spec) + expect(result).toEqual({ + GET: { + label: 'GET', + kind: 1, + detail: 'GET key', + // eslint-disable-next-line no-template-curly-in-string + insertText: 'GET ${1:key} ', + documentation: { + value: getCommandMarkdown('GET', MOCK_COMMANDS_SPEC.GET) + }, + insertTextRules: 4, + range: { + endColumn: 0, + endLineNumber: 0, + startColumn: 0, + startLineNumber: 0 + } + } + }) + }) +}) From 05b6dc787e6c164943df49b9a16af4d150c020f3 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 2 Dec 2022 10:52:01 +0400 Subject: [PATCH 038/107] #RI-3865 - renamed browser view --- .../src/pages/browser/components/keys-header/KeysHeader.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx index dc4c1ac28d..25b3a0351d 100644 --- a/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx +++ b/redisinsight/ui/src/pages/browser/components/keys-header/KeysHeader.tsx @@ -82,8 +82,8 @@ const KeysHeader = (props: Props) => { const viewTypes: ISwitchType[] = [ { type: KeyViewType.Browser, - tooltipText: 'Browser', - ariaLabel: 'Browser view button', + tooltipText: 'List View', + ariaLabel: 'List view button', dataTestId: 'view-type-browser-btn', isActiveView() { return viewType === this.type }, getClassName() { From 93bd6e11ba0ff6255e8630a28f2d1d86d5d1b004 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 2 Dec 2022 14:34:29 +0400 Subject: [PATCH 039/107] #RI-3796 - fix bulk actions context --- .../ui/src/pages/browser/BrowserPage.spec.tsx | 28 +++++++++++++++++-- .../ui/src/pages/browser/BrowserPage.tsx | 7 +++++ .../components/bulk-actions/BulkActions.tsx | 4 --- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx index 908d2c14a2..c0f4838d82 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.spec.tsx @@ -5,14 +5,20 @@ import { render, screen, fireEvent, mockedStore, cleanup } from 'uiSrc/utils/tes import { setConnectedInstanceId } from 'uiSrc/slices/instances/instances' import { loadKeys, resetKeyInfo, toggleBrowserFullScreen } from 'uiSrc/slices/browser/keys' import { resetErrors } from 'uiSrc/slices/app/notifications' -import { setBrowserBulkActionOpen } from 'uiSrc/slices/app/context' +import { + setBrowserBulkActionOpen, + setBrowserPanelSizes, + setBrowserSelectedKey, + setLastPageContext +} from 'uiSrc/slices/app/context' import BrowserPage from './BrowserPage' import KeyList, { Props as KeyListProps } from './components/key-list/KeyList' import KeyDetailsWrapper, { Props as KeyDetailsWrapperProps } from './components/key-details/KeyDetailsWrapper' import AddKey, { Props as AddKeyProps } from './components/add-key/AddKey' -import KeysHeader, { Props as KeysHeaderProps } from './components/keys-header' +import KeysHeader from './components/keys-header' +import { Props as KeysHeaderProps } from './components/keys-header/KeysHeader' jest.mock('./components/key-list/KeyList', () => ({ __esModule: true, @@ -123,7 +129,7 @@ describe('BrowserPage', () => { fireEvent.click(screen.getByTestId('handleBulkActionsPanel-btn')) - const expectedActions = [resetKeyInfo(), toggleBrowserFullScreen(false), setBrowserBulkActionOpen(true)] + const expectedActions = [resetKeyInfo(), toggleBrowserFullScreen(false)] expect(store.getActions()).toEqual([...afterRenderActions, ...expectedActions]) }) @@ -144,4 +150,20 @@ describe('BrowserPage', () => { expect(store.getActions()).toEqual([...afterRenderActions, toggleBrowserFullScreen(true)]) }) + + it('should call proper actions on onmount', () => { + const { unmount } = render() + const afterRenderActions = [...store.getActions()] + + unmount() + + const unmountActions = [ + setBrowserPanelSizes(expect.any(Object)), + setBrowserBulkActionOpen(expect.any(Boolean)), + setBrowserSelectedKey(null), + setLastPageContext('browser'), + ] + + expect(store.getActions()).toEqual([...afterRenderActions, ...unmountActions]) + }) }) diff --git a/redisinsight/ui/src/pages/browser/BrowserPage.tsx b/redisinsight/ui/src/pages/browser/BrowserPage.tsx index d8169d7f41..e888ebe054 100644 --- a/redisinsight/ui/src/pages/browser/BrowserPage.tsx +++ b/redisinsight/ui/src/pages/browser/BrowserPage.tsx @@ -27,6 +27,7 @@ import { appContextBrowser, setBrowserPanelSizes, setLastPageContext, + setBrowserBulkActionOpen, } from 'uiSrc/slices/app/context' import { resetErrors } from 'uiSrc/slices/app/notifications' import { appAnalyticsInfoSelector } from 'uiSrc/slices/app/info' @@ -68,6 +69,7 @@ const BrowserPage = () => { const prevSelectedType = useRef(type) const selectedKeyRef = useRef>(selectedKey) + const isBulkActionsPanelOpenRef = useRef(isBulkActionsPanelOpen) const dispatch = useDispatch() @@ -86,11 +88,16 @@ const BrowserPage = () => { dispatch(setBrowserPanelSizes(prevSizes)) return {} }) + dispatch(setBrowserBulkActionOpen(isBulkActionsPanelOpenRef.current)) dispatch(setBrowserSelectedKey(selectedKeyRef.current)) dispatch(setLastPageContext('browser')) } }, []) + useEffect(() => { + isBulkActionsPanelOpenRef.current = isBulkActionsPanelOpen + }, [isBulkActionsPanelOpen]) + useEffect(() => { selectedKeyRef.current = selectedKey }, [selectedKey]) diff --git a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx index 716518a834..1191b10b2f 100644 --- a/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx +++ b/redisinsight/ui/src/pages/browser/components/bulk-actions/BulkActions.tsx @@ -22,7 +22,6 @@ import { import { BulkActionsType } from 'uiSrc/constants' import { keysSelector } from 'uiSrc/slices/browser/keys' import { getMatchType, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { setBrowserBulkActionOpen } from 'uiSrc/slices/app/context' import BulkDelete from './BulkDelete' import BulkActionsTabs from './BulkActionsTabs' @@ -52,8 +51,6 @@ const BulkActions = (props: Props) => { const dispatch = useDispatch() useEffect(() => { - dispatch(setBrowserBulkActionOpen(true)) - let matchValue = '*' if (search !== '*' && !!search) { matchValue = getMatchType(search) @@ -86,7 +83,6 @@ const BulkActions = (props: Props) => { const closePanel = () => { onBulkActionsPanel(false) dispatch(setBulkActionsInitialState()) - dispatch(setBrowserBulkActionOpen(false)) onClosePanel() From d76fb0b3f613d5a3822f985a8de400f8dfdd3e85 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 5 Dec 2022 11:57:21 +0100 Subject: [PATCH 040/107] Add deleteKeyByNames to after hook --- .../tests/critical-path/browser/search-capabilities.e2e.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 4c3cf5e250..f55e7312ae 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -247,6 +247,7 @@ test }); test + .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); await addNewStandaloneDatabaseApi(ossStandaloneConfig); @@ -259,6 +260,11 @@ test await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameSimpleDb}`); + + await t.click(browserPage.patternModeBtn); + + await browserPage.deleteKeysByNames(keyNames); + // Clear and delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); From e43ed05b4f49575c7f63e9850177d041502e253a Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 5 Dec 2022 12:02:11 +0100 Subject: [PATCH 041/107] delete .only --- tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index f55e7312ae..283caa23fe 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -247,7 +247,6 @@ test }); test - .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); await addNewStandaloneDatabaseApi(ossStandaloneConfig); From 296bd7307d2771c35feb9a743ee1b81f2b7eef8f Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 5 Dec 2022 12:40:38 +0100 Subject: [PATCH 042/107] Re-orginize steps in test case and delete indents --- .../browser/search-capabilities.e2e.ts | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 283caa23fe..1f70b61754 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -247,27 +247,23 @@ test }); test + .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); await addNewStandaloneDatabaseApi(ossStandaloneConfig); }) .after(async () => { + //clear database await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameBigDb}`); - await t.click(browserPage.myRedisDbIcon); // go back to database selection page - await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database - await cliPage.sendCommandInCli(`FT.DROPINDEX ${indexNameSimpleDb}`); - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); - // Clear and delete database + //delete database await deleteStandaloneDatabaseApi(ossStandaloneConfig); await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - // delete index and keys })('Verify that indexed keys from previous DB are NOT displayed when user connects to another DB', async t => { /* Link to ticket: https://redislabs.atlassian.net/browse/RI-3863 @@ -287,12 +283,7 @@ test await cliPage.sendCommandsInCli(commandsForBigStandalone); - await t.click(browserPage.treeViewButton); // switch to tree view - - await t.click(browserPage.redisearchModeBtn); - await t.click(browserPage.myRedisDbIcon); // go back to database selection page - await myRedisDatabasePage.clickOnDBByName(simpleDbName); // click standalone database const commandsForStandalone = [ @@ -306,21 +297,17 @@ test // Create 5 keys and index await cliPage.sendCommandsInCli(commandsForStandalone); - await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily - - await common.reloadPage(); // reload page - + await t.click(browserPage.treeViewButton); // switch to tree view await t.click(browserPage.redisearchModeBtn); // click redisearch button - await browserPage.selectIndexByName(indexNameSimpleDb); // select pre-created index in the standalone database + await browserPage.changeDelimiterInTreeView('-'); // change delimiter in tree view to be able to verify keys easily await verifyKeysDisplayedInTheList(keyNames); // verify created keys are visible await t.click(browserPage.myRedisDbIcon); // go back to database selection page - await myRedisDatabasePage.clickOnDBByName(bigDbName); // click database name from ossStandaloneBigConfig.databaseName await verifyKeysNotDisplayedInTheList(keyNames); // Verify that standandalone database keys are NOT visible - await t.expect(Selector('span').withText('Select Index').exists).ok('verify index is not selected'); + await t.expect(Selector('span').withText('Select Index').exists).ok('Index is still selected'); }); From 0f163e6fb0fa310c6c4e26762d43d779769b7f0d Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 5 Dec 2022 12:41:58 +0100 Subject: [PATCH 043/107] delete .only --- tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts index 1f70b61754..c250444527 100644 --- a/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts +++ b/tests/e2e/tests/critical-path/browser/search-capabilities.e2e.ts @@ -247,7 +247,6 @@ test }); test - .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, bigDbName); await addNewStandaloneDatabaseApi(ossStandaloneConfig); From f945ff1d06b35641cbb3c7a20db5bc464ce96266 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 5 Dec 2022 15:45:47 +0200 Subject: [PATCH 044/107] #RI-3902 import result with statuses --- .../api/src/__mocks__/database-import.ts | 37 ++++- .../api/src/common/exceptions/index.ts | 1 + .../common/exceptions/validation.exception.ts | 3 + .../api/src/constants/telemetry-events.ts | 1 + .../database-import.analytics.ts | 20 ++- .../database-import.controller.ts | 4 +- .../database-import.service.spec.ts | 6 +- .../database-import.service.ts | 148 ++++++++++++------ .../dto/database-import.response.ts | 83 +++++++++- .../POST-databases-import.test.ts | 30 +++- 10 files changed, 254 insertions(+), 79 deletions(-) create mode 100644 redisinsight/api/src/common/exceptions/index.ts create mode 100644 redisinsight/api/src/common/exceptions/validation.exception.ts diff --git a/redisinsight/api/src/__mocks__/database-import.ts b/redisinsight/api/src/__mocks__/database-import.ts index 2ce3ed50a5..210c90e1e4 100644 --- a/redisinsight/api/src/__mocks__/database-import.ts +++ b/redisinsight/api/src/__mocks__/database-import.ts @@ -1,7 +1,7 @@ -import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; +import { DatabaseImportResponse, DatabaseImportStatus } from 'src/modules/database-import/dto/database-import.response'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { mockDatabase } from 'src/__mocks__/databases'; -import { ValidationError } from 'class-validator'; +import { ValidationException } from 'src/common/exceptions'; export const mockDatabasesToImportArray = new Array(10).fill(mockDatabase); @@ -12,10 +12,33 @@ export const mockDatabaseImportFile = { buffer: Buffer.from(JSON.stringify(mockDatabasesToImportArray)), }; +export const mockDatabaseImportResultSuccess = { + index: 0, + status: DatabaseImportStatus.Success, + host: mockDatabase.host, + port: mockDatabase.port, +}; + +export const mockDatabaseImportResultFail = { + index: 0, + status: DatabaseImportStatus.Fail, + host: mockDatabase.host, + port: mockDatabase.port, + error: new BadRequestException(), +}; + export const mockDatabaseImportResponse = Object.assign(new DatabaseImportResponse(), { total: 10, - success: 7, - errors: [new ValidationError(), new BadRequestException(), new ForbiddenException()], + success: (new Array(7).fill(mockDatabaseImportResultSuccess)).map((v, index) => ({ + ...v, + index: index + 3, + })), + partial: [], + fail: [new ValidationException([]), new BadRequestException(), new ForbiddenException()].map((error, index) => ({ + ...mockDatabaseImportResultFail, + index, + error, + })), }); export const mockDatabaseImportParseFailedAnalyticsPayload = { @@ -23,12 +46,12 @@ export const mockDatabaseImportParseFailedAnalyticsPayload = { }; export const mockDatabaseImportFailedAnalyticsPayload = { - failed: mockDatabaseImportResponse.errors.length, - errors: ['ValidationError', 'BadRequestException', 'ForbiddenException'], + failed: mockDatabaseImportResponse.fail.length, + errors: ['ValidationException', 'BadRequestException', 'ForbiddenException'], }; export const mockDatabaseImportSucceededAnalyticsPayload = { - succeed: mockDatabaseImportResponse.success, + succeed: mockDatabaseImportResponse.success.length, }; export const mockDatabaseImportAnalytics = jest.fn(() => ({ diff --git a/redisinsight/api/src/common/exceptions/index.ts b/redisinsight/api/src/common/exceptions/index.ts new file mode 100644 index 0000000000..b640b9cfae --- /dev/null +++ b/redisinsight/api/src/common/exceptions/index.ts @@ -0,0 +1 @@ +export * from './validation.exception'; diff --git a/redisinsight/api/src/common/exceptions/validation.exception.ts b/redisinsight/api/src/common/exceptions/validation.exception.ts new file mode 100644 index 0000000000..9ce546d66c --- /dev/null +++ b/redisinsight/api/src/common/exceptions/validation.exception.ts @@ -0,0 +1,3 @@ +import { BadRequestException } from '@nestjs/common'; + +export class ValidationException extends BadRequestException {} diff --git a/redisinsight/api/src/constants/telemetry-events.ts b/redisinsight/api/src/constants/telemetry-events.ts index f583d17048..15b36f1531 100644 --- a/redisinsight/api/src/constants/telemetry-events.ts +++ b/redisinsight/api/src/constants/telemetry-events.ts @@ -18,6 +18,7 @@ export enum TelemetryEvents { DatabaseImportParseFailed = 'CONFIG_DATABASES_REDIS_IMPORT_PARSE_FAILED', DatabaseImportFailed = 'CONFIG_DATABASES_REDIS_IMPORT_FAILED', DatabaseImportSucceeded = 'CONFIG_DATABASES_REDIS_IMPORT_SUCCEEDED', + DatabaseImportPartiallySucceeded = 'CONFIG_DATABASES_REDIS_IMPORT_PARTIALLY_SUCCEEDED', // Events for autodiscovery flows REClusterDiscoverySucceed = 'CONFIG_DATABASES_RE_CLUSTER_AUTODISCOVERY_SUCCEEDED', diff --git a/redisinsight/api/src/modules/database-import/database-import.analytics.ts b/redisinsight/api/src/modules/database-import/database-import.analytics.ts index e363db5a0e..6d4894d448 100644 --- a/redisinsight/api/src/modules/database-import/database-import.analytics.ts +++ b/redisinsight/api/src/modules/database-import/database-import.analytics.ts @@ -11,21 +11,31 @@ export class DatabaseImportAnalytics extends TelemetryBaseService { } sendImportResults(importResult: DatabaseImportResponse): void { - if (importResult.success) { + if (importResult.success?.length) { this.sendEvent( TelemetryEvents.DatabaseImportSucceeded, { - succeed: importResult.success, + succeed: importResult.success.length, }, ); } - if (importResult.errors?.length) { + if (importResult.fail?.length) { this.sendEvent( TelemetryEvents.DatabaseImportFailed, { - failed: importResult.errors.length, - errors: importResult.errors.map((e) => (e?.constructor?.name || 'UncaughtError')), + failed: importResult.fail.length, + errors: importResult.fail.map((res) => (res?.error?.constructor?.name || 'UncaughtError')), + }, + ); + } + + if (importResult.partial?.length) { + this.sendEvent( + TelemetryEvents.DatabaseImportPartiallySucceeded, + { + partially: importResult.partial.length, + errors: importResult.partial.map((res) => (res?.error?.constructor?.name || 'UncaughtError')), }, ); } diff --git a/redisinsight/api/src/modules/database-import/database-import.controller.ts b/redisinsight/api/src/modules/database-import/database-import.controller.ts index 3bfabfb86e..eb0d02780e 100644 --- a/redisinsight/api/src/modules/database-import/database-import.controller.ts +++ b/redisinsight/api/src/modules/database-import/database-import.controller.ts @@ -1,6 +1,7 @@ import { + ClassSerializerInterceptor, Controller, HttpCode, Post, UploadedFile, - UseInterceptors, UsePipes, ValidationPipe, + UseInterceptors, UsePipes, ValidationPipe } from '@nestjs/common'; import { ApiBody, ApiConsumes, ApiResponse, ApiTags, @@ -10,6 +11,7 @@ import { FileInterceptor } from '@nestjs/platform-express'; import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; @UsePipes(new ValidationPipe({ transform: true })) +@UseInterceptors(ClassSerializerInterceptor) @ApiTags('Database') @Controller('/databases') export class DatabaseImportController { diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts index 49de47ea42..1c38ba3435 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -138,7 +138,7 @@ describe('DatabaseImportService', () => { it('should create standalone database', async () => { await service['createDatabase']({ ...mockDatabase, - }); + }, 0); expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), @@ -148,7 +148,7 @@ describe('DatabaseImportService', () => { await service['createDatabase']({ ...mockDatabase, name: undefined, - }); + }, 0); expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), @@ -159,7 +159,7 @@ describe('DatabaseImportService', () => { await service['createDatabase']({ ...mockDatabase, cluster: true, - }); + }, 0); expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name']), diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 8fdc6c9fdd..b551e79b28 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -1,18 +1,24 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { isArray, get, set } from 'lodash'; +import { HttpException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { get, isArray, set } from 'lodash'; import { Database } from 'src/modules/database/models/database'; import { plainToClass } from 'class-transformer'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; -import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; -import { Validator } from 'class-validator'; +import { + DatabaseImportResponse, + DatabaseImportResult, + DatabaseImportStatus, +} from 'src/modules/database-import/dto/database-import.response'; +import { ValidationError, Validator } from 'class-validator'; import { ImportDatabaseDto } from 'src/modules/database-import/dto/import.database.dto'; import { classToClass } from 'src/utils'; import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; import { + NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException, - NoDatabaseImportFileProvidedException, UnableToParseDatabaseImportFileException, + UnableToParseDatabaseImportFileException, } from 'src/modules/database-import/exceptions'; +import { ValidationException } from 'src/common/exceptions'; @Injectable() export class DatabaseImportService { @@ -61,28 +67,33 @@ export class DatabaseImportService { let response = { total: items.length, - success: 0, - errors: [], + success: [], + partial: [], + fail: [], }; // it is very important to insert databases on-by-one to avoid db constraint errors - await items.reduce((prev, item) => prev.finally(() => this.createDatabase(item) - .then(() => { - response.success += 1; - }) - .catch((e) => { - let error = e; - if (isArray(e)) { - [error] = e; + await items.reduce((prev, item, index) => prev.finally(() => this.createDatabase(item, index) + .then((result) => { + switch (result.status) { + case DatabaseImportStatus.Fail: + response.fail.push(result); + break; + case DatabaseImportStatus.Partial: + response.partial.push(result); + break; + case DatabaseImportStatus.Success: + response.success.push(result); + break; + default: + // do not include into repost, since some unexpected behaviour } - this.logger.warn(`Unable to import database: ${error?.constructor?.name || 'UncaughtError'}`, error); - response.errors.push(error); })), Promise.resolve()); - this.analytics.sendImportResults(response); - response = plainToClass(DatabaseImportResponse, response); + this.analytics.sendImportResults(response); + return response; } catch (e) { this.logger.warn(`Unable to import databases: ${e?.constructor?.name || 'UncaughtError'}`, e); @@ -97,51 +108,84 @@ export class DatabaseImportService { * Map data to known model, validate it and create database if possible * Note: will not create connection, simply create database * @param item + * @param index * @private */ - private async createDatabase(item: any): Promise { - const data: any = {}; + private async createDatabase(item: any, index: number): Promise { + try { + const data: any = {}; - this.fieldsMapSchema.forEach(([field, paths]) => { - let value; + this.fieldsMapSchema.forEach(([field, paths]) => { + let value; - paths.every((path) => { - value = get(item, path); - return value === undefined; + paths.every((path) => { + value = get(item, path); + return value === undefined; + }); + + set(data, field, value); }); - set(data, field, value); - }); + // set database name if needed + if (!data.name) { + data.name = `${data.host}:${data.port}`; + } - // set database name if needed - if (!data.name) { - data.name = `${data.host}:${data.port}`; - } + // determine database type + if (data.isCluster) { + data.connectionType = ConnectionType.CLUSTER; + } else { + data.connectionType = ConnectionType.STANDALONE; + } - // determine database type - if (data.isCluster) { - data.connectionType = ConnectionType.CLUSTER; - } else { - data.connectionType = ConnectionType.STANDALONE; - } + const dto = plainToClass( + ImportDatabaseDto, + // additionally replace empty strings ("") with null + Object.keys(data) + .reduce((acc, key) => { + acc[key] = data[key] === '' ? null : data[key]; + return acc; + }, {}), + ); + + await this.validator.validateOrReject(dto, { + whitelist: true, + }); + + const database = classToClass(Database, dto); - const dto = plainToClass( - ImportDatabaseDto, - // additionally replace empty strings ("") with null - Object.keys(data) - .reduce((acc, key) => { - acc[key] = data[key] === '' ? null : data[key]; - return acc; - }, {}), - ); + await this.databaseRepository.create(database); - await this.validator.validateOrReject(dto, { - whitelist: true, - }); + return { + index, + status: DatabaseImportStatus.Success, + host: database.host, + port: database.port, + }; + } catch (e) { + let error = e; + if (isArray(e)) { + [error] = e; + } - const database = classToClass(Database, dto); + if (error instanceof ValidationError) { + error = new ValidationException(Object.values(error?.constraints || {}) || 'Bad request'); + } - return this.databaseRepository.create(database); + if (!(error instanceof HttpException)) { + error = new InternalServerErrorException(error?.message); + } + + this.logger.warn(`Unable to import database: ${error?.constructor?.name || 'UncaughtError'}`, error); + + return { + index, + status: DatabaseImportStatus.Fail, + host: item?.host, + port: item?.port, + error, + }; + } } /** diff --git a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts index dfe8f374da..a830dadced 100644 --- a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts +++ b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts @@ -1,5 +1,60 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Exclude, Expose } from 'class-transformer'; +import { isArray } from 'lodash'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { Expose, Transform, Type } from 'class-transformer'; + +export enum DatabaseImportStatus { + Success = 'success', + Partial = 'partial', + Fail = 'fail', +} + +export class DatabaseImportResult { + @ApiProperty({ + description: 'Entry index from original json', + type: Number, + }) + @Expose() + index: number; + + @ApiProperty({ + description: 'Import status', + enum: DatabaseImportStatus, + }) + @Expose() + status: DatabaseImportStatus; + + @ApiPropertyOptional({ + description: 'Database host', + type: String, + }) + @Expose() + host?: string; + + @ApiPropertyOptional({ + description: 'Database port', + type: Number, + }) + @Expose() + port?: number; + + @ApiPropertyOptional({ + description: 'Error message if any', + type: String, + }) + @Expose() + @Transform((e) => { + if (!e) { + return undefined; + } + + if (e?.response?.message) { + return isArray(e.response.message) ? e.response.message[e.response.message.length - 1] : e.response.message; + } + + return e?.message || 'Unhandled Error'; + }, { toPlainOnly: true }) + error?: Error; +} export class DatabaseImportResponse { @ApiProperty({ @@ -10,12 +65,26 @@ export class DatabaseImportResponse { total: number; @ApiProperty({ - description: 'Number of imported database', - type: Number, + description: 'List of successfully imported database', + type: DatabaseImportResult, + }) + @Expose() + @Type(() => DatabaseImportResult) + success: DatabaseImportResult[]; + + @ApiProperty({ + description: 'List of partially imported database', + type: DatabaseImportResult, }) @Expose() - success: number; + @Type(() => DatabaseImportResult) + partial: DatabaseImportResult[]; - @Exclude() - errors: Error[]; + @ApiProperty({ + description: 'List of databases failed to import', + type: DatabaseImportResult, + }) + @Expose() + @Type(() => DatabaseImportResult) + fail: DatabaseImportResult[]; } diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index d46dd73631..cd4d8063c5 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -66,7 +66,8 @@ describe('POST /databases/import', () => { attach: ['file', Buffer.from(JSON.stringify([database])), 'file.json'], responseBody: { total: 1, - success: 0, + success: [], + partial: [], } } }) @@ -148,7 +149,14 @@ describe('POST /databases/import', () => { ])), 'file.json'], responseBody: { total: 1, - success: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: parseInt(importDatabaseFormat1.port, 10), + }], + partial: [], + fail: [], }, }); @@ -178,7 +186,14 @@ describe('POST /databases/import', () => { ])), 'file.json'], responseBody: { total: 1, - success: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: parseInt(importDatabaseFormat1.port, 10), + }], + partial: [], + fail: [], }, }); @@ -252,7 +267,14 @@ describe('POST /databases/import', () => { ])).toString('base64')), 'file.ano'], responseBody: { total: 1, - success: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: parseInt(importDatabaseFormat1.port, 10), + }], + partial: [], + fail: [], }, }); From 84cea2b730143395990bc698357ad9fb22946518 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 5 Dec 2022 16:29:08 +0200 Subject: [PATCH 045/107] #RI-3902 add transformers for host, port fields + add tests (resolve pr comments) --- .../database-import/dto/database-import.response.ts | 4 +++- .../database-import/POST-databases-import.test.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts index a830dadced..535c8d7869 100644 --- a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts +++ b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts @@ -1,4 +1,4 @@ -import { isArray } from 'lodash'; +import { isArray, isString, isNumber } from 'lodash'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Transform, Type } from 'class-transformer'; @@ -28,6 +28,7 @@ export class DatabaseImportResult { type: String, }) @Expose() + @Transform((v) => (isString(v) ? v : undefined), { toPlainOnly: true }) host?: string; @ApiPropertyOptional({ @@ -35,6 +36,7 @@ export class DatabaseImportResult { type: Number, }) @Expose() + @Transform((v) => (isNumber(v) ? v : undefined), { toPlainOnly: true }) port?: number; @ApiPropertyOptional({ diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index cd4d8063c5..f848c06bc0 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -68,6 +68,18 @@ describe('POST /databases/import', () => { total: 1, success: [], partial: [], + }, + checkFn: ({ body }) => { + expect(body.fail.length).to.eq(1); + expect(body.fail[0].status).to.eq('fail'); + expect(body.fail[0].index).to.eq(0); + expect(body.fail[0].error).to.be.a('string'); + if (body.fail[0].host) { + expect(body.fail[0].host).to.be.a('string'); + } + if (body.fail[0].port) { + expect(body.fail[0].port).to.be.a('number'); + } } } }) From 937e3432b3d875bcbbe2dde05efc8ea93e93329d Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Tue, 6 Dec 2022 18:40:36 +0100 Subject: [PATCH 046/107] #RI-3219 - [BE] Indicate new connections --- .../migration/1670252337342-database-new.ts | 20 +++++++++++++++++++ redisinsight/api/migration/index.ts | 2 ++ redisinsight/api/src/__mocks__/databases.ts | 6 ++++++ .../database-import.service.spec.ts | 5 ++++- .../database-import.service.ts | 3 +++ .../dto/import.database.dto.ts | 2 +- .../database-connection.service.spec.ts | 2 +- .../database/database-connection.service.ts | 2 ++ .../database/entities/database.entity.ts | 4 ++++ .../src/modules/database/models/database.ts | 10 ++++++++++ .../database/providers/database.factory.ts | 2 ++ .../repositories/local.database.repository.ts | 2 +- .../test/api/database/POST-databases.test.ts | 4 +++- 13 files changed, 59 insertions(+), 5 deletions(-) create mode 100644 redisinsight/api/migration/1670252337342-database-new.ts diff --git a/redisinsight/api/migration/1670252337342-database-new.ts b/redisinsight/api/migration/1670252337342-database-new.ts new file mode 100644 index 0000000000..be73bccf35 --- /dev/null +++ b/redisinsight/api/migration/1670252337342-database-new.ts @@ -0,0 +1,20 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class databaseNew1670252337342 implements MigrationInterface { + name = 'databaseNew1670252337342' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "temporary_database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, "new" boolean, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername" FROM "database_instance"`); + await queryRunner.query(`DROP TABLE "database_instance"`); + await queryRunner.query(`ALTER TABLE "temporary_database_instance" RENAME TO "database_instance"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "database_instance" RENAME TO "temporary_database_instance"`); + await queryRunner.query(`CREATE TABLE "database_instance" ("id" varchar PRIMARY KEY NOT NULL, "host" varchar NOT NULL, "port" integer NOT NULL, "name" varchar NOT NULL, "username" varchar, "password" varchar, "tls" boolean, "verifyServerCert" boolean, "lastConnection" datetime, "caCertId" varchar, "clientCertId" varchar, "connectionType" varchar NOT NULL DEFAULT ('STANDALONE'), "nodes" varchar DEFAULT ('[]'), "nameFromProvider" varchar, "sentinelMasterName" varchar, "sentinelMasterUsername" varchar, "sentinelMasterPassword" varchar, "provider" varchar DEFAULT ('UNKNOWN'), "modules" varchar NOT NULL DEFAULT ('[]'), "db" integer, "encryption" varchar, "tlsServername" varchar, CONSTRAINT "FK_3b9b625266c00feb2d66a9f36e4" FOREIGN KEY ("clientCertId") REFERENCES "client_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_d1bc747b5938e22b4b708d8e9a5" FOREIGN KEY ("caCertId") REFERENCES "ca_certificate" ("id") ON DELETE SET NULL ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "database_instance"("id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername") SELECT "id", "host", "port", "name", "username", "password", "tls", "verifyServerCert", "lastConnection", "caCertId", "clientCertId", "connectionType", "nodes", "nameFromProvider", "sentinelMasterName", "sentinelMasterUsername", "sentinelMasterPassword", "provider", "modules", "db", "encryption", "tlsServername" FROM "temporary_database_instance"`); + await queryRunner.query(`DROP TABLE "temporary_database_instance"`); + } + +} diff --git a/redisinsight/api/migration/index.ts b/redisinsight/api/migration/index.ts index 954f1fd4e4..a2a4d1b284 100644 --- a/redisinsight/api/migration/index.ts +++ b/redisinsight/api/migration/index.ts @@ -21,6 +21,7 @@ import { databaseAnalysis1664785208236 } from './1664785208236-database-analysis import { databaseAnalysisExpirationGroups1664886479051 } from './1664886479051-database-analysis-expiration-groups'; import { workbenchExecutionTime1667368983699 } from './1667368983699-workbench-execution-time'; import { database1667477693934 } from './1667477693934-database'; +import { databaseNew1670252337342 } from './1670252337342-database-new'; export default [ initialMigration1614164490968, @@ -46,4 +47,5 @@ export default [ databaseAnalysisExpirationGroups1664886479051, workbenchExecutionTime1667368983699, database1667477693934, + databaseNew1670252337342, ]; diff --git a/redisinsight/api/src/__mocks__/databases.ts b/redisinsight/api/src/__mocks__/databases.ts index 580c710427..52227c6836 100644 --- a/redisinsight/api/src/__mocks__/databases.ts +++ b/redisinsight/api/src/__mocks__/databases.ts @@ -27,6 +27,7 @@ export const mockDatabase = Object.assign(new Database(), { host: '127.0.100.1', port: 6379, connectionType: ConnectionType.STANDALONE, + new: false, }); export const mockDatabaseEntity = Object.assign(new DatabaseEntity(), { @@ -115,6 +116,11 @@ export const mockClusterDatabaseWithTlsAuthEntity = Object.assign(new DatabaseEn nodes: JSON.stringify(mockClusterNodes), }); +export const mockNewDatabase = Object.assign(new Database(), { + ...mockDatabase, + new: true, +}); + export const mockClientMetadata: ClientMetadata = { databaseId: mockDatabase.id, namespace: AppTool.Common, diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts index 49de47ea42..7ff52aeae5 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -118,7 +118,7 @@ describe('DatabaseImportService', () => { } }); - it('should faile due to incorrect base64 + truncate filename', async () => { + it('should fail due to incorrect base64 + truncate filename', async () => { try { await service.import({ ...mockDatabaseImportFile, @@ -142,6 +142,7 @@ describe('DatabaseImportService', () => { expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), + new: true, }); }); it('should create standalone with created name', async () => { @@ -153,6 +154,7 @@ describe('DatabaseImportService', () => { expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name', 'connectionType']), name: `${mockDatabase.host}:${mockDatabase.port}`, + new: true, }); }); it('should create cluster database', async () => { @@ -164,6 +166,7 @@ describe('DatabaseImportService', () => { expect(databaseRepository.create).toHaveBeenCalledWith({ ...pick(mockDatabase, ['host', 'port', 'name']), connectionType: ConnectionType.CLUSTER, + new: true, }); }); }); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 8fdc6c9fdd..ee0bb670f6 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -102,6 +102,9 @@ export class DatabaseImportService { private async createDatabase(item: any): Promise { const data: any = {}; + // set this is a new connection + data.new = true + this.fieldsMapSchema.forEach(([field, paths]) => { let value; diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index de696bcafc..14e7ad5dd3 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -7,7 +7,7 @@ import { export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', - 'connectionType', + 'connectionType', 'new', ] as const) { @Expose() @IsNotEmpty() diff --git a/redisinsight/api/src/modules/database/database-connection.service.spec.ts b/redisinsight/api/src/modules/database/database-connection.service.spec.ts index 029fd625ff..ba8787498c 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.spec.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.spec.ts @@ -82,7 +82,7 @@ describe('DatabaseConnectionService', () => { }); describe('createClient', () => { - it('should create client for standalone datbaase', async () => { + it('should create client for standalone database', async () => { expect(await service.createClient(mockClientMetadata)).toEqual(mockIORedisClient); }); it('should throw Unauthorized error in case of NOAUTH', async () => { diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index 932458daaa..b95d276574 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -37,9 +37,11 @@ export class DatabaseConnectionService { }); // refresh modules list and last connected time + // mark database as not a new // will be refreshed after user navigate to particular database from the databases list // Note: move to a different place in case if we need to update such info more often const toUpdate: Partial = { + new: false, lastConnection: new Date(), modules: await this.databaseInfoProvider.determineDatabaseModules(client), }; diff --git a/redisinsight/api/src/modules/database/entities/database.entity.ts b/redisinsight/api/src/modules/database/entities/database.entity.ts index 179df10fe0..25be2cc1ad 100644 --- a/redisinsight/api/src/modules/database/entities/database.entity.ts +++ b/redisinsight/api/src/modules/database/entities/database.entity.ts @@ -157,4 +157,8 @@ export class DatabaseEntity { @Column({ nullable: true }) encryption: string; + + @Expose() + @Column({ nullable: true }) + new: boolean; } diff --git a/redisinsight/api/src/modules/database/models/database.ts b/redisinsight/api/src/modules/database/models/database.ts index 8531689622..0834ca80ff 100644 --- a/redisinsight/api/src/modules/database/models/database.ts +++ b/redisinsight/api/src/modules/database/models/database.ts @@ -203,4 +203,14 @@ export class Database { @Type(() => ClientCertificate) @ValidateNested() clientCert?: ClientCertificate; + + @ApiPropertyOptional({ + description: 'A new created connection', + type: Boolean, + default: false, + }) + @Expose() + @IsOptional() + @IsBoolean({ always: true }) + new?: boolean; } diff --git a/redisinsight/api/src/modules/database/providers/database.factory.ts b/redisinsight/api/src/modules/database/providers/database.factory.ts index a8623558cf..654abae9a3 100644 --- a/redisinsight/api/src/modules/database/providers/database.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.factory.ts @@ -46,6 +46,8 @@ export class DatabaseFactory { model.modules = await this.databaseInfoProvider.determineDatabaseModules(client); model.lastConnection = new Date(); + model.new = true + await client.disconnect(); return model; diff --git a/redisinsight/api/src/modules/database/repositories/local.database.repository.ts b/redisinsight/api/src/modules/database/repositories/local.database.repository.ts index 120084906a..b0f82d6e59 100644 --- a/redisinsight/api/src/modules/database/repositories/local.database.repository.ts +++ b/redisinsight/api/src/modules/database/repositories/local.database.repository.ts @@ -67,7 +67,7 @@ export class LocalDatabaseRepository extends DatabaseRepository { const entities = await this.repository .createQueryBuilder('d') .select([ - 'd.id', 'd.name', 'd.host', 'd.port', 'd.db', + 'd.id', 'd.name', 'd.host', 'd.port', 'd.db', 'd.new', 'd.connectionType', 'd.modules', 'd.lastConnection', ]) .getMany(); diff --git a/redisinsight/api/test/api/database/POST-databases.test.ts b/redisinsight/api/test/api/database/POST-databases.test.ts index 7d8fee3d41..a52114ff47 100644 --- a/redisinsight/api/test/api/database/POST-databases.test.ts +++ b/redisinsight/api/test/api/database/POST-databases.test.ts @@ -238,7 +238,9 @@ describe('POST /databases', () => { }, }); - expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + const db = await localDb.getInstanceByName(dbName) + expect(db).to.be.an('object'); + expect(db.new).to.eql(true); }); // todo: cover connection error for incorrect username/password }); From 1cce58a737179d603152a040793a9ac785787be3 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Tue, 6 Dec 2022 18:45:41 +0100 Subject: [PATCH 047/107] #RI-3219 - Indicate new connections --- .../DatabasesListWrapper.spec.tsx | 33 +++++++++++++++++++ .../DatabasesListWrapper.tsx | 29 +++++++++++----- .../DatabasesListComponent/styles.module.scss | 31 +++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx index 4cdcd2dc82..273665373e 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx @@ -2,9 +2,11 @@ import React from 'react' import { instance, mock } from 'ts-mockito' import { render, screen, fireEvent } from 'uiSrc/utils/test-utils' import { EuiInMemoryTable } from '@elastic/eui' +import { useSelector } from 'react-redux' import { first } from 'lodash' import { ConnectionType } from 'uiSrc/slices/interfaces' +import store, { RootState } from 'uiSrc/slices/store' import DatabasesListWrapper, { Props } from './DatabasesListWrapper' import DatabasesList, { Props as DatabasesListProps } from './DatabasesList/DatabasesList' @@ -16,6 +18,11 @@ jest.mock('./DatabasesList/DatabasesList', () => ({ default: jest.fn(), })) +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn() +})) + const mockInstances = [ { id: 'e37cc441-a4f2-402c-8bdb-fc2413cbbaff', @@ -26,6 +33,7 @@ const mockInstances = [ password: null, connectionType: ConnectionType.Standalone, nameFromProvider: null, + new: true, lastConnection: new Date('2021-04-22T09:03:56.917Z'), }, { @@ -62,6 +70,21 @@ const mockDatabasesList = (props: DatabasesListProps) => (
) +beforeEach(() => { + const state: RootState = store.getState(); + + (useSelector as jest.Mock).mockImplementation((callback: (arg0: RootState) => RootState) => callback({ + ...state, + analytics: { + ...state.analytics + }, + connections: { + ...state.connections, + instances: mockInstances, + } + })) +}) + describe('DatabasesListWrapper', () => { beforeAll(() => { DatabasesList.mockImplementation(mockDatabasesList) @@ -77,4 +100,14 @@ describe('DatabasesListWrapper', () => { fireEvent.click(screen.getByTestId('onDelete-btn')) expect(component).toBeTruthy() }) + + it('should show indicator for a new connection', () => { + const { queryByTestId } = render() + + const dbIdWithNewIndicator = mockInstances.find(({ new: newState }) => newState)?.id ?? '' + const dbIdWithoutNewIndicator = mockInstances.find(({ new: newState }) => !newState)?.id ?? '' + + expect(queryByTestId(`database-status-${dbIdWithNewIndicator}`)).toBeInTheDocument() + expect(queryByTestId(`database-status-${dbIdWithoutNewIndicator}`)).not.toBeInTheDocument() + }) }) diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx index 4ba8195f60..c054534a68 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx @@ -178,12 +178,21 @@ const DatabasesListWrapper = ({ sortable: ({ name }) => name?.toLowerCase(), width: '30%', render: function InstanceCell(name: string = '', instance: Instance) { - const { id, db } = instance + const { id, db, new: newStatus = false } = instance const cellContent = replaceSpaces(name.substring(0, 200)) return (
+ {newStatus && ( + +
+ + )} +
+ +
) } diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss index 3422b0bbf0..986a5659a8 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/styles.module.scss @@ -139,3 +139,34 @@ $breakpoint-l: 1400px; text-decoration: none !important; } } + +.columnNew { + padding: 0 !important; + + .euiFlexItem { + margin: 0 !important; + } +} + +.newStatus { + background-color: var(--euiColorPrimary) !important; + cursor: pointer; + width: 11px !important; + min-width: 11px !important; + height: 11px !important; + border-radius: 6px; +} + +.newStatusAnchor { + margin-top: 20px; + margin-left: -19px; + position: absolute; +} + +.container { + // Database alias column + tr > th:nth-child(2), + tr > td:nth-child(2) { + padding-left: 5px !important; + } +} From 62726454358a343041616a7089ab6f237a1668e2 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Tue, 6 Dec 2022 19:52:31 +0100 Subject: [PATCH 048/107] #RI-3219 - Integration tests --- .../api/test/api/database-import/POST-databases-import.test.ts | 2 ++ redisinsight/api/test/api/database/GET-databases.test.ts | 1 + redisinsight/api/test/api/database/POST-databases.test.ts | 1 + redisinsight/api/test/api/database/PUT-databases-id.test.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index d46dd73631..0afb183d23 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -158,6 +158,7 @@ describe('POST /databases/import', () => { endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), statusCode: 200, }); + expect(database.new).to.eq(true); }); describe('Oss', () => { requirements('!rte.re'); @@ -260,6 +261,7 @@ describe('POST /databases/import', () => { // check connection const database = await localDb.getInstanceByName(name); + expect(database.new).to.eq(true); expect(database.nodes).to.eq('[]'); expect(database.connectionType).to.eq('CLUSTER'); diff --git a/redisinsight/api/test/api/database/GET-databases.test.ts b/redisinsight/api/test/api/database/GET-databases.test.ts index 23ba91e164..41cfba07ee 100644 --- a/redisinsight/api/test/api/database/GET-databases.test.ts +++ b/redisinsight/api/test/api/database/GET-databases.test.ts @@ -11,6 +11,7 @@ const responseSchema = Joi.array().items(Joi.object().keys({ port: Joi.number().integer().required(), db: Joi.number().integer().allow(null).required(), name: Joi.string().required(), + new: Joi.boolean().allow(null).required(), connectionType: Joi.string().valid('STANDALONE', 'SENTINEL', 'CLUSTER').required(), lastConnection: Joi.string().isoDate().allow(null).required(), modules: Joi.array().items(Joi.object().keys({ diff --git a/redisinsight/api/test/api/database/POST-databases.test.ts b/redisinsight/api/test/api/database/POST-databases.test.ts index a52114ff47..ff2f6be0fe 100644 --- a/redisinsight/api/test/api/database/POST-databases.test.ts +++ b/redisinsight/api/test/api/database/POST-databases.test.ts @@ -22,6 +22,7 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), + new: Joi.boolean().allow(null).required(), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index 8e14b05eb0..c0a98cf03e 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -23,6 +23,7 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), + new: Joi.boolean().allow(null).required(), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), From 5cb450ccd9f24ff0e41b24b947c08590d584058a Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Wed, 7 Dec 2022 00:05:07 +0100 Subject: [PATCH 049/107] #RI-3219 - Integration tests --- redisinsight/api/test/api/database/POST-databases.test.ts | 7 ++++++- .../api/test/api/database/PUT-databases-id.test.ts | 3 ++- redisinsight/api/test/api/database/constants.ts | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/redisinsight/api/test/api/database/POST-databases.test.ts b/redisinsight/api/test/api/database/POST-databases.test.ts index ff2f6be0fe..9bcd1c03eb 100644 --- a/redisinsight/api/test/api/database/POST-databases.test.ts +++ b/redisinsight/api/test/api/database/POST-databases.test.ts @@ -22,7 +22,7 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), - new: Joi.boolean().allow(null).required(), + new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), @@ -120,6 +120,7 @@ describe('POST /databases', () => { username: null, password: null, connectionType: constants.STANDALONE, + new: true, }, }); }); @@ -167,6 +168,7 @@ describe('POST /databases', () => { username: null, password: null, connectionType: constants.STANDALONE, + new: true, }, checkFn: ({ body }) => { addedId = body.id; @@ -236,6 +238,7 @@ describe('POST /databases', () => { username: null, password: constants.TEST_REDIS_PASSWORD, connectionType: constants.STANDALONE, + new: true, }, }); @@ -269,6 +272,7 @@ describe('POST /databases', () => { connectionType: constants.STANDALONE, tls: true, verifyServerCert: false, + new: true, }, }); @@ -302,6 +306,7 @@ describe('POST /databases', () => { tls: true, verifyServerCert: true, tlsServername: null, + new: true, }, checkFn: async ({ body }) => { expect(body.caCert.id).to.be.a('string'); diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index c0a98cf03e..53c1827514 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -23,7 +23,7 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), - new: Joi.boolean().allow(null).required(), + new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), @@ -185,6 +185,7 @@ describe(`PUT /databases/:id`, () => { newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); expect(newDatabase).to.contain({ ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection']), + new: true, host: constants.TEST_REDIS_HOST, port: constants.TEST_REDIS_PORT, }); diff --git a/redisinsight/api/test/api/database/constants.ts b/redisinsight/api/test/api/database/constants.ts index c82bc8277e..6cc2ed73fd 100644 --- a/redisinsight/api/test/api/database/constants.ts +++ b/redisinsight/api/test/api/database/constants.ts @@ -13,6 +13,7 @@ export const databaseSchema = Joi.object().keys({ nameFromProvider: Joi.string().allow(null), lastConnection: Joi.string().isoDate().allow(null), provider: Joi.string().valid('LOCALHOST', 'UNKNOWN', 'RE_CLOUD', 'RE_CLUSTER'), + new: Joi.boolean().allow(null).required(), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), From 501bbe14c6ac2ba2b640d4d25ccc41c25da5148d Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Wed, 7 Dec 2022 00:23:53 +0100 Subject: [PATCH 050/107] #RI-3219 - Integration tests --- redisinsight/api/test/api/database/POST-databases.test.ts | 1 - redisinsight/api/test/api/database/PUT-databases-id.test.ts | 1 - redisinsight/api/test/api/database/constants.ts | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/redisinsight/api/test/api/database/POST-databases.test.ts b/redisinsight/api/test/api/database/POST-databases.test.ts index 9bcd1c03eb..c43600a3dc 100644 --- a/redisinsight/api/test/api/database/POST-databases.test.ts +++ b/redisinsight/api/test/api/database/POST-databases.test.ts @@ -22,7 +22,6 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), - new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index 53c1827514..f25b3e7ab8 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -23,7 +23,6 @@ const dataSchema = Joi.object({ db: Joi.number().integer().allow(null), username: Joi.string().allow(null), password: Joi.string().allow(null), - new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), diff --git a/redisinsight/api/test/api/database/constants.ts b/redisinsight/api/test/api/database/constants.ts index 6cc2ed73fd..ec4831d3f7 100644 --- a/redisinsight/api/test/api/database/constants.ts +++ b/redisinsight/api/test/api/database/constants.ts @@ -13,7 +13,7 @@ export const databaseSchema = Joi.object().keys({ nameFromProvider: Joi.string().allow(null), lastConnection: Joi.string().isoDate().allow(null), provider: Joi.string().valid('LOCALHOST', 'UNKNOWN', 'RE_CLOUD', 'RE_CLUSTER'), - new: Joi.boolean().allow(null).required(), + new: Joi.boolean().allow(null), tls: Joi.boolean().allow(null), tlsServername: Joi.string().allow(null), verifyServerCert: Joi.boolean().allow(null), From 0d3b83f4e955e9d871e39db4efb525f0d5982b7c Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Wed, 7 Dec 2022 14:23:49 +0300 Subject: [PATCH 051/107] fix pr comments --- .../components/DatabasesListComponent/DatabasesListWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx index c054534a68..b19b7fe823 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.tsx @@ -190,7 +190,7 @@ const DatabasesListWrapper = ({ position="top" anchorClassName={styles.newStatusAnchor} > -
+
)} Date: Wed, 7 Dec 2022 14:25:06 +0300 Subject: [PATCH 052/107] fix pr comments --- .../DatabasesListComponent/DatabasesListWrapper.spec.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx index 273665373e..3fd6d7c316 100644 --- a/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/DatabasesListComponent/DatabasesListWrapper.spec.tsx @@ -107,7 +107,7 @@ describe('DatabasesListWrapper', () => { const dbIdWithNewIndicator = mockInstances.find(({ new: newState }) => newState)?.id ?? '' const dbIdWithoutNewIndicator = mockInstances.find(({ new: newState }) => !newState)?.id ?? '' - expect(queryByTestId(`database-status-${dbIdWithNewIndicator}`)).toBeInTheDocument() - expect(queryByTestId(`database-status-${dbIdWithoutNewIndicator}`)).not.toBeInTheDocument() + expect(queryByTestId(`database-status-new-${dbIdWithNewIndicator}`)).toBeInTheDocument() + expect(queryByTestId(`database-status-new-${dbIdWithoutNewIndicator}`)).not.toBeInTheDocument() }) }) From 4d3e4ebb70af059ef70a31beae89a25ff84a2f9d Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Wed, 7 Dec 2022 17:26:48 +0400 Subject: [PATCH 053/107] #RI-3854 - add features highlighting, add highlighting for import databases --- .../ui/src/components/config/Config.spec.tsx | 98 ++++++++++++++- .../ui/src/components/config/Config.tsx | 33 +++++ .../HighlightedFeature.spec.tsx | 116 ++++++++++++++++++ .../HighlightedFeature.tsx | 75 +++++++++++ .../hightlighted-feature/styles.modules.scss | 25 ++++ .../navigation-menu/NavigationMenu.tsx | 73 +++++++---- .../navigation-menu/styles.module.scss | 9 ++ .../ui/src/constants/featuresHighlighting.tsx | 18 +++ redisinsight/ui/src/constants/pages.ts | 5 +- redisinsight/ui/src/constants/storage.ts | 3 +- .../home/components/HomeHeader/HomeHeader.tsx | 46 +++++-- .../src/slices/app/features-highlighting.ts | 52 ++++++++ redisinsight/ui/src/slices/interfaces/app.ts | 7 ++ redisinsight/ui/src/slices/store.ts | 4 +- .../tests/app/features-highlighting.spec.ts | 93 ++++++++++++++ .../themes/dark_theme/_dark_theme.lazy.scss | 1 + .../themes/dark_theme/_theme_color.scss | 1 + .../themes/light_theme/_light_theme.lazy.scss | 1 + .../themes/light_theme/_theme_color.scss | 1 + redisinsight/ui/src/utils/highlighting.ts | 19 +++ redisinsight/ui/src/utils/test-utils.tsx | 25 +++- .../ui/src/utils/tests/highlighting.spec.ts | 12 ++ 22 files changed, 672 insertions(+), 45 deletions(-) create mode 100644 redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx create mode 100644 redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx create mode 100644 redisinsight/ui/src/components/hightlighted-feature/styles.modules.scss create mode 100644 redisinsight/ui/src/constants/featuresHighlighting.tsx create mode 100644 redisinsight/ui/src/slices/app/features-highlighting.ts create mode 100644 redisinsight/ui/src/slices/tests/app/features-highlighting.spec.ts create mode 100644 redisinsight/ui/src/utils/highlighting.ts create mode 100644 redisinsight/ui/src/utils/tests/highlighting.spec.ts diff --git a/redisinsight/ui/src/components/config/Config.spec.tsx b/redisinsight/ui/src/components/config/Config.spec.tsx index 7f88619a89..5c32e8901f 100644 --- a/redisinsight/ui/src/components/config/Config.spec.tsx +++ b/redisinsight/ui/src/components/config/Config.spec.tsx @@ -1,14 +1,17 @@ import React from 'react' import { cloneDeep } from 'lodash' +import { BuildType } from 'uiSrc/constants/env' +import { localStorageService } from 'uiSrc/services' +import { setFeaturesToHighlight } from 'uiSrc/slices/app/features-highlighting' import { getNotifications } from 'uiSrc/slices/app/notifications' -import { render, mockedStore, cleanup } from 'uiSrc/utils/test-utils' +import { render, mockedStore, cleanup, MOCKED_HIGHLIGHTING_FEATURES } from 'uiSrc/utils/test-utils' import { getUserConfigSettings, setSettingsPopupState, userSettingsSelector, } from 'uiSrc/slices/user/user-settings' -import { getServerInfo } from 'uiSrc/slices/app/info' +import { appServerInfoSelector, getServerInfo } from 'uiSrc/slices/app/info' import { processCliClient } from 'uiSrc/slices/cli/cli-settings' import { getRedisCommands } from 'uiSrc/slices/app/redis-commands' import Config from './Config' @@ -32,6 +35,19 @@ jest.mock('uiSrc/slices/user/user-settings', () => ({ }), })) +jest.mock('uiSrc/slices/app/info', () => ({ + ...jest.requireActual('uiSrc/slices/app/info'), + appServerInfoSelector: jest.fn() +})) + +jest.mock('uiSrc/services', () => ({ + ...jest.requireActual('uiSrc/services'), + localStorageService: { + set: jest.fn(), + get: jest.fn(), + }, +})) + describe('Config', () => { it('should render', () => { render() @@ -75,4 +91,82 @@ describe('Config', () => { ] expect(store.getActions()).toEqual([...afterRenderActions]) }) + + it('should call updateHighlightingFeatures for new user with empty features', () => { + const userSettingsSelectorMock = jest.fn().mockReturnValue({ + config: { + agreements: null, + } + }) + const appServerInfoSelectorMock = jest.fn().mockReturnValue({ + buildType: BuildType.Electron, + appVersion: '2.0.0' + }) + userSettingsSelector.mockImplementation(userSettingsSelectorMock) + appServerInfoSelector.mockImplementation(appServerInfoSelectorMock) + + render() + + expect(store.getActions()) + .toEqual(expect.arrayContaining([setFeaturesToHighlight({ version: '2.0.0', features: [] })])) + }) + + it('should call updateHighlightingFeatures for existing user with proper data', () => { + const userSettingsSelectorMock = jest.fn().mockReturnValue({ + config: { + agreements: {}, + } + }) + const appServerInfoSelectorMock = jest.fn().mockReturnValue({ + buildType: BuildType.Electron, + appVersion: '2.0.0' + }) + userSettingsSelector.mockImplementation(userSettingsSelectorMock) + appServerInfoSelector.mockImplementation(appServerInfoSelectorMock) + + render() + + expect(store.getActions()) + .toEqual(expect.arrayContaining([setFeaturesToHighlight({ version: '2.0.0', features: MOCKED_HIGHLIGHTING_FEATURES })])) + }) + + it('should call updateHighlightingFeatures for existing user with proper data with features from LS', () => { + localStorageService.get = jest.fn().mockReturnValue({ version: '2.0.0', features: ['importDatabases'] }) + const userSettingsSelectorMock = jest.fn().mockReturnValue({ + config: { + agreements: {}, + } + }) + const appServerInfoSelectorMock = jest.fn().mockReturnValue({ + buildType: BuildType.Electron, + appVersion: '2.0.0' + }) + userSettingsSelector.mockImplementation(userSettingsSelectorMock) + appServerInfoSelector.mockImplementation(appServerInfoSelectorMock) + + render() + + expect(store.getActions()) + .toEqual(expect.arrayContaining([setFeaturesToHighlight({ version: '2.0.0', features: ['importDatabases'] })])) + }) + + it('should call updateHighlightingFeatures for existing user with proper data with features from LS for different version', () => { + localStorageService.get = jest.fn().mockReturnValue({ version: '2.0.0', features: ['importDatabases'] }) + const userSettingsSelectorMock = jest.fn().mockReturnValue({ + config: { + agreements: {}, + } + }) + const appServerInfoSelectorMock = jest.fn().mockReturnValue({ + buildType: BuildType.Electron, + appVersion: '2.0.12' + }) + userSettingsSelector.mockImplementation(userSettingsSelectorMock) + appServerInfoSelector.mockImplementation(appServerInfoSelectorMock) + + render() + + expect(store.getActions()) + .toEqual(expect.arrayContaining([setFeaturesToHighlight({ version: '2.0.12', features: MOCKED_HIGHLIGHTING_FEATURES })])) + }) }) diff --git a/redisinsight/ui/src/components/config/Config.tsx b/redisinsight/ui/src/components/config/Config.tsx index 0c2847c696..9d682c9761 100644 --- a/redisinsight/ui/src/components/config/Config.tsx +++ b/redisinsight/ui/src/components/config/Config.tsx @@ -1,6 +1,11 @@ import { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useLocation } from 'react-router-dom' +import { BrowserStorageItem } from 'uiSrc/constants' +import { BuildType } from 'uiSrc/constants/env' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' +import { localStorageService } from 'uiSrc/services' +import { setFeaturesToHighlight } from 'uiSrc/slices/app/features-highlighting' import { fetchNotificationsAction } from 'uiSrc/slices/app/notifications' import { @@ -61,8 +66,36 @@ const Config = () => { dispatch(setAnalyticsIdentified(true)) })() } + + featuresHighlight() }, [serverInfo, config]) + const featuresHighlight = () => { + if (serverInfo?.buildType === BuildType.Electron && config) { + // new user, set all features as viewed + if (!config.agreements) { + updateHighlightingFeatures({ version: serverInfo.appVersion, features: [] }) + return + } + + const userFeatures = localStorageService.get(BrowserStorageItem.featuresHighlighting) + + // existing user with the same version of app, get not viewed features from LS + if (userFeatures && userFeatures.version === serverInfo.appVersion) { + dispatch(setFeaturesToHighlight(userFeatures)) + return + } + + // existing user, no any new features viewed (after application update e.g.) + updateHighlightingFeatures({ version: serverInfo.appVersion, features: Object.keys(BUILD_FEATURES) }) + } + } + + const updateHighlightingFeatures = (data: { version: string, features: string[] }) => { + dispatch(setFeaturesToHighlight(data)) + localStorageService.set(BrowserStorageItem.featuresHighlighting, data) + } + const checkSettingsToShowPopup = () => { const specConsents = spec?.agreements const appliedConsents = config?.agreements diff --git a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx new file mode 100644 index 0000000000..564e2b8075 --- /dev/null +++ b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx @@ -0,0 +1,116 @@ +import { EuiToolTip } from '@elastic/eui' +import { fireEvent } from '@testing-library/react' +import React from 'react' +import { act, render, screen, waitForEuiToolTipVisible } from 'uiSrc/utils/test-utils' + +import HighlightedFeature from './HighlightedFeature' + +const Content = () =>
+describe('HighlightedFeature', () => { + it('should render', () => { + expect(render( + + + + )).toBeTruthy() + }) + + it('should render content', () => { + render( + + + + ) + + expect(screen.getByTestId('some-feature')).toBeInTheDocument() + }) + + it('should render dot highlighting', () => { + render( + + + + ) + + expect(screen.getByTestId('some-feature')).toBeInTheDocument() + expect(screen.getByTestId('dot-highlighting')).toBeInTheDocument() + }) + + it('should not render highlighting', () => { + render( + + + + ) + + expect(screen.getByTestId('some-feature')).toBeInTheDocument() + expect(screen.queryByTestId('dot-highlighting')).not.toBeInTheDocument() + }) + + it('should render tooltip highlighting', async () => { + render( + + + + ) + + expect(screen.getByTestId('some-feature')).toBeInTheDocument() + expect(screen.getByTestId('dot-highlighting')).toBeInTheDocument() + + await act(async () => { + fireEvent.mouseOver(screen.getByTestId('tooltip-highlighting-inner')) + }) + + await waitForEuiToolTipVisible() + + expect(screen.queryByTestId('tooltip-highlighting')).toBeInTheDocument() + expect(screen.queryByTestId('tooltip-highlighting')).toHaveTextContent('title') + expect(screen.queryByTestId('tooltip-highlighting')).toHaveTextContent('content') + }) + + it('should call onClick', () => { + const onClick = jest.fn() + render( + + + + ) + + fireEvent.click(screen.getByTestId('feature-highlighted')) + + expect(onClick).toBeCalled() + }) + + it('should not render second tooltip', async () => { + render( + + + + + + ) + + await act(async () => { + fireEvent.mouseOver(screen.getByTestId('some-feature')) + }) + + await waitForEuiToolTipVisible() + + expect(screen.queryByTestId('tooltip-highlighting')).toBeInTheDocument() + expect(screen.queryByTestId('no-render-tooltip')).not.toBeInTheDocument() + }) +}) diff --git a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx new file mode 100644 index 0000000000..6ac1a97898 --- /dev/null +++ b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx @@ -0,0 +1,75 @@ +import { EuiToolTip } from '@elastic/eui' +import { ToolTipPositions } from '@elastic/eui/src/components/tool_tip/tool_tip' +import cx from 'classnames' +import React from 'react' +import { FeaturesHighlightingType } from 'uiSrc/constants/featuresHighlighting' + +import styles from './styles.modules.scss' + +export interface Props { + isHighlight?: boolean + children: React.ReactElement + title?: string | React.ReactElement + content?: string | React.ReactElement + type?: FeaturesHighlightingType + transformOnHover?: boolean + onClick?: () => void + wrapperClassName?: string + dotClassName?: string + tooltipPosition?: ToolTipPositions + hideFirstChild?: boolean +} +const HighlightedFeature = (props: Props) => { + const { + isHighlight, + children, + title, + content, + type = 'plain', + transformOnHover, + onClick, + wrapperClassName, + dotClassName, + tooltipPosition, + hideFirstChild + } = props + + const innerContent = hideFirstChild ? children.props.children : children + + const DotHighlighting = () => ( + <> + {innerContent} + + + ) + + const TooltipHighlighting = () => ( + +
+ +
+
+ ) + + if (!isHighlight) return (<>{children}) + + return ( +
onClick?.()} + role="presentation" + data-testid="feature-highlighted" + > + {type === 'plain' && ()} + {type === 'tooltip' && ()} + {type === 'popover' && ()} +
+ ) +} + +export default HighlightedFeature diff --git a/redisinsight/ui/src/components/hightlighted-feature/styles.modules.scss b/redisinsight/ui/src/components/hightlighted-feature/styles.modules.scss new file mode 100644 index 0000000000..645cc23f8a --- /dev/null +++ b/redisinsight/ui/src/components/hightlighted-feature/styles.modules.scss @@ -0,0 +1,25 @@ +.wrapper { + position: relative; + + &:global(.transform-on-hover) { + &:hover { + .dot { + transform: translateY(-1px); + } + } + } + + .dot { + position: absolute; + background: var(--highlightDotColor); + top: -4px; + right: -4px; + z-index: 1; + + width: 12px; + height: 12px; + border-radius: 50%; + + transition: transform 250ms ease-in-out; + } +} diff --git a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx index 16f3193d17..00674f8915 100644 --- a/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx +++ b/redisinsight/ui/src/components/navigation-menu/NavigationMenu.tsx @@ -17,11 +17,13 @@ import { EuiTitle, EuiToolTip } from '@elastic/eui' +import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' import { ANALYTICS_ROUTES } from 'uiSrc/components/main-router/constants/sub-routes' import { PageNames, Pages } from 'uiSrc/constants' import { EXTERNAL_LINKS } from 'uiSrc/constants/links' import { getRouterLinkProps } from 'uiSrc/services' +import { appFeaturePagesHighlightingSelector } from 'uiSrc/slices/app/features-highlighting' import { connectedInstanceSelector } from 'uiSrc/slices/instances/instances' import { appElectronInfoSelector, @@ -53,14 +55,15 @@ const browserPath = `/${PageNames.browser}` const pubSubPath = `/${PageNames.pubSub}` interface INavigations { - isActivePage: boolean; - tooltipText: string; - ariaLabel: string; - dataTestId: string; - connectedInstanceId?: string; - onClick: () => void; - getClassName: () => string; - getIconType: () => string; + isActivePage: boolean + pageName: string + tooltipText: string + ariaLabel: string + dataTestId: string + connectedInstanceId?: string + onClick: () => void + getClassName: () => string + getIconType: () => string } const NavigationMenu = () => { @@ -74,6 +77,7 @@ const NavigationMenu = () => { const { id: connectedInstanceId = '' } = useSelector(connectedInstanceSelector) const { isReleaseNotesViewed } = useSelector(appElectronInfoSelector) const { server } = useSelector(appInfoSelector) + const highlightedPages = useSelector(appFeaturePagesHighlightingSelector) useEffect(() => { setActivePage(`/${last(location.pathname.split('/'))}`) @@ -93,6 +97,7 @@ const NavigationMenu = () => { const privateRoutes: INavigations[] = [ { tooltipText: 'Browser', + pageName: PageNames.browser, isActivePage: activePage === browserPath, ariaLabel: 'Browser page button', onClick: () => handleGoPage(Pages.browser(connectedInstanceId)), @@ -107,6 +112,7 @@ const NavigationMenu = () => { }, { tooltipText: 'Workbench', + pageName: PageNames.workbench, ariaLabel: 'Workbench page button', onClick: () => handleGoPage(Pages.workbench(connectedInstanceId)), dataTestId: 'workbench-page-btn', @@ -121,6 +127,7 @@ const NavigationMenu = () => { }, { tooltipText: 'Analysis Tools', + pageName: PageNames.analytics, ariaLabel: 'Analysis Tools', onClick: () => handleGoPage(Pages.analytics(connectedInstanceId)), dataTestId: 'analytics-page-btn', @@ -135,6 +142,7 @@ const NavigationMenu = () => { }, { tooltipText: 'Pub/Sub', + pageName: PageNames.pubSub, ariaLabel: 'Pub/Sub page button', onClick: () => handleGoPage(Pages.pubSub(connectedInstanceId)), dataTestId: 'pub-sub-page-btn', @@ -152,6 +160,7 @@ const NavigationMenu = () => { const publicRoutes: INavigations[] = [ { tooltipText: 'Settings', + pageName: PageNames.settings, ariaLabel: 'Settings page button', onClick: () => handleGoPage(Pages.settings), dataTestId: 'settings-page-btn', @@ -281,7 +290,36 @@ const NavigationMenu = () => { {connectedInstanceId && ( privateRoutes.map((nav) => ( - + + + + + + )) + )} +
+
+ + {HelpMenu()} + {publicRoutes.map((nav) => ( + + { data-testid={nav.dataTestId} /> - )) - )} -
-
- - {HelpMenu()} - {publicRoutes.map((nav) => ( - - - + ))} `/?editInstance=${instanceId}`, redisEnterpriseAutodiscovery: '/redis-enterprise-autodiscovery', - settings: '/settings', + settings: `/${PageNames.settings}`, redisCloud, redisCloudSubscriptions: `${redisCloud}/subscriptions`, redisCloudDatabases: `${redisCloud}/databases`, diff --git a/redisinsight/ui/src/constants/storage.ts b/redisinsight/ui/src/constants/storage.ts index 060adb42c9..acae91b5b4 100644 --- a/redisinsight/ui/src/constants/storage.ts +++ b/redisinsight/ui/src/constants/storage.ts @@ -20,7 +20,8 @@ enum BrowserStorageItem { wbCleanUp = 'wbCleanUp', viewFormat = 'viewFormat', wbGroupMode = 'wbGroupMode', - keyDetailSizes = 'keyDetailSizes' + keyDetailSizes = 'keyDetailSizes', + featuresHighlighting = 'featuresHighlighting' } export default BrowserStorageItem diff --git a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx index 959ffc6da9..de6aa94762 100644 --- a/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx +++ b/redisinsight/ui/src/pages/home/components/HomeHeader/HomeHeader.tsx @@ -9,9 +9,15 @@ import { EuiToolTip, } from '@elastic/eui' import { isEmpty } from 'lodash' -import { useSelector } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import cx from 'classnames' import { ImportDatabasesDialog } from 'uiSrc/components' +import HighlightedFeature from 'uiSrc/components/hightlighted-feature/HighlightedFeature' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' +import { + appFeaturesToHighlightSelector, + removeFeatureFromHighlighting +} from 'uiSrc/slices/app/features-highlighting' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import HelpLinksMenu from 'uiSrc/pages/home/components/HelpLinksMenu' import PromoLink from 'uiSrc/components/promo-link/PromoLink' @@ -42,6 +48,10 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => const [guides, setGuides] = useState([]) const [isImportDialogOpen, setIsImportDialogOpen] = useState(false) + const { importDatabases: importDatabasesHighlighting } = useSelector(appFeaturesToHighlightSelector) ?? {} + + const dispatch = useDispatch() + useEffect(() => { if (loading || !data || isEmpty(data)) { return @@ -116,20 +126,30 @@ const HomeHeader = ({ onAddInstance, direction, welcomePage = false }: Props) => ) const ImportDatabasesBtn = () => ( - dispatch(removeFeatureFromHighlighting('importDatabases'))} + transformOnHover + hideFirstChild > - - - - + + + + + ) const Guides = () => ( diff --git a/redisinsight/ui/src/slices/app/features-highlighting.ts b/redisinsight/ui/src/slices/app/features-highlighting.ts new file mode 100644 index 0000000000..9d2d2d37cb --- /dev/null +++ b/redisinsight/ui/src/slices/app/features-highlighting.ts @@ -0,0 +1,52 @@ +import { createSlice } from '@reduxjs/toolkit' +import { remove } from 'lodash' +import { BrowserStorageItem } from 'uiSrc/constants' +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' +import { localStorageService } from 'uiSrc/services' +import { StateAppFeaturesHighlighting } from 'uiSrc/slices/interfaces' +import { RootState } from 'uiSrc/slices/store' +import { getPagesForFeatures } from 'uiSrc/utils/highlighting' + +export const initialState: StateAppFeaturesHighlighting = { + version: '', + features: [], + pages: {} +} + +const appFeaturesHighlightingSlice = createSlice({ + name: 'appFeaturesHighlighting', + initialState, + reducers: { + setFeaturesInitialState: () => initialState, + setFeaturesToHighlight: (state, { payload }: { payload: { version: string, features: string[] } }) => { + state.features = payload.features + state.version = payload.version + state.pages = getPagesForFeatures(payload.features) + }, + removeFeatureFromHighlighting: (state, { payload }: { payload: string }) => { + remove(state.features, (f) => f === payload) + + const pageName = BUILD_FEATURES[payload].page + if (pageName && pageName in state.pages) { + remove(state.pages[pageName], (f) => f === payload) + } + + const { version, features } = state + localStorageService.set(BrowserStorageItem.featuresHighlighting, { version, features }) + } + } +}) + +export const { + setFeaturesInitialState, + setFeaturesToHighlight, + removeFeatureFromHighlighting +} = appFeaturesHighlightingSlice.actions + +export const appFeatureHighlightingSelector = (state: RootState) => state.app.featuresHighlighting +export const appFeaturesToHighlightSelector = (state: RootState): { [key: string]: boolean } => + state.app.featuresHighlighting.features + .reduce((prev, next) => ({ ...prev, [next]: true }), {}) +export const appFeaturePagesHighlightingSelector = (state: RootState) => state.app.featuresHighlighting.pages + +export default appFeaturesHighlightingSlice.reducer diff --git a/redisinsight/ui/src/slices/interfaces/app.ts b/redisinsight/ui/src/slices/interfaces/app.ts index b6ca87b5de..af806062a4 100644 --- a/redisinsight/ui/src/slices/interfaces/app.ts +++ b/redisinsight/ui/src/slices/interfaces/app.ts @@ -141,6 +141,13 @@ export interface StateAppSocketConnection { isConnected: boolean } +export interface StateAppFeaturesHighlighting { + version: string + features: string[] + pages: { + [key: string]: string[] + } +} export enum NotificationType { Global = 'global' } diff --git a/redisinsight/ui/src/slices/store.ts b/redisinsight/ui/src/slices/store.ts index c397cfaa6d..6241bdb18d 100644 --- a/redisinsight/ui/src/slices/store.ts +++ b/redisinsight/ui/src/slices/store.ts @@ -26,6 +26,7 @@ import appContextReducer from './app/context' import appRedisCommandsReducer from './app/redis-commands' import appPluginsReducer from './app/plugins' import appsSocketConnectionReducer from './app/socket-connection' +import appFeaturesHighlightingReducer from './app/features-highlighting' import workbenchResultsReducer from './workbench/wb-results' import workbenchGuidesReducer from './workbench/wb-guides' import workbenchTutorialsReducer from './workbench/wb-tutorials' @@ -46,7 +47,8 @@ export const rootReducer = combineReducers({ context: appContextReducer, redisCommands: appRedisCommandsReducer, plugins: appPluginsReducer, - socketConnection: appsSocketConnectionReducer + socketConnection: appsSocketConnectionReducer, + featuresHighlighting: appFeaturesHighlightingReducer }), connections: combineReducers({ instances: instancesReducer, diff --git a/redisinsight/ui/src/slices/tests/app/features-highlighting.spec.ts b/redisinsight/ui/src/slices/tests/app/features-highlighting.spec.ts new file mode 100644 index 0000000000..5c2b8355a5 --- /dev/null +++ b/redisinsight/ui/src/slices/tests/app/features-highlighting.spec.ts @@ -0,0 +1,93 @@ +import { cloneDeep } from 'lodash' +import reducer, { + initialState, + setFeaturesInitialState, + appFeatureHighlightingSelector, + setFeaturesToHighlight, + removeFeatureFromHighlighting +} from 'uiSrc/slices/app/features-highlighting' +import { + cleanup, + initialStateDefault, + MOCKED_HIGHLIGHTING_FEATURES, + mockedStore +} from 'uiSrc/utils/test-utils' + +let store: typeof mockedStore +beforeEach(() => { + cleanup() + store = cloneDeep(mockedStore) + store.clearActions() +}) + +const mockFeatures = MOCKED_HIGHLIGHTING_FEATURES +describe('slices', () => { + describe('setFeaturesInitialState', () => { + it('should properly set initial state', () => { + const nextState = reducer(initialState, setFeaturesInitialState()) + const rootState = Object.assign(initialStateDefault, { + app: { featuresHighlighting: nextState }, + }) + expect(appFeatureHighlightingSelector(rootState)).toEqual(initialState) + }) + }) + + describe('setFeaturesToHighlight', () => { + it('should properly set features to highlight', () => { + const payload = { + features: mockFeatures, + version: '2.0.0' + } + const state = { + ...initialState, + features: payload.features, + version: payload.version, + pages: { + browser: payload.features + } + } + + // Act + const nextState = reducer(initialState, setFeaturesToHighlight(payload)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { featuresHighlighting: nextState }, + }) + + expect(appFeatureHighlightingSelector(rootState)).toEqual(state) + }) + }) + + describe('removeFeatureFromHighlighting', () => { + it('should properly remove feature to highlight', () => { + const prevState = { + ...initialState, + features: mockFeatures, + version: '2.0.0', + pages: { + browser: mockFeatures + } + } + + const payload = mockFeatures[0] + const state = { + ...prevState, + features: [mockFeatures[1]], + pages: { + browser: [mockFeatures[1]] + } + } + + // Act + const nextState = reducer(prevState, removeFeatureFromHighlighting(payload)) + + // Assert + const rootState = Object.assign(initialStateDefault, { + app: { featuresHighlighting: nextState }, + }) + + expect(appFeatureHighlightingSelector(rootState)).toEqual(state) + }) + }) +}) diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index 2dee6e4865..8671c02ef9 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -130,6 +130,7 @@ --overlayPromoNYColor: #{$overlayPromoNYColor}; --monacoBgColor: #{$monacoBgColor}; + --highlightDotColor: #{$highlightDotColor}; // KeyTypes --typeHashColor: #{$typeHashColor}; diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index bcfb32c4f2..863cecce12 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -90,6 +90,7 @@ $commandGroupBadgeColor: #3f4b5f; $overlayPromoNYColor: #0000001a; $monacoBgColor: #111; +$highlightDotColor: #2BBBB2; // Types colors $typeHashColor: #364cff; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index a9fbfa450e..baf19dbb73 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -132,6 +132,7 @@ --overlayPromoNYColor: #{$overlayPromoNYColor}; --monacoBgColor: #{$monacoBgColor}; + --highlightDotColor: #{$highlightDotColor}; // KeyTypes --typeHashColor: #{$typeHashColor}; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index e925a43fe6..844d962be1 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -87,6 +87,7 @@ $callOutBackgroundColor: #e9edfa; $overlayPromoNYColor: #ffffff1a; $monacoBgColor: #f0f2f7; +$highlightDotColor: #2BBBB2; // Types colors $typeHashColor: #cdddf8; diff --git a/redisinsight/ui/src/utils/highlighting.ts b/redisinsight/ui/src/utils/highlighting.ts new file mode 100644 index 0000000000..95479d32cf --- /dev/null +++ b/redisinsight/ui/src/utils/highlighting.ts @@ -0,0 +1,19 @@ +import { BUILD_FEATURES } from 'uiSrc/constants/featuresHighlighting' + +export const getPagesForFeatures = (features: string[] = []) => { + const result: { [key: string]: string[] } = {} + features.forEach((f) => { + if (f in BUILD_FEATURES) { + const pageName = BUILD_FEATURES[f].page + if (!pageName) return + + if (result[pageName]) { + result[pageName] = [...result[pageName], f] + } else { + result[pageName] = [f] + } + } + }) + + return result +} diff --git a/redisinsight/ui/src/utils/test-utils.tsx b/redisinsight/ui/src/utils/test-utils.tsx index a27a8356ca..4917988491 100644 --- a/redisinsight/ui/src/utils/test-utils.tsx +++ b/redisinsight/ui/src/utils/test-utils.tsx @@ -29,6 +29,7 @@ import { initialState as initialStateAppContext } from 'uiSrc/slices/app/context import { initialState as initialStateAppRedisCommands } from 'uiSrc/slices/app/redis-commands' import { initialState as initialStateAppPluginsReducer } from 'uiSrc/slices/app/plugins' import { initialState as initialStateAppSocketConnectionReducer } from 'uiSrc/slices/app/socket-connection' +import { initialState as initialStateAppFeaturesHighlightingReducer } from 'uiSrc/slices/app/features-highlighting' import { initialState as initialStateCliSettings } from 'uiSrc/slices/cli/cli-settings' import { initialState as initialStateCliOutput } from 'uiSrc/slices/cli/cli-output' import { initialState as initialStateMonitor } from 'uiSrc/slices/cli/monitor' @@ -61,7 +62,8 @@ const initialStateDefault: RootState = { context: cloneDeep(initialStateAppContext), redisCommands: cloneDeep(initialStateAppRedisCommands), plugins: cloneDeep(initialStateAppPluginsReducer), - socketConnection: cloneDeep(initialStateAppSocketConnectionReducer) + socketConnection: cloneDeep(initialStateAppSocketConnectionReducer), + featuresHighlighting: cloneDeep(initialStateAppFeaturesHighlightingReducer) }, connections: { instances: cloneDeep(initialStateInstances), @@ -214,6 +216,27 @@ jest.mock( }) ) +export const MOCKED_HIGHLIGHTING_FEATURES = ['importDatabases', 'anotherFeature'] +jest.mock( + 'uiSrc/constants/featuresHighlighting', + () => ({ + BUILD_FEATURES: { + importDatabases: { + type: 'tooltip', + title: 'Import Database Connections', + content: 'Import your database connections from other Redis UIs', + page: 'browser' + }, + anotherFeature: { + type: 'tooltip', + title: 'Import Database Connections', + content: 'Import your database connections from other Redis UIs', + page: 'browser' + } + } + }) +) + export const localStorageMock = { getItem: jest.fn(), setItem: jest.fn(), diff --git a/redisinsight/ui/src/utils/tests/highlighting.spec.ts b/redisinsight/ui/src/utils/tests/highlighting.spec.ts new file mode 100644 index 0000000000..df999bd68a --- /dev/null +++ b/redisinsight/ui/src/utils/tests/highlighting.spec.ts @@ -0,0 +1,12 @@ +import { getPagesForFeatures } from 'uiSrc/utils/highlighting' +import { MOCKED_HIGHLIGHTING_FEATURES } from 'uiSrc/utils/test-utils' + +describe('getPagesForFeatures', () => { + it('should return proper pages for features', () => { + expect(getPagesForFeatures()).toEqual({}) + expect(getPagesForFeatures([])).toEqual({}) + expect(getPagesForFeatures(['a'])).toEqual({}) + expect(getPagesForFeatures(['importDatabases'])).toEqual({ browser: ['importDatabases'] }) + expect(getPagesForFeatures(MOCKED_HIGHLIGHTING_FEATURES)).toEqual({ browser: MOCKED_HIGHLIGHTING_FEATURES }) + }) +}) From 990cb90c827c537fad0a51aaa9501a68311008ec Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Wed, 7 Dec 2022 17:36:12 +0400 Subject: [PATCH 054/107] #RI-3854 - remove page from build_features config --- redisinsight/ui/src/constants/featuresHighlighting.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/redisinsight/ui/src/constants/featuresHighlighting.tsx b/redisinsight/ui/src/constants/featuresHighlighting.tsx index 0b203843ef..6dd60756de 100644 --- a/redisinsight/ui/src/constants/featuresHighlighting.tsx +++ b/redisinsight/ui/src/constants/featuresHighlighting.tsx @@ -12,7 +12,6 @@ export const BUILD_FEATURES: { [key: string]: BuildHighlightingFeature } = { importDatabases: { type: 'tooltip', title: 'Import Database Connections', - content: 'Import your database connections from other Redis UIs', - page: 'settings' + content: 'Import your database connections from other Redis UIs' } } From c81758a035b196613d7e5b22932a2193d5308280 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Wed, 7 Dec 2022 19:09:25 +0100 Subject: [PATCH 055/107] added createRandomIndexNamewithCLI, openTreeFolders --- tests/e2e/pageObjects/browser-page.ts | 34 +++++ tests/e2e/pageObjects/cli-page.ts | 15 ++ .../tree-view-improvements.e2e copy.ts | 144 ++++++++++++++++++ 3 files changed, 193 insertions(+) create mode 100644 tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 4aba59ac9c..eaecb7d353 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,6 +22,7 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS + streamDeleteButton = Selector(`[data-testid="stream-delete-btn"]`); myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); @@ -1013,6 +1014,39 @@ export class BrowserPage { .click(this.selectIndexDdn) .click(option); } + + /** + * Get text from first tree element + */ + + async getTextFromFirstTreeElement(): Promise { + return (await Selector(`[role="treeitem"]`).nth(0).find(`div`).textContent).replace(/\s/g, ''); + } + + /** + * Get text from first tree element + * @param names folder names with sequence of subfolder + * Example: if names ['mobile', '2'] + * It will go to mobile -> 2 -> keys + */ + + async openTreeFolders(names: string[]): Promise { + let base = `node-item_${names[0]}:` + await t.click(Selector(`[data-testid="${base}"]`)); + if (names.length > 1) { + for (let i = 1; i < names.length; i++) { + base = base + `${names[i]}:`; + await t.click(Selector(`[data-testid="${base}"]`)); + } + } + await t.click(Selector(`[data-testid="${base}keys:keys:"]`)); + + await t.expect( + Selector(`[data-testid="${base}keys:keys:"]`).visible) + .ok("Folder is not selected"); + } + + } /** diff --git a/tests/e2e/pageObjects/cli-page.ts b/tests/e2e/pageObjects/cli-page.ts index c9080e87ca..67b2d6d5cb 100644 --- a/tests/e2e/pageObjects/cli-page.ts +++ b/tests/e2e/pageObjects/cli-page.ts @@ -171,4 +171,19 @@ export class CliPage { await t.click(this.readMoreButton); await t.expect(getPageUrl()).eql(url, 'The opened page not correct'); } + + /** + * Create random index name with CLI and return + */ + + async createRandomIndexNamewithCLI(): Promise { + let word = common.generateWord(10); + let index = 'idx:' + word; + const commands = [ + `FT.CREATE ${index} ON HASH SCHEMA "name" TEXT`, + ]; + await this.sendCommandsInCli(commands); + + return index; + } } diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts new file mode 100644 index 0000000000..afd84765dc --- /dev/null +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts @@ -0,0 +1,144 @@ +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { + BrowserPage, CliPage +} from '../../../pageObjects'; +import { + commonUrl, + ossStandaloneBigConfig, + ossStandaloneConfig +} from '../../../helpers/conf'; +import { KeyTypesTexts, rte } from '../../../helpers/constants'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; +import { Selector, t } from 'testcafe'; +import { verifyKeysDisplayedInTheList } from '../../../helpers/keys'; + +const browserPage = new BrowserPage(); +const common = new Common(); + +const cliPage = new CliPage(); + +let keyNames: string[]; + +let keyName1: string; +let keyName2: string; +let keynameSingle: string; + +let index: string; + +fixture`Tree view navigations improvement tests` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl) +test + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }) + .after(async () => { + await t.click(browserPage.patternModeBtn); + await browserPage.deleteKeysByNames(keyNames); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + }) + ('Verify that if any keys are found and will not be redirected from the previously selected directory', async t => { + + keyName1 = common.generateWord(10); // used to create index name + keyName2 = common.generateWord(10); // used to create index name + keynameSingle = common.generateWord(10); + keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; + + const commands = [ + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value`, + `HSET ${keyNames[3]} field value`, + `HSET ${keyNames[4]} field value`, + ]; + + // Create 5 keys + await cliPage.sendCommandsInCli(commands); + + // Switch to tree view + await browserPage.deleteKeyByName(keyNames[4]); + await t.click(browserPage.clearFilterButton); + await t.click(browserPage.treeViewButton); + + // get first folder name + const firstTreeItemText = await browserPage.getTextFromFirstTreeElement(); + const treeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened + + // verify that the first folder with namespaces is expanded and selected + await t.expect( + treeItemKeys.visible). + ok("First folder is not expanded"); + await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible + + const commands1 = [ + `HSET ${keyNames[4]} field value`, + ]; + + // Create 4 keys and index + await cliPage.sendCommandsInCli(commands1); + await t.click(browserPage.refreshKeysButton); // refresh keys + + await t.expect( + treeItemKeys.visible). + ok("Folder is not selected"); + await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible + + await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); + + await t.expect(treeItemKeys.visible).ok("Folder is not selected after searching with HASH"); + await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible + + await browserPage.searchByKeyName('*'); + + await t.expect(treeItemKeys.visible).ok("Folder is not selected"); + await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible + + await t.click(browserPage.clearFilterButton); + + await t.expect(treeItemKeys.visible).ok("Folder is not selected"); + await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible + + await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); + + await t.expect( + Selector(`[role="rowgroup"]`).find(`div`).withText("No results found.").visible). + ok("No results text is visible"); // verify no results found + + await t.click(browserPage.streamDeleteButton); // clear stream from filter + + await t.expect( + Selector(`[role="rowgroup"]`).find(`div`).withText("No results found.").visible). + notOk("No result text is still visible"); + await t.expect( + treeItemKeys.visible). + notOk("First folder is expanded"); + + }); + +test + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + }) + .after(async () => { + await cliPage.sendCommandInCli(`FT.DROPINDEX ${index}`); + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + }) + ('Verify tree view navigation for index based search', async t => { + // generate index based on keyName + let folders = ["mobile", "2"]; + + index = await cliPage.createRandomIndexNamewithCLI(); + + await t.click(browserPage.redisearchModeBtn); // click redisearch button + await browserPage.selectIndexByName(index); + + await t.click(browserPage.treeViewButton); + await browserPage.openTreeFolders(folders); + await t.click(browserPage.refreshKeysButton); + + await t.expect( + Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) + .ok("Folder is not selected"); + + }); From c4838074bbe968e17a73fccfdbc3c68683560aca Mon Sep 17 00:00:00 2001 From: nmammadli Date: Thu, 8 Dec 2022 09:17:28 +0100 Subject: [PATCH 056/107] Impelement feedback, cleaning code --- tests/e2e/pageObjects/browser-page.ts | 2 +- tests/e2e/pageObjects/cli-page.ts | 4 ++-- .../tree-view/tree-view-improvements.e2e copy.ts | 4 ---- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index eaecb7d353..47c6bdf650 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -1016,7 +1016,7 @@ export class BrowserPage { } /** - * Get text from first tree element + *Get text from first tree element */ async getTextFromFirstTreeElement(): Promise { diff --git a/tests/e2e/pageObjects/cli-page.ts b/tests/e2e/pageObjects/cli-page.ts index 67b2d6d5cb..4ee881ff64 100644 --- a/tests/e2e/pageObjects/cli-page.ts +++ b/tests/e2e/pageObjects/cli-page.ts @@ -177,8 +177,8 @@ export class CliPage { */ async createRandomIndexNamewithCLI(): Promise { - let word = common.generateWord(10); - let index = 'idx:' + word; + const word = common.generateWord(10); + const index = `idx:${word}`; const commands = [ `FT.CREATE ${index} ON HASH SCHEMA "name" TEXT`, ]; diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts index afd84765dc..60efe85fb5 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts @@ -15,15 +15,11 @@ import { verifyKeysDisplayedInTheList } from '../../../helpers/keys'; const browserPage = new BrowserPage(); const common = new Common(); - const cliPage = new CliPage(); - let keyNames: string[]; - let keyName1: string; let keyName2: string; let keynameSingle: string; - let index: string; fixture`Tree view navigations improvement tests` From cb816e1cdeaa356b9e2635d8a3c8d035430bce68 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Thu, 8 Dec 2022 14:19:59 +0400 Subject: [PATCH 057/107] #RI-3854 - fix pr comments --- redisinsight/ui/src/components/config/Config.tsx | 2 +- .../hightlighted-feature/HighlightedFeature.spec.tsx | 4 ++-- .../components/hightlighted-feature/HighlightedFeature.tsx | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/redisinsight/ui/src/components/config/Config.tsx b/redisinsight/ui/src/components/config/Config.tsx index 9d682c9761..62152a4dfd 100644 --- a/redisinsight/ui/src/components/config/Config.tsx +++ b/redisinsight/ui/src/components/config/Config.tsx @@ -81,7 +81,7 @@ const Config = () => { const userFeatures = localStorageService.get(BrowserStorageItem.featuresHighlighting) // existing user with the same version of app, get not viewed features from LS - if (userFeatures && userFeatures.version === serverInfo.appVersion) { + if (userFeatures?.version === serverInfo.appVersion) { dispatch(setFeaturesToHighlight(userFeatures)) return } diff --git a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx index 564e2b8075..135531463d 100644 --- a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx +++ b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.spec.tsx @@ -76,12 +76,12 @@ describe('HighlightedFeature', () => { it('should call onClick', () => { const onClick = jest.fn() render( - + ) - fireEvent.click(screen.getByTestId('feature-highlighted')) + fireEvent.click(screen.getByTestId('feature-highlighted-feature')) expect(onClick).toBeCalled() }) diff --git a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx index 6ac1a97898..f26c1f5532 100644 --- a/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx +++ b/redisinsight/ui/src/components/hightlighted-feature/HighlightedFeature.tsx @@ -18,6 +18,7 @@ export interface Props { dotClassName?: string tooltipPosition?: ToolTipPositions hideFirstChild?: boolean + dataTestPostfix?: string } const HighlightedFeature = (props: Props) => { const { @@ -31,7 +32,8 @@ const HighlightedFeature = (props: Props) => { wrapperClassName, dotClassName, tooltipPosition, - hideFirstChild + hideFirstChild, + dataTestPostfix = '' } = props const innerContent = hideFirstChild ? children.props.children : children @@ -63,7 +65,7 @@ const HighlightedFeature = (props: Props) => { className={cx(styles.wrapper, wrapperClassName, { 'transform-on-hover': transformOnHover })} onClick={() => onClick?.()} role="presentation" - data-testid="feature-highlighted" + data-testid={`feature-highlighted-${dataTestPostfix}`} > {type === 'plain' && ()} {type === 'tooltip' && ()} From b83fd5f22e98c5ae0cb4b469a8c25741e31d3890 Mon Sep 17 00:00:00 2001 From: Artem Date: Thu, 8 Dec 2022 14:07:17 +0200 Subject: [PATCH 058/107] #RI-3902 rework errors to return --- .../api/src/__mocks__/database-import.ts | 10 +++++-- .../database-import.analytics.ts | 19 ++++++++++-- .../database-import.service.spec.ts | 6 +--- .../database-import.service.ts | 29 ++++++++++++------- .../dto/database-import.response.ts | 18 ++++++++---- .../POST-databases-import.test.ts | 5 +++- 6 files changed, 58 insertions(+), 29 deletions(-) diff --git a/redisinsight/api/src/__mocks__/database-import.ts b/redisinsight/api/src/__mocks__/database-import.ts index 210c90e1e4..f94da3ceb5 100644 --- a/redisinsight/api/src/__mocks__/database-import.ts +++ b/redisinsight/api/src/__mocks__/database-import.ts @@ -24,7 +24,7 @@ export const mockDatabaseImportResultFail = { status: DatabaseImportStatus.Fail, host: mockDatabase.host, port: mockDatabase.port, - error: new BadRequestException(), + errors: [new BadRequestException()], }; export const mockDatabaseImportResponse = Object.assign(new DatabaseImportResponse(), { @@ -34,10 +34,14 @@ export const mockDatabaseImportResponse = Object.assign(new DatabaseImportRespon index: index + 3, })), partial: [], - fail: [new ValidationException([]), new BadRequestException(), new ForbiddenException()].map((error, index) => ({ + fail: [ + new ValidationException('Bad request'), + new BadRequestException(), + new ForbiddenException(), + ].map((error, index) => ({ ...mockDatabaseImportResultFail, index, - error, + errors: [error], })), }); diff --git a/redisinsight/api/src/modules/database-import/database-import.analytics.ts b/redisinsight/api/src/modules/database-import/database-import.analytics.ts index 6d4894d448..b343b92df6 100644 --- a/redisinsight/api/src/modules/database-import/database-import.analytics.ts +++ b/redisinsight/api/src/modules/database-import/database-import.analytics.ts @@ -1,8 +1,9 @@ +import { uniq } from 'lodash'; import { Injectable } from '@nestjs/common'; import { TelemetryBaseService } from 'src/modules/analytics/telemetry.base.service'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { TelemetryEvents } from 'src/constants'; -import { DatabaseImportResponse } from 'src/modules/database-import/dto/database-import.response'; +import { DatabaseImportResponse, DatabaseImportResult } from 'src/modules/database-import/dto/database-import.response'; @Injectable() export class DatabaseImportAnalytics extends TelemetryBaseService { @@ -25,7 +26,7 @@ export class DatabaseImportAnalytics extends TelemetryBaseService { TelemetryEvents.DatabaseImportFailed, { failed: importResult.fail.length, - errors: importResult.fail.map((res) => (res?.error?.constructor?.name || 'UncaughtError')), + errors: DatabaseImportAnalytics.getUniqueErrorNamesFromResults(importResult.fail), }, ); } @@ -35,7 +36,7 @@ export class DatabaseImportAnalytics extends TelemetryBaseService { TelemetryEvents.DatabaseImportPartiallySucceeded, { partially: importResult.partial.length, - errors: importResult.partial.map((res) => (res?.error?.constructor?.name || 'UncaughtError')), + errors: DatabaseImportAnalytics.getUniqueErrorNamesFromResults(importResult.partial), }, ); } @@ -49,4 +50,16 @@ export class DatabaseImportAnalytics extends TelemetryBaseService { }, ); } + + static getUniqueErrorNamesFromResults(results: DatabaseImportResult[]) { + return uniq( + [].concat( + ...results.map( + (res) => (res?.errors || []).map( + (error) => error?.constructor?.name || 'UncaughtError', + ), + ), + ), + ); + } } diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts index 1c38ba3435..56773f0890 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -59,10 +59,7 @@ describe('DatabaseImportService', () => { it('should import databases from json', async () => { const response = await service.import(mockDatabaseImportFile); - expect(response).toEqual({ - ...mockDatabaseImportResponse, - errors: undefined, // errors omitted from response - }); + expect(response).toEqual(mockDatabaseImportResponse); expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse); }); @@ -75,7 +72,6 @@ describe('DatabaseImportService', () => { expect(response).toEqual({ ...mockDatabaseImportResponse, - errors: undefined, // errors omitted from response }); expect(analytics.sendImportResults).toHaveBeenCalledWith(mockDatabaseImportResponse); }); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index b551e79b28..234bd6e6ba 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -1,4 +1,6 @@ -import { HttpException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common'; +import { + HttpException, Injectable, InternalServerErrorException, Logger, +} from '@nestjs/common'; import { get, isArray, set } from 'lodash'; import { Database } from 'src/modules/database/models/database'; import { plainToClass } from 'class-transformer'; @@ -163,27 +165,32 @@ export class DatabaseImportService { port: database.port, }; } catch (e) { - let error = e; + let errors = [e]; if (isArray(e)) { - [error] = e; + errors = e; } - if (error instanceof ValidationError) { - error = new ValidationException(Object.values(error?.constraints || {}) || 'Bad request'); - } + errors = errors.map((error) => { + if (error instanceof ValidationError) { + const messages = Object.values(error?.constraints || {}); + return new ValidationException(messages[messages.length - 1] || 'Bad request'); + } - if (!(error instanceof HttpException)) { - error = new InternalServerErrorException(error?.message); - } + if (!(error instanceof HttpException)) { + return new InternalServerErrorException(error?.message); + } + + return error; + }); - this.logger.warn(`Unable to import database: ${error?.constructor?.name || 'UncaughtError'}`, error); + this.logger.warn(`Unable to import database: ${errors[0]?.constructor?.name || 'UncaughtError'}`, errors[0]); return { index, status: DatabaseImportStatus.Fail, host: item?.host, port: item?.port, - error, + errors, }; } } diff --git a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts index 535c8d7869..79f29f659b 100644 --- a/redisinsight/api/src/modules/database-import/dto/database-import.response.ts +++ b/redisinsight/api/src/modules/database-import/dto/database-import.response.ts @@ -1,4 +1,4 @@ -import { isArray, isString, isNumber } from 'lodash'; +import { isString, isNumber } from 'lodash'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Expose, Transform, Type } from 'class-transformer'; @@ -49,13 +49,19 @@ export class DatabaseImportResult { return undefined; } - if (e?.response?.message) { - return isArray(e.response.message) ? e.response.message[e.response.message.length - 1] : e.response.message; - } + return e.map((error) => { + if (error?.response) { + return error.response; + } - return e?.message || 'Unhandled Error'; + return { + statusCode: 500, + message: error?.message || 'Unhandled Error', + error: 'Unhandled Error', + }; + }); }, { toPlainOnly: true }) - error?: Error; + errors?: Error[]; } export class DatabaseImportResponse { diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index f848c06bc0..015ad07ab6 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -73,7 +73,10 @@ describe('POST /databases/import', () => { expect(body.fail.length).to.eq(1); expect(body.fail[0].status).to.eq('fail'); expect(body.fail[0].index).to.eq(0); - expect(body.fail[0].error).to.be.a('string'); + expect(body.fail[0].errors.length).to.eq(1); + expect(body.fail[0].errors[0].message).to.be.a('string'); + expect(body.fail[0].errors[0].statusCode).to.eq(400); + expect(body.fail[0].errors[0].error).to.eq('Bad Request'); if (body.fail[0].host) { expect(body.fail[0].host).to.be.a('string'); } From 0ecc9465a86357050b76a97a8bd516b8e46771e4 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Thu, 8 Dec 2022 16:45:25 +0100 Subject: [PATCH 059/107] add list view instead of browser in the advanced section on settings page --- .../ui/src/components/settings-item/SettingItem.spec.tsx | 2 +- .../settings/components/advanced-settings/AdvancedSettings.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx b/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx index c99288ad59..9df56baded 100644 --- a/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx +++ b/redisinsight/ui/src/components/settings-item/SettingItem.spec.tsx @@ -21,7 +21,7 @@ const mockedProps = { initValue: '10000', onApply: jest.fn(), validation: jest.fn((x) => x), - title: 'Keys to Scan in Browser', + title: 'Keys to Scan in List view', summary: 'Sets the amount of keys to scan per one iteration. Filtering by pattern per a large number of keys may decrease performance.', testid: 'keys-to-scan', placeholder: '10 000', diff --git a/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx index 93852940c9..685e578450 100644 --- a/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx +++ b/redisinsight/ui/src/pages/settings/components/advanced-settings/AdvancedSettings.tsx @@ -29,7 +29,7 @@ const AdvancedSettings = () => { initValue={scanThreshold.toString()} onApply={handleApplyKeysToScanChanges} validation={validateCountNumber} - title="Keys to Scan in Browser" + title="Keys to Scan in List view" summary="Sets the amount of keys to scan per one iteration. Filtering by pattern per a large number of keys may decrease performance." testid="keys-to-scan" placeholder="10 000" From d7e7024dc172a8ab75548ea76268645662e3e8eb Mon Sep 17 00:00:00 2001 From: nmammadli Date: Thu, 8 Dec 2022 16:51:52 +0100 Subject: [PATCH 060/107] Initial Commit --- tests/e2e/pageObjects/browser-page.ts | 24 +++++++++----- .../database/clone-databases.e2e.ts | 20 +++++++----- .../smoke/database/add-standalone-db.e2e.ts | 31 +++++++++++++------ 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 5259a3e495..37a218a6f4 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,6 +22,8 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS + streamDeleteButton = Selector(`[data-testid="stream-delete-btn"]`); + myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); editKeyTTLButton = Selector('[data-testid=edit-ttl-btn]'); @@ -98,8 +100,8 @@ export class BrowserPage { workbenchLinkButton = Selector('[data-test-subj=workbench-page-btn]'); cancelStreamGroupBtn = Selector('[data-testid=cancel-stream-groups-btn]'); submitTooltipBtn = Selector('[data-testid=submit-tooltip-btn]'); - patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]'); - redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]'); + patternModeBtn = Selector('[data-testid=search-mode-pattern-btn]'); + redisearchModeBtn = Selector('[data-testid=search-mode-redisearch-btn]'); //CONTAINERS streamGroupsContainer = Selector('[data-testid=stream-groups-container]'); streamConsumersContainer = Selector('[data-testid=stream-consumers-container]'); @@ -136,7 +138,7 @@ export class BrowserPage { createIndexBtn = Selector('[data-testid=create-index-btn]'); cancelIndexCreationBtn = Selector('[data-testid=create-index-cancel-btn]'); confirmIndexCreationBtn = Selector('[data-testid=create-index-btn]'); - resizeTrigger = Selector('[data-testid^=resize-trigger-]'); + resizeTrigger = Selector('[data-testid^=resize-trigger-]'); //TABS streamTabGroups = Selector('[data-testid=stream-tab-Groups]'); streamTabConsumers = Selector('[data-testid=stream-tab-Consumers]'); @@ -445,7 +447,7 @@ export class BrowserPage { async addEntryToStream(field: string, value: string, entryId?: string): Promise { await t .click(this.addNewStreamEntry) - // Specify field, value and add new entry + // Specify field, value and add new entry .typeText(this.streamField, field, { replace: true, paste: true }) .typeText(this.streamValue, value, { replace: true, paste: true }); if (entryId !== undefined) { @@ -453,7 +455,7 @@ export class BrowserPage { } await t .click(this.saveElementButton) - // Validate that new entry is added + // Validate that new entry is added .expect(this.streamEntriesContainer.textContent).contains(field, 'Field parameter not correct') .expect(this.streamEntriesContainer.textContent).contains(value, 'Value parameter not correct'); } @@ -566,8 +568,8 @@ export class BrowserPage { * Delete keys by their Names * @param keyNames The names of the key array */ - async deleteKeysByNames(keyNames: string[]): Promise { - for(const name of keyNames) { + async deleteKeysByNames(keyNames: string[]): Promise { + for (const name of keyNames) { await this.deleteKeyByName(name); } } @@ -1012,6 +1014,14 @@ export class BrowserPage { .click(this.selectIndexDdn) .click(option); } + + /** + * Verify database status is visible + */ + async verifyDatabaseStatusIsVisible(): Promise { + await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible). + ok("Database status is not visible"); + } } /** diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts index 290c4712ca..20e140dec5 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -1,5 +1,5 @@ import { rte } from '../../../helpers/constants'; -import { AddRedisDatabasePage, MyRedisDatabasePage } from '../../../pageObjects'; +import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossClusterConfig, ossSentinelConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; import { @@ -16,17 +16,18 @@ const addRedisDatabasePage = new AddRedisDatabasePage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const common = new Common(); const newOssDatabaseAlias = 'cloned oss cluster'; +const browserPage = new BrowserPage(); -fixture `Clone databases` +fixture`Clone databases` .meta({ type: 'critical_path' }) .page(commonUrl); test - .before(async() => { + .before(async () => { await acceptLicenseTerms(); await addNewStandaloneDatabaseApi(ossStandaloneConfig); await common.reloadPage(); }) - .after(async() => { + .after(async () => { // Delete databases const dbNumber = await myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count; for (let i = 0; i < dbNumber; i++) { @@ -37,6 +38,7 @@ test await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button await t.click(addRedisDatabasePage.cloneDatabaseButton); + await browserPage.verifyDatabaseStatusIsVisible(); await t.click(addRedisDatabasePage.cancelButton); await t.expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 }); await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); @@ -53,12 +55,12 @@ test await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossStandaloneConfig.databaseName).count).eql(2, 'DB was not cloned'); }); test - .before(async() => { + .before(async () => { await acceptLicenseTerms(); await addNewOSSClusterDatabaseApi(ossClusterConfig); await common.reloadPage(); }) - .after(async() => { + .after(async () => { // Delete database await deleteOSSClusterDatabaseApi(ossClusterConfig); await myRedisDatabasePage.deleteDatabaseByName(newOssDatabaseAlias); @@ -66,6 +68,7 @@ test .meta({ rte: rte.ossCluster })('Verify that user can clone OSS Cluster', async t => { await clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName); await t.click(addRedisDatabasePage.cloneDatabaseButton); + await browserPage.verifyDatabaseStatusIsVisible(); await t .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).ok('Clone panel is not displayed') .expect(addRedisDatabasePage.portInput.getAttribute('value')).eql(ossClusterConfig.ossClusterPort, 'Wrong port value') @@ -77,13 +80,13 @@ test await t.expect(myRedisDatabasePage.dbNameList.withExactText(ossClusterConfig.ossClusterDatabaseName).exists).ok('Original DB is not displayed'); }); test - .before(async() => { + .before(async () => { await acceptLicenseTerms(); // Add Sentinel databases await discoverSentinelDatabaseApi(ossSentinelConfig); await common.reloadPage(); }) - .after(async() => { + .after(async () => { // Delete all primary groups const sentinelCopy = ossSentinelConfig; sentinelCopy.masters.push(ossSentinelConfig.masters[1]); @@ -94,6 +97,7 @@ test .meta({ rte: rte.sentinel })('Verify that user can clone Sentinel', async t => { await clickOnEditDatabaseByName(ossSentinelConfig.name[1]); await t.click(addRedisDatabasePage.cloneDatabaseButton); + await browserPage.verifyDatabaseStatusIsVisible(); // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field await t .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).ok('Clone panel is not displayed') diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index 0c31265b2b..f14bdf37eb 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -6,37 +6,48 @@ import { redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; +import { Selector, t } from 'testcafe'; +import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; -fixture `Add database` +const browserPage = new BrowserPage() + +fixture`Add database` .meta({ type: 'smoke' }) .page(commonUrl) - .beforeEach(async() => { + .beforeEach(async () => { await acceptLicenseTerms(); }); test .meta({ rte: rte.standalone }) - .after(async() => { + .after(async () => { await deleteDatabase(ossStandaloneConfig.databaseName); - })('Verify that user can add Standalone Database', async() => { + })('Verify that user can add Standalone Database', async () => { + const myRedisDatabasePage = new MyRedisDatabasePage(); await addNewStandaloneDatabase(ossStandaloneConfig); + await browserPage.verifyDatabaseStatusIsVisible(); + await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); + await t.click(browserPage.myRedisDbIcon); + await browserPage.verifyDatabaseStatusIsVisible(); }); test .meta({ rte: rte.reCluster }) - .after(async() => { + .after(async () => { await deleteDatabase(redisEnterpriseClusterConfig.databaseName); - })('Verify that user can add database from RE Cluster via auto-discover flow', async() => { + })('Verify that user can add database from RE Cluster via auto-discover flow', async () => { await addNewREClusterDatabase(redisEnterpriseClusterConfig); + await browserPage.verifyDatabaseStatusIsVisible(); }); test - .meta({ env: env.web, rte: rte.ossCluster}) - .after(async() => { + .meta({ env: env.web, rte: rte.ossCluster }) + .after(async () => { await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); - })('Verify that user can add OSS Cluster DB', async() => { + })('Verify that user can add OSS Cluster DB', async () => { await addOSSClusterDatabase(ossClusterConfig); + await browserPage.verifyDatabaseStatusIsVisible(); }); //skiped until the RE Cloud connection is implemented test.skip - .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async() => { + .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async () => { //TODO: add api keys from env await addNewRECloudDatabase('', ''); }); From f96392bfce5c964a5fbdb40ad1ff0c906ab40693 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 12 Dec 2022 13:09:01 +0200 Subject: [PATCH 061/107] #RI-3728 - Base BE implementation. Import certs by plain values #RI-3684 - Base BE implementation. Import certs from files --- .../common/utils/certificate-import.util.ts | 7 + redisinsight/api/src/common/utils/index.ts | 1 + .../api/src/constants/error-messages.ts | 4 + .../certificate-import.service.ts | 216 ++++++++++++++++++ .../database-import/database-import.module.ts | 2 + .../database-import.service.ts | 44 +++- .../dto/import.database.dto.ts | 34 ++- .../database-import/exceptions/index.ts | 4 + .../invalid-ca-certificate-body.exception.ts | 14 ++ .../invalid-certificate-name.exception.ts | 14 ++ ...valid-client-certificate-body.exception.ts | 14 ++ .../invalid-client-private-key.exception.ts | 14 ++ 12 files changed, 364 insertions(+), 4 deletions(-) create mode 100644 redisinsight/api/src/common/utils/certificate-import.util.ts create mode 100644 redisinsight/api/src/common/utils/index.ts create mode 100644 redisinsight/api/src/modules/database-import/certificate-import.service.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/invalid-ca-certificate-body.exception.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/invalid-certificate-name.exception.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/invalid-client-certificate-body.exception.ts create mode 100644 redisinsight/api/src/modules/database-import/exceptions/invalid-client-private-key.exception.ts diff --git a/redisinsight/api/src/common/utils/certificate-import.util.ts b/redisinsight/api/src/common/utils/certificate-import.util.ts new file mode 100644 index 0000000000..77b5fe3312 --- /dev/null +++ b/redisinsight/api/src/common/utils/certificate-import.util.ts @@ -0,0 +1,7 @@ +import { parse } from 'path'; +import { readFileSync } from 'fs'; + +export const isValidPemCertificate = (cert: string): boolean => cert.startsWith('-----BEGIN CERTIFICATE-----'); +export const isValidPemPrivateKey = (cert: string): boolean => cert.startsWith('-----BEGIN PRIVATE KEY-----'); +export const getPemBodyFromFileSync = (path: string): string => readFileSync(path).toString('utf8'); +export const getCertNameFromFilename = (path: string): string => parse(path).name; diff --git a/redisinsight/api/src/common/utils/index.ts b/redisinsight/api/src/common/utils/index.ts new file mode 100644 index 0000000000..ee0efba08b --- /dev/null +++ b/redisinsight/api/src/common/utils/index.ts @@ -0,0 +1 @@ +export * from './certificate-import.util'; diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index afd6aa4340..ff4d26de4a 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -17,6 +17,10 @@ export default { INCORRECT_CREDENTIALS: (url) => `Could not connect to ${url}, please check the Username or Password.`, CA_CERT_EXIST: 'This ca certificate name is already in use.', + INVALID_CA_BODY: 'Invalid CA body', + INVALID_CERTIFICATE_BODY: 'Invalid certificate body', + INVALID_PRIVATE_KEY: 'Invalid private key', + CERTIFICATE_NAME_IS_NOT_DEFINED: 'Certificate name is not defined', CLIENT_CERT_EXIST: 'This client certificate name is already in use.', INVALID_CERTIFICATE_ID: 'Invalid certificate id.', SENTINEL_MASTER_NAME_REQUIRED: 'Sentinel master name must be specified.', diff --git a/redisinsight/api/src/modules/database-import/certificate-import.service.ts b/redisinsight/api/src/modules/database-import/certificate-import.service.ts new file mode 100644 index 0000000000..a5e30f212a --- /dev/null +++ b/redisinsight/api/src/modules/database-import/certificate-import.service.ts @@ -0,0 +1,216 @@ +import { Injectable } from '@nestjs/common'; +import { CaCertificate } from 'src/modules/certificate/models/ca-certificate'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity'; +import { Repository } from 'typeorm'; +import { EncryptionService } from 'src/modules/encryption/encryption.service'; +import { ModelEncryptor } from 'src/modules/encryption/model.encryptor'; +import { ClientCertificate } from 'src/modules/certificate/models/client-certificate'; +import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity'; +import { classToClass } from 'src/utils'; +import { + getCertNameFromFilename, + getPemBodyFromFileSync, + isValidPemCertificate, + isValidPemPrivateKey, +} from 'src/common/utils'; +import { + InvalidCaCertificateBodyException, InvalidCertificateNameException, + InvalidClientCertificateBodyException, InvalidClientPrivateKeyException, +} from 'src/modules/database-import/exceptions'; + +@Injectable() +export class CertificateImportService { + private caCertEncryptor: ModelEncryptor; + + private clientCertEncryptor: ModelEncryptor; + + constructor( + @InjectRepository(CaCertificateEntity) + private readonly caCertRepository: Repository, + @InjectRepository(ClientCertificateEntity) + private readonly clientCertRepository: Repository, + private readonly encryptionService: EncryptionService, + ) { + this.caCertEncryptor = new ModelEncryptor(encryptionService, ['certificate']); + this.clientCertEncryptor = new ModelEncryptor(encryptionService, ['certificate', 'key']); + } + + /** + * Validate data + prepare CA certificate to be imported along with new database + * @param cert + */ + async processCaCertificate(cert: Partial): Promise { + let toImport: Partial = { + certificate: null, + name: cert.name, + }; + + if (isValidPemCertificate(cert.certificate)) { + toImport.certificate = cert.certificate; + } else { + try { + toImport.certificate = getPemBodyFromFileSync(cert.certificate); + toImport.name = getCertNameFromFilename(cert.certificate); + } catch (e) { + // ignore error + toImport = null; + } + } + + if (!toImport?.certificate || !isValidPemCertificate(toImport.certificate)) { + throw new InvalidCaCertificateBodyException(); + } + + if (!toImport?.name) { + throw new InvalidCertificateNameException(); + } + + return this.prepareCaCertificateForImport(toImport); + } + + /** + * Use existing certificate if found + * Generate unique name for new certificate + * @param cert + * @private + */ + private async prepareCaCertificateForImport(cert: Partial): Promise { + const encryptedModel = await this.caCertEncryptor.encryptEntity(cert as CaCertificate); + const existing = await this.caCertRepository.createQueryBuilder('c') + .select('c.id') + .where({ certificate: cert.certificate }) + .orWhere({ certificate: encryptedModel.certificate }) + .getOne(); + + if (existing) { + return existing; + } + + const name = await CertificateImportService.determineAvailableName( + cert.name, + this.caCertRepository, + ); + + return classToClass(CaCertificate, { + ...cert, + name, + }); + } + + /** + * Validate data + prepare CA certificate to be imported along with new database + * @param cert + */ + async processClientCertificate(cert: Partial): Promise { + const toImport: Partial = { + certificate: null, + key: null, + name: cert.name, + }; + + if (isValidPemCertificate(cert.certificate)) { + toImport.certificate = cert.certificate; + } else { + try { + toImport.certificate = getPemBodyFromFileSync(cert.certificate); + toImport.name = getCertNameFromFilename(cert.certificate); + } catch (e) { + // ignore error + toImport.certificate = null; + toImport.name = null; + } + } + + if (isValidPemPrivateKey(cert.key)) { + toImport.key = cert.key; + } else { + try { + toImport.key = getPemBodyFromFileSync(cert.key); + } catch (e) { + // ignore error + toImport.key = null; + } + } + + if (!toImport?.certificate || !isValidPemCertificate(toImport.certificate)) { + throw new InvalidClientCertificateBodyException(); + } + + if (!toImport?.key || !isValidPemPrivateKey(toImport.key)) { + throw new InvalidClientPrivateKeyException(); + } + + if (!toImport?.name) { + throw new InvalidCertificateNameException(); + } + + return this.prepareClientCertificateForImport(toImport); + } + + /** + * Use existing certificate if found + * Generate unique name for new certificate + * @param cert + * @private + */ + private async prepareClientCertificateForImport(cert: Partial): Promise { + const encryptedModel = await this.clientCertEncryptor.encryptEntity(cert as ClientCertificate); + const existing = await this.clientCertRepository.createQueryBuilder('c') + .select('c.id') + .where({ + certificate: cert.certificate, + key: cert.key, + }) + .orWhere({ + certificate: encryptedModel.certificate, + key: encryptedModel.key, + }) + .getOne(); + + if (existing) { + return existing; + } + + const name = await CertificateImportService.determineAvailableName( + cert.name, + this.clientCertRepository, + ); + + return classToClass(ClientCertificate, { + ...cert, + name, + }); + } + + /** + * Find available name for certificate using such pattern "{N}_{name}" + * @param originalName + * @param repository + */ + static async determineAvailableName(originalName: string, repository: Repository): Promise { + let index = 0; + + // temporary solution + // investigate how to make working "regexp" for sqlite + // https://github.com/kriasoft/node-sqlite/issues/55 + // https://www.sqlite.org/c3ref/create_function.html + while (true) { + let name = originalName; + + if (index) { + name = `${index}_${name}`; + } + + if (!await repository + .createQueryBuilder('c') + .where({ name }) + .select(['c.id']) + .getOne()) { + return name; + } + + index += 1; + } + } +} diff --git a/redisinsight/api/src/modules/database-import/database-import.module.ts b/redisinsight/api/src/modules/database-import/database-import.module.ts index ae7e036f30..b4814589dc 100644 --- a/redisinsight/api/src/modules/database-import/database-import.module.ts +++ b/redisinsight/api/src/modules/database-import/database-import.module.ts @@ -2,11 +2,13 @@ import { Module } from '@nestjs/common'; import { DatabaseImportController } from 'src/modules/database-import/database-import.controller'; import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; import { DatabaseImportAnalytics } from 'src/modules/database-import/database-import.analytics'; +import { CertificateImportService } from 'src/modules/database-import/certificate-import.service'; @Module({ controllers: [DatabaseImportController], providers: [ DatabaseImportService, + CertificateImportService, DatabaseImportAnalytics, ], }) diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 234bd6e6ba..bd5965d3a5 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -21,6 +21,7 @@ import { UnableToParseDatabaseImportFileException, } from 'src/modules/database-import/exceptions'; import { ValidationException } from 'src/common/exceptions'; +import { CertificateImportService } from 'src/modules/database-import/certificate-import.service'; @Injectable() export class DatabaseImportService { @@ -36,9 +37,17 @@ export class DatabaseImportService { ['port', ['port']], ['db', ['db']], ['isCluster', ['cluster']], + ['tls', ['tls', 'ssl']], + ['tlsServername', ['tlsServername', 'sni_name', 'sni_server_name']], + ['tlsCaName', ['caCert.name']], + ['tlsCaCert', ['caCert.certificate', 'sslOptions.ca', 'ssl_ca_cert_path']], + ['tlsClientName', ['clientCert.name']], + ['tlsClientCert', ['clientCert.certificate', 'sslOptions.cert', 'ssl_local_cert_path']], + ['tlsClientKey', ['clientCert.key', 'sslOptions.key', 'ssl_private_key_path']], ]; constructor( + private readonly certificateImportService: CertificateImportService, private readonly databaseRepository: DatabaseRepository, private readonly analytics: DatabaseImportAnalytics, ) {} @@ -115,6 +124,8 @@ export class DatabaseImportService { */ private async createDatabase(item: any, index: number): Promise { try { + let status = DatabaseImportStatus.Success; + const errors = []; const data: any = {}; this.fieldsMapSchema.forEach(([field, paths]) => { @@ -140,6 +151,33 @@ export class DatabaseImportService { data.connectionType = ConnectionType.STANDALONE; } + if (data?.tlsCaCert) { + try { + data.tls = true; + data.caCert = await this.certificateImportService.processCaCertificate({ + certificate: data.tlsCaCert, + name: data?.tlsCaName, + }); + } catch (e) { + status = DatabaseImportStatus.Partial; + errors.push(e); + } + } + + if (data?.tlsClientCert || data?.tlsClientKey) { + try { + data.tls = true; + data.clientCert = await this.certificateImportService.processClientCertificate({ + certificate: data.tlsClientCert, + key: data.tlsClientKey, + name: data?.tlsClientName, + }); + } catch (e) { + status = DatabaseImportStatus.Partial; + errors.push(e); + } + } + const dto = plainToClass( ImportDatabaseDto, // additionally replace empty strings ("") with null @@ -148,6 +186,9 @@ export class DatabaseImportService { acc[key] = data[key] === '' ? null : data[key]; return acc; }, {}), + { + groups: ['security'], + }, ); await this.validator.validateOrReject(dto, { @@ -160,9 +201,10 @@ export class DatabaseImportService { return { index, - status: DatabaseImportStatus.Success, + status, host: database.host, port: database.port, + errors: errors?.length ? errors : undefined, }; } catch (e) { let errors = [e]; diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index de696bcafc..46215ef872 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -1,13 +1,19 @@ -import { PickType } from '@nestjs/swagger'; +import { ApiPropertyOptional, getSchemaPath, PickType } from '@nestjs/swagger'; import { Database } from 'src/modules/database/models/database'; import { Expose, Type } from 'class-transformer'; import { - IsInt, IsNotEmpty, Max, Min, + IsInt, IsNotEmpty, IsNotEmptyObject, IsOptional, Max, Min, ValidateNested, } from 'class-validator'; +import { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer'; +import { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto'; +import { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto'; +import { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto'; +import { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer'; +import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto'; export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', - 'connectionType', + 'connectionType', 'tls', 'verifyServerCert', ] as const) { @Expose() @IsNotEmpty() @@ -16,4 +22,26 @@ export class ImportDatabaseDto extends PickType(Database, [ @Min(0) @Max(65535) port: number; + + @Expose() + @IsOptional() + @IsNotEmptyObject() + @Type(caCertTransformer) + @ValidateNested() + caCert?: CreateCaCertificateDto | UseCaCertificateDto; + + @ApiPropertyOptional({ + description: 'Client Certificate', + oneOf: [ + { $ref: getSchemaPath(CreateClientCertificateDto) }, + { $ref: getSchemaPath(UseCaCertificateDto) }, + ], + }) + @Expose() + @IsOptional() + @IsNotEmptyObject() + @Type(clientCertTransformer) + @ValidateNested() + clientCert?: CreateClientCertificateDto | UseClientCertificateDto; + } diff --git a/redisinsight/api/src/modules/database-import/exceptions/index.ts b/redisinsight/api/src/modules/database-import/exceptions/index.ts index d62d9a0259..2ba9fdfb68 100644 --- a/redisinsight/api/src/modules/database-import/exceptions/index.ts +++ b/redisinsight/api/src/modules/database-import/exceptions/index.ts @@ -1,3 +1,7 @@ +export * from './invalid-ca-certificate-body.exception'; +export * from './invalid-client-certificate-body.exception'; +export * from './invalid-client-private-key.exception'; +export * from './invalid-certificate-name.exception'; export * from './size-limit-exceeded-database-import-file.exception'; export * from './no-database-import-file-provided.exception'; export * from './unable-to-parse-database-import-file.exception'; diff --git a/redisinsight/api/src/modules/database-import/exceptions/invalid-ca-certificate-body.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/invalid-ca-certificate-body.exception.ts new file mode 100644 index 0000000000..fcffc334f4 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/invalid-ca-certificate-body.exception.ts @@ -0,0 +1,14 @@ +import { HttpException } from '@nestjs/common'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class InvalidCaCertificateBodyException extends HttpException { + constructor(message: string = ERROR_MESSAGES.INVALID_CA_BODY) { + const response = { + message, + statusCode: 400, + error: 'Invalid Ca Certificate Body', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/invalid-certificate-name.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/invalid-certificate-name.exception.ts new file mode 100644 index 0000000000..4826f69c67 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/invalid-certificate-name.exception.ts @@ -0,0 +1,14 @@ +import { HttpException } from '@nestjs/common'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class InvalidCertificateNameException extends HttpException { + constructor(message: string = ERROR_MESSAGES.CERTIFICATE_NAME_IS_NOT_DEFINED) { + const response = { + message, + statusCode: 400, + error: 'Invalid Certificate Name', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/invalid-client-certificate-body.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/invalid-client-certificate-body.exception.ts new file mode 100644 index 0000000000..6caa948b84 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/invalid-client-certificate-body.exception.ts @@ -0,0 +1,14 @@ +import { HttpException } from '@nestjs/common'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class InvalidClientCertificateBodyException extends HttpException { + constructor(message: string = ERROR_MESSAGES.INVALID_CERTIFICATE_BODY) { + const response = { + message, + statusCode: 400, + error: 'Invalid Client Certificate Body', + }; + + super(response, 400); + } +} diff --git a/redisinsight/api/src/modules/database-import/exceptions/invalid-client-private-key.exception.ts b/redisinsight/api/src/modules/database-import/exceptions/invalid-client-private-key.exception.ts new file mode 100644 index 0000000000..1b372ab8b3 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/exceptions/invalid-client-private-key.exception.ts @@ -0,0 +1,14 @@ +import { HttpException } from '@nestjs/common'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +export class InvalidClientPrivateKeyException extends HttpException { + constructor(message: string = ERROR_MESSAGES.INVALID_PRIVATE_KEY) { + const response = { + message, + statusCode: 400, + error: 'Invalid Client Private Key', + }; + + super(response, 400); + } +} From 4702ba573634987ee99463193ad93f3db831cd1c Mon Sep 17 00:00:00 2001 From: nmammadli Date: Mon, 12 Dec 2022 14:20:25 +0100 Subject: [PATCH 062/107] Clear code --- .../tree-view/tree-view-improvements.e2e.ts | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts new file mode 100644 index 0000000000..e869d449fa --- /dev/null +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -0,0 +1,140 @@ +import { Selector, t } from 'testcafe'; +import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; +import { + BrowserPage, CliPage +} from '../../../pageObjects'; +import { + commonUrl, + ossStandaloneBigConfig, + ossStandaloneConfig +} from '../../../helpers/conf'; +import { KeyTypesTexts, rte } from '../../../helpers/constants'; +import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; +import { Common } from '../../../helpers/common'; +import { verifyKeysDisplayedInTheList, verifyKeysNotDisplayedInTheList } from '../../../helpers/keys'; + +const browserPage = new BrowserPage(); +const common = new Common(); +const cliPage = new CliPage(); +let keyNames: string[]; +let keyName1: string; +let keyName2: string; +let keynameSingle: string; +let index: string; + +fixture`Tree view navigations improvement tests` + .meta({ type: 'critical_path', rte: rte.standalone }) + .page(commonUrl); +test + .only + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }) + .after(async () => { + await t.click(browserPage.patternModeBtn); + await browserPage.deleteKeysByNames(keyNames); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('Tree view preselected folder', async t => { + + keyName1 = common.generateWord(10); // used to create index name + keyName2 = common.generateWord(10); // used to create index name + keynameSingle = common.generateWord(10); + keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; + + const commands = [ + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value`, + `HSET ${keyNames[3]} field value`, + `HSET ${keyNames[4]} field value` + ]; + + // Create 5 keys + await cliPage.sendCommandsInCli(commands); + await t.click(browserPage.treeViewButton); + // verify that first element in the tree is "keys" and other folders are closed + await verifyKeysDisplayedInTheList([keynameSingle]); + await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + // switch between browser view and tree view + await t.click(browserPage.browserViewButton); + await t.click(browserPage.treeViewButton); + // Switch to tree view + await browserPage.deleteKeyByName(keyNames[4]); + await t.click(browserPage.clearFilterButton); + // get first folder name + const firstTreeItemText = await browserPage.getTextFromFirstTreeElement(); + const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened + // verify that the first folder with namespaces is expanded and selected + await t.expect(firstTreeItemKeys.visible) + .ok('First folder is not expanded'); + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + + const commands1 = [ + `HSET ${keyNames[4]} field value` + ]; + + // Create 4 keys and index + await cliPage.sendCommandsInCli(commands1); + await t.click(browserPage.refreshKeysButton); // refresh keys + + await t.expect(firstTreeItemKeys.visible) + .ok('Folder is not selected'); + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + + await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); + + await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + + await browserPage.searchByKeyName('*'); + + await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + + await t.click(browserPage.clearFilterButton); + + await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + + await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); + + await t.expect( + Selector('[role="rowgroup"]').find('div').withText('No results found.').visible) + .ok('No results text is visible'); // verify no results found + + await t.click(browserPage.streamDeleteButton); // clear stream from filter + + await t.expect( + Selector('[role="rowgroup"]').find('div').withText('No results found.').visible) + .notOk('No result text is still visible'); + await t.expect( + firstTreeItemKeys.visible). + notOk('First folder is expanded'); + + }); + +test + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + }) + .after(async () => { + await cliPage.sendCommandInCli(`FT.DROPINDEX ${index}`); + await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + })('Verify tree view navigation for index based search', async t => { + // generate index based on keyName + const folders = ['mobile', '2']; + + index = await cliPage.createRandomIndexNamewithCLI(); + + await t.click(browserPage.redisearchModeBtn); // click redisearch button + await browserPage.selectIndexByName(index); + + await t.click(browserPage.treeViewButton); + await browserPage.openTreeFolders(folders); + await t.click(browserPage.refreshKeysButton); + + await t.expect( + Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) + .ok('Folder is not selected'); + + }); From 732a7176ab9184562f68131d91b43e92a1323a66 Mon Sep 17 00:00:00 2001 From: zalenskiSofteq Date: Tue, 13 Dec 2022 16:01:19 +0800 Subject: [PATCH 063/107] #RI-3898 - Do not use JSON.DEBUG MEMORY when it is not allowed --- .../rejson-rl-business.service.spec.ts | 87 +++++++++++++++---- .../rejson-rl-business.service.ts | 16 ++-- .../POST-databases-id-rejson_rl-get.test.ts | 25 +++++- 3 files changed, 102 insertions(+), 26 deletions(-) diff --git a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts index a6d6349d37..db0f59a672 100644 --- a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts +++ b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.spec.ts @@ -172,23 +172,6 @@ describe('JsonBusinessService', () => { expect(err).toBeInstanceOf(BadRequestException); } }); - it('should throw Forbidden error when no perms for an action for getJson', async () => { - const replyError: ReplyError = { - ...mockRedisNoPermError, - command: 'JSON.DEBUG', - }; - browserTool.execCommand.mockRejectedValue(replyError); - - try { - await service.getJson(mockBrowserClientMetadata, { - keyName: testKey, - path: testPath, - }); - fail(); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } - }); it('should throw BadRequest error when module not loaded for getJson', async () => { const replyError: ReplyError = { name: 'ReplyError', @@ -414,6 +397,76 @@ describe('JsonBusinessService', () => { }); }); }); + + describe('user has no PERM for JSON.DEBUG', () => { + beforeEach(() => { + const replyError: ReplyError = { + ...mockRedisNoPermError, + command: 'JSON.DEBUG', + }; + + when(browserTool.execCommand) + .calledWith( + mockBrowserClientMetadata, + BrowserToolRejsonRlCommands.JsonDebug, + ['MEMORY', testKey, testPath], + ) + .mockRejectedValue(replyError); + }); + + it('should return data (string)', async () => { + const testData = 'some string'; + when(browserTool.execCommand) + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ + testKey, + testPath, + ], 'utf8') + .mockReturnValue(JSON.stringify(testData)); + + const result = await service.getJson(mockBrowserClientMetadata, { + keyName: testKey, + path: testPath, + }); + + expect(result).toEqual({ + downloaded: true, + path: testPath, + data: testData, + }); + }); + + it('should return full json value even if size is above the limit', async () => { + const testData = {arr:[randomBytes(2000).toString('hex')]}; + when(browserTool.execCommand) + .calledWith(mockBrowserClientMetadata, BrowserToolRejsonRlCommands.JsonGet, [ + testKey, + testPath, + ], 'utf8') + .mockReturnValue(JSON.stringify(testData)); + + when(browserTool.execCommand) + .calledWith( + mockBrowserClientMetadata, + BrowserToolRejsonRlCommands.JsonType, [ + testKey, + testPath, + ], + 'utf8', + ).mockReturnValue('object'); + + const result = await service.getJson(mockBrowserClientMetadata, { + keyName: testKey, + path: testPath, + }); + + expect(result).toEqual({ + downloaded: true, + path: testPath, + data: testData, + }); + }); + + }); describe('partial json download', () => { beforeEach(() => { when(browserTool.execCommand) diff --git a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts index 0dcf718d75..3b45cac315 100644 --- a/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts +++ b/redisinsight/api/src/modules/browser/services/rejson-rl-business/rejson-rl-business.service.ts @@ -61,11 +61,17 @@ export class RejsonRlBusinessService { keyName: RedisString, path: string, ): Promise { - const size = await this.browserTool.execCommand( - clientMetadata, - BrowserToolRejsonRlCommands.JsonDebug, - ['MEMORY', keyName, path], - ); + let size = 0 + + try { + size = await this.browserTool.execCommand( + clientMetadata, + BrowserToolRejsonRlCommands.JsonDebug, + ['MEMORY', keyName, path], + ); + } catch (error) { + this.logger.error('Failed to estimate size of json.', error); + } if (size === null) { throw new BadRequestException( diff --git a/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts b/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts index 12e8c7df41..4746c7bdb6 100644 --- a/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts +++ b/redisinsight/api/test/api/rejson-rl/POST-databases-id-rejson_rl-get.test.ts @@ -229,17 +229,34 @@ describe('POST /databases/:instanceId/rejson-rl/get', () => { before: () => rte.data.setAclUserRules('~* +@all -json.get') }, { - name: 'Should throw error if no permissions for "json.debug" command', + name: 'Should return regular item if no permissions for "json.debug" command', endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), data: { keyName: constants.TEST_REJSON_KEY_3, path: '.', forceRetrieve: false, }, - statusCode: 403, + responseSchema, responseBody: { - statusCode: 403, - error: 'Forbidden', + downloaded: true, + path: '.', + data: constants.TEST_REJSON_VALUE_3, + }, + before: () => rte.data.setAclUserRules('~* +@all -json.debug') + }, + { + name: 'Should get full json if no permissions for "json.debug" command', + endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID), + data: { + keyName: constants.TEST_REJSON_KEY_3, + path: '.', + forceRetrieve: false, + }, + responseSchema, + responseBody: { + downloaded: true, + path: '.', + data: constants.TEST_REJSON_VALUE_3, }, before: () => rte.data.setAclUserRules('~* +@all -json.debug') }, From 38b05128644971f519adfcc30d2af5065b8098f2 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 13 Dec 2022 12:35:13 +0100 Subject: [PATCH 064/107] Impelementation of last comments from review --- tests/e2e/pageObjects/browser-page.ts | 22 +++--- tests/e2e/pageObjects/cli-page.ts | 5 +- .../tree-view/tree-view-improvements.e2e.ts | 78 +++++++++++++------ 3 files changed, 64 insertions(+), 41 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 47c6bdf650..3d6099c935 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,7 +22,9 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS - streamDeleteButton = Selector(`[data-testid="stream-delete-btn"]`); + hashDeleteButton = Selector('[data-testid=hash-delete-btn]'); + setDeleteButton = Selector('[data-testid=set-delete-btn]'); + streamDeleteButton = Selector('[data-testid=stream-delete-btn]'); myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); @@ -988,7 +990,7 @@ export class BrowserPage { /** * Verify that keys can be scanned more and results increased - */ + */ async verifyScannningMore(): Promise { for (let i = 10; i < 100; i += 10) { // Remember results value @@ -1007,7 +1009,7 @@ export class BrowserPage { /** * Open Select Index droprown and select option * @param index The name of format - */ + */ async selectIndexByName(index: string): Promise { const option = Selector(`[data-test-subj="mode-option-type-${index}"]`); await t @@ -1016,20 +1018,16 @@ export class BrowserPage { } /** - *Get text from first tree element + * Get text from first tree element */ - - async getTextFromFirstTreeElement(): Promise { - return (await Selector(`[role="treeitem"]`).nth(0).find(`div`).textContent).replace(/\s/g, ''); + async getTextFromNthTreeElement(number: number): Promise { + return (await Selector(`[role="treeitem"]`).nth(number).find(`div`).textContent).replace(/\s/g, ''); } /** - * Get text from first tree element + * Open tree folder with multiple level * @param names folder names with sequence of subfolder - * Example: if names ['mobile', '2'] - * It will go to mobile -> 2 -> keys */ - async openTreeFolders(names: string[]): Promise { let base = `node-item_${names[0]}:` await t.click(Selector(`[data-testid="${base}"]`)); @@ -1045,8 +1043,6 @@ export class BrowserPage { Selector(`[data-testid="${base}keys:keys:"]`).visible) .ok("Folder is not selected"); } - - } /** diff --git a/tests/e2e/pageObjects/cli-page.ts b/tests/e2e/pageObjects/cli-page.ts index 4ee881ff64..8a88a49b72 100644 --- a/tests/e2e/pageObjects/cli-page.ts +++ b/tests/e2e/pageObjects/cli-page.ts @@ -176,14 +176,13 @@ export class CliPage { * Create random index name with CLI and return */ - async createRandomIndexNamewithCLI(): Promise { + async createIndexwithCLI(prefix: string): Promise { const word = common.generateWord(10); const index = `idx:${word}`; const commands = [ - `FT.CREATE ${index} ON HASH SCHEMA "name" TEXT`, + `FT.CREATE ${index} ON HASH PREFIX 1 ${prefix} SCHEMA "name" TEXT`, ]; await this.sendCommandsInCli(commands); - return index; } } diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index e869d449fa..396f927b10 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -26,7 +26,6 @@ fixture`Tree view navigations improvement tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl); test - .only .before(async () => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) @@ -35,7 +34,6 @@ test await browserPage.deleteKeysByNames(keyNames); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Tree view preselected folder', async t => { - keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name keynameSingle = common.generateWord(10); @@ -46,23 +44,31 @@ test `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, `HSET ${keyNames[3]} field value`, - `HSET ${keyNames[4]} field value` + `SADD ${keyNames[4]} value` ]; // Create 5 keys await cliPage.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); + await verifyKeysDisplayedInTheList([keynameSingle]); + + await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); + await browserPage.selectFilterGroupType(KeyTypesTexts.Set); + await verifyKeysDisplayedInTheList([keynameSingle]); + + await t.click(browserPage.setDeleteButton); // verify that first element in the tree is "keys" and other folders are closed await verifyKeysDisplayedInTheList([keynameSingle]); await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); + // switch between browser view and tree view - await t.click(browserPage.browserViewButton); - await t.click(browserPage.treeViewButton); + await t.click(browserPage.browserViewButton) + .click(browserPage.treeViewButton); // Switch to tree view await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name - const firstTreeItemText = await browserPage.getTextFromFirstTreeElement(); + const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened // verify that the first folder with namespaces is expanded and selected await t.expect(firstTreeItemKeys.visible) @@ -72,45 +78,35 @@ test const commands1 = [ `HSET ${keyNames[4]} field value` ]; - // Create 4 keys and index await cliPage.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); // refresh keys - await t.expect(firstTreeItemKeys.visible) .ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible await browserPage.searchByKeyName('*'); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible await t.click(browserPage.clearFilterButton); - await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - - await t.expect( - Selector('[role="rowgroup"]').find('div').withText('No results found.').visible) - .ok('No results text is visible'); // verify no results found + await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); await t.click(browserPage.streamDeleteButton); // clear stream from filter - await t.expect( Selector('[role="rowgroup"]').find('div').withText('No results found.').visible) .notOk('No result text is still visible'); await t.expect( - firstTreeItemKeys.visible). - notOk('First folder is expanded'); - + firstTreeItemKeys.visible) + .notOk('First folder is expanded'); }); test @@ -122,19 +118,51 @@ test await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); })('Verify tree view navigation for index based search', async t => { // generate index based on keyName - const folders = ['mobile', '2']; - - index = await cliPage.createRandomIndexNamewithCLI(); - + const folders = ['mobile', '2014']; + index = await cliPage.createIndexwithCLI(folders.join(':')); await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); - await t.click(browserPage.treeViewButton); + await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder await browserPage.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); - await t.expect( Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) .ok('Folder is not selected'); + }); +test + .before(async () => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); + }) + .after(async () => { + await t.click(browserPage.patternModeBtn); + await browserPage.deleteKeysByNames(keyNames.slice(1)); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); + })('', async t => { + keyName1 = common.generateWord(10); // used to create index name + keyName2 = common.generateWord(10); // used to create index name + keynameSingle = common.generateWord(10); + keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; + const commands = [ + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `RPUSH ${keyNames[2]} field`, + `RPUSH ${keyNames[3]} field`, + `SADD ${keyNames[4]} value` + ]; + await cliPage.sendCommandsInCli(commands); + await t.click(browserPage.treeViewButton); + await verifyKeysDisplayedInTheList([keynameSingle]); + + await browserPage.openTreeFolders([keyName1]); // Type: hash + await browserPage.openTreeFolders([keyName2]); // Type: list + await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); + await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); + + await t.click(browserPage.hashDeleteButton); + await cliPage.sendCommandsInCli([`DEL ${keyNames[0]}`]); + await t.click(browserPage.refreshKeysButton); // refresh keys + await verifyKeysDisplayedInTheList([keyNames[1]]); + await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); }); From f3fe413a2e33aeec4c7f3948157b73a7313886ae Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 13 Dec 2022 14:25:59 +0100 Subject: [PATCH 065/107] Implement feedback from comments --- tests/e2e/pageObjects/browser-page.ts | 14 +++++++++++--- .../database/clone-databases.e2e.ts | 3 +++ .../smoke/database/add-standalone-db.e2e.ts | 18 +++++++++++------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index 37a218a6f4..b9257a1a2e 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -22,7 +22,7 @@ export class BrowserPage { //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- //BUTTONS - streamDeleteButton = Selector(`[data-testid="stream-delete-btn"]`); + streamDeleteButton = Selector('[data-testid=stream-delete-btn]'); myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); @@ -1019,8 +1019,16 @@ export class BrowserPage { * Verify database status is visible */ async verifyDatabaseStatusIsVisible(): Promise { - await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible). - ok("Database status is not visible"); + await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) + .ok("Database status is not visible"); + } + + /** + * Verify database status is not visible + */ + async verifyDatabaseStatusIsNotVisible(): Promise { + await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) + .notOk("Database status is still visible"); } } diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts index 20e140dec5..b5dc7e1df2 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -38,6 +38,7 @@ test await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button await t.click(addRedisDatabasePage.cloneDatabaseButton); + // New connections indicator await browserPage.verifyDatabaseStatusIsVisible(); await t.click(addRedisDatabasePage.cancelButton); await t.expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 }); @@ -68,6 +69,7 @@ test .meta({ rte: rte.ossCluster })('Verify that user can clone OSS Cluster', async t => { await clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName); await t.click(addRedisDatabasePage.cloneDatabaseButton); + // New connections indicator await browserPage.verifyDatabaseStatusIsVisible(); await t .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).ok('Clone panel is not displayed') @@ -97,6 +99,7 @@ test .meta({ rte: rte.sentinel })('Verify that user can clone Sentinel', async t => { await clickOnEditDatabaseByName(ossSentinelConfig.name[1]); await t.click(addRedisDatabasePage.cloneDatabaseButton); + // New connections indicator await browserPage.verifyDatabaseStatusIsVisible(); // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field await t diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index f14bdf37eb..50044f4e4e 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -6,10 +6,11 @@ import { redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { Selector, t } from 'testcafe'; +import { t } from 'testcafe'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; -const browserPage = new BrowserPage() +const browserPage = new BrowserPage(); +const myRedisDatabasePage = new MyRedisDatabasePage(); fixture`Add database` .meta({ type: 'smoke' }) @@ -18,16 +19,17 @@ fixture`Add database` await acceptLicenseTerms(); }); test + .only .meta({ rte: rte.standalone }) .after(async () => { await deleteDatabase(ossStandaloneConfig.databaseName); })('Verify that user can add Standalone Database', async () => { - const myRedisDatabasePage = new MyRedisDatabasePage(); await addNewStandaloneDatabase(ossStandaloneConfig); await browserPage.verifyDatabaseStatusIsVisible(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.click(browserPage.myRedisDbIcon); - await browserPage.verifyDatabaseStatusIsVisible(); + // New connections indicator + await browserPage.verifyDatabaseStatusIsNotVisible(); }); test .meta({ rte: rte.reCluster }) @@ -35,6 +37,7 @@ test await deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can add database from RE Cluster via auto-discover flow', async () => { await addNewREClusterDatabase(redisEnterpriseClusterConfig); + // New connections indicator await browserPage.verifyDatabaseStatusIsVisible(); }); test @@ -43,11 +46,12 @@ test await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); })('Verify that user can add OSS Cluster DB', async () => { await addOSSClusterDatabase(ossClusterConfig); + // New connections indicator await browserPage.verifyDatabaseStatusIsVisible(); }); -//skiped until the RE Cloud connection is implemented -test.skip + +test .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async () => { - //TODO: add api keys from env + // New connections indicator await addNewRECloudDatabase('', ''); }); From f4c46eda52ab59fdcc8075a7fefe41a0dc42e488 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 13 Dec 2022 14:48:25 +0100 Subject: [PATCH 066/107] After eslint --- .../smoke/database/add-standalone-db.e2e.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index 50044f4e4e..772522361a 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -1,3 +1,4 @@ +import { t } from 'testcafe'; import { addNewStandaloneDatabase, addNewREClusterDatabase, addNewRECloudDatabase, addOSSClusterDatabase, acceptLicenseTerms, deleteDatabase } from '../../../helpers/database'; import { commonUrl, @@ -6,7 +7,6 @@ import { redisEnterpriseClusterConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; -import { t } from 'testcafe'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; const browserPage = new BrowserPage(); @@ -15,15 +15,15 @@ const myRedisDatabasePage = new MyRedisDatabasePage(); fixture`Add database` .meta({ type: 'smoke' }) .page(commonUrl) - .beforeEach(async () => { + .beforeEach(async() => { await acceptLicenseTerms(); }); test .only .meta({ rte: rte.standalone }) - .after(async () => { + .after(async() => { await deleteDatabase(ossStandaloneConfig.databaseName); - })('Verify that user can add Standalone Database', async () => { + })('Verify that user can add Standalone Database', async() => { await addNewStandaloneDatabase(ossStandaloneConfig); await browserPage.verifyDatabaseStatusIsVisible(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); @@ -33,25 +33,25 @@ test }); test .meta({ rte: rte.reCluster }) - .after(async () => { + .after(async() => { await deleteDatabase(redisEnterpriseClusterConfig.databaseName); - })('Verify that user can add database from RE Cluster via auto-discover flow', async () => { - await addNewREClusterDatabase(redisEnterpriseClusterConfig); + })('Verify that user can add database from RE Cluster via auto-discover flow', async() => { // New connections indicator + await addNewREClusterDatabase(redisEnterpriseClusterConfig); await browserPage.verifyDatabaseStatusIsVisible(); }); test .meta({ env: env.web, rte: rte.ossCluster }) - .after(async () => { + .after(async() => { await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); - })('Verify that user can add OSS Cluster DB', async () => { - await addOSSClusterDatabase(ossClusterConfig); + })('Verify that user can add OSS Cluster DB', async() => { // New connections indicator + await addOSSClusterDatabase(ossClusterConfig); await browserPage.verifyDatabaseStatusIsVisible(); }); test - .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async () => { + .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async() => { // New connections indicator await addNewRECloudDatabase('', ''); }); From 24831d9067533d94d122bff07936e535f2d5faf1 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 13 Dec 2022 14:50:46 +0100 Subject: [PATCH 067/107] Delete copy --- .../tree-view-improvements.e2e copy.ts | 140 ------------------ 1 file changed, 140 deletions(-) delete mode 100644 tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts deleted file mode 100644 index 60efe85fb5..0000000000 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e copy.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { acceptLicenseTermsAndAddDatabaseApi } from '../../../helpers/database'; -import { - BrowserPage, CliPage -} from '../../../pageObjects'; -import { - commonUrl, - ossStandaloneBigConfig, - ossStandaloneConfig -} from '../../../helpers/conf'; -import { KeyTypesTexts, rte } from '../../../helpers/constants'; -import { deleteStandaloneDatabaseApi } from '../../../helpers/api/api-database'; -import { Common } from '../../../helpers/common'; -import { Selector, t } from 'testcafe'; -import { verifyKeysDisplayedInTheList } from '../../../helpers/keys'; - -const browserPage = new BrowserPage(); -const common = new Common(); -const cliPage = new CliPage(); -let keyNames: string[]; -let keyName1: string; -let keyName2: string; -let keynameSingle: string; -let index: string; - -fixture`Tree view navigations improvement tests` - .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl) -test - .before(async () => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); - }) - .after(async () => { - await t.click(browserPage.patternModeBtn); - await browserPage.deleteKeysByNames(keyNames); - await deleteStandaloneDatabaseApi(ossStandaloneConfig); - }) - ('Verify that if any keys are found and will not be redirected from the previously selected directory', async t => { - - keyName1 = common.generateWord(10); // used to create index name - keyName2 = common.generateWord(10); // used to create index name - keynameSingle = common.generateWord(10); - keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; - - const commands = [ - `HSET ${keyNames[0]} field value`, - `HSET ${keyNames[1]} field value`, - `HSET ${keyNames[2]} field value`, - `HSET ${keyNames[3]} field value`, - `HSET ${keyNames[4]} field value`, - ]; - - // Create 5 keys - await cliPage.sendCommandsInCli(commands); - - // Switch to tree view - await browserPage.deleteKeyByName(keyNames[4]); - await t.click(browserPage.clearFilterButton); - await t.click(browserPage.treeViewButton); - - // get first folder name - const firstTreeItemText = await browserPage.getTextFromFirstTreeElement(); - const treeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - - // verify that the first folder with namespaces is expanded and selected - await t.expect( - treeItemKeys.visible). - ok("First folder is not expanded"); - await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible - - const commands1 = [ - `HSET ${keyNames[4]} field value`, - ]; - - // Create 4 keys and index - await cliPage.sendCommandsInCli(commands1); - await t.click(browserPage.refreshKeysButton); // refresh keys - - await t.expect( - treeItemKeys.visible). - ok("Folder is not selected"); - await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible - - await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); - - await t.expect(treeItemKeys.visible).ok("Folder is not selected after searching with HASH"); - await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible - - await browserPage.searchByKeyName('*'); - - await t.expect(treeItemKeys.visible).ok("Folder is not selected"); - await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible - - await t.click(browserPage.clearFilterButton); - - await t.expect(treeItemKeys.visible).ok("Folder is not selected"); - await verifyKeysDisplayedInTheList([firstTreeItemText + ":1", firstTreeItemText + ":2"]); // verify created keys are visible - - await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - - await t.expect( - Selector(`[role="rowgroup"]`).find(`div`).withText("No results found.").visible). - ok("No results text is visible"); // verify no results found - - await t.click(browserPage.streamDeleteButton); // clear stream from filter - - await t.expect( - Selector(`[role="rowgroup"]`).find(`div`).withText("No results found.").visible). - notOk("No result text is still visible"); - await t.expect( - treeItemKeys.visible). - notOk("First folder is expanded"); - - }); - -test - .before(async () => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); - }) - .after(async () => { - await cliPage.sendCommandInCli(`FT.DROPINDEX ${index}`); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); - }) - ('Verify tree view navigation for index based search', async t => { - // generate index based on keyName - let folders = ["mobile", "2"]; - - index = await cliPage.createRandomIndexNamewithCLI(); - - await t.click(browserPage.redisearchModeBtn); // click redisearch button - await browserPage.selectIndexByName(index); - - await t.click(browserPage.treeViewButton); - await browserPage.openTreeFolders(folders); - await t.click(browserPage.refreshKeysButton); - - await t.expect( - Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) - .ok("Folder is not selected"); - - }); From 1ec6d337f4fb4415379a3dbec8854e2e2e605409 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Tue, 13 Dec 2022 15:06:32 +0100 Subject: [PATCH 068/107] Add comments from checklist --- .../critical-path/tree-view/tree-view-improvements.e2e.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index 396f927b10..1380cd2b77 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -33,7 +33,8 @@ test await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Tree view preselected folder', async t => { + })('Tree view preselected folder, Filterred Tree view preselected folder, Refreshed Tree view preselected folder, Search capability Filterred Tree view preselected folder', async t => { + // Search capability Refreshed Tree view preselected folder keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name keynameSingle = common.generateWord(10); @@ -139,7 +140,7 @@ test await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames.slice(1)); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('', async t => { + })('Refreshed Tree view preselected folder, Filterred Tree view preselected folder', async t => { keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name keynameSingle = common.generateWord(10); From 7b89ce16e821399eda5f3957e75314993650f3aa Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Tue, 13 Dec 2022 17:19:42 +0300 Subject: [PATCH 069/107] #RI-3903 - add imports results --- .../ImportDatabasesDialog.tsx | 33 +++---- .../components/ResultsLog/ResultsLog.tsx | 87 +++++++++++++++++++ .../components/ResultsLog/index.ts | 3 + .../components/ResultsLog/styles.module.scss | 85 ++++++++++++++++++ .../components/TableResult/TableResult.tsx | 74 ++++++++++++++++ .../components/TableResult/index.ts | 3 + .../components/TableResult/styles.module.scss | 29 +++++++ .../styles.module.scss | 3 +- .../ui/src/slices/interfaces/instances.ts | 33 ++++++- .../ui/src/styles/components/_table.scss | 31 ++++++- redisinsight/ui/src/telemetry/events.ts | 1 + 11 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.tsx create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/index.ts create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.tsx create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/TableResult/index.ts create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/TableResult/styles.module.scss diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx index 8215352ca8..9737f4e9dc 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -2,8 +2,7 @@ import { EuiButton, EuiFilePicker, EuiFlexGroup, - EuiFlexItem, - EuiIcon, + EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiModal, EuiModalBody, @@ -24,6 +23,7 @@ import { } from 'uiSrc/slices/instances/instances' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { Nullable } from 'uiSrc/utils' +import ResultsLog from './components/ResultsLog' import styles from './styles.module.scss' @@ -50,7 +50,9 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { const handleOnClose = () => { dispatch(resetImportInstances()) - data?.success && dispatch(fetchInstancesAction()) + if (data?.success?.length || data?.partial?.length) { + dispatch(fetchInstancesAction()) + } onClose(!data) } @@ -72,17 +74,17 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { - - Import Database Connections + + {(!data && !error) ? 'Import Database Connections' : 'Import results'} - + {isShowForm && ( - <> + { File should not exceed {MAX_MB_FILE} MB )} - + )} {loading && ( @@ -107,17 +109,8 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { Uploading...
)} - - {data && data.success !== 0 && ( -
- - - Successfully added {data.success} of {data.total} database connections - -
- )} - - {(data?.success === 0 || error) && ( + {data && ()} + {error && (
@@ -129,7 +122,7 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { - {data && data.success !== 0 && ( + {data && ( { + const [openedNav, setOpenedNav] = useState('') + + const onToggle = (length: number = 0, isOpen: boolean, name: string) => { + if (length === 0) return + setOpenedNav(isOpen ? name : '') + + if (isOpen) { + sendEventTelemetry({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED, + eventData: { + length, + name + } + }) + } + } + + const CollapsibleNavTitle = ({ title, length = 0 }: { title: string, length: number }) => ( +
+ {title} + {length} +
+ ) + + const getNavGroupState = (name: ResultsStatus) => (openedNav === name ? 'open' : 'closed') + + return ( + <> + } + className={cx(styles.collapsibleNav, ResultsStatus.Success, { [styles.disabled]: !data?.success?.length })} + isCollapsible + initialIsOpen={false} + onToggle={(isOpen) => onToggle(data?.success?.length, isOpen, ResultsStatus.Success)} + forceState={getNavGroupState(ResultsStatus.Success)} + data-testid={`success-results-${getNavGroupState(ResultsStatus.Success)}`} + > + + + } + className={cx(styles.collapsibleNav, ResultsStatus.Partial, { [styles.disabled]: !data?.partial?.length })} + isCollapsible + initialIsOpen={false} + onToggle={(isOpen) => onToggle(data?.partial?.length, isOpen, ResultsStatus.Partial)} + forceState={getNavGroupState(ResultsStatus.Partial)} + data-testid={`partial-results-${getNavGroupState(ResultsStatus.Partial)}`} + > + + + } + className={cx(styles.collapsibleNav, ResultsStatus.Failed, { [styles.disabled]: !data?.fail?.length })} + isCollapsible + initialIsOpen={false} + onToggle={(isOpen) => onToggle(data?.fail?.length, isOpen, ResultsStatus.Failed)} + forceState={getNavGroupState(ResultsStatus.Failed)} + data-testid={`failed-results-${getNavGroupState(ResultsStatus.Failed)}`} + > + + + + ) +} + +export default ResultsLog diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/index.ts b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/index.ts new file mode 100644 index 0000000000..1eb9fdbb50 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/index.ts @@ -0,0 +1,3 @@ +import ResultsLog from './ResultsLog' + +export default ResultsLog diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss new file mode 100644 index 0000000000..6b15697083 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss @@ -0,0 +1,85 @@ +.collapsibleNav { + width: 100%; + margin-top: 5px !important; + position: relative; + + &.disabled { + :global { + .euiAccordion__button { + cursor: auto; + pointer-events: none; + } + .euiAccordion__iconWrapper { + display: none; + } + } + } + + &:global(.euiAccordion-isOpen) { + :global { + .euiAccordion__triggerWrapper { + border-radius: 0; + border-color: transparent; + } + } + } + + &:global(.success) { + &:before { + background-color: #13A450; + } + } + + &:global(.partial) { + &:before { + background-color: #9D6901; + } + } + + &:global(.failed) { + &:before { + background-color: #AD0017; + } + } + + &:before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 4px; + + border-radius: 4px 0 0 4px; + + z-index: 2; + } + + :global { + .euiCollapsibleNavGroup__title { + font-size: 12px !important; + font-weight: 400 !important; + color: var(--euiTextSubduedColor) !important; + } + .euiAccordion__triggerWrapper { + background-color: var(--euiColorEmptyShade); + padding: 12px 24px 12px 32px !important; + border-radius: 4px; + border: 1px solid #3D3D3D; + } + .euiCollapsibleNavGroup__children { + padding: 0 !important; + } + + .euiIEFlexWrapFix { + flex-grow: 1; + padding-right: 4px; + } + } +} + +.collapsibleNavTitle { + display: flex; + justify-content: space-between; + align-items: center; +} diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.tsx b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.tsx new file mode 100644 index 0000000000..5fd5b112d4 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.tsx @@ -0,0 +1,74 @@ +import { EuiBasicTableColumn, EuiInMemoryTable } from '@elastic/eui' +import cx from 'classnames' +import React from 'react' + +import { ErrorImportResult } from 'uiSrc/slices/interfaces' +import { Maybe } from 'uiSrc/utils' + +import styles from './styles.module.scss' + +export interface DataImportResult { + index: number + status: string + errors?: Array + host?: string + port?: number +} +export interface Props { + data: Array +} + +const TableResult = (props: Props) => { + const { data } = props + + const ErrorResult = ({ errors }: { errors: string[] }) => ( +
    + {errors.map((message, i) => ( +
  • {message}
  • + ))} +
+ ) + + const columns: EuiBasicTableColumn[] = [ + { + name: '#', + field: 'index', + width: '4%', + render: (index: number) => (({index})) + }, + { + name: 'Host:Port', + field: 'host', + width: '25%', + truncateText: true, + render: (_host, { host, port, index }) => (
{host}:{port}
) + }, + { + name: 'Result', + field: 'errors', + width: '25%', + render: (errors: Maybe, { index }) => ( +
+ {errors ? ( e.message)} />) : 'Successful'} +
+ ) + } + ] + + if (data?.length === 0) return null + + return ( +
+ +
+ ) +} + +export default TableResult diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/index.ts b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/index.ts new file mode 100644 index 0000000000..9e622e5f46 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/index.ts @@ -0,0 +1,3 @@ +import TableResult from './TableResult' + +export default TableResult diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/styles.module.scss new file mode 100644 index 0000000000..dad38b82f2 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/styles.module.scss @@ -0,0 +1,29 @@ +@import "@elastic/eui/src/global_styling/mixins/helpers"; +@import "@elastic/eui/src/components/table/mixins"; +@import "@elastic/eui/src/global_styling/index"; + +.tableWrapper { + max-height: 200px; + @include euiScrollBar; + + overflow: auto; + + .table { + :global { + .euiTableHeaderCell { + background-color: var(--browserTableRowEven); + } + + .euiTableRowCell { + vertical-align: top !important; + } + + .euiTableCellContent { + white-space: normal !important; + font-size: 12px !important; + padding: 8px 14px; + } + } + } + +} diff --git a/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss index 3f240cd6bf..8fda92df89 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss +++ b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss @@ -1,11 +1,12 @@ .modal { background: var(--euiColorLightestShade) !important; min-width: 500px !important; + max-width: 700px !important; min-height: 270px !important; :global { .euiModalHeader { - padding: 4px 42px 42px 30px; + padding: 4px 42px 20px 30px; } .euiModalBody__overflow { diff --git a/redisinsight/ui/src/slices/interfaces/instances.ts b/redisinsight/ui/src/slices/interfaces/instances.ts index 538900f412..61234c9b6a 100644 --- a/redisinsight/ui/src/slices/interfaces/instances.ts +++ b/redisinsight/ui/src/slices/interfaces/instances.ts @@ -285,13 +285,38 @@ export interface InitialStateInstances { importInstances: { loading: boolean error: string - data: Nullable<{ - success: number, - total: number - }> + data: Nullable } } +export interface ErrorImportResult { + statusCode: number + message: string + error: string +} + +export interface ImportDatabasesData { + fail: Array + partial: Array + success: Array + total: number +} + +export interface FailedImportStatusResult { + host?: string + port?: number + index: number + errors: Array + status: string +} + +export interface SuccessImportStatusResult { + host: string + port: number + index: number + status: string +} + export interface InitialStateEditedInstances { loading: boolean error: string diff --git a/redisinsight/ui/src/styles/components/_table.scss b/redisinsight/ui/src/styles/components/_table.scss index b5173ee337..9b6aba7b3d 100644 --- a/redisinsight/ui/src/styles/components/_table.scss +++ b/redisinsight/ui/src/styles/components/_table.scss @@ -62,7 +62,7 @@ table { overflow: hidden; } - &.noHeaderBorders { + &.noHeaderBorders, &.noBorders { .euiTableRow { &:not(:first-child) { .euiTableRowCell { @@ -81,6 +81,35 @@ table { } } + &.noBorders { + .euiTableCaption { + height: 0; + } + .euiTableRowCell { + border-top: 0; + } + .euiTableRow:nth-child(odd) { + td { + &:first-child { + border-left: 1px solid var(--euiColorEmptyShade); + } + &:last-child { + border-right: 1px solid var(--euiColorEmptyShade); + } + } + } + .euiTableRow:nth-child(even) { + td { + &:first-child { + border-left: 1px solid var(--browserTableRowEven); + } + &:last-child { + border-right: 1px solid var(--browserTableRowEven); + } + } + } + } + &.stickyHeader { .euiTableHeaderCell { position: sticky; diff --git a/redisinsight/ui/src/telemetry/events.ts b/redisinsight/ui/src/telemetry/events.ts index 1185656b4c..435ae4db64 100644 --- a/redisinsight/ui/src/telemetry/events.ts +++ b/redisinsight/ui/src/telemetry/events.ts @@ -30,6 +30,7 @@ export enum TelemetryEvent { CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED = 'CONFIG_DATABASES_REDIS_IMPORT_SUBMITTED', CONFIG_DATABASES_REDIS_IMPORT_CANCELLED = 'CONFIG_DATABASES_REDIS_IMPORT_CANCELLED', CONFIG_DATABASES_REDIS_IMPORT_CLICKED = 'CONFIG_DATABASES_REDIS_IMPORT_CLICKED', + CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED = 'CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED', BUILD_FROM_SOURCE_CLICKED = 'BUILD_FROM_SOURCE_CLICKED', BUILD_USING_DOCKER_CLICKED = 'BUILD_USING_DOCKER_CLICKED', From 0ce880f1ecb66798046e906e7099f4aaa0147e67 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 13 Dec 2022 16:31:48 +0100 Subject: [PATCH 070/107] update for re cloud --- .../smoke/database/add-standalone-db.e2e.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index 772522361a..eedc1724e2 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -1,10 +1,18 @@ import { t } from 'testcafe'; -import { addNewStandaloneDatabase, addNewREClusterDatabase, addNewRECloudDatabase, addOSSClusterDatabase, acceptLicenseTerms, deleteDatabase } from '../../../helpers/database'; +import { + addNewStandaloneDatabase, + addNewREClusterDatabase, + addOSSClusterDatabase, + acceptLicenseTerms, + deleteDatabase, + acceptLicenseTermsAndAddRECloudDatabase +} from '../../../helpers/database'; import { commonUrl, ossStandaloneConfig, ossClusterConfig, - redisEnterpriseClusterConfig + redisEnterpriseClusterConfig, + cloudDatabaseConfig } from '../../../helpers/conf'; import { env, rte } from '../../../helpers/constants'; import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; @@ -12,14 +20,13 @@ import { BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); -fixture`Add database` +fixture `Add database` .meta({ type: 'smoke' }) .page(commonUrl) .beforeEach(async() => { await acceptLicenseTerms(); }); test - .only .meta({ rte: rte.standalone }) .after(async() => { await deleteDatabase(ossStandaloneConfig.databaseName); @@ -53,5 +60,6 @@ test test .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async() => { // New connections indicator - await addNewRECloudDatabase('', ''); + await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); + await browserPage.verifyDatabaseStatusIsVisible(); }); From e87480dedff3536b73c435caedc707d58ea1a3c1 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 13 Dec 2022 16:55:15 +0100 Subject: [PATCH 071/107] moved verifyDatabaseStatusIsVisible() from browser page object to my redis databases also added comments with test names --- tests/e2e/pageObjects/browser-page.ts | 16 --------------- .../pageObjects/my-redis-databases-page.ts | 16 +++++++++++++++ .../database/clone-databases.e2e.ts | 16 ++++++++++----- .../smoke/database/add-standalone-db.e2e.ts | 20 ++++++++++--------- .../database/connecting-to-the-db.e2e.ts | 4 ++++ 5 files changed, 42 insertions(+), 30 deletions(-) diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index b9257a1a2e..2caf1cf679 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -1014,22 +1014,6 @@ export class BrowserPage { .click(this.selectIndexDdn) .click(option); } - - /** - * Verify database status is visible - */ - async verifyDatabaseStatusIsVisible(): Promise { - await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) - .ok("Database status is not visible"); - } - - /** - * Verify database status is not visible - */ - async verifyDatabaseStatusIsNotVisible(): Promise { - await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) - .notOk("Database status is still visible"); - } } /** diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index 668c1d5f40..ce4841cb02 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -177,4 +177,20 @@ export class MyRedisDatabasePage { await t.expect(actualList[k].trim()).eql(sortedList[k].trim()); } } + + /** + * Verify database status is visible + */ + async verifyDatabaseStatusIsVisible(): Promise { + await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) + .ok("Database status is not visible"); + } + + /** + * Verify database status is not visible + */ + async verifyDatabaseStatusIsNotVisible(): Promise { + await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) + .notOk("Database status is still visible"); + } } diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts index b5dc7e1df2..8a11c236a0 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -38,8 +38,10 @@ test await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); // Verify that user can cancel the Clone by clicking the “Cancel” or the “x” button await t.click(addRedisDatabasePage.cloneDatabaseButton); - // New connections indicator - await browserPage.verifyDatabaseStatusIsVisible(); + + // Verify new connection badge for cloned database + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); + await t.click(addRedisDatabasePage.cancelButton); await t.expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).notOk('Clone panel is still displayed', { timeout: 2000 }); await clickOnEditDatabaseByName(ossStandaloneConfig.databaseName); @@ -69,8 +71,10 @@ test .meta({ rte: rte.ossCluster })('Verify that user can clone OSS Cluster', async t => { await clickOnEditDatabaseByName(ossClusterConfig.ossClusterDatabaseName); await t.click(addRedisDatabasePage.cloneDatabaseButton); + // New connections indicator - await browserPage.verifyDatabaseStatusIsVisible(); + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); + await t .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).ok('Clone panel is not displayed') .expect(addRedisDatabasePage.portInput.getAttribute('value')).eql(ossClusterConfig.ossClusterPort, 'Wrong port value') @@ -99,8 +103,10 @@ test .meta({ rte: rte.sentinel })('Verify that user can clone Sentinel', async t => { await clickOnEditDatabaseByName(ossSentinelConfig.name[1]); await t.click(addRedisDatabasePage.cloneDatabaseButton); - // New connections indicator - await browserPage.verifyDatabaseStatusIsVisible(); + + // Verify new connection badge for Sentinel db + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); + // Verify that for Sentinel Host and Port fields are replaced with editable Primary Group Name field await t .expect(myRedisDatabasePage.editAliasButton.withText('Clone ').exists).ok('Clone panel is not displayed') diff --git a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts index eedc1724e2..bee944e72c 100644 --- a/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/add-standalone-db.e2e.ts @@ -32,34 +32,36 @@ test await deleteDatabase(ossStandaloneConfig.databaseName); })('Verify that user can add Standalone Database', async() => { await addNewStandaloneDatabase(ossStandaloneConfig); - await browserPage.verifyDatabaseStatusIsVisible(); + // Verify that user can see an indicator of databases that are added manually and not opened yet + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); await myRedisDatabasePage.clickOnDBByName(ossStandaloneConfig.databaseName); await t.click(browserPage.myRedisDbIcon); - // New connections indicator - await browserPage.verifyDatabaseStatusIsNotVisible(); + // Verify that user can't see an indicator of databases that were opened + await myRedisDatabasePage.verifyDatabaseStatusIsNotVisible(); }); test .meta({ rte: rte.reCluster }) .after(async() => { await deleteDatabase(redisEnterpriseClusterConfig.databaseName); })('Verify that user can add database from RE Cluster via auto-discover flow', async() => { - // New connections indicator await addNewREClusterDatabase(redisEnterpriseClusterConfig); - await browserPage.verifyDatabaseStatusIsVisible(); + // Verify that user can see an indicator of databases that are added using autodiscovery and not opened yet + // Verify new connection badge for RE cluster + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); }); test .meta({ env: env.web, rte: rte.ossCluster }) .after(async() => { await deleteDatabase(ossClusterConfig.ossClusterDatabaseName); })('Verify that user can add OSS Cluster DB', async() => { - // New connections indicator await addOSSClusterDatabase(ossClusterConfig); - await browserPage.verifyDatabaseStatusIsVisible(); + // Verify new connection badge for OSS cluster + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); }); test .meta({ rte: rte.reCloud })('Verify that user can add database from RE Cloud via auto-discover flow', async() => { - // New connections indicator await acceptLicenseTermsAndAddRECloudDatabase(cloudDatabaseConfig); - await browserPage.verifyDatabaseStatusIsVisible(); + // Verify new connection badge for RE cloud + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); }); diff --git a/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts b/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts index 96f7820ab0..785134bb3b 100644 --- a/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts +++ b/tests/e2e/tests/smoke/database/connecting-to-the-db.e2e.ts @@ -27,6 +27,10 @@ test })('Verify that user can connect to Sentinel DB', async t => { // Add OSS Sentinel DB await discoverSentinelDatabase(ossSentinelConfig); + + // Verify new connection badge for Sentinel db + await myRedisDatabasePage.verifyDatabaseStatusIsVisible(); + // Get groups & their count const sentinelGroups = myRedisDatabasePage.dbNameList; const sentinelGroupsCount = await sentinelGroups.count; From 94de3e92a42f549414c9304e5513aa7a955e17a9 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 13 Dec 2022 16:56:35 +0100 Subject: [PATCH 072/107] deleted unused import --- .../e2e/tests/critical-path/database/clone-databases.e2e.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts index 8a11c236a0..dd292f834c 100644 --- a/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/clone-databases.e2e.ts @@ -1,5 +1,5 @@ import { rte } from '../../../helpers/constants'; -import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; +import { AddRedisDatabasePage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl, ossClusterConfig, ossSentinelConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; import { @@ -16,9 +16,8 @@ const addRedisDatabasePage = new AddRedisDatabasePage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const common = new Common(); const newOssDatabaseAlias = 'cloned oss cluster'; -const browserPage = new BrowserPage(); -fixture`Clone databases` +fixture `Clone databases` .meta({ type: 'critical_path' }) .page(commonUrl); test From dbe0380da869d24f83b2f79003d0001e78cc372b Mon Sep 17 00:00:00 2001 From: nmammadli Date: Wed, 14 Dec 2022 08:41:12 +0100 Subject: [PATCH 073/107] Comments from check list --- .../tree-view/tree-view-improvements.e2e.ts | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index 1380cd2b77..fe3b036161 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -33,8 +33,7 @@ test await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Tree view preselected folder, Filterred Tree view preselected folder, Refreshed Tree view preselected folder, Search capability Filterred Tree view preselected folder', async t => { - // Search capability Refreshed Tree view preselected folder + })('Tree view preselected folder', async t => { keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name keynameSingle = common.generateWord(10); @@ -51,60 +50,65 @@ test // Create 5 keys await cliPage.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); + // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns await verifyKeysDisplayedInTheList([keynameSingle]); await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); + // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter await verifyKeysDisplayedInTheList([keynameSingle]); await t.click(browserPage.setDeleteButton); - // verify that first element in the tree is "keys" and other folders are closed + // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns await verifyKeysDisplayedInTheList([keynameSingle]); await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); // switch between browser view and tree view await t.click(browserPage.browserViewButton) .click(browserPage.treeViewButton); - // Switch to tree view await browserPage.deleteKeyByName(keyNames[4]); await t.click(browserPage.clearFilterButton); // get first folder name const firstTreeItemText = await browserPage.getTextFromNthTreeElement(0); const firstTreeItemKeys = Selector(`[data-testid="node-item_${firstTreeItemText}:keys:keys:"]`); // keys after node item opened - // verify that the first folder with namespaces is expanded and selected + // The first folder with namespaces is expanded and selected when there is no folder without any patterns await t.expect(firstTreeItemKeys.visible) .ok('First folder is not expanded'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); const commands1 = [ `HSET ${keyNames[4]} field value` ]; - // Create 4 keys and index + await cliPage.sendCommandsInCli(commands1); - await t.click(browserPage.refreshKeysButton); // refresh keys + await t.click(browserPage.refreshKeysButton); + // Refreshed Tree view preselected folder await t.expect(firstTreeItemKeys.visible) .ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + // Filterred Tree view preselected folder + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.searchByKeyName('*'); + // Search capability Filterred Tree view preselected folder await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await t.click(browserPage.clearFilterButton); + // Filterred Tree view preselected folder await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); - await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); // verify created keys are visible + await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); + // Filterred Tree view preselected folder await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); await t.click(browserPage.streamDeleteButton); // clear stream from filter - await t.expect( - Selector('[role="rowgroup"]').find('div').withText('No results found.').visible) - .notOk('No result text is still visible'); + // Filterred Tree view preselected folder + await t.expect(browserPage.keyListTable.textContent).notContains('No results found.', 'Key is not found message still displayed'); await t.expect( firstTreeItemKeys.visible) .notOk('First folder is expanded'); @@ -127,6 +131,7 @@ test await t.click(Selector(`[data-testid="${`node-item_${folders[0]}:`}"]`)); // close folder await browserPage.openTreeFolders(folders); await t.click(browserPage.refreshKeysButton); + // Refreshed Tree view preselected folder for index based search await t.expect( Selector(`[data-testid="node-item_${folders[0]}:${folders[1]}:keys:keys:"]`).visible) .ok('Folder is not selected'); @@ -140,7 +145,7 @@ test await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames.slice(1)); await deleteStandaloneDatabaseApi(ossStandaloneConfig); - })('Refreshed Tree view preselected folder, Filterred Tree view preselected folder', async t => { + })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name keynameSingle = common.generateWord(10); @@ -154,16 +159,25 @@ test ]; await cliPage.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); + // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns await verifyKeysDisplayedInTheList([keynameSingle]); await browserPage.openTreeFolders([keyName1]); // Type: hash await browserPage.openTreeFolders([keyName2]); // Type: list await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); + // The first folder with namespaces is expanded and selected when folder and folder without any namespaces does not exist after search/filter await verifyKeysDisplayedInTheList([keyNames[0], keyNames[1]]); await t.click(browserPage.hashDeleteButton); await cliPage.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys + // The previously selected folder is preselected when key does not exist after keys refresh + await verifyKeysDisplayedInTheList([keyNames[1]]); + await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); + + await browserPage.searchByKeyName('*'); + await t.click(browserPage.refreshKeysButton); + // Search capability Refreshed Tree view preselected folder await verifyKeysDisplayedInTheList([keyNames[1]]); await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); }); From 4258ede2505b40e35ed011acb5a2ab4aea0cf791 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 09:55:52 +0200 Subject: [PATCH 074/107] resolve merge conflicts --- redisinsight/api/src/__mocks__/databases.ts | 4 +++- .../api/src/modules/database/database-connection.service.ts | 3 +-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/redisinsight/api/src/__mocks__/databases.ts b/redisinsight/api/src/__mocks__/databases.ts index cd9db9d127..b86f8eb7b7 100644 --- a/redisinsight/api/src/__mocks__/databases.ts +++ b/redisinsight/api/src/__mocks__/databases.ts @@ -8,6 +8,7 @@ import { mockSentinelMasterDto } from 'src/__mocks__/redis-sentinel'; import { pick } from 'lodash'; import { RedisDatabaseInfoResponse } from 'src/modules/database/dto/redis-info.dto'; import { DatabaseOverview } from 'src/modules/database/models/database-overview'; +import { ClientContext, ClientMetadata } from 'src/common/models'; export const mockDatabaseId = 'a77b23c1-7816-4ea4-b61f-d37795a0f805-db-id'; @@ -120,8 +121,9 @@ export const mockNewDatabase = Object.assign(new Database(), { }); export const mockClientMetadata: ClientMetadata = { + session: undefined, databaseId: mockDatabase.id, - namespace: AppTool.Common, + context: ClientContext.Common, }; export const mockDatabaseOverview: DatabaseOverview = { diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index 895d129c32..8acb049c0d 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -32,7 +32,6 @@ export class DatabaseConnectionService { // mark database as not a new // will be refreshed after user navigate to particular database from the databases list // Note: move to a different place in case if we need to update such info more often - await this.repository.update(clientMetadata.databaseId, { const toUpdate: Partial = { new: false, lastConnection: new Date(), @@ -52,7 +51,7 @@ export class DatabaseConnectionService { })); } - await this.repository.update(databaseId, toUpdate); + await this.repository.update(clientMetadata.databaseId, toUpdate); this.logger.log(`Succeed to connect to database ${clientMetadata.databaseId}`); } From c09f6d96c2d8a5e66599d9ef8849d00b5ef8a40d Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 10:09:12 +0200 Subject: [PATCH 075/107] resolve merge conflicts --- .../modules/database-import/database-import.service.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 1ecb0b5a1b..5ceea13dd8 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -117,11 +117,9 @@ export class DatabaseImportService { try { const data: any = {}; - // set this is a new connection - data.new = true + // set this is a new connection + data.new = true; - this.fieldsMapSchema.forEach(([field, paths]) => { - let value; this.fieldsMapSchema.forEach(([field, paths]) => { let value; @@ -130,8 +128,8 @@ export class DatabaseImportService { return value === undefined; }); - set(data, field, value); - }); + set(data, field, value); + }); // set database name if needed if (!data.name) { From 65fbe73813b439c1ca0b00e9ecb14b2e1a0b27c2 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 10:41:05 +0200 Subject: [PATCH 076/107] resolve merge conflicts --- .../api/src/__mocks__/database-import.ts | 4 + .../database-import.service.spec.ts | 70 ++++++++++++++++- .../database-import.service.ts | 77 ++++++++++++++++--- .../dto/import.database.dto.ts | 3 +- .../database/database-connection.service.ts | 21 ++++- .../database/entities/database.entity.ts | 1 + .../src/modules/database/models/database.ts | 2 + .../api/src/modules/redis/redis.service.ts | 24 +++++- 8 files changed, 188 insertions(+), 14 deletions(-) diff --git a/redisinsight/api/src/__mocks__/database-import.ts b/redisinsight/api/src/__mocks__/database-import.ts index f94da3ceb5..e15d864f41 100644 --- a/redisinsight/api/src/__mocks__/database-import.ts +++ b/redisinsight/api/src/__mocks__/database-import.ts @@ -62,3 +62,7 @@ export const mockDatabaseImportAnalytics = jest.fn(() => ({ sendImportResults: jest.fn(), sendImportFailed: jest.fn(), })); + +export const mockCertificateImportService = jest.fn(() => { + +}); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts index 56773f0890..472e01ae02 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -1,6 +1,7 @@ import { pick } from 'lodash'; import { DatabaseImportService } from 'src/modules/database-import/database-import.service'; import { + mockCertificateImportService, mockDatabase, mockDatabaseImportAnalytics, mockDatabaseImportFile, @@ -14,9 +15,11 @@ import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { ValidationError } from 'class-validator'; import { - NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException, + NoDatabaseImportFileProvidedException, + SizeLimitExceededDatabaseImportFileException, UnableToParseDatabaseImportFileException, } from 'src/modules/database-import/exceptions'; +import { CertificateImportService } from 'src/modules/database-import/certificate-import.service'; describe('DatabaseImportService', () => { let service: DatabaseImportService; @@ -36,6 +39,10 @@ describe('DatabaseImportService', () => { create: jest.fn().mockResolvedValue(mockDatabase), })), }, + { + provide: CertificateImportService, + useFactory: mockCertificateImportService, + }, { provide: DatabaseImportAnalytics, useFactory: mockDatabaseImportAnalytics, @@ -154,6 +161,7 @@ describe('DatabaseImportService', () => { it('should create cluster database', async () => { await service['createDatabase']({ ...mockDatabase, + connectionType: undefined, cluster: true, }, 0); @@ -163,4 +171,64 @@ describe('DatabaseImportService', () => { }); }); }); + + describe('determineConnectionType', () => { + const tcs = [ + // common + { input: {}, output: ConnectionType.NOT_CONNECTED }, + // isCluster + { input: { isCluster: true }, output: ConnectionType.CLUSTER }, + { input: { isCluster: false }, output: ConnectionType.NOT_CONNECTED }, + { input: { isCluster: undefined }, output: ConnectionType.NOT_CONNECTED }, + // sentinelMasterName + { input: { sentinelMasterName: 'some name' }, output: ConnectionType.SENTINEL }, + // connectionType + { input: { connectionType: ConnectionType.STANDALONE }, output: ConnectionType.STANDALONE }, + { input: { connectionType: ConnectionType.CLUSTER }, output: ConnectionType.CLUSTER }, + { input: { connectionType: ConnectionType.SENTINEL }, output: ConnectionType.SENTINEL }, + { input: { connectionType: 'something not supported' }, output: ConnectionType.NOT_CONNECTED }, + // type + { input: { type: 'standalone' }, output: ConnectionType.STANDALONE }, + { input: { type: 'cluster' }, output: ConnectionType.CLUSTER }, + { input: { type: 'sentinel' }, output: ConnectionType.SENTINEL }, + { input: { type: 'something not supported' }, output: ConnectionType.NOT_CONNECTED }, + // priority tests + { + input: { + connectionType: ConnectionType.SENTINEL, + type: 'standalone', + isCluster: true, + sentinelMasterName: 'some name', + }, + output: ConnectionType.SENTINEL, + }, + { + input: { + type: 'standalone', + isCluster: true, + sentinelMasterName: 'some name', + }, + output: ConnectionType.STANDALONE, + }, + { + input: { + isCluster: true, + sentinelMasterName: 'some name', + }, + output: ConnectionType.CLUSTER, + }, + { + input: { + sentinelMasterName: 'some name', + }, + output: ConnectionType.SENTINEL, + }, + ]; + + tcs.forEach((tc) => { + it(`should return ${tc.output} when called with ${JSON.stringify(tc.input)}`, () => { + expect(DatabaseImportService.determineConnectionType(tc.input)).toEqual(tc.output); + }); + }); + }); }); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index bd5965d3a5..2c010a80f0 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -37,13 +37,20 @@ export class DatabaseImportService { ['port', ['port']], ['db', ['db']], ['isCluster', ['cluster']], + ['type', ['type']], + ['connectionType', ['connectionType']], ['tls', ['tls', 'ssl']], - ['tlsServername', ['tlsServername', 'sni_name', 'sni_server_name']], + ['tlsServername', ['tlsServername']], ['tlsCaName', ['caCert.name']], - ['tlsCaCert', ['caCert.certificate', 'sslOptions.ca', 'ssl_ca_cert_path']], + ['tlsCaCert', ['caCert.certificate', 'caCert', 'sslOptions.ca', 'ssl_ca_cert_path']], ['tlsClientName', ['clientCert.name']], - ['tlsClientCert', ['clientCert.certificate', 'sslOptions.cert', 'ssl_local_cert_path']], - ['tlsClientKey', ['clientCert.key', 'sslOptions.key', 'ssl_private_key_path']], + ['tlsClientCert', ['clientCert.certificate', 'certificate', 'sslOptions.cert', 'ssl_local_cert_path']], + ['tlsClientKey', ['clientCert.key', 'keyFile', 'sslOptions.key', 'ssl_private_key_path']], + ['sentinelMasterName', ['sentinelMaster.name', 'sentinelOptions.masterName', 'sentinelOptions.name']], + ['sentinelMasterUsername', ['sentinelMaster.username']], + ['sentinelMasterPassword', [ + 'sentinelMaster.password', 'sentinelOptions.nodePassword', 'sentinelOptions.sentinelPassword', + ]], ]; constructor( @@ -83,6 +90,8 @@ export class DatabaseImportService { fail: [], }; + console.log('___ items', items); + // it is very important to insert databases on-by-one to avoid db constraint errors await items.reduce((prev, item, index) => prev.finally(() => this.createDatabase(item, index) .then((result) => { @@ -144,11 +153,18 @@ export class DatabaseImportService { data.name = `${data.host}:${data.port}`; } - // determine database type - if (data.isCluster) { - data.connectionType = ConnectionType.CLUSTER; - } else { - data.connectionType = ConnectionType.STANDALONE; + data.connectionType = DatabaseImportService.determineConnectionType(data); + + if (data?.sentinelMasterName) { + data.sentinelMaster = { + name: data.sentinelMasterName, + username: data.sentinelMasterUsername, + password: data.sentinelMasterPassword, + }; + data.nodes = [{ + host: data.host, + port: parseInt(data.port, 10), + }]; } if (data?.tlsCaCert) { @@ -237,6 +253,49 @@ export class DatabaseImportService { } } + /** + * Try to determine connection type based on input data + * Should return NOT_CONNECTED when it is not possible + * @param data + */ + static determineConnectionType(data: any = {}): ConnectionType { + if (data?.connectionType) { + switch (data.connectionType) { + case ConnectionType.CLUSTER: + return ConnectionType.CLUSTER; + case ConnectionType.SENTINEL: + return ConnectionType.SENTINEL; + case ConnectionType.STANDALONE: + return ConnectionType.STANDALONE; + default: + return ConnectionType.NOT_CONNECTED; + } + } + + if (data?.type) { + switch (data.type) { + case 'cluster': + return ConnectionType.CLUSTER; + case 'sentinel': + return ConnectionType.SENTINEL; + case 'standalone': + return ConnectionType.STANDALONE; + default: + return ConnectionType.NOT_CONNECTED; + } + } + + if (data?.isCluster === true) { + return ConnectionType.CLUSTER; + } + + if (data?.sentinelMasterName) { + return ConnectionType.SENTINEL; + } + + return ConnectionType.NOT_CONNECTED; + } + /** * Try to parse file based on mimetype and known\supported formats * @param file diff --git a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts index 46215ef872..e62ba1a3be 100644 --- a/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts +++ b/redisinsight/api/src/modules/database-import/dto/import.database.dto.ts @@ -13,7 +13,7 @@ import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client- export class ImportDatabaseDto extends PickType(Database, [ 'host', 'port', 'name', 'db', 'username', 'password', - 'connectionType', 'tls', 'verifyServerCert', + 'connectionType', 'tls', 'verifyServerCert', 'sentinelMaster', 'nodes', ] as const) { @Expose() @IsNotEmpty() @@ -43,5 +43,4 @@ export class ImportDatabaseDto extends PickType(Database, [ @Type(clientCertTransformer) @ValidateNested() clientCert?: CreateClientCertificateDto | UseClientCertificateDto; - } diff --git a/redisinsight/api/src/modules/database/database-connection.service.ts b/redisinsight/api/src/modules/database/database-connection.service.ts index 932458daaa..be490865a1 100644 --- a/redisinsight/api/src/modules/database/database-connection.service.ts +++ b/redisinsight/api/src/modules/database/database-connection.service.ts @@ -9,6 +9,7 @@ import { ClientMetadata } from 'src/modules/redis/models/client-metadata'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { Database } from 'src/modules/database/models/database'; +import { ConnectionType } from 'src/modules/database/entities/database.entity'; @Injectable() export class DatabaseConnectionService { @@ -111,11 +112,29 @@ export class DatabaseConnectionService { const connectionName = generateRedisConnectionName(clientMetadata.namespace, clientMetadata.databaseId); try { - return await this.redisService.connectToDatabaseInstance( + const client = await this.redisService.connectToDatabaseInstance( database, clientMetadata.namespace, connectionName, ); + + if (database.connectionType === ConnectionType.NOT_CONNECTED) { + let connectionType = ConnectionType.STANDALONE; + + // cluster check + if (client.isCluster) { + connectionType = ConnectionType.CLUSTER; + } + + // sentinel check + if (client?.options?.['sentinels']?.length) { + connectionType = ConnectionType.SENTINEL; + } + + await this.repository.update(database.id, { connectionType }); + } + + return client; } catch (error) { this.logger.error('Failed to create database client', error); const exception = getRedisConnectionException( diff --git a/redisinsight/api/src/modules/database/entities/database.entity.ts b/redisinsight/api/src/modules/database/entities/database.entity.ts index 179df10fe0..6d9b8decbd 100644 --- a/redisinsight/api/src/modules/database/entities/database.entity.ts +++ b/redisinsight/api/src/modules/database/entities/database.entity.ts @@ -21,6 +21,7 @@ export enum ConnectionType { STANDALONE = 'STANDALONE', CLUSTER = 'CLUSTER', SENTINEL = 'SENTINEL', + NOT_CONNECTED = 'NOT CONNECTED', } @Entity('database_instance') diff --git a/redisinsight/api/src/modules/database/models/database.ts b/redisinsight/api/src/modules/database/models/database.ts index 8531689622..2abac76c3d 100644 --- a/redisinsight/api/src/modules/database/models/database.ts +++ b/redisinsight/api/src/modules/database/models/database.ts @@ -142,6 +142,8 @@ export class Database { type: Endpoint, isArray: true, }) + @IsOptional() + @Type(() => Endpoint) @Expose() nodes?: Endpoint[]; diff --git a/redisinsight/api/src/modules/redis/redis.service.ts b/redisinsight/api/src/modules/redis/redis.service.ts index 1780ad3b6e..4e845114de 100644 --- a/redisinsight/api/src/modules/redis/redis.service.ts +++ b/redisinsight/api/src/modules/redis/redis.service.ts @@ -195,12 +195,34 @@ export class RedisService { client = await this.createSentinelClient(database, nodes, tool, true, connectionName); break; default: - client = await this.createStandaloneClient(database, tool, true, connectionName); + // AUTO + client = await this.createClientAutomatically(database, tool, connectionName); } return client; } + public async createClientAutomatically(database: Database, tool: AppTool, connectionName) { + // try sentinel connection + if (database?.sentinelMaster) { + try { + return await this.createSentinelClient(database, database.nodes, tool, true, connectionName); + } catch (e) { + // ignore error + } + } + + // try cluster connection + try { + return await this.createClusterClient(database, database.nodes, true, connectionName); + } catch (e) { + // ignore error + } + + // Standalone in any other case + return this.createStandaloneClient(database, tool, true, connectionName); + } + public isClientConnected(client: Redis | Cluster): boolean { try { return client.status === 'ready'; From 6f8868fce24b52ca7909b5064d74bd95d057b527 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 10:42:14 +0200 Subject: [PATCH 077/107] temporary run all tests --- .circleci/config.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dcb3c7bac8..f7aac81540 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -896,10 +896,11 @@ workflows: matrix: alias: itest-code parameters: - rte: *iTestsNamesShort +# rte: *iTestsNamesShort + rte: *iTestsNames name: ITest - << matrix.rte >> (code) - requires: - - UTest - API +# requires: +# - UTest - API # E2E tests for "e2e/feature" or "e2e/bugfix" branches only e2e-tests: jobs: From 2e9856f95750184a0aef1e225a793ecea34b78e8 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Wed, 14 Dec 2022 13:18:15 +0300 Subject: [PATCH 078/107] #RI-3903 - add tests, ui changes --- .../ImportDatabasesDialog.tsx | 10 +- .../components/ResultsLog/ResultsLog.spec.tsx | 122 ++++++++++++++++++ .../components/ResultsLog/styles.module.scss | 8 +- .../TableResult/TableResult.spec.tsx | 46 +++++++ .../ui/src/slices/instances/instances.ts | 1 - .../slices/tests/instances/instances.spec.ts | 25 ++-- .../themes/dark_theme/_dark_theme.lazy.scss | 4 + .../themes/dark_theme/_theme_color.scss | 4 + .../themes/light_theme/_light_theme.lazy.scss | 4 + .../themes/light_theme/_theme_color.scss | 4 + 10 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.spec.tsx create mode 100644 redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.spec.tsx diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx index 9737f4e9dc..9187411a36 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -2,7 +2,8 @@ import { EuiButton, EuiFilePicker, EuiFlexGroup, - EuiFlexItem, EuiIcon, + EuiFlexItem, + EuiIcon, EuiLoadingSpinner, EuiModal, EuiModalBody, @@ -49,11 +50,11 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { } const handleOnClose = () => { - dispatch(resetImportInstances()) if (data?.success?.length || data?.partial?.length) { dispatch(fetchInstancesAction()) } onClose(!data) + dispatch(resetImportInstances()) } const onSubmit = () => { @@ -75,7 +76,7 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { - {(!data && !error) ? 'Import Database Connections' : 'Import results'} + {(!data && !error) ? 'Import Database Connections' : 'Import Results'} @@ -113,9 +114,10 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { {error && (
- + Failed to add database connections + {error}
)} diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.spec.tsx b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.spec.tsx new file mode 100644 index 0000000000..46f3717d5b --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.spec.tsx @@ -0,0 +1,122 @@ +import React from 'react' +import { render, screen, fireEvent, within } from 'uiSrc/utils/test-utils' +import { ImportDatabasesData } from 'uiSrc/slices/interfaces' +import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' +import ResultsLog from './ResultsLog' + +jest.mock('uiSrc/telemetry', () => ({ + ...jest.requireActual('uiSrc/telemetry'), + sendEventTelemetry: jest.fn(), +})) + +const mockedError = { statusCode: 400, message: 'message', error: 'error' } +describe('ResultsLog', () => { + it('should render', () => { + const mockedData = { total: 0, fail: [], partial: [], success: [] } + render() + }) + + it('should be all collapsed nav groups', () => { + const mockedData: ImportDatabasesData = { + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] + } + render() + + expect(screen.getByTestId('success-results-closed')).toBeInTheDocument() + expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument() + expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument() + }) + + it('should open and collapse other groups', () => { + const mockedData: ImportDatabasesData = { + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] + } + render() + + fireEvent.click( + within(screen.getByTestId('success-results-closed')).getByRole('button') + ) + expect(screen.getByTestId('success-results-open')).toBeInTheDocument() + + expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument() + expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument() + + fireEvent.click( + within(screen.getByTestId('failed-results-closed')).getByRole('button') + ) + expect(screen.getByTestId('failed-results-open')).toBeInTheDocument() + + expect(screen.getByTestId('partial-results-closed')).toBeInTheDocument() + expect(screen.getByTestId('success-results-closed')).toBeInTheDocument() + + fireEvent.click( + within(screen.getByTestId('partial-results-closed')).getByRole('button') + ) + expect(screen.getByTestId('partial-results-open')).toBeInTheDocument() + + expect(screen.getByTestId('failed-results-closed')).toBeInTheDocument() + expect(screen.getByTestId('success-results-closed')).toBeInTheDocument() + }) + + it('should show proper items length', () => { + const mockedData: ImportDatabasesData = { + total: 4, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [ + { index: 1, status: 'success', port: 1233, host: 'localhost' }, + { index: 3, status: 'success', port: 1233, host: 'localhost' } + ] + } + render() + + expect( + within(screen.getByTestId('success-results-closed')).getByTestId('number-of-dbs') + ).toHaveTextContent('2') + expect( + within(screen.getByTestId('partial-results-closed')).getByTestId('number-of-dbs') + ).toHaveTextContent('1') + expect( + within(screen.getByTestId('failed-results-closed')).getByTestId('number-of-dbs') + ).toHaveTextContent('1') + }) + + it('should call proper telemetry event after click', () => { + const sendEventTelemetryMock = jest.fn(); + (sendEventTelemetry as jest.Mock).mockImplementation(() => sendEventTelemetryMock) + + const mockedData: ImportDatabasesData = { + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] + } + render() + + fireEvent.click( + within(screen.getByTestId('success-results-closed')).getByRole('button') + ) + + expect(sendEventTelemetry).toBeCalledWith({ + event: TelemetryEvent.CONFIG_DATABASES_REDIS_IMPORT_LOG_VIEWED, + eventData: { + length: 1, + name: 'success' + } + }); + + (sendEventTelemetry as jest.Mock).mockRestore() + + fireEvent.click( + within(screen.getByTestId('success-results-open')).getByRole('button') + ) + + expect(sendEventTelemetry).not.toBeCalled() + }) +}) diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss index 6b15697083..8ce336e68f 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss +++ b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/styles.module.scss @@ -26,19 +26,19 @@ &:global(.success) { &:before { - background-color: #13A450; + background-color: var(--successBorderColor); } } &:global(.partial) { &:before { - background-color: #9D6901; + background-color: var(--warningBorderColor); } } &:global(.failed) { &:before { - background-color: #AD0017; + background-color: var(--errorBorderColor); } } @@ -65,7 +65,7 @@ background-color: var(--euiColorEmptyShade); padding: 12px 24px 12px 32px !important; border-radius: 4px; - border: 1px solid #3D3D3D; + border: 1px solid var(--separatorColor); } .euiCollapsibleNavGroup__children { padding: 0 !important; diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.spec.tsx b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.spec.tsx new file mode 100644 index 0000000000..84aaded8a5 --- /dev/null +++ b/redisinsight/ui/src/components/import-databases-dialog/components/TableResult/TableResult.spec.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { render, screen } from 'uiSrc/utils/test-utils' +import TableResult from './TableResult' + +const mockedError = { statusCode: 400, message: 'message', error: 'error' } + +describe('TableResult', () => { + it('should render', () => { + render() + }) + + it('should not render table for empty data', () => { + render() + + expect(screen.queryByTestId('result-log-table')).not.toBeInTheDocument() + }) + + it('should render table data with success messages', () => { + render( + + ) + + expect(screen.getByTestId('table-index-0')).toHaveTextContent('(0)') + expect(screen.getByTestId('table-index-1')).toHaveTextContent('(1)') + expect(screen.getByTestId('table-host-port-0')).toHaveTextContent('localhost:1233') + expect(screen.getByTestId('table-host-port-1')).toHaveTextContent('localhost2:5233') + expect(screen.getByTestId('table-result-0')).toHaveTextContent('Successful') + expect(screen.getByTestId('table-result-1')).toHaveTextContent('Successful') + }) + + it('should render table data with error messages', () => { + render( + + ) + expect(screen.getByTestId('table-result-0')).toHaveTextContent([mockedError, mockedError].map((e) => e.message).join('')) + expect(screen.getByTestId('table-result-1')).toHaveTextContent([mockedError].map((e) => e.message).join('')) + }) +}) diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index d523757f91..7fc52c2789 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -544,7 +544,6 @@ export function uploadInstancesFile( } catch (error) { const errorMessage = getApiErrorMessage(error) dispatch(importInstancesFromFileFailure(errorMessage)) - dispatch(addErrorNotification(error)) onFailAction?.() } } diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index d148825388..24a5cff3dc 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -47,7 +47,8 @@ import reducer, { importInstancesFromFileSuccess, importInstancesFromFileFailure, resetImportInstances, - importInstancesSelector, uploadInstancesFile + importInstancesSelector, + uploadInstancesFile } from '../../instances/instances' import { addErrorNotification, addMessageNotification, IAddInstanceErrorPayload } from '../../app/notifications' import { ConnectionType, InitialStateInstances, Instance } from '../../interfaces' @@ -542,9 +543,12 @@ describe('instances slice', () => { describe('importInstancesFromFileSuccess', () => { it('should properly set state', () => { // Arrange + const mockedError = { statusCode: 400, message: 'message', error: 'error' } const data = { - success: 3, - total: 5 + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] } const state = { ...initialState.importInstances, @@ -591,13 +595,16 @@ describe('instances slice', () => { describe('resetImportInstances', () => { it('should properly set state', () => { // Arrange + const mockedError = { statusCode: 400, message: 'message', error: 'error' } const currentState = { ...initialState, importInstances: { ...initialState.importInstances, data: { - success: 1, - total: 2 + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] } } } @@ -1110,9 +1117,12 @@ describe('instances slice', () => { it('should call proper actions on success', async () => { // Arrange const formData = new FormData() + const mockedError = { statusCode: 400, message: 'message', error: 'error' } const data = { - success: 0, - total: 1 + total: 3, + fail: [{ index: 0, status: 'fail', errors: [mockedError] }], + partial: [{ index: 2, status: 'fail', errors: [mockedError] }], + success: [{ index: 1, status: 'success', port: 1233, host: 'localhost' }] } const responsePayload = { data, status: 200 } @@ -1150,7 +1160,6 @@ describe('instances slice', () => { const expectedActions = [ importInstancesFromFile(), importInstancesFromFileFailure(responsePayload.response.data.message), - addErrorNotification(responsePayload as AxiosError), ] expect(store.getActions()).toEqual(expectedActions) }) diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss index 2dee6e4865..4311b3f876 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_dark_theme.lazy.scss @@ -131,6 +131,10 @@ --monacoBgColor: #{$monacoBgColor}; + --successBorderColor: #{$successBorderColor}; + --warningBorderColor: #{$warningBorderColor}; + --errorBorderColor: #{$errorBorderColor}; + // KeyTypes --typeHashColor: #{$typeHashColor}; --typeListColor: #{$typeListColor}; diff --git a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss index bcfb32c4f2..f17f38159a 100644 --- a/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/dark_theme/_theme_color.scss @@ -91,6 +91,10 @@ $overlayPromoNYColor: #0000001a; $monacoBgColor: #111; +$successBorderColor: #13A450; +$warningBorderColor: #9D6901; +$errorBorderColor: #AD0017; + // Types colors $typeHashColor: #364cff; $typeListColor: #008556; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss index a9fbfa450e..0d0b4ae649 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_light_theme.lazy.scss @@ -133,6 +133,10 @@ --monacoBgColor: #{$monacoBgColor}; + --successBorderColor: #{$successBorderColor}; + --warningBorderColor: #{$warningBorderColor}; + --errorBorderColor: #{$errorBorderColor}; + // KeyTypes --typeHashColor: #{$typeHashColor}; --typeListColor: #{$typeListColor}; diff --git a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss index e925a43fe6..3683d216f7 100644 --- a/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss +++ b/redisinsight/ui/src/styles/themes/light_theme/_theme_color.scss @@ -88,6 +88,10 @@ $overlayPromoNYColor: #ffffff1a; $monacoBgColor: #f0f2f7; +$successBorderColor: #5BC69B; +$warningBorderColor: #FFAF2B; +$errorBorderColor: #F74B57; + // Types colors $typeHashColor: #cdddf8; $typeListColor: #a5d4c3; From ddb6ebdcefba719809961b503a8cca9cd66e8227 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Wed, 14 Dec 2022 13:40:13 +0300 Subject: [PATCH 079/107] #RI-3903 - fix tests --- .../ImportDatabasesDialog.spec.tsx | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx index 61be09a4f4..ef6e712a3b 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.spec.tsx @@ -92,31 +92,25 @@ describe('ImportDatabasesDialog', () => { expect(screen.getByTestId('file-loading-indicator')).toBeInTheDocument() }) - it('should render success message when at least 1 database added', () => { + it('should not render error message without error', () => { (importInstancesSelector as jest.Mock).mockImplementation(() => ({ loading: false, - data: { - success: 1, - total: 2 - } + data: {} })) render() - expect(screen.getByTestId('result-success')).toBeInTheDocument() expect(screen.queryByTestId('result-failed')).not.toBeInTheDocument() }) it('should render error message when 0 success databases added', () => { (importInstancesSelector as jest.Mock).mockImplementation(() => ({ loading: false, - data: { - success: 0, - total: 2 - } + data: null, + error: 'Error message' })) render() expect(screen.getByTestId('result-failed')).toBeInTheDocument() - expect(screen.queryByTestId('result-success')).not.toBeInTheDocument() + expect(screen.getByTestId('result-failed')).toHaveTextContent('Error message') }) }) From b02beea00e7f02d24eb3b8f9014e4213af7b7758 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 13:29:07 +0200 Subject: [PATCH 080/107] rollback CircleCI configs + fix PR comments + fix bug when unable to descover sentinel databases with existing certs --- .circleci/config.yml | 7 +++---- .../database-import/database-import.service.ts | 15 +++------------ .../redis-sentinel/redis-sentinel.service.spec.ts | 6 ++++++ .../redis-sentinel/redis-sentinel.service.ts | 5 ++++- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index f7aac81540..dcb3c7bac8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -896,11 +896,10 @@ workflows: matrix: alias: itest-code parameters: -# rte: *iTestsNamesShort - rte: *iTestsNames + rte: *iTestsNamesShort name: ITest - << matrix.rte >> (code) -# requires: -# - UTest - API + requires: + - UTest - API # E2E tests for "e2e/feature" or "e2e/bugfix" branches only e2e-tests: jobs: diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 2c010a80f0..7d146edfe4 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -90,8 +90,6 @@ export class DatabaseImportService { fail: [], }; - console.log('___ items', items); - // it is very important to insert databases on-by-one to avoid db constraint errors await items.reduce((prev, item, index) => prev.finally(() => this.createDatabase(item, index) .then((result) => { @@ -260,16 +258,9 @@ export class DatabaseImportService { */ static determineConnectionType(data: any = {}): ConnectionType { if (data?.connectionType) { - switch (data.connectionType) { - case ConnectionType.CLUSTER: - return ConnectionType.CLUSTER; - case ConnectionType.SENTINEL: - return ConnectionType.SENTINEL; - case ConnectionType.STANDALONE: - return ConnectionType.STANDALONE; - default: - return ConnectionType.NOT_CONNECTED; - } + return (data.connectionType in ConnectionType) + ? ConnectionType[data.connectionType] + : ConnectionType.NOT_CONNECTED; } if (data?.type) { diff --git a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.spec.ts b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.spec.ts index 94591a26ab..384a8ebcd4 100644 --- a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.spec.ts +++ b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.spec.ts @@ -3,6 +3,7 @@ import { BadRequestException } from '@nestjs/common'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { RedisService } from 'src/modules/redis/redis.service'; import { + mockDatabaseFactory, mockDatabaseInfoProvider, mockDatabaseService, mockIORedisClient, @@ -15,6 +16,7 @@ import { RedisSentinelService } from 'src/modules/redis-sentinel/redis-sentinel. import { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; +import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; describe('RedisSentinelService', () => { let service: RedisSentinelService; @@ -38,6 +40,10 @@ describe('RedisSentinelService', () => { provide: DatabaseService, useFactory: mockDatabaseService, }, + { + provide: DatabaseFactory, + useFactory: mockDatabaseFactory, + }, { provide: DatabaseInfoProvider, useFactory: mockDatabaseInfoProvider, diff --git a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts index 540d5a6292..2ae406f4f5 100644 --- a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts +++ b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts @@ -12,6 +12,7 @@ import { getRedisConnectionException } from 'src/utils'; import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master'; import { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; +import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; @Injectable() export class RedisSentinelService { @@ -20,6 +21,7 @@ export class RedisSentinelService { constructor( private readonly redisService: RedisService, private readonly databaseService: DatabaseService, + private readonly databaseFactory: DatabaseFactory, private readonly databaseInfoProvider: DatabaseInfoProvider, private readonly redisSentinelAnalytics: RedisSentinelAnalytics, ) {} @@ -113,7 +115,8 @@ export class RedisSentinelService { this.logger.log('Connection and getting sentinel masters.'); let result: SentinelMaster[]; try { - const client = await this.redisService.createStandaloneClient(dto, AppTool.Common, false); + const database = await this.databaseFactory.createStandaloneDatabaseModel(dto); + const client = await this.redisService.createStandaloneClient(database, AppTool.Common, false); result = await this.databaseInfoProvider.determineSentinelMasterGroups(client); this.redisSentinelAnalytics.sendGetSentinelMastersSucceedEvent(result); From 9e3444d59967255fbbc5759b0c7f32e320a33001 Mon Sep 17 00:00:00 2001 From: Artem Date: Wed, 14 Dec 2022 13:45:46 +0200 Subject: [PATCH 081/107] resolve conflicts. again... --- redisinsight/api/src/modules/redis/redis.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/redis/redis.service.ts b/redisinsight/api/src/modules/redis/redis.service.ts index 24c5b8c8d5..de94a88e79 100644 --- a/redisinsight/api/src/modules/redis/redis.service.ts +++ b/redisinsight/api/src/modules/redis/redis.service.ts @@ -190,7 +190,7 @@ export class RedisService { return client; } - public async createClientAutomatically(database: Database, tool: AppTool, connectionName) { + public async createClientAutomatically(database: Database, tool: ClientContext, connectionName) { // try sentinel connection if (database?.sentinelMaster) { try { From fdc4acc989628569f427eda0f122b6215539cfb8 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Thu, 15 Dec 2022 12:59:05 +0300 Subject: [PATCH 082/107] #RI-3541 - add default values for fields, update host for autodiscovery --- .../utils/autodiscovery.util.spec.ts | 42 +++++++++---------- .../autodiscovery/utils/autodiscovery.util.ts | 2 +- .../InstanceForm/InstanceForm.spec.tsx | 34 +++++++++++---- .../InstanceForm/InstanceForm.tsx | 21 ++++++---- .../AddInstanceForm/InstanceFormWrapper.tsx | 7 ++-- .../utils/{ => events}/handlePasteHostName.ts | 0 redisinsight/ui/src/utils/events/index.ts | 7 ++++ .../ui/src/utils/events/selectOnFocus.ts | 6 +++ redisinsight/ui/src/utils/index.ts | 3 +- 9 files changed, 80 insertions(+), 42 deletions(-) rename redisinsight/ui/src/utils/{ => events}/handlePasteHostName.ts (100%) create mode 100644 redisinsight/ui/src/utils/events/index.ts create mode 100644 redisinsight/ui/src/utils/events/selectOnFocus.ts diff --git a/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.spec.ts b/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.spec.ts index f40ed44f91..ded19a8696 100644 --- a/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.spec.ts +++ b/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.spec.ts @@ -124,39 +124,39 @@ describe('getTCPEndpoints', () => { name: 'win output', input: mockWinNetstat.split('\n'), output: [ - { host: 'localhost', port: 5000 }, - { host: 'localhost', port: 6379 }, - { host: 'localhost', port: 6380 }, - { host: 'localhost', port: 135 }, - { host: 'localhost', port: 445 }, - { host: 'localhost', port: 808 }, - { host: 'localhost', port: 2701 }, + { host: '127.0.0.1', port: 5000 }, + { host: '127.0.0.1', port: 6379 }, + { host: '127.0.0.1', port: 6380 }, + { host: '127.0.0.1', port: 135 }, + { host: '127.0.0.1', port: 445 }, + { host: '127.0.0.1', port: 808 }, + { host: '127.0.0.1', port: 2701 }, ], }, { name: 'linux output', input: mockLinuxNetstat.split('\n'), output: [ - { host: 'localhost', port: 5000 }, - { host: 'localhost', port: 6379 }, - { host: 'localhost', port: 6380 }, - { host: 'localhost', port: 28100 }, - { host: 'localhost', port: 8100 }, - { host: 'localhost', port: 8101 }, - { host: 'localhost', port: 8102 }, - { host: 'localhost', port: 8103 }, - { host: 'localhost', port: 8200 }, + { host: '127.0.0.1', port: 5000 }, + { host: '127.0.0.1', port: 6379 }, + { host: '127.0.0.1', port: 6380 }, + { host: '127.0.0.1', port: 28100 }, + { host: '127.0.0.1', port: 8100 }, + { host: '127.0.0.1', port: 8101 }, + { host: '127.0.0.1', port: 8102 }, + { host: '127.0.0.1', port: 8103 }, + { host: '127.0.0.1', port: 8200 }, ], }, { name: 'mac output', input: mockMacNetstat.split('\n'), output: [ - { host: 'localhost', port: 5000 }, - { host: 'localhost', port: 6379 }, - { host: 'localhost', port: 6380 }, - { host: 'localhost', port: 5002 }, - { host: 'localhost', port: 52167 }, + { host: '127.0.0.1', port: 5000 }, + { host: '127.0.0.1', port: 6379 }, + { host: '127.0.0.1', port: 6380 }, + { host: '127.0.0.1', port: 5002 }, + { host: '127.0.0.1', port: 52167 }, ], }, ]; diff --git a/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.ts b/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.ts index cdd65ba419..2db3e54fe5 100644 --- a/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.ts +++ b/redisinsight/api/src/modules/autodiscovery/utils/autodiscovery.util.ts @@ -57,7 +57,7 @@ export const getTCPEndpoints = (processes: string[]): IEndpoint[] => { if (match) { endpoints.set(match[4], { - host: 'localhost', + host: '127.0.0.1', port: parseInt(match[4], 10), }); } diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx index 08ddbdb4eb..ee2056f797 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.spec.tsx @@ -1,12 +1,8 @@ import React from 'react' import { instance, mock } from 'ts-mockito' -import { render, screen, fireEvent, act } from 'uiSrc/utils/test-utils' -import { ConnectionType } from 'uiSrc/slices/interfaces' -import InstanceForm, { - ADD_NEW_CA_CERT, - DbConnectionInfo, - Props, -} from './InstanceForm' +import { act, fireEvent, render, screen } from 'uiSrc/utils/test-utils' +import { ConnectionType, InstanceType } from 'uiSrc/slices/interfaces' +import InstanceForm, { ADD_NEW_CA_CERT, DbConnectionInfo, Props, } from './InstanceForm' const BTN_SUBMIT = 'btn-submit' const NEW_CA_CERT = 'new-ca-cert' @@ -613,5 +609,29 @@ describe('InstanceForm', () => { ) expect(screen.getByTestId('db-alias')).toHaveTextContent('Clone ') }) + + it('should render proper default values for standalone', () => { + render( + + ) + expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') + expect(screen.getByTestId('port')).toHaveValue('6379') + expect(screen.getByTestId('name')).toHaveValue('127.0.0.1:6379') + }) + + it('should render proper default values for sentinel', () => { + render( + + ) + expect(screen.getByTestId('host')).toHaveValue('127.0.0.1') + expect(screen.getByTestId('port')).toHaveValue('26379') + }) }) }) diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx index c9af186d30..be062ddf5d 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceForm/InstanceForm.tsx @@ -48,7 +48,7 @@ import { import { ConnectionType, Instance, InstanceType, } from 'uiSrc/slices/interfaces' import { getRedisModulesSummary, sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' -import { handlePasteHostName, getDiffKeysOfObjectValues, checkRediStackModules } from 'uiSrc/utils' +import { handlePasteHostName, getDiffKeysOfObjectValues, checkRediStackModules, selectOnFocus } from 'uiSrc/utils' import { MAX_PORT_NUMBER, validateCertName, @@ -137,12 +137,15 @@ const getInitFieldsDisplayNames = ({ host, port, name, instanceType }: any) => { return {} } +const getDefaultHost = () => '127.0.0.1' +const getDefaultPort = (instanceType: InstanceType) => (instanceType === InstanceType.Sentinel ? '26379' : '6379') + const AddStandaloneForm = (props: Props) => { const { formFields: { id, host, - name = '', + name, port, tls, db = null, @@ -181,9 +184,9 @@ const AddStandaloneForm = (props: Props) => { const { contextInstanceId, lastPage } = useSelector(appContextSelector) const prepareInitialValues = () => ({ - host, - port: port?.toString(), - name, + host: host ?? getDefaultHost(), + port: port ? port.toString() : getDefaultPort(instanceType), + name: name ?? `${getDefaultHost()}:${getDefaultPort(instanceType)}`, username, password, tls, @@ -674,7 +677,7 @@ const AddStandaloneForm = (props: Props) => { { validateField(e.target.value.trim()) ) }} - onPaste={(event: React.ClipboardEvent) => - handlePasteHostName(onHostNamePaste, event)} + onPaste={(event: React.ClipboardEvent) => handlePasteHostName(onHostNamePaste, event)} + onFocus={selectOnFocus} append={} /> @@ -711,6 +714,7 @@ const AddStandaloneForm = (props: Props) => { validatePortNumber(e.target.value.trim()) ) }} + onFocus={selectOnFocus} type="text" min={0} max={MAX_PORT_NUMBER} @@ -734,6 +738,7 @@ const AddStandaloneForm = (props: Props) => { id="name" data-testid="name" placeholder="Enter Database Alias" + onFocus={selectOnFocus} value={formik.values.name ?? ''} maxLength={500} onChange={formik.handleChange} diff --git a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx index 0c1fea2dd7..2dcaa05f96 100644 --- a/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx +++ b/redisinsight/ui/src/pages/home/components/AddInstanceForm/InstanceFormWrapper.tsx @@ -54,9 +54,10 @@ export enum TitleDatabaseText { } const getInitialValues = (editedInstance: Nullable) => ({ - host: editedInstance?.host ?? '', - port: editedInstance?.port?.toString() ?? '', - name: editedInstance?.name ?? '', + // undefined - to show default value, empty string - for existing db + host: editedInstance?.host ?? (editedInstance ? '' : undefined), + port: editedInstance?.port?.toString() ?? (editedInstance ? '' : undefined), + name: editedInstance?.name ?? (editedInstance ? '' : undefined), username: editedInstance?.username ?? '', password: editedInstance?.password ?? '', tls: !!editedInstance?.tls ?? false, diff --git a/redisinsight/ui/src/utils/handlePasteHostName.ts b/redisinsight/ui/src/utils/events/handlePasteHostName.ts similarity index 100% rename from redisinsight/ui/src/utils/handlePasteHostName.ts rename to redisinsight/ui/src/utils/events/handlePasteHostName.ts diff --git a/redisinsight/ui/src/utils/events/index.ts b/redisinsight/ui/src/utils/events/index.ts new file mode 100644 index 0000000000..e56a2ea439 --- /dev/null +++ b/redisinsight/ui/src/utils/events/index.ts @@ -0,0 +1,7 @@ +import handlePasteHostName from './handlePasteHostName' +import selectOnFocus from './selectOnFocus' + +export { + handlePasteHostName, + selectOnFocus +} diff --git a/redisinsight/ui/src/utils/events/selectOnFocus.ts b/redisinsight/ui/src/utils/events/selectOnFocus.ts new file mode 100644 index 0000000000..cebe8615d7 --- /dev/null +++ b/redisinsight/ui/src/utils/events/selectOnFocus.ts @@ -0,0 +1,6 @@ +const selectOnFocus = (e: React.FocusEvent, callback?: (e: React.FocusEvent) => void) => { + (e.target as HTMLInputElement)?.select() + callback?.(e) +} + +export default selectOnFocus diff --git a/redisinsight/ui/src/utils/index.ts b/redisinsight/ui/src/utils/index.ts index 42982ce13e..198f518023 100644 --- a/redisinsight/ui/src/utils/index.ts +++ b/redisinsight/ui/src/utils/index.ts @@ -1,5 +1,4 @@ import type { Nullable, Maybe } from './types' -import handlePasteHostName from './handlePasteHostName' import getLetterByIndex from './getLetterByIndex' import RouterWithSubRoutes from './routerWithSubRoutes' @@ -26,11 +25,11 @@ export * from './formatters' export * from './groupTypes' export * from './modules' export * from './optimizations' +export * from './events' export { Maybe, Nullable, - handlePasteHostName, RouterWithSubRoutes, getLetterByIndex } From feebb5213f220828443ed7cde9b07719935c39ee Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Thu, 15 Dec 2022 16:44:36 +0300 Subject: [PATCH 083/107] #RI-3938 - fix modal width, add ":" --- .../import-databases-dialog/ImportDatabasesDialog.tsx | 3 ++- .../components/ResultsLog/ResultsLog.tsx | 2 +- .../import-databases-dialog/styles.module.scss | 9 +++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx index 9187411a36..439dd337d2 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -24,6 +24,7 @@ import { } from 'uiSrc/slices/instances/instances' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { Nullable } from 'uiSrc/utils' +import cx from 'classnames' import ResultsLog from './components/ResultsLog' import styles from './styles.module.scss' @@ -72,7 +73,7 @@ const ImportDatabasesDialog = ({ onClose }: Props) => { const isShowForm = !loading && !data && !error return ( - + diff --git a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.tsx b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.tsx index 3a0359445b..3446ea3dd3 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/components/ResultsLog/ResultsLog.tsx @@ -38,7 +38,7 @@ const ResultsLog = ({ data }: Props) => { const CollapsibleNavTitle = ({ title, length = 0 }: { title: string, length: number }) => (
- {title} + {title}: {length}
) diff --git a/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss index 8fda92df89..cb918d12c3 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss +++ b/redisinsight/ui/src/components/import-databases-dialog/styles.module.scss @@ -4,6 +4,15 @@ max-width: 700px !important; min-height: 270px !important; + &.result { + width: 500px !important; + + @media screen and (min-width: 1024px) { + width: 700px !important; + min-width: 700px !important; + } + } + :global { .euiModalHeader { padding: 4px 42px 20px 30px; From 753342c5f77932b0856c128b3ac0141dea57d1c7 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Thu, 15 Dec 2022 17:06:56 +0300 Subject: [PATCH 084/107] #RI-3938 - fix pr comments --- .../import-databases-dialog/ImportDatabasesDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx index 439dd337d2..5065e3ee6e 100644 --- a/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx +++ b/redisinsight/ui/src/components/import-databases-dialog/ImportDatabasesDialog.tsx @@ -16,6 +16,7 @@ import { } from '@elastic/eui' import React, { useState } from 'react' import { useDispatch, useSelector } from 'react-redux' +import cx from 'classnames' import { fetchInstancesAction, importInstancesSelector, @@ -24,7 +25,7 @@ import { } from 'uiSrc/slices/instances/instances' import { sendEventTelemetry, TelemetryEvent } from 'uiSrc/telemetry' import { Nullable } from 'uiSrc/utils' -import cx from 'classnames' + import ResultsLog from './components/ResultsLog' import styles from './styles.module.scss' From 7f36cda53538d1b2bd300413afcf11a68ee025f2 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Thu, 15 Dec 2022 22:53:35 +0800 Subject: [PATCH 085/107] #RI-3940 - When we edit db name, we are getting "new indicator" --- redisinsight/api/src/modules/database/database.service.ts | 7 ++++--- .../api/src/modules/database/providers/database.factory.ts | 1 - .../api/test/api/database/PUT-databases-id.test.ts | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 8c29e28050..8c9800fc85 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -88,9 +88,10 @@ export class DatabaseService { try { this.logger.log('Creating new database.'); - const database = await this.repository.create( - await this.databaseFactory.createDatabaseModel(classToClass(Database, dto)), - ); + const database = await this.repository.create({ + ...await this.databaseFactory.createDatabaseModel(classToClass(Database, dto)), + new: true, + }); // todo: clarify if we need this and if yes - rethink implementation try { diff --git a/redisinsight/api/src/modules/database/providers/database.factory.ts b/redisinsight/api/src/modules/database/providers/database.factory.ts index a8273bef69..24723f3d8a 100644 --- a/redisinsight/api/src/modules/database/providers/database.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.factory.ts @@ -46,7 +46,6 @@ export class DatabaseFactory { model.modules = await this.databaseInfoProvider.determineDatabaseModules(client); model.lastConnection = new Date(); - model.new = true await client.disconnect(); diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index f25b3e7ab8..95a09152cf 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -183,8 +183,7 @@ describe(`PUT /databases/:id`, () => { after: async () => { newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); expect(newDatabase).to.contain({ - ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection']), - new: true, + ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection', 'new']), host: constants.TEST_REDIS_HOST, port: constants.TEST_REDIS_PORT, }); From 61243540322e424c2067fe94e5330a76bfcccaa8 Mon Sep 17 00:00:00 2001 From: "Roman.Sergeenko" Date: Fri, 16 Dec 2022 12:21:21 +0300 Subject: [PATCH 086/107] #RI-3930 - fix text for scan --- .../components/keys-summary/KeysSummary.spec.tsx | 14 ++++++++++++++ .../ui/src/components/keys-summary/KeysSummary.tsx | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/redisinsight/ui/src/components/keys-summary/KeysSummary.spec.tsx b/redisinsight/ui/src/components/keys-summary/KeysSummary.spec.tsx index 66b948ace0..05a03d4275 100644 --- a/redisinsight/ui/src/components/keys-summary/KeysSummary.spec.tsx +++ b/redisinsight/ui/src/components/keys-summary/KeysSummary.spec.tsx @@ -24,6 +24,20 @@ describe('KeysSummary', () => { expect(queryByTestId('keys-summary')).toBeInTheDocument() }) + it('should Keys summary show proper text with count = 1', () => { + const { queryByTestId } = render( + + ) + expect(queryByTestId('keys-summary')).toHaveTextContent('Results: 1 key.') + }) + + it('should Keys summary show proper text with count > 1', () => { + const { queryByTestId } = render( + + ) + expect(queryByTestId('keys-summary')).toHaveTextContent('Results: 2 keys.') + }) + it('should not render Scan more button if showScanMore = false ', () => { const { queryByTestId } = render( diff --git a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx index 382662798b..9d4bd4b4e8 100644 --- a/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx +++ b/redisinsight/ui/src/components/keys-summary/KeysSummary.tsx @@ -47,7 +47,7 @@ const KeysSummary = (props: Props) => { {'Results: '} {numberWithSpaces(resultsLength)} - {` key${resultsLength !== 1 && 's'}. `} + {` key${resultsLength !== 1 ? 's' : ''}. `} {'Scanned '} From 017af2cbdd9f32eff7272f9ea80b238ba3733ab4 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 16 Dec 2022 14:04:57 +0100 Subject: [PATCH 087/107] add test file with certificates to import --- tests/e2e/common-actions/databases-actions.ts | 3 +- .../pageObjects/my-redis-databases-page.ts | 6 + tests/e2e/test-data/racompass-invalid.json | 1 - tests/e2e/test-data/rdm-full.json | 218 ++++++++++++++++++ .../database/import-databases.e2e.ts | 59 ++++- 5 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 tests/e2e/test-data/rdm-full.json diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index 3167ce1b2c..08bea3bda9 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -24,9 +24,8 @@ export class DatabasesActions { .click(myRedisDatabasePage.importDatabasesBtn) .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [fileParameters.path]) .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.successImportMessage.exists).ok(`Successfully added ${fileParameters.type} databases message not displayed`); + .expect(myRedisDatabasePage.importDialogTitle.textContent).eql('Import Results', `Databases from ${fileParameters.type} not imported`); } - } /** diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index ce4841cb02..c45ab329e1 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -7,6 +7,8 @@ export class MyRedisDatabasePage { //*Target any element/component via data-id, if possible! //*The following categories are ordered alphabetically (Alerts, Buttons, Checkboxes, etc.). //------------------------------------------------------------------------------------------- + // CSS Selectors + cssNumberOfDbs = '[data-testid=number-of-dbs]'; //BUTTONS settingsButton = Selector('[data-testid=settings-page-btn]'); workbenchButton = Selector('[data-testid=workbench-page-btn]'); @@ -63,8 +65,12 @@ export class MyRedisDatabasePage { noResultsFoundText = Selector('div').withExactText('No databases matched your search. Try reducing the criteria.'); failedImportMessage = Selector('[data-testid=result-failed]'); successImportMessage = Selector('[data-testid=result-success]'); + importDialogTitle = Selector('[data-testid=import-dbs-dialog-title]'); // DIALOG importDbDialog = Selector('[data-testid=import-dbs-dialog]'); + successResultsAccordion = Selector('[data-testid^=success-results-]'); + partialResultsAccordion = Selector('[data-testid^=partial-results-]'); + failedResultsAccordion = Selector('[data-testid^=failed-results-]'); /** * Click on the database by name diff --git a/tests/e2e/test-data/racompass-invalid.json b/tests/e2e/test-data/racompass-invalid.json index aab8b3e325..fac9e31a5a 100644 --- a/tests/e2e/test-data/racompass-invalid.json +++ b/tests/e2e/test-data/racompass-invalid.json @@ -167,5 +167,4 @@ "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", "cluster": true, "name": "vd long host" - } ] \ No newline at end of file diff --git a/tests/e2e/test-data/rdm-full.json b/tests/e2e/test-data/rdm-full.json new file mode 100644 index 0000000000..ec25750c6a --- /dev/null +++ b/tests/e2e/test-data/rdm-full.json @@ -0,0 +1,218 @@ +[ + { + "host": "localhost", + "port": 8100, + "name": "rdmHost+Port+Name", + "result": "success" + }, + { + "host": "localhost", + "port": 8101, + "name": "rdmHost+Port+Name+Username+Password", + "username": "rdmUsername", + "auth": "rdmAuth", + "result": "success" + }, + { + "host": "172.30.100.151", + "port": 6379, + "name": "rdmHost+Port+Name+ClusterTrue", + "cluster": true, + "result": "success" + }, + { + "host": "172.30.100.151", + "port": 6379, + "name": "rdmHost+Port+Name+ClusterFalse", + "cluster": false, + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+Index", + "db": 2, + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+otherFields", + "ssh_port": 22, + "timeout_connect": 60000, + "timeout_execute": 60000, + "other_field": "inv", + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+CaCert", + "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+clientCert+privateKey", + "ssl": true, + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+CaCert+clientCert+privateKey", + "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmHost+Port+Name+CaCert+clientCert+privateKey(notbypath)", + "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "result": "success" + }, + { + "host": "172.30.100.103", + "port": 6379, + "name": "rdmHost+Port+Name+username+pass+CaCert+clientCert+privateKey", + "ssl": true, + "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "username": "admin", + "auth": "pass", + "result": "success" + }, + { + "host": "redis-13237.c263.us-east-1-2.ec2.cloud.redislabs.com", + "port": 13237, + "name": "rdmHost+Port+Name CloudDb", + "auth": "fRVdFcJftYnVoJmTCuiAPW6yu9qimXA1", + "result": "success" + }, + { + "host": "3.85.88.37", + "port": 14547, + "name": "rdmHost+Port+Name Cluster+CloudDb", + "auth": "Admin123!", + "result": "success" + }, + { + "host": "172.30.100.201", + "port": 6379, + "name": "rdmHost+Port+Name Sentinel", + "auth": "defaultpass", + "result": "success" + }, + { + "port": 8100, + "name": "rdmnoHost", + "result": "failed" + }, + { + "host": "localhost", + "name": "rdmnoPort", + "result": "failed" + }, + { + "name": "rdmnoHost+noPort", + "result": "failed" + }, + + { + "host": "rdmLargeName", + "port": "8101", + "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed consectetur metus in libero pretium congue. Curabitur eget eleifend nibh, cursus tincidunt lorem. Vivamus magna erat, vestibulum at sem et, bibendum volutpat velit. Cras dapibus lorem quam, at efficitur mi sollicitudin id. Vivamus dapibus nec elit ut tincidunt. Sed porta tempus lorem id iaculis. Vestibulum ut arcu vitae massa dapibus egestas. Suspendisse ante tortor, tristique vel malesuada id, finibus et libero. Nulla suscipit libero.1", + "result": "failed" + }, + { + "host": "localhost", + "port": 65536, + "name": "rdmLargePort", + "result": "failed" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmIndexNotNumber", + "db": "dsad", + "result": "failed" + }, + { + "host": "localhost", + "port": 8102, + "name": "rdmCaCertInvalidBody", + "ssl_ca_cert_path": "invalid body", + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmOnlyClientCert", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmOnlyPrivateKey", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmInvalidClientCert", + "ssl_local_cert_path": "invalid client cert", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmInvalidPrivateKey", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_private_key_path": "invalid private key", + "result": "partial" + }, + { + "auth": "", + "db_scan_limit": 21, + "host": "127.0.0.1", + "keys_pattern": "*1", + "lua_keys_loading": true, + "name": "rdmInvalidAllCertificates", + "namespace_separator": ":3", + "port": 8100, + "ssh_port": 22, + "ssl": false, + "ssl_ca_cert_path": "fdsafsadfsad", + "ssl_local_cert_path": "fsdfds", + "ssl_private_key_path": "fdsafasdf", + "timeout_connect": 61000, + "timeout_execute": 61000, + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmCaCertInvalidPath", + "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/caInvalid.crt", + "result": "partial" + }, + { + "host": "localhost", + "port": 8103, + "name": "rdmClientCert+PrivateKeyInvalidPathes", + "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/clientInvalid.crt", + "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/clientInvalid.key", + "result": "partial" + } +] diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index ce738b9dac..d25b1da523 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -1,34 +1,49 @@ +import * as fs from 'fs'; +import * as path from 'path'; import { rte } from '../../../helpers/constants'; import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl } from '../../../helpers/conf'; import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; import { DatabasesActions } from '../../../common-actions/databases-actions'; +// import rdmJson from '../../../test-data/rdm-full.json' assert {type: 'json'}; +// import * as fil from '../../../test-data/rdm-full.json'; +const file = path.join('test-data', 'rdm-full.json'); const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); const databasesActions = new DatabasesActions(); const addRedisDatabasePage = new AddRedisDatabasePage(); -const invalidJsonPath = '../../../test-data/racompass-invalid.json'; +const racompassValidJson = 'racompass-valid.json'; +const racompassInvalidJson = 'racompass-invalid.json'; +const rdmValidJson = 'rdm-valid.json'; +const rdmFullJson = 'rdm-full.json'; +const ardmValidAno = 'ardm-valid.ano'; +const listOfDB = JSON.parse(fs.readFileSync(file, 'utf-8')); +const dbSuccessNames = listOfDB.filter(element => element.result === 'success').map(item => item.name); + const rdmData = { type: 'rdm', - path: '../../../test-data/rdm-valid.json', + path: path.join('..', '..', '..', 'test-data', rdmFullJson), dbNames: ['rdmWithUsernameAndPass1:1561', 'rdmOnlyHostPortDB2:6379'], userName: 'rdmUsername', password: 'rdmAuth', connectionType: 'Cluster', - fileName: 'rdm-valid.json' + fileName: rdmFullJson, + successNumber: dbSuccessNames.length, + partialNumber: 8, + failedNumber: 6 }; const dbData = [ { type: 'racompass', - path: '../../../test-data/racompass-valid.json', + path: path.join('..', '..', '..', 'test-data', racompassValidJson), dbNames: ['racompassCluster', 'racompassDbWithIndex:8100 [1]'] }, { type: 'ardm', - path: '../../../test-data/ardm-valid.ano', + path: path.join('..', '..', '..', 'test-data', ardmValidAno), dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] } ]; @@ -54,8 +69,9 @@ test deleteStandaloneDatabasesByNamesApi(databases); })('Connection import from JSON', async t => { const tooltipText = 'Import Database Connections'; - const partialImportedMsg = 'Successfully added 2 of 6 database connections'; const defaultText = 'Select or drag and drop a file'; + const parseFailedMsg = 'Failed to add database connections'; + const parseFailedMsg2 = `Unable to parse ${racompassInvalidJson}`; // Verify that user can see the “Import Database Connections” tooltip await t.expect(myRedisDatabasePage.importDatabasesBtn.visible).ok('The import databases button not displayed'); @@ -70,9 +86,11 @@ test // Verify that user see the message when parse error appears await t - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [invalidJsonPath]) + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [path.join('..', '..', '..', 'test-data', racompassInvalidJson)]) .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed'); + .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed') + .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg) + .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg2); // Verify that user can remove file from import input await t.click(myRedisDatabasePage.closeDialogBtn); @@ -87,8 +105,16 @@ test await t.click(myRedisDatabasePage.closeDialogBtn); await databasesActions.importDatabase(rdmData); // Verify that success message is displayed - await t.expect(myRedisDatabasePage.successImportMessage.textContent).contains(partialImportedMsg, 'Databases not imported successfully'); - + await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); + await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number'); + await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.failedNumber}`, 'Not correct failed to import number'); + + + + // Verify that list of databases is reloaded when database added await t.click(myRedisDatabasePage.okDialogBtn); await databasesActions.verifyDatabasesDisplayed(rdmData.dbNames); @@ -110,3 +136,16 @@ test await databasesActions.verifyDatabasesDisplayed(db.dbNames); } }); + test.only + .before(async() => { + await acceptLicenseTerms() + })('Connection import from JSON', async t => { + console.log(dbSuccessNames); + console.log(rdmData.successNumber); + + await databasesActions.importDatabase(rdmData); + // Verify that success message is displayed + await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); + await databasesActions.verifyDatabasesDisplayed(dbSuccessNames); + }); From 77bc5dbfb0a789b9912256968cffd2b1a3c06f22 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Fri, 16 Dec 2022 16:15:55 +0100 Subject: [PATCH 088/107] added tests for show process results for importeed databases --- tests/e2e/docker.web.docker-compose.yml | 2 + tests/e2e/local.web.docker-compose.yml | 2 + tests/e2e/pageObjects/browser-page.ts | 2 - tests/e2e/test-data/certs/ca.crt | 3 + tests/e2e/test-data/certs/client.crt | 3 + tests/e2e/test-data/certs/client.key | 3 + tests/e2e/test-data/rdm-full.json | 41 ++-- tests/e2e/test-data/rdm-valid.json | 62 ------ .../database/import-databases.e2e.ts | 181 ++++++++---------- 9 files changed, 115 insertions(+), 184 deletions(-) create mode 100644 tests/e2e/test-data/certs/ca.crt create mode 100644 tests/e2e/test-data/certs/client.crt create mode 100644 tests/e2e/test-data/certs/client.key delete mode 100644 tests/e2e/test-data/rdm-valid.json diff --git a/tests/e2e/docker.web.docker-compose.yml b/tests/e2e/docker.web.docker-compose.yml index e93aca690f..ff4a10c0c1 100644 --- a/tests/e2e/docker.web.docker-compose.yml +++ b/tests/e2e/docker.web.docker-compose.yml @@ -12,6 +12,7 @@ services: - ./plugins:/usr/src/app/plugins - .redisinsight-v2:/root/.redisinsight-v2 - .ritmp:/tmp + - ./test-data/certs:/root/certs env_file: - ./.env entrypoint: [ @@ -40,4 +41,5 @@ services: volumes: - .redisinsight-v2:/root/.redisinsight-v2 - .ritmp:/tmp + - ./test-data/certs:/root/certs diff --git a/tests/e2e/local.web.docker-compose.yml b/tests/e2e/local.web.docker-compose.yml index ed864c84da..8e5dbb6eb9 100644 --- a/tests/e2e/local.web.docker-compose.yml +++ b/tests/e2e/local.web.docker-compose.yml @@ -10,6 +10,7 @@ services: - ./results:/usr/src/app/results - ./plugins:/usr/src/app/plugins - .redisinsight-v2:/root/.redisinsight-v2 + - ./test-data/certs:/root/certs env_file: - ./.env environment: @@ -41,5 +42,6 @@ services: dockerfile: Dockerfile volumes: - .redisinsight-v2:/root/.redisinsight-v2 + - ./test-data/certs:/root/certs ports: - 5000:5000 diff --git a/tests/e2e/pageObjects/browser-page.ts b/tests/e2e/pageObjects/browser-page.ts index b1a768f8ae..3d6099c935 100644 --- a/tests/e2e/pageObjects/browser-page.ts +++ b/tests/e2e/pageObjects/browser-page.ts @@ -26,8 +26,6 @@ export class BrowserPage { setDeleteButton = Selector('[data-testid=set-delete-btn]'); streamDeleteButton = Selector('[data-testid=stream-delete-btn]'); myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); - streamDeleteButton = Selector('[data-testid=stream-delete-btn]'); - myRedisDbIcon = Selector('[data-testid=my-redis-db-icon]'); deleteKeyButton = Selector('[data-testid=delete-key-btn]'); confirmDeleteKeyButton = Selector('[data-testid=delete-key-confirm-btn]'); editKeyTTLButton = Selector('[data-testid=edit-ttl-btn]'); diff --git a/tests/e2e/test-data/certs/ca.crt b/tests/e2e/test-data/certs/ca.crt new file mode 100644 index 0000000000..0722ac4149 --- /dev/null +++ b/tests/e2e/test-data/certs/ca.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedCACertificate +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/client.crt b/tests/e2e/test-data/certs/client.crt new file mode 100644 index 0000000000..43dee13d0a --- /dev/null +++ b/tests/e2e/test-data/certs/client.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedClientCrt +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/client.key b/tests/e2e/test-data/certs/client.key new file mode 100644 index 0000000000..d3f63edc85 --- /dev/null +++ b/tests/e2e/test-data/certs/client.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +mockedPrivateKey +-----END PRIVATE KEY----- diff --git a/tests/e2e/test-data/rdm-full.json b/tests/e2e/test-data/rdm-full.json index ec25750c6a..6e7a00af9d 100644 --- a/tests/e2e/test-data/rdm-full.json +++ b/tests/e2e/test-data/rdm-full.json @@ -32,7 +32,8 @@ "port": 8102, "name": "rdmHost+Port+Name+Index", "db": 2, - "result": "success" + "result": "success", + "indName": "rdmHost+Port+Name+Index [2]" }, { "host": "localhost", @@ -48,7 +49,7 @@ "host": "localhost", "port": 8102, "name": "rdmHost+Port+Name+CaCert", - "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", + "ssl_ca_cert_path": "/root/certs/ca.crt", "result": "success" }, { @@ -56,26 +57,26 @@ "port": 8102, "name": "rdmHost+Port+Name+clientCert+privateKey", "ssl": true, - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_local_cert_path": "/root/certs/client.crt", + "ssl_private_key_path": "/root/certs/client.key", "result": "success" }, { "host": "localhost", "port": 8102, "name": "rdmHost+Port+Name+CaCert+clientCert+privateKey", - "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_ca_cert_path": "/root/certs/ca.crt", + "ssl_local_cert_path": "/root/certs/client.crt", + "ssl_private_key_path": "/root/certs/client.key", "result": "success" }, { "host": "localhost", "port": 8102, "name": "rdmHost+Port+Name+CaCert+clientCert+privateKey(notbypath)", - "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_ca_cert_path": "/root/certs/ca.crt", + "ssl_local_cert_path": "/root/certs/client.crt", + "ssl_private_key_path": "/root/certs/client.key", "result": "success" }, { @@ -83,9 +84,9 @@ "port": 6379, "name": "rdmHost+Port+Name+username+pass+CaCert+clientCert+privateKey", "ssl": true, - "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/ca.crt", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_ca_cert_path": "/root/certs/ca.crt", + "ssl_local_cert_path": "/root/certs/client.crt", + "ssl_private_key_path": "/root/certs/client.key", "username": "admin", "auth": "pass", "result": "success" @@ -156,14 +157,14 @@ "host": "localhost", "port": 8103, "name": "rdmOnlyClientCert", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_local_cert_path": "/root/certs/client.crt", "result": "partial" }, { "host": "localhost", "port": 8103, "name": "rdmOnlyPrivateKey", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_private_key_path": "/root/certs/client.key", "result": "partial" }, { @@ -171,14 +172,14 @@ "port": 8103, "name": "rdmInvalidClientCert", "ssl_local_cert_path": "invalid client cert", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.key", + "ssl_private_key_path": "/root/certs/client.key", "result": "partial" }, { "host": "localhost", "port": 8103, "name": "rdmInvalidPrivateKey", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/client.crt", + "ssl_local_cert_path": "/root/certs/client.crt", "ssl_private_key_path": "invalid private key", "result": "partial" }, @@ -204,15 +205,15 @@ "host": "localhost", "port": 8103, "name": "rdmCaCertInvalidPath", - "ssl_ca_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/caInvalid.crt", + "ssl_ca_cert_path": "/root/certs/caInvalid.crt", "result": "partial" }, { "host": "localhost", "port": 8103, "name": "rdmClientCert+PrivateKeyInvalidPathes", - "ssl_local_cert_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/clientInvalid.crt", - "ssl_private_key_path": "C:/Projects/redisinsight-redis/RedisInsight/tests/e2e/.redisinsight-v2/clientInvalid.key", + "ssl_local_cert_path": "/root/certs/clientInvalid.crt", + "ssl_private_key_path": "/root/certs/clientInvalid.key", "result": "partial" } ] diff --git a/tests/e2e/test-data/rdm-valid.json b/tests/e2e/test-data/rdm-valid.json deleted file mode 100644 index 845fe507e7..0000000000 --- a/tests/e2e/test-data/rdm-valid.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "password": "new", - "filter_history": { - "*": 1, - "stream1": 1 - }, - "host": "localhost", - "name": "vfd das jashd ashdkjh kjhasfh hfjks dahfjk shdk fhskjad hfkj sdhkj fashk dhfk sahkj fhsak dhfskja hsd kfjh dsakj fhsdjk fhskdjal fhksjd hfkjsd hfkjs hakdjfh sjkdah fjksdh fjksdh jkfh ksdh fsdkjhfksjhfkhsdkf hksjdhf kjsdhf skdhf sdf sdfsda fsdfsd fsd fsdf sd f sdf sd f sd fsd f sd f sd fsd f sad fs df sd f s dsa fsdf vfd das jashd ashdkjh", - "ssh_port": 22, - "timeout_connect": 60000, - "timeout_execute": 60000, - "cluster": false, - "invalid_field": "inv", - "db": 0 - }, - { - "host": "rdmWithUsernameAndPass1", - "port": "1561", - "cluster": true, - "username": "rdmUsername", - "auth": "rdmAuth" - }, - { - "auth": "", - "host": "172.30.100.151", - "name": "oss cluster", - "ssh_port": 22, - "cluster": true, - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "auth": "longname database >500 symbols invalid", - "host": "172.30.100.181", - "port": 1000, - "keys_pattern": "*", - "name": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla rutrum nec libero aliquet ultricies. Donec ut blandit dui, ac hendrerit risus. Vestibulum sodales sed risus ac auctor. Integer quis justo vel leo gravida volutpat quis et risus. Nam vestibulum fermentum eros, in ullamcorper nulla laoreet quis. Vestibulum ut arcu nec turpis elementum malesuada. Maecenas congue felis nec posuere ullamcorper. Phasellus auctor leo sit amet ligula scelerisque, quis elementum ligula auctor. Quisque id leo r", - "namespace_separator": ":", - "ssh_port": 22, - "ssl": true, - "ssl_ca_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/ca.crt", - "ssl_local_cert_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.crt", - "ssl_private_key_path": "E:/Redis/redisinsight-envs/cluster-tls-client/certs/client.key", - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "auth": "pass", - "host": "localhost", - "name": "plain-certs+pass", - "port": 65537, - "ssh_port": 22, - "ssl_local_cert_path": "C:/Users/enaboko/Downloads/certificates", - "timeout_connect": 60000, - "timeout_execute": 60000 - }, - { - "host": "rdmOnlyHostPortDB2", - "port": 6379 - } -] diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index d25b1da523..79812dd615 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -6,9 +6,6 @@ import { commonUrl } from '../../../helpers/conf'; import { acceptLicenseTerms, clickOnEditDatabaseByName } from '../../../helpers/database'; import { deleteStandaloneDatabasesByNamesApi } from '../../../helpers/api/api-database'; import { DatabasesActions } from '../../../common-actions/databases-actions'; -// import rdmJson from '../../../test-data/rdm-full.json' assert {type: 'json'}; -// import * as fil from '../../../test-data/rdm-full.json'; -const file = path.join('test-data', 'rdm-full.json'); const browserPage = new BrowserPage(); const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -17,23 +14,20 @@ const addRedisDatabasePage = new AddRedisDatabasePage(); const racompassValidJson = 'racompass-valid.json'; const racompassInvalidJson = 'racompass-invalid.json'; -const rdmValidJson = 'rdm-valid.json'; const rdmFullJson = 'rdm-full.json'; const ardmValidAno = 'ardm-valid.ano'; -const listOfDB = JSON.parse(fs.readFileSync(file, 'utf-8')); +const listOfDB = JSON.parse(fs.readFileSync(path.join('test-data', 'rdm-full.json'), 'utf-8')); const dbSuccessNames = listOfDB.filter(element => element.result === 'success').map(item => item.name); - +const dbPartialNames = listOfDB.filter(element => element.result === 'partial').map(item => item.name); +const dbFailedNames = listOfDB.filter(element => element.result === 'failed').map(item => item.name); +const dbImportedNames = [...dbSuccessNames, ...dbPartialNames]; const rdmData = { type: 'rdm', path: path.join('..', '..', '..', 'test-data', rdmFullJson), - dbNames: ['rdmWithUsernameAndPass1:1561', 'rdmOnlyHostPortDB2:6379'], - userName: 'rdmUsername', - password: 'rdmAuth', connectionType: 'Cluster', - fileName: rdmFullJson, successNumber: dbSuccessNames.length, - partialNumber: 8, - failedNumber: 6 + partialNumber: dbPartialNames.length, + failedNumber: dbFailedNames.length }; const dbData = [ { @@ -49,103 +43,90 @@ const dbData = [ ]; // List of all created databases to delete const databases = [ - rdmData.dbNames[0], - rdmData.dbNames[1], dbData[0].dbNames[0], dbData[0].dbNames[1].split(' ')[0], dbData[1].dbNames[0], dbData[1].dbNames[1] ]; +const databasesToDelete = [...dbImportedNames, ...databases]; fixture `Import databases` .meta({ type: 'critical_path', rte: rte.standalone }) - .page(commonUrl); -test - .before(async() => { + .page(commonUrl) + .beforeEach(async() => { await acceptLicenseTerms(); }) - .after(async() => { + .afterEach(async() => { // Delete databases - deleteStandaloneDatabasesByNamesApi(databases); - })('Connection import from JSON', async t => { - const tooltipText = 'Import Database Connections'; - const defaultText = 'Select or drag and drop a file'; - const parseFailedMsg = 'Failed to add database connections'; - const parseFailedMsg2 = `Unable to parse ${racompassInvalidJson}`; - - // Verify that user can see the “Import Database Connections” tooltip - await t.expect(myRedisDatabasePage.importDatabasesBtn.visible).ok('The import databases button not displayed'); - await t.hover(myRedisDatabasePage.importDatabasesBtn); - await t.expect(browserPage.tooltip.innerText).contains(tooltipText, 'The tooltip message not displayed/correct'); - - // Verify that Import dialogue is not closed when clicking any area outside the box - await t.click(myRedisDatabasePage.importDatabasesBtn); - await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not opened'); - await t.click(myRedisDatabasePage.myRedisDBButton); - await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not displayed'); - - // Verify that user see the message when parse error appears - await t - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [path.join('..', '..', '..', 'test-data', racompassInvalidJson)]) - .click(myRedisDatabasePage.submitImportBtn) - .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed') - .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg) - .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg2); - - // Verify that user can remove file from import input - await t.click(myRedisDatabasePage.closeDialogBtn); - await t.click(myRedisDatabasePage.importDatabasesBtn); - await t.setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [rdmData.path]); - await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(rdmData.fileName, 'Filename not displayed in import input'); - // Click on remove button - await t.click(myRedisDatabasePage.removeImportedFileBtn); - await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); - - // Verify that user can import database with mandatory fields - await t.click(myRedisDatabasePage.closeDialogBtn); - await databasesActions.importDatabase(rdmData); - // Verify that success message is displayed - await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); - await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number'); - await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.failedNumber}`, 'Not correct failed to import number'); - - - - - // Verify that list of databases is reloaded when database added - await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(rdmData.dbNames); - - // Verify that user can import database with mandatory+optional data - await clickOnEditDatabaseByName(rdmData.dbNames[0]); - // Verify username imported - await t.expect(addRedisDatabasePage.usernameInput.value).eql(rdmData.userName); - // Verify password imported - await t.click(addRedisDatabasePage.showPasswordBtn); - await t.expect(addRedisDatabasePage.passwordInput.value).eql(rdmData.password); - // Verify cluster connection type imported - await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType); - - // Verify that user can import files from Racompass, ARDM, RDM - for (const db of dbData) { - await databasesActions.importDatabase(db); - await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(db.dbNames); - } - }); - test.only - .before(async() => { - await acceptLicenseTerms() - })('Connection import from JSON', async t => { - console.log(dbSuccessNames); - console.log(rdmData.successNumber); - - await databasesActions.importDatabase(rdmData); - // Verify that success message is displayed - await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); - await databasesActions.verifyDatabasesDisplayed(dbSuccessNames); + deleteStandaloneDatabasesByNamesApi(databasesToDelete); }); +test('Connection import from JSON', async t => { + const tooltipText = 'Import Database Connections'; + const defaultText = 'Select or drag and drop a file'; + const parseFailedMsg = 'Failed to add database connections'; + const parseFailedMsg2 = `Unable to parse ${racompassInvalidJson}`; + + // Verify that user can see the “Import Database Connections” tooltip + await t.expect(myRedisDatabasePage.importDatabasesBtn.visible).ok('The import databases button not displayed'); + await t.hover(myRedisDatabasePage.importDatabasesBtn); + await t.expect(browserPage.tooltip.innerText).contains(tooltipText, 'The tooltip message not displayed/correct'); + + // Verify that Import dialogue is not closed when clicking any area outside the box + await t.click(myRedisDatabasePage.importDatabasesBtn); + await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not opened'); + await t.click(myRedisDatabasePage.myRedisDBButton); + await t.expect(myRedisDatabasePage.importDbDialog.exists).ok('Import Database Connections dialog not displayed'); + + // Verify that user see the message when parse error appears + await t + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [path.join('..', '..', '..', 'test-data', racompassInvalidJson)]) + .click(myRedisDatabasePage.submitImportBtn) + .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed') + .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg) + .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg2); + + // Verify that user can remove file from import input + await t.click(myRedisDatabasePage.closeDialogBtn); + await t.click(myRedisDatabasePage.importDatabasesBtn); + await t.setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [rdmData.path]); + await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(rdmFullJson, 'Filename not displayed in import input'); + // Click on remove button + await t.click(myRedisDatabasePage.removeImportedFileBtn); + await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); + + // Verify that user can import database with mandatory/optional fields + await t.click(myRedisDatabasePage.closeDialogBtn); + await databasesActions.importDatabase(rdmData); + + // Fully imported table + await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); + // Partially imported table + await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number'); + // Failed to import table + await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.failedNumber}`, 'Not correct import failed number'); + + // Verify that list of databases is reloaded when database added + await t.click(myRedisDatabasePage.okDialogBtn); + await databasesActions.verifyDatabasesDisplayed(dbImportedNames); + + await clickOnEditDatabaseByName(dbImportedNames[1]); + // Verify username imported + await t.expect(addRedisDatabasePage.usernameInput.value).eql(listOfDB[1].username); + // Verify password imported + await t.click(addRedisDatabasePage.showPasswordBtn); + await t.expect(addRedisDatabasePage.passwordInput.value).eql(listOfDB[1].auth); + + // Verify cluster connection type imported + await clickOnEditDatabaseByName(dbImportedNames[2]); + await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType); + + // Verify that user can import files from Racompass, ARDM, RDM + for (const db of dbData) { + await databasesActions.importDatabase(db); + await t.click(myRedisDatabasePage.okDialogBtn); + await databasesActions.verifyDatabasesDisplayed(db.dbNames); + } +}); From 25e1e92f06d6af56b4110f051de13af218f5d8bb Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 09:15:18 +0200 Subject: [PATCH 089/107] all tests --- redisinsight/api/src/__mocks__/common.ts | 1 + .../api/src/__mocks__/database-import.ts | 39 +- ...ocal.client-certificate.repository.spec.ts | 1 - .../certificate-import.service.spec.ts | 341 ++++ .../database-import.analytics.spec.ts | 8 +- .../database-import.service.spec.ts | 16 +- .../database-import.service.ts | 6 +- .../POST-databases-import.test.ts | 1386 ++++++++++++++++- redisinsight/api/test/helpers/constants.ts | 7 + redisinsight/api/test/helpers/redis.ts | 10 + .../api/test/test-runs/docker.build.env | 1 + .../api/test/test-runs/docker.build.yml | 2 + .../api/test/test-runs/local.build.env | 1 + .../api/test/test-runs/local.build.yml | 2 + 14 files changed, 1754 insertions(+), 67 deletions(-) create mode 100644 redisinsight/api/src/modules/database-import/certificate-import.service.spec.ts diff --git a/redisinsight/api/src/__mocks__/common.ts b/redisinsight/api/src/__mocks__/common.ts index 466a8bd0fd..c4844f6c3b 100644 --- a/redisinsight/api/src/__mocks__/common.ts +++ b/redisinsight/api/src/__mocks__/common.ts @@ -32,6 +32,7 @@ export const mockQueryBuilderExecute = jest.fn(); export const mockCreateQueryBuilder = jest.fn(() => ({ // where: jest.fn().mockReturnThis(), where: mockQueryBuilderWhere, + orWhere: mockQueryBuilderWhere, update: jest.fn().mockReturnThis(), select: jest.fn().mockReturnThis(), set: jest.fn().mockReturnThis(), diff --git a/redisinsight/api/src/__mocks__/database-import.ts b/redisinsight/api/src/__mocks__/database-import.ts index e15d864f41..fc8c44777a 100644 --- a/redisinsight/api/src/__mocks__/database-import.ts +++ b/redisinsight/api/src/__mocks__/database-import.ts @@ -1,9 +1,13 @@ import { DatabaseImportResponse, DatabaseImportStatus } from 'src/modules/database-import/dto/database-import.response'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { mockDatabase } from 'src/__mocks__/databases'; +import { mockDatabase, mockSentinelDatabaseWithTlsAuth } from 'src/__mocks__/databases'; import { ValidationException } from 'src/common/exceptions'; +import { mockCaCertificate, mockClientCertificate } from 'src/__mocks__/certificates'; +import { + InvalidCaCertificateBodyException, InvalidCertificateNameException, +} from 'src/modules/database-import/exceptions'; -export const mockDatabasesToImportArray = new Array(10).fill(mockDatabase); +export const mockDatabasesToImportArray = new Array(10).fill(mockSentinelDatabaseWithTlsAuth); export const mockDatabaseImportFile = { originalname: 'filename.json', @@ -27,13 +31,28 @@ export const mockDatabaseImportResultFail = { errors: [new BadRequestException()], }; +export const mockDatabaseImportResultPartial = { + index: 0, + status: DatabaseImportStatus.Partial, + host: mockDatabase.host, + port: mockDatabase.port, + errors: [new InvalidCaCertificateBodyException()], +}; + export const mockDatabaseImportResponse = Object.assign(new DatabaseImportResponse(), { total: 10, - success: (new Array(7).fill(mockDatabaseImportResultSuccess)).map((v, index) => ({ + success: (new Array(5).fill(mockDatabaseImportResultSuccess)).map((v, index) => ({ ...v, + index: index + 5, + })), + partial: [ + [new InvalidCaCertificateBodyException(), new InvalidCertificateNameException()], + [new InvalidCertificateNameException()], + ].map((errors, index) => ({ + ...mockDatabaseImportResultPartial, index: index + 3, + errors, })), - partial: [], fail: [ new ValidationException('Bad request'), new BadRequestException(), @@ -45,8 +64,9 @@ export const mockDatabaseImportResponse = Object.assign(new DatabaseImportRespon })), }); -export const mockDatabaseImportParseFailedAnalyticsPayload = { - +export const mockDatabaseImportPartialAnalyticsPayload = { + partially: mockDatabaseImportResponse.partial.length, + errors: ['InvalidCaCertificateBodyException', 'InvalidCertificateNameException'], }; export const mockDatabaseImportFailedAnalyticsPayload = { @@ -63,6 +83,7 @@ export const mockDatabaseImportAnalytics = jest.fn(() => ({ sendImportFailed: jest.fn(), })); -export const mockCertificateImportService = jest.fn(() => { - -}); +export const mockCertificateImportService = jest.fn(() => ({ + processCaCertificate: jest.fn().mockResolvedValue(mockCaCertificate), + processClientCertificate: jest.fn().mockResolvedValue(mockClientCertificate), +})); diff --git a/redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.spec.ts b/redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.spec.ts index ba88c967bb..6847becd99 100644 --- a/redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.spec.ts +++ b/redisinsight/api/src/modules/certificate/repositories/local.client-certificate.repository.spec.ts @@ -15,7 +15,6 @@ import { mockRepository, MockType, } from 'src/__mocks__'; -import { LocalCaCertificateRepository } from 'src/modules/certificate/repositories/local.ca-certificate.repository'; import { EncryptionService } from 'src/modules/encryption/encryption.service'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import ERROR_MESSAGES from 'src/constants/error-messages'; diff --git a/redisinsight/api/src/modules/database-import/certificate-import.service.spec.ts b/redisinsight/api/src/modules/database-import/certificate-import.service.spec.ts new file mode 100644 index 0000000000..2939250c52 --- /dev/null +++ b/redisinsight/api/src/modules/database-import/certificate-import.service.spec.ts @@ -0,0 +1,341 @@ +import { when } from 'jest-when'; +import { + mockCaCertificate, + mockCaCertificateCertificateEncrypted, + mockCaCertificateCertificatePlain, + mockCaCertificateEntity, + mockClientCertificate, + mockClientCertificateCertificateEncrypted, + mockClientCertificateCertificatePlain, + mockClientCertificateEntity, + mockClientCertificateKeyEncrypted, + mockClientCertificateKeyPlain, + mockEncryptionService, + mockRepository, + MockType, +} from 'src/__mocks__'; +import * as utils from 'src/common/utils'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + InvalidCaCertificateBodyException, + InvalidCertificateNameException, + InvalidClientCertificateBodyException, + InvalidClientPrivateKeyException, +} from 'src/modules/database-import/exceptions'; +import { CertificateImportService } from 'src/modules/database-import/certificate-import.service'; +import { EncryptionService } from 'src/modules/encryption/encryption.service'; +import { Repository } from 'typeorm'; +import { CaCertificateEntity } from 'src/modules/certificate/entities/ca-certificate.entity'; +import { ClientCertificateEntity } from 'src/modules/certificate/entities/client-certificate.entity'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +jest.mock('src/common/utils', () => ({ + ...jest.requireActual('src/common/utils') as object, + getPemBodyFromFileSync: jest.fn(), +})); + +describe('CertificateImportService', () => { + let service: CertificateImportService; + let caRepository: MockType>; + let clientRepository: MockType>; + let encryptionService: MockType; + + beforeEach(async () => { + jest.clearAllMocks(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CertificateImportService, + { + provide: getRepositoryToken(CaCertificateEntity), + useFactory: mockRepository, + }, + { + provide: getRepositoryToken(ClientCertificateEntity), + useFactory: mockRepository, + }, + { + provide: EncryptionService, + useFactory: mockEncryptionService, + }, + ], + }).compile(); + + service = await module.get(CertificateImportService); + caRepository = await module.get(getRepositoryToken(CaCertificateEntity)); + clientRepository = await module.get(getRepositoryToken(ClientCertificateEntity)); + encryptionService = await module.get(EncryptionService); + + when(encryptionService.decrypt).calledWith(mockCaCertificateCertificateEncrypted, jasmine.anything()) + .mockResolvedValue(mockCaCertificateCertificatePlain); + when(encryptionService.encrypt).calledWith(mockCaCertificateCertificatePlain) + .mockResolvedValue({ + data: mockCaCertificateCertificateEncrypted, + encryption: mockCaCertificateEntity.encryption, + }); + + when(encryptionService.decrypt) + .calledWith(mockClientCertificateCertificateEncrypted, jasmine.anything()) + .mockResolvedValue(mockClientCertificateCertificatePlain) + .calledWith(mockClientCertificateKeyEncrypted, jasmine.anything()) + .mockResolvedValue(mockClientCertificateKeyPlain); + when(encryptionService.encrypt) + .calledWith(mockClientCertificateCertificatePlain) + .mockResolvedValue({ + data: mockClientCertificateCertificateEncrypted, + encryption: mockClientCertificateEntity.encryption, + }) + .calledWith(mockClientCertificateKeyPlain) + .mockResolvedValue({ + data: mockClientCertificateKeyEncrypted, + encryption: mockClientCertificateEntity.encryption, + }); + }); + + let determineAvailableNameSpy; + let getPemBodyFromFileSyncSpy; + let prepareCaCertificateForImportSpy; + let prepareClientCertificateForImportSpy; + + describe('processCaCertificate', () => { + beforeEach(() => { + getPemBodyFromFileSyncSpy = jest.spyOn(utils as any, 'getPemBodyFromFileSync'); + getPemBodyFromFileSyncSpy.mockReturnValue(mockCaCertificate.certificate); + prepareCaCertificateForImportSpy = jest.spyOn(service as any, 'prepareCaCertificateForImport'); + prepareCaCertificateForImportSpy.mockResolvedValueOnce(mockCaCertificate); + }); + + it('should successfully process certificate', async () => { + const response = await service['processCaCertificate']({ + name: mockCaCertificate.name, + certificate: mockCaCertificate.certificate, + }); + + expect(response).toEqual(mockCaCertificate); + }); + + it('should fail when no name defined', async () => { + try { + await service['processCaCertificate']({ + name: undefined, + certificate: mockCaCertificate.certificate, + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(InvalidCertificateNameException); + } + }); + + it('should successfully process certificate from file', async () => { + const response = await service['processCaCertificate']({ + certificate: '/path/ca.crt', + }); + + expect(response).toEqual(mockCaCertificate); + expect(prepareCaCertificateForImportSpy).toHaveBeenCalledWith({ + name: 'ca', + certificate: mockCaCertificate.certificate, + }); + }); + + it('should fail when no file found', async () => { + getPemBodyFromFileSyncSpy.mockImplementationOnce(() => { throw new Error(); }); + + try { + await service['processCaCertificate']({ + name: undefined, + certificate: '/path/ca.crt', + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(InvalidCaCertificateBodyException); + } + }); + }); + + describe('prepareCaCertificateForImport', () => { + beforeEach(() => { + determineAvailableNameSpy = jest.spyOn(CertificateImportService, 'determineAvailableName'); + }); + + it('should return existing certificate', async () => { + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockCaCertificate); + + const response = await service['prepareCaCertificateForImport']({ + name: mockCaCertificate.name, + certificate: mockCaCertificate.certificate, + }); + + expect(response).toEqual(mockCaCertificate); + expect(determineAvailableNameSpy).not.toHaveBeenCalled(); + }); + + it('should return new certificate', async () => { + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search + + const response = await service['prepareCaCertificateForImport']({ + name: `${mockCaCertificate.name}_new`, + certificate: mockCaCertificate.certificate, + }); + + expect(response).toEqual({ + ...mockCaCertificate, + id: undefined, // return not-existing model + name: `${mockCaCertificate.name}_new`, + }); + }); + + it('should generate name with prefix', async () => { + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockCaCertificate); // for name search 1st attempt + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockCaCertificate); // for name search 2nd attempt + caRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search 3rd attempt + + const response = await service['prepareCaCertificateForImport']({ + name: `${mockCaCertificate.name}_new`, + certificate: mockCaCertificate.certificate, + }); + + expect(response).toEqual({ + ...mockCaCertificate, + id: undefined, // return not-existing model + name: `2_${mockCaCertificate.name}_new`, + }); + }); + }); + + describe('processClientCertificate', () => { + beforeEach(() => { + getPemBodyFromFileSyncSpy = jest.spyOn(utils as any, 'getPemBodyFromFileSync'); + prepareClientCertificateForImportSpy = jest.spyOn(service as any, 'prepareClientCertificateForImport'); + prepareClientCertificateForImportSpy.mockResolvedValueOnce(mockClientCertificate); + }); + + it('should successfully process client certificate', async () => { + const response = await service['processClientCertificate']({ + name: mockClientCertificate.name, + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + + expect(response).toEqual(mockClientCertificate); + }); + + it('should fail when no name defined', async () => { + try { + await service['processClientCertificate']({ + name: undefined, + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(InvalidCertificateNameException); + } + }); + + it('should successfully process certificate from file', async () => { + getPemBodyFromFileSyncSpy.mockReturnValueOnce(mockClientCertificate.certificate); + getPemBodyFromFileSyncSpy.mockReturnValueOnce(mockClientCertificate.key); + + const response = await service['processClientCertificate']({ + certificate: '/path/client.crt', + key: '/path/key.key', + }); + + expect(response).toEqual(mockClientCertificate); + expect(prepareClientCertificateForImportSpy).toHaveBeenCalledWith({ + name: 'client', + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + }); + + it('should fail when no cert file found', async () => { + getPemBodyFromFileSyncSpy.mockImplementationOnce(() => { throw new Error(); }); + + try { + await service['processClientCertificate']({ + name: undefined, + certificate: '/path/client1.crt', + key: '/path/key1.key', + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(InvalidClientCertificateBodyException); + } + }); + + it('should fail when no key file found', async () => { + getPemBodyFromFileSyncSpy.mockReturnValueOnce(mockClientCertificate.certificate); + getPemBodyFromFileSyncSpy.mockImplementationOnce(() => { throw new Error(); }); + + try { + await service['processClientCertificate']({ + name: undefined, + certificate: '/path/client.crt', + key: '/path/key.key', + }); + fail(); + } catch (e) { + expect(e).toBeInstanceOf(InvalidClientPrivateKeyException); + } + }); + }); + + describe('prepareClientCertificateForImport', () => { + beforeEach(() => { + determineAvailableNameSpy = jest.spyOn(CertificateImportService, 'determineAvailableName'); + }); + + it('should return existing certificate', async () => { + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockClientCertificate); + + const response = await service['prepareClientCertificateForImport']({ + name: mockClientCertificate.name, + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + + expect(response).toEqual(mockClientCertificate); + expect(determineAvailableNameSpy).not.toHaveBeenCalled(); + }); + + it('should return new certificate', async () => { + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for name search + + const response = await service['prepareClientCertificateForImport']({ + name: `${mockClientCertificate.name}_new`, + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + + expect(response).toEqual({ + ...mockClientCertificate, + id: undefined, // return not-existing model + name: `${mockClientCertificate.name}_new`, + }); + }); + + it('should generate name with prefix', async () => { + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // for cert search + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockClientCertificate); // name 1st attempt + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(mockClientCertificate); // name 2nd attempt + clientRepository.createQueryBuilder().getOne.mockResolvedValueOnce(null); // name 3rd attempt + + const response = await service['prepareClientCertificateForImport']({ + name: `${mockClientCertificate.name}_new`, + certificate: mockClientCertificate.certificate, + key: mockClientCertificate.key, + }); + + expect(response).toEqual({ + ...mockClientCertificate, + id: undefined, // return not-existing model + name: `2_${mockClientCertificate.name}_new`, + }); + }); + }); +}); diff --git a/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts b/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts index 920cc215f4..a2554d742e 100644 --- a/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.analytics.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { - mockDatabaseImportFailedAnalyticsPayload, + mockDatabaseImportFailedAnalyticsPayload, mockDatabaseImportPartialAnalyticsPayload, mockDatabaseImportResponse, mockDatabaseImportSucceededAnalyticsPayload, } from 'src/__mocks__'; import { TelemetryEvents } from 'src/constants'; @@ -44,6 +44,12 @@ describe('DatabaseImportAnalytics', () => { TelemetryEvents.DatabaseImportFailed, mockDatabaseImportFailedAnalyticsPayload, ); + + expect(sendEventSpy).toHaveBeenNthCalledWith( + 3, + TelemetryEvents.DatabaseImportPartiallySucceeded, + mockDatabaseImportPartialAnalyticsPayload, + ); }); }); diff --git a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts index 3f6fa114a2..45a4957e33 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.spec.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.spec.ts @@ -15,14 +15,16 @@ import { ConnectionType } from 'src/modules/database/entities/database.entity'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { ValidationError } from 'class-validator'; import { + InvalidCaCertificateBodyException, InvalidCertificateNameException, InvalidClientCertificateBodyException, NoDatabaseImportFileProvidedException, SizeLimitExceededDatabaseImportFileException, - UnableToParseDatabaseImportFileException, + UnableToParseDatabaseImportFileException } from 'src/modules/database-import/exceptions'; import { CertificateImportService } from 'src/modules/database-import/certificate-import.service'; describe('DatabaseImportService', () => { let service: DatabaseImportService; + let certificateImportService: MockType; let databaseRepository: MockType; let analytics: MockType; let validatoSpy; @@ -52,6 +54,7 @@ describe('DatabaseImportService', () => { service = await module.get(DatabaseImportService); databaseRepository = await module.get(DatabaseRepository); + certificateImportService = await module.get(CertificateImportService); analytics = await module.get(DatabaseImportAnalytics); validatoSpy = jest.spyOn(service['validator'], 'validateOrReject'); }); @@ -61,6 +64,17 @@ describe('DatabaseImportService', () => { databaseRepository.create.mockRejectedValueOnce(new BadRequestException()); databaseRepository.create.mockRejectedValueOnce(new ForbiddenException()); validatoSpy.mockRejectedValueOnce([new ValidationError()]); + certificateImportService.processCaCertificate + .mockRejectedValueOnce(new InvalidCaCertificateBodyException()) + .mockRejectedValueOnce(new InvalidCaCertificateBodyException()) + .mockRejectedValueOnce(new InvalidCaCertificateBodyException()) + .mockRejectedValueOnce(new InvalidCaCertificateBodyException()) + .mockRejectedValueOnce(new InvalidCertificateNameException()); + certificateImportService.processClientCertificate + .mockRejectedValueOnce(new InvalidClientCertificateBodyException()) + .mockRejectedValueOnce(new InvalidClientCertificateBodyException()) + .mockRejectedValueOnce(new InvalidClientCertificateBodyException()) + .mockRejectedValueOnce(new InvalidCertificateNameException()); }); it('should import databases from json', async () => { diff --git a/redisinsight/api/src/modules/database-import/database-import.service.ts b/redisinsight/api/src/modules/database-import/database-import.service.ts index 6f3db4959a..78e0ac1df6 100644 --- a/redisinsight/api/src/modules/database-import/database-import.service.ts +++ b/redisinsight/api/src/modules/database-import/database-import.service.ts @@ -297,11 +297,9 @@ export class DatabaseImportService { static parseFile(file): any { const data = file?.buffer?.toString(); - let databases; + let databases = DatabaseImportService.parseJson(data); - if (file?.mimetype === 'application/json') { - databases = DatabaseImportService.parseJson(data); - } else { + if (!databases) { databases = DatabaseImportService.parseBase64(data); } diff --git a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts index fa8c0d9a4d..d7c6f49fc1 100644 --- a/redisinsight/api/test/api/database-import/POST-databases-import.test.ts +++ b/redisinsight/api/test/api/database-import/POST-databases-import.test.ts @@ -6,7 +6,9 @@ import { deps, requirements, validateApiCall, - getMainCheckFn, generateInvalidDataArray, + getMainCheckFn, + generateInvalidDataArray, + _, } from '../deps'; import { randomBytes } from 'crypto'; import { cloneDeep, set } from 'lodash'; @@ -40,23 +42,173 @@ const baseDatabaseData = { password: constants.TEST_REDIS_PASSWORD || '', } +const baseTls = { + tls: constants.TEST_REDIS_TLS_CA ? true : undefined, + caCert: constants.TEST_REDIS_TLS_CA ? { + name: constants.TEST_CA_NAME, + certificate: constants.TEST_REDIS_TLS_CA, + } : undefined, + clientCert: constants.TEST_USER_TLS_CERT ? { + name: constants.TEST_CLIENT_CERT_NAME, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + } : undefined, +}; + const baseSentinelData = { - name: constants.TEST_SENTINEL_MASTER_GROUP, - username: constants.TEST_SENTINEL_MASTER_USER || null, - password: constants.TEST_SENTINEL_MASTER_PASS || null, + sentinelMaster: constants.TEST_RTE_TYPE === 'SENTINEL' ? { + name: constants.TEST_SENTINEL_MASTER_GROUP, + username: constants.TEST_SENTINEL_MASTER_USER || null, + password: constants.TEST_SENTINEL_MASTER_PASS || null, + } : undefined, + username: constants.TEST_SENTINEL_MASTER_USER ? constants.TEST_SENTINEL_MASTER_USER : constants.TEST_REDIS_USER, + password: constants.TEST_SENTINEL_MASTER_PASS ? constants.TEST_SENTINEL_MASTER_PASS : constants.TEST_REDIS_PASSWORD, } +const importDatabaseFormat0 = { + ...baseDatabaseData, + ...baseTls, + ...baseSentinelData, + connectionType: 'STANDALONE', + verifyServerCert: true, +}; + +const baseSentinelDataFormat1 = { + sentinelOptions: baseSentinelData.sentinelMaster ? { + sentinelPassword: baseSentinelData.password, + name: baseSentinelData.sentinelMaster.name, + } : undefined, + username: constants.TEST_SENTINEL_MASTER_USER ? constants.TEST_SENTINEL_MASTER_USER : constants.TEST_REDIS_USER, + password: constants.TEST_SENTINEL_MASTER_PASS ? constants.TEST_SENTINEL_MASTER_PASS : constants.TEST_REDIS_PASSWORD, +}; + const importDatabaseFormat1 = { + id: "1393c216-3fd0-4ad5-8412-209a8e8ec77c", name: baseDatabaseData.name, + type: 'standalone', + keyPrefix: null, + host: baseDatabaseData.host, + port: baseDatabaseData.port, + username: baseDatabaseData.username, + password: baseDatabaseData.password, + db: 0, + ssl: !!baseTls.tls, + caCert: baseTls.caCert ? constants.TEST_CA_CERT_PATH : null, + certificate: baseTls.clientCert ? constants.TEST_CLIENT_CERT_PATH : null, + keyFile: baseTls.clientCert ? constants.TEST_CLIENT_KEY_PATH : null, + ...baseSentinelDataFormat1, +} + + +const baseSentinelDataFormat2 = { + sentinelOptions: baseSentinelData.sentinelMaster ? { + masterName: baseSentinelData.sentinelMaster.name, + nodePassword: baseSentinelData.password, + } : undefined, + username: constants.TEST_SENTINEL_MASTER_USER ? constants.TEST_SENTINEL_MASTER_USER : constants.TEST_REDIS_USER, + auth: constants.TEST_SENTINEL_MASTER_PASS ? constants.TEST_SENTINEL_MASTER_PASS : constants.TEST_REDIS_PASSWORD, +}; + +const importDatabaseFormat2 = { host: baseDatabaseData.host, port: `${baseDatabaseData.port}`, + auth: baseDatabaseData.password, username: baseDatabaseData.username, + connectionName: baseDatabaseData.name, + cluster: false, + sslOptions: baseTls.caCert ? { + key: baseTls.clientCert ? constants.TEST_CLIENT_KEY_PATH : undefined, + cert: baseTls.clientCert ? constants.TEST_CLIENT_CERT_PATH : undefined, + ca: baseTls.caCert ? constants.TEST_CA_CERT_PATH : undefined, + } : undefined, + ...baseSentinelDataFormat2, +} + + +const baseSentinelDataFormat3 = { + username: constants.TEST_SENTINEL_MASTER_USER ? constants.TEST_SENTINEL_MASTER_USER : constants.TEST_REDIS_USER, + auth: constants.TEST_SENTINEL_MASTER_PASS ? constants.TEST_SENTINEL_MASTER_PASS : constants.TEST_REDIS_PASSWORD, +}; + +const importDatabaseFormat3 = { + name: baseDatabaseData.name, + host: baseDatabaseData.host, + port: baseDatabaseData.port, auth: baseDatabaseData.password, + username: baseDatabaseData.username, + ssl: !!baseTls.tls, + ssl_ca_cert_path: baseTls.caCert ? constants.TEST_CA_CERT_PATH : undefined, + ssl_local_cert_path: baseTls.clientCert ? constants.TEST_CLIENT_CERT_PATH : undefined, + ssl_private_key_path: baseTls.clientCert ? constants.TEST_CLIENT_KEY_PATH : undefined, + ...baseSentinelDataFormat3, } const mainCheckFn = getMainCheckFn(endpoint); +const checkConnection = async (databaseId: string, statusCode = 200) => { + await validateApiCall({ + endpoint: () => request(server).get(`/${constants.API.DATABASES}/${databaseId}/connect`), + statusCode, + }); +}; + +const checkDataManagement = async (databaseId: string) => { + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${databaseId}/workbench/command-executions`), + data: { + commands: ['set string value'], + }, + checkFn: ({ body }) => { + expect(body[0].result).to.deep.eq([{ + status: 'success', + response: 'OK', + }]) + } + }); +}; + +const validateImportedDatabase = async ( + name: string, + initType: string, + detectedType: string, + dataCheck = true, +) => { + let database = await localDb.getInstanceByName(name); + expect(database.connectionType).to.eq(initType); + expect(database.new).to.eq(true); + + await checkConnection(database.id); + database = await localDb.getInstanceByName(name); + + expect(database.connectionType).to.eq(detectedType); + expect(database.new).to.eq(false); + + if (dataCheck) { + await checkDataManagement(database.id) + } +}; + +const validatePartialImportedDatabase = async ( + name: string, + initType: string, + detectedType: string, + statusCode = 400, +) => { + let database = await localDb.getInstanceByName(name); + expect(database.connectionType).to.eq(initType); + expect(database.new).to.eq(true); + + await checkConnection(database.id, statusCode); + database = await localDb.getInstanceByName(name); + + expect(database.connectionType).to.eq(detectedType); + expect(database.new).to.eq(true); +}; + +let name; + describe('POST /databases/import', () => { + beforeEach(() => { name = constants.getRandomString(); }) describe('Validation', function () { generateInvalidDataArray(databaseSchema) .map(({ path, value }) => { @@ -147,13 +299,296 @@ describe('POST /databases/import', () => { }, ].map(mainCheckFn); }); + describe('Certificates', () => { + describe('CA', () => { + it('Should create only 1 certificate', async () => { + const caCertName = constants.getRandomString(); + + const caCerts = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts2 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff = _.differenceWith(caCerts2, caCerts, _.isEqual); + + expect(diff.length).to.eq(1); + expect(diff[0].name).to.eq(caCertName); + + // import more + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts3 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff2 = _.differenceWith(caCerts3, caCerts2, _.isEqual); + + expect(diff2.length).to.eq(0); + }); + it('Should create multiple certs with name prefixes', async () => { + const caCertName = constants.getRandomString(); + + const caCerts = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert_${idx}`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts2 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff = _.differenceWith(caCerts2, caCerts, _.isEqual); + + expect(diff.length).to.eq(10); + expect(diff[0].name).to.eq(caCertName); + expect(diff[1].name).to.eq(`1_${caCertName}`); + expect(diff[2].name).to.eq(`2_${caCertName}`); + expect(diff[3].name).to.eq(`3_${caCertName}`); + expect(diff[9].name).to.eq(`9_${caCertName}`); + }); + }); + describe('CLIENT', () => { + it('Should create only 1 certificate', async () => { + const caCertName = constants.getRandomString(); + const clientCertName = constants.getRandomString(); + + const caCerts = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const clientCerts = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)).find(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert__`, + }, + clientCert: { + name: clientCertName, + certificate: `-----BEGIN CERTIFICATE-----clientCert__`, + key: `-----BEGIN PRIVATE KEY-----clientKey__`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts2 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff = _.differenceWith(caCerts2, caCerts, _.isEqual); + expect(diff.length).to.eq(1); + expect(diff[0].name).to.eq(caCertName); + + const clientCerts2 = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)).find(); + const clientDiff = _.differenceWith(clientCerts2, clientCerts, _.isEqual); + expect(clientDiff.length).to.eq(1); + expect(clientDiff[0].name).to.eq(clientCertName); + + + // import more + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert__`, + }, + clientCert: { + name: clientCertName, + certificate: `-----BEGIN CERTIFICATE-----clientCert__`, + key: `-----BEGIN PRIVATE KEY-----clientKey__`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts3 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff2 = _.differenceWith(caCerts3, caCerts2, _.isEqual); + expect(diff2.length).to.eq(0); + + const clientCerts3 = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)).find(); + const clientDiff2 = _.differenceWith(clientCerts3, clientCerts2, _.isEqual); + expect(clientDiff2.length).to.eq(0); + }); + it('Should create multiple certs with name prefixes', async () => { + const caCertName = constants.getRandomString(); + const clientCertName = constants.getRandomString(); + + const caCerts = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const clientCerts = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)).find(); + + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify((new Array(10).fill(1)).map((c, idx) => { + return { + ...baseDatabaseData, + tls: true, + caCert: { + name: caCertName, + certificate: `-----BEGIN CERTIFICATE-----caCert__${idx}`, + }, + clientCert: { + name: clientCertName, + certificate: `-----BEGIN CERTIFICATE-----clientCert__${idx}`, + key: `-----BEGIN PRIVATE KEY-----clientKey__${idx}`, + }, + name, + } + }))), 'file.json'], + responseBody: { + total: 10, + success: (new Array(10).fill(1)).map((_v, index) => ({ + index, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + })), + partial: [], + fail: [], + }, + }); + + const caCerts2 = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)).find(); + const diff = _.differenceWith(caCerts2, caCerts, _.isEqual); + expect(diff.length).to.eq(10); + expect(diff[0].name).to.eq(caCertName); + expect(diff[1].name).to.eq(`1_${caCertName}`); + expect(diff[2].name).to.eq(`2_${caCertName}`); + expect(diff[3].name).to.eq(`3_${caCertName}`); + expect(diff[9].name).to.eq(`9_${caCertName}`); + + const clientCerts2 = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)).find(); + const clientDiff = _.differenceWith(clientCerts2, clientCerts, _.isEqual); + expect(clientDiff.length).to.eq(10); + expect(clientDiff[0].name).to.eq(clientCertName); + expect(clientDiff[1].name).to.eq(`1_${clientCertName}`); + expect(clientDiff[2].name).to.eq(`2_${clientCertName}`); + expect(clientDiff[3].name).to.eq(`3_${clientCertName}`); + expect(clientDiff[9].name).to.eq(`9_${clientCertName}`); + }); + }); + }); describe('STANDALONE', () => { requirements('rte.type=STANDALONE'); describe('NO TLS', function () { requirements('!rte.tls'); - it('Import standalone without tls (format 1)', async () => { - const name = constants.getRandomString(); + it('Import standalone (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone (format 1)', async () => { await validateApiCall({ endpoint, attach: ['file', Buffer.from(JSON.stringify([ @@ -168,24 +603,68 @@ describe('POST /databases/import', () => { index: 0, status: 'success', host: importDatabaseFormat1.host, - port: parseInt(importDatabaseFormat1.port, 10), + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), }], partial: [], fail: [], }, }); - // check connection - const database = await localDb.getInstanceByName(name); + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); + }); + it('Import standalone (format 3)', async () => { + const name = constants.getRandomString(); + await validateApiCall({ - endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), - statusCode: 200, + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, }); - expect(database.new).to.eq(true); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); }); describe('Oss', () => { requirements('!rte.re'); - it('Import standalone with particular db index', async () => { + it('Import standalone with particular db index (format 1)', async () => { const name = constants.getRandomString(); const cliUuid = constants.getRandomString(); const browserKeyName = constants.getRandomString(); @@ -195,7 +674,7 @@ describe('POST /databases/import', () => { endpoint, attach: ['file', Buffer.from(JSON.stringify([ { - ...importDatabaseFormat1, + ...importDatabaseFormat0, name, db: constants.TEST_REDIS_DB_INDEX, } @@ -205,8 +684,8 @@ describe('POST /databases/import', () => { success: [{ index: 0, status: 'success', - host: importDatabaseFormat1.host, - port: parseInt(importDatabaseFormat1.port, 10), + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, }], partial: [], fail: [], @@ -258,63 +737,868 @@ describe('POST /databases/import', () => { }); }); }); - xdescribe('TLS CA', function () { + describe('TLS CA', function () { requirements('rte.tls', '!rte.tlsAuth'); - }); - xdescribe('TLS AUTH', function () { - requirements('rte.tls', 'rte.tlsAuth'); - }); - }); - describe('CLUSTER', () => { - requirements('rte.type=CLUSTER'); - describe('NO TLS', function () { - requirements('!rte.tls'); - it('should import cluster database (base64)', async () => { - const name = constants.getRandomString(); - + it('Import standalone with CA tls (format 0)', async () => { await validateApiCall({ endpoint, - attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + attach: ['file', Buffer.from(JSON.stringify([ { - ...importDatabaseFormat1, + ...importDatabaseFormat0, name, - cluster: true, } - ])).toString('base64')), 'file.ano'], + ])), 'file.json'], responseBody: { total: 1, success: [{ index: 0, status: 'success', - host: importDatabaseFormat1.host, - port: parseInt(importDatabaseFormat1.port, 10), + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, }], partial: [], fail: [], }, }); + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA tls partial with wrong body (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + caCert: { + ...importDatabaseFormat0.caCert, + certificate: 'bad body', + }, + name, + } + ])), 'file'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + errors: [ + { + message: 'Invalid CA body', + statusCode: 400, + error: 'Invalid Ca Certificate Body', + } + ], + }], + fail: [], + }, + }); - // check connection - const database = await localDb.getInstanceByName(name); - - expect(database.new).to.eq(true); - expect(database.nodes).to.eq('[]'); - expect(database.connectionType).to.eq('CLUSTER'); + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA tls partial with no ca name (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + caCert: { + ...importDatabaseFormat0.caCert, + name: undefined, + }, + name, + } + ])), 'file'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + errors: [ + { + message: 'Certificate name is not defined', + statusCode: 400, + error: 'Invalid Certificate Name', + }, + ], + }], + fail: [], + }, + }); + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA tls (format 1)', async () => { await validateApiCall({ - endpoint: () => request(server).get(`/${constants.API.DATABASES}/${database.id}/connect`), - statusCode: 200, + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, }); - expect((await localDb.getInstanceByName(name)).nodes).to.not.eq('[]'); + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); }); - }); - xdescribe('TLS CA', function () { - requirements('rte.tls', '!rte.tlsAuth'); - }); - }); - xdescribe('SENTINEL', () => { - requirements('rte.type=SENTINEL'); + it('Import standalone with CA tls partial with no ca file (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + caCert: 'not-existing-path', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + errors: [ + { + message: 'Invalid CA body', + statusCode: 400, + error: 'Invalid Ca Certificate Body', + } + ], + }], + fail: [], + }, + }); + + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA tls (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); + }); + it('Import standalone with CA tls (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); + }); + + }); + describe('TLS AUTH', function () { + requirements('rte.tls', 'rte.tlsAuth'); + it('Import standalone with CA + CLIENT tls (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls partial with wrong bodies (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + caCert: { + ...importDatabaseFormat0.caCert, + certificate: 'bad body', + }, + clientCert: { + ...importDatabaseFormat0.clientCert, + certificate: 'bad body', + }, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + errors: [ + { + message: 'Invalid CA body', + statusCode: 400, + error: 'Invalid Ca Certificate Body' + }, + { + message: 'Invalid certificate body', + statusCode: 400, + error: 'Invalid Client Certificate Body' + } + ], + }], + fail: [], + }, + }); + + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls partial with no cert name (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + caCert: { + ...importDatabaseFormat0.caCert, + certificate: 'bad body', + }, + clientCert: { + ...importDatabaseFormat0.clientCert, + name: undefined, + }, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + errors: [ + { + message: 'Invalid CA body', + statusCode: 400, + error: 'Invalid Ca Certificate Body' + }, + { + message: 'Certificate name is not defined', + statusCode: 400, + error: 'Invalid Certificate Name', + }, + ], + }], + fail: [], + }, + }); + + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls partial with wrong key (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + clientCert: { + ...importDatabaseFormat0.clientCert, + key: 'bad path', + }, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [], + partial: [{ + index: 0, + status: 'partial', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + errors: [ + { + message: 'Invalid private key', + statusCode: 400, + error: 'Invalid Client Private Key', + }, + ], + }], + fail: [], + }, + }); + + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); + }); + it('Import standalone with CA + CLIENT tls (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE'); + }); + }); + }); + describe('CLUSTER', () => { + requirements('rte.type=CLUSTER'); + describe('NO TLS', function () { + requirements('!rte.tls'); + it('Import cluster (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + connectionType: 'CLUSTER', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + type: 'cluster', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + cluster: true, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster auto discovered (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + cluster: false, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER'); + }); + it('Import cluster (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER'); + }); + }); + describe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + it('Import cluster with CA tls (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + connectionType: 'CLUSTER', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster with CA tls (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + type: 'cluster', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster with CA tls (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + cluster: true, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'CLUSTER', 'CLUSTER'); + }); + it('Import cluster with CA tls (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'NOT CONNECTED', 'CLUSTER'); + }); + }); + }); + describe('SENTINEL', () => { + requirements('rte.type=SENTINEL'); + describe('NO TLS', function () { + requirements('!rte.tls'); + it('Import sentinel (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + connectionType: 'SENTINEL', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + type: 'sentinel', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + // should determine connection type as standalone since we don't have sentinel auto discovery + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE', false); + }); + }); + describe('TLS AUTH', function () { + requirements('rte.tls', 'rte.tlsAuth'); + it('Import sentinel with CA + CLIENT tls (format 0)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat0, + connectionType: 'SENTINEL', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat0.host, + port: importDatabaseFormat0.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel with CA + CLIENT tls (format 1)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat1, + type: 'sentinel', + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat1.host, + port: importDatabaseFormat1.port, + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel with CA + CLIENT tls (format 2)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat2, + name, + } + ])).toString('base64')), 'file.ano'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat2.host, + port: parseInt(importDatabaseFormat2.port, 10), + }], + partial: [], + fail: [], + }, + }); + + await validateImportedDatabase(name, 'SENTINEL', 'SENTINEL'); + }); + it('Import sentinel with CA + CLIENT tls (format 3)', async () => { + await validateApiCall({ + endpoint, + attach: ['file', Buffer.from(JSON.stringify([ + { + ...importDatabaseFormat3, + name, + } + ])), 'file.json'], + responseBody: { + total: 1, + success: [{ + index: 0, + status: 'success', + host: importDatabaseFormat3.host, + port: importDatabaseFormat3.port, + }], + partial: [], + fail: [], + }, + }); + + // should determine connection type as standalone since we don't have sentinel auto discovery + await validateImportedDatabase(name, 'NOT CONNECTED', 'STANDALONE', false); + }); + }); }); }); diff --git a/redisinsight/api/test/helpers/constants.ts b/redisinsight/api/test/helpers/constants.ts index cf3b0b64c9..5f43ac0df7 100644 --- a/redisinsight/api/test/helpers/constants.ts +++ b/redisinsight/api/test/helpers/constants.ts @@ -1,4 +1,5 @@ import { v4 as uuidv4 } from 'uuid'; +import * as path from 'path'; import { randomBytes } from 'crypto'; import { getASCIISafeStringFromBuffer, getBufferFromSafeASCIIString } from "src/utils/cli-helper"; @@ -21,6 +22,8 @@ const unprintableBuf = Buffer.concat([ Buffer.from(CLUSTER_HASH_SLOT), ]); +const CERTS_FOLDER = process.env.CERTS_FOLDER || './coverage'; + export const constants = { // api API, @@ -112,6 +115,10 @@ export const constants = { TEST_CA_NAME: 'ca certificate', TEST_CA_FILENAME: 'redisCA.crt', TEST_CA_CERT: '-----BEGIN CERTIFICATE-----\nMIIFazCCA1OgAwIBAgIUavmmz7/4r2muhE1He1u/6S1jLXEwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMTA2MjIxNjMwNTJaFw00ODEx\nMDcxNjMwNTJaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB\nAQUAA4ICDwAwggIKAoICAQDGUx5tjluzcotvXlr4XCqAbdO+ehD/djqzb3KxB4p4\nUNE5tqS4TykvbisASOnNj/643J0BSYKEZwNBjy/oAWmc3cVMq30WnWRENGRUKyr+\nqhgjR0OGMHxpAU8DiDgsJAuvvh86SU0xIo6PUWyO38XNIOGt05s61My9fW+Allai\n5/jj6knBej42cRY7B9hUgHfko9NXE5oUVFKE+dpH9IiMUGBm7SDi1ysB1vIMQhcT\n8ugQHdwXAiQfhDODNuDG48z6OprhGgHN5lYNFd3oFlweoFaqE0psFRh9bR5AuqES\nubxEFqMVwEjyJa8BgObRBwdHoipZt1FLDeKTP5/MGUm5n/2X+pcAi4Q7+9i+aVz5\ngFiCz6ndOFEj3X4CXcHHLVzI8ukQ3wQiDFXnomLOcFcuAJ9t+MisUOwts/Nvmqa0\n+copNgXu2N8K01G77HX1qbJ0uyF6pupw2EWW0yJXkoSeOeaFegHPMx6y3RUx1adl\nKu9vQ8JDodK4OwHfQcSBgj8aKA7huBnclgpBmM6B1czC6pw7DN6orLOlsx6cUusP\n4mELM2CNNYLUQuxhghTO8lAQTgvvth5MNSpxA6x/gKFGmLN9XUJIZweQQymeY137\n8elXS2yuoSyppisB+HDvp6MbegN1ldzhI0AjdUj9NDiiO5sDk+XscKA8tsZz/MgW\nMQIDAQABo1MwUTAdBgNVHQ4EFgQU0CzAfHYx+Tr/axoAsurYNR/t2RMwHwYDVR0j\nBBgwFoAU0CzAfHYx+Tr/axoAsurYNR/t2RMwDwYDVR0TAQH/BAUwAwEB/zANBgkq\nhkiG9w0BAQsFAAOCAgEAd6Fqt+Ji1DV/7XA6e5QeCjqhrPxsXaUorbNSy2a4U59y\nRj5lmI8RUPBt6AtSLWpeZ5JU2NQpK+4YfbopSPnVtc8Xipta1VmSr2grjT0n4cjY\nXkMHV4bwaHBhr1OI2REcBOiwNP2QzXK7uFa75nZUyQSC0C3Qi5EJri2+a6xMsuF5\nE8a9eyIvst1ESXJ9IJITc8e/eYFtpGw7WRClcm1UblwqYpO9sW9fFuZDpuBC0UH1\nGXolRnFYN8PstjxmXHtrjHGcmOY+t1yFnyxOgZ01rmaFt+JEFbPOmgN17wcAidrV\nAuXKWal9zrtlJc1J8GPHPpBTlZ+Qq5TlPI7Z3Boj9FCZdl3JEWUZGP7TPjxCWLoH\n2/wJppE7w2bQcnidQngZhf2PN5RNQASUa2QBae7rkztReJ6A/xMWXAOfgkj13IbS\nPIDZnBQYp5DKAxL9PRB/javL57/fUtYAxxzZK4xbvwY/lygv3+NetPqRHnx/IVBj\nuEal2rpdwyFcoJ3DODbh9eh6tWJB4wR8QyYm3ATF1VV+x6XX5u5t5Z4IUt8WJkgn\nHGzepJVYxzJMzjlyjqF1IG9e1da8c4DdRgmOn3R55G5BWQR3i6J+RAQY/O1S3VKA\n0FDYT/EDZRbtXWwStSWUIPxNZt62vNGgwzprQow9OfJHRuOzlzIiK2BqnixboOs=\n-----END CERTIFICATE-----\n', + CERTS_FOLDER, + TEST_CA_CERT_PATH: path.join(CERTS_FOLDER, 'ca.crt'), + TEST_CLIENT_CERT_PATH: path.join(CERTS_FOLDER, 'client.crt'), + TEST_CLIENT_KEY_PATH: path.join(CERTS_FOLDER, 'client.key'), // Redis Strings TEST_STRING_TYPE: 'string', diff --git a/redisinsight/api/test/helpers/redis.ts b/redisinsight/api/test/helpers/redis.ts index 076481ee24..4ded8914e1 100644 --- a/redisinsight/api/test/helpers/redis.ts +++ b/redisinsight/api/test/helpers/redis.ts @@ -1,6 +1,7 @@ import * as Redis from 'ioredis'; import * as IORedis from 'ioredis'; import * as semverCompare from 'node-version-compare'; +import * as fs from 'fs'; import { constants } from './constants'; import { parseReplToObject, parseClusterNodesResponse } from './utils'; import { initDataHelper } from './data/redis'; @@ -202,6 +203,15 @@ export const initRTE = async () => { rte.data = await initDataHelper(rte); + // generate cert files + if (rte.env.tls) { + fs.writeFileSync(constants.TEST_CA_CERT_PATH, constants.TEST_REDIS_TLS_CA); + } + if (rte.env.tlsAuth) { + fs.writeFileSync(constants.TEST_CLIENT_CERT_PATH, constants.TEST_USER_TLS_CERT); + fs.writeFileSync(constants.TEST_CLIENT_KEY_PATH, constants.TEST_USER_TLS_KEY); + } + return rte; }; diff --git a/redisinsight/api/test/test-runs/docker.build.env b/redisinsight/api/test/test-runs/docker.build.env index a4d48a9ba8..806b310d67 100644 --- a/redisinsight/api/test/test-runs/docker.build.env +++ b/redisinsight/api/test/test-runs/docker.build.env @@ -4,3 +4,4 @@ RTE=defaultrte APP_IMAGE=riv2:latest TEST_BE_SERVER=https://app:5000/api NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json +CERTS_FOLDER=/root/.redisinsight-v2.0 diff --git a/redisinsight/api/test/test-runs/docker.build.yml b/redisinsight/api/test/test-runs/docker.build.yml index 85aa785340..fbde408d96 100644 --- a/redisinsight/api/test/test-runs/docker.build.yml +++ b/redisinsight/api/test/test-runs/docker.build.yml @@ -20,6 +20,7 @@ services: - redis - app environment: + CERTS_FOLDER: "/root/.redisinsight-v2.0" TEST_REDIS_HOST: "redis" DB_SYNC: "true" TEST_BE_SERVER: ${TEST_BE_SERVER} @@ -34,6 +35,7 @@ services: volumes: - ${COV_FOLDER}:/root/.redisinsight-v2.0 environment: + CERTS_FOLDER: "/root/.redisinsight-v2.0" DB_SYNC: "true" DB_MIGRATIONS: "false" APP_FOLDER_NAME: ".redisinsight-v2.0" diff --git a/redisinsight/api/test/test-runs/local.build.env b/redisinsight/api/test/test-runs/local.build.env index d38d8d84b4..7b32446487 100644 --- a/redisinsight/api/test/test-runs/local.build.env +++ b/redisinsight/api/test/test-runs/local.build.env @@ -2,3 +2,4 @@ COV_FOLDER=./coverage ID=defaultid RTE=defaultrte NOTIFICATION_UPDATE_URL=https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json +CERTS_FOLDER=/root/.redisinsight-v2.0 diff --git a/redisinsight/api/test/test-runs/local.build.yml b/redisinsight/api/test/test-runs/local.build.yml index a3f47c446b..e8e04c9173 100644 --- a/redisinsight/api/test/test-runs/local.build.yml +++ b/redisinsight/api/test/test-runs/local.build.yml @@ -14,9 +14,11 @@ services: tty: true volumes: - ${COV_FOLDER}:/usr/src/app/coverage + - ${COV_FOLDER}:/root/.redisinsight-v2.0 depends_on: - redis environment: + CERTS_FOLDER: "/root/.redisinsight-v2.0" TEST_REDIS_HOST: "redis" NOTIFICATION_UPDATE_URL: "https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json" From 565c50168a54e1656e1a3802581007825621ed3d Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 09:27:27 +0200 Subject: [PATCH 090/107] fix failed test --- redisinsight/api/test/api/database/GET-databases.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/redisinsight/api/test/api/database/GET-databases.test.ts b/redisinsight/api/test/api/database/GET-databases.test.ts index 41cfba07ee..c128a6987a 100644 --- a/redisinsight/api/test/api/database/GET-databases.test.ts +++ b/redisinsight/api/test/api/database/GET-databases.test.ts @@ -12,7 +12,7 @@ const responseSchema = Joi.array().items(Joi.object().keys({ db: Joi.number().integer().allow(null).required(), name: Joi.string().required(), new: Joi.boolean().allow(null).required(), - connectionType: Joi.string().valid('STANDALONE', 'SENTINEL', 'CLUSTER').required(), + connectionType: Joi.string().valid('STANDALONE', 'SENTINEL', 'CLUSTER', 'NOT CONNECTED').required(), lastConnection: Joi.string().isoDate().allow(null).required(), modules: Joi.array().items(Joi.object().keys({ name: Joi.string().required(), From 8ec556d0f8538f653eb07ba0ed43f0d9626e74c0 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 19 Dec 2022 15:55:34 +0800 Subject: [PATCH 091/107] #RI-3936 - build snapcraft in the circleci --- .circleci/config.yml | 1 + electron-builder.json | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index dcb3c7bac8..6d9a4e70fe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -554,6 +554,7 @@ jobs: - release/RedisInsight*.deb - release/RedisInsight*.rpm - release/RedisInsight*.AppImage + - release/RedisInsight*.snap - release/*-linux.yml - release/redisstack macosx: diff --git a/electron-builder.json b/electron-builder.json index ece3cdfcd6..f14a51f549 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -67,6 +67,10 @@ { "target": "rpm", "arch": ["x64"] + }, + { + "target": "snap", + "arch": ["x64"] } ], "synopsis": "Redis GUI by Redis Ltd.", @@ -78,6 +82,14 @@ "Comment": "Redis GUI by Redis Ltd" } }, + "snap": { + "plugs": [ + "default", + "password-manager-service" + ], + "confinement": "strict", + "stagePackages": ["default"] + }, "directories": { "app": "redisinsight", "buildResources": "resources", From dd00d7a1cf8edfd3ee7e4260cf74032e46a7224d Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 10:33:18 +0200 Subject: [PATCH 092/107] #RI-3926 workaround for pika db when there is no keys displayed due to dbsize = 0 --- .../strategies/standalone.strategy.spec.ts | 23 +++++++++++++++---- .../scanner/strategies/standalone.strategy.ts | 10 +++++--- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts index b7c06c2ce0..80229c3dc8 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.spec.ts @@ -229,8 +229,17 @@ describe('Standalone Scanner Strategy', () => { ]); expect(strategy.getKeysInfo).toHaveBeenCalledTimes(0); }); - it('should not call scan when total is 0', async () => { - jest.spyOn(Utils, 'getTotal').mockResolvedValue(mockGetTotalResponse_3); + it('should call scan N times until threshold exceeds (even when total 0)', async () => { + jest.spyOn(Utils, 'getTotal').mockResolvedValue(0); + + when(browserTool.execCommand) + .calledWith( + mockBrowserClientMetadata, + BrowserToolKeysCommands.Scan, + expect.anything(), + null, + ) + .mockResolvedValue(['1', []]); strategy.getKeysInfo = jest.fn().mockResolvedValue([]); @@ -239,10 +248,16 @@ describe('Standalone Scanner Strategy', () => { expect(result).toEqual([ { ...mockNodeEmptyResult, + cursor: 1, + total: null, + scanned: + Math.trunc(REDIS_SCAN_CONFIG.countThreshold / getKeysDto.count) + * getKeysDto.count + + getKeysDto.count, + keys: [], }, ]); - expect(browserTool.execCommand).toBeCalledTimes(0); - expect(strategy.getKeysInfo).toBeCalledTimes(0); + expect(strategy.getKeysInfo).toHaveBeenCalledTimes(0); }); it('should call scan with required args', async () => { jest.spyOn(Utils, 'getTotal').mockResolvedValue(mockGetTotalResponse_3); diff --git a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts index bb1be1fa36..552b996156 100644 --- a/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts +++ b/redisinsight/api/src/modules/browser/services/keys-business/scanner/strategies/standalone.strategy.ts @@ -72,6 +72,11 @@ export class StandaloneStrategy extends AbstractStrategy { node.keys = node.keys.map((name) => ({ name })); } + // workaround for "pika" databases + if (!node.total && (node.cursor > 0 || node.keys?.length)) { + node.total = null; + } + return [node]; } @@ -86,12 +91,11 @@ export class StandaloneStrategy extends AbstractStrategy { // todo: remove settings from here. threshold should be part of query? const settings = await this.settingsService.getAppSettings('1'); while ( - (node.total > 0 || isNull(node.total)) + (node.total >= 0 || isNull(node.total)) && !fullScanned && node.keys.length < count && ( - (node.total < settings.scanThreshold && node.cursor) - || node.scanned < settings.scanThreshold + node.scanned < settings.scanThreshold ) ) { let commandArgs = [`${node.cursor}`, 'MATCH', match, 'COUNT', count]; From 17025b9aeea87e05f0211b06a68d4eba1984c26e Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 11:01:45 +0200 Subject: [PATCH 093/107] #RI-3907 - close opened connection after analysis performed --- .../modules/database-analysis/database-analysis.service.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts index 28db2bc7d8..258bd135da 100644 --- a/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts +++ b/redisinsight/api/src/modules/database-analysis/database-analysis.service.ts @@ -29,8 +29,10 @@ export class DatabaseAnalysisService { clientMetadata: ClientMetadata, dto: CreateDatabaseAnalysisDto, ): Promise { + let client; + try { - const client = await this.databaseConnectionService.createClient(clientMetadata); + client = await this.databaseConnectionService.createClient(clientMetadata); const scanResults = await this.scanner.scan(client, { filter: dto.filter, @@ -54,8 +56,10 @@ export class DatabaseAnalysisService { progress, }, [].concat(...scanResults.map((nodeResult) => nodeResult.keys)))); + client.disconnect(); return this.databaseAnalysisProvider.create(analysis); } catch (e) { + client?.disconnect(); this.logger.error('Unable to analyze database', e); if (e instanceof HttpException) { From dc2e7a3e3a31f48c1f56e77199d846a53aa3a4f4 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 19 Dec 2022 19:08:53 +0800 Subject: [PATCH 094/107] #RI-3937 - build flatpack in the circleci --- .circleci/config.yml | 9 ++++++++- electron-builder.json | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index dcb3c7bac8..1ee3bac0a8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -511,7 +511,13 @@ jobs: - run: name: install dependencies command: | - sudo apt-get update -y && sudo apt-get install -y rpm + sudo apt-get update -y && sudo apt-get install -y rpm flatpak flatpak-builder ca-certificates + flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + flatpak install flathub --no-deps --arch x86_64 --assumeyes \ + runtime/org.freedesktop.Sdk/x86_64/20.08 \ + runtime/org.freedesktop.Platform/x86_64/20.08 \ + org.electronjs.Electron2.BaseApp/x86_64/20.08 + yarn --cwd redisinsight/api/ install yarn install yarn build:statics @@ -554,6 +560,7 @@ jobs: - release/RedisInsight*.deb - release/RedisInsight*.rpm - release/RedisInsight*.AppImage + - release/RedisInsight*.flatpak - release/*-linux.yml - release/redisstack macosx: diff --git a/electron-builder.json b/electron-builder.json index ece3cdfcd6..bb6715d9f4 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -64,6 +64,10 @@ "target": "deb", "arch": ["x64"] }, + { + "target": "flatpak", + "arch": ["x64"] + }, { "target": "rpm", "arch": ["x64"] @@ -78,6 +82,41 @@ "Comment": "Redis GUI by Redis Ltd" } }, + "flatpak": { + "runtimeVersion": "20.08", + "modules": [ + { + "name": "libsecret", + "buildsystem": "meson", + "config-opts": [ + "-Dmanpage=false", + "-Dvapi=false", + "-Dgtk_doc=false", + "-Dintrospection=false" + ], + "cleanup": ["/bin", "/include", "/lib/pkgconfig", "/share/man"], + "sources": [ + { + "type": "archive", + "url": "https://download.gnome.org/sources/libsecret/0.20/libsecret-0.20.5.tar.xz", + "sha256": "3fb3ce340fcd7db54d87c893e69bfc2b1f6e4d4b279065ffe66dac9f0fd12b4d" + } + ] + } + ], + "finishArgs": [ + "--share=ipc", + "--share=network", + "--filesystem=home", + "--device=dri", + "--talk-name=org.freedesktop.secrets", + "--talk-name=org.freedesktop.Notifications", + "--talk-name=org.freedesktop.Flatpak", + "--socket=fallback-x11", + "--socket=wayland", + "--socket=x11" + ] + }, "directories": { "app": "redisinsight", "buildResources": "resources", From d1516af6c09465d26d899349ade2ffa5c5ddeffe Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 19 Dec 2022 14:33:31 +0300 Subject: [PATCH 095/107] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e7cc1441df..9f75855d81 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "lint:e2e": "yarn --cwd tests/e2e lint", "package": "yarn package:dev", "package:prod": "yarn build:prod && electron-builder build -p never", - "package:stage": "yarn build:stage && electron-builder build -p never", + "package:stage": "yarn build:stage && cross-env DEBUG=\"@malept/flatpak-bundler\" electron-builder build -p never", "package:dev": "yarn build && cross-env DEBUG=electron-builder electron-builder build -p never", "package:win": "yarn build:prod && electron-builder build --win --x64 -p never", "package:mac": "yarn build:prod && electron-builder build --mac -p never", From 0477800245c14cb4bb31b91470c6104e586a40b8 Mon Sep 17 00:00:00 2001 From: Zalenski Egor <63463140+zalenskiSofteq@users.noreply.github.com> Date: Mon, 19 Dec 2022 19:44:29 +0800 Subject: [PATCH 096/107] remove huge icon --- package.json | 2 +- resources/icons/1024x1024.png | Bin 102236 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/icons/1024x1024.png diff --git a/package.json b/package.json index 9f75855d81..e7cc1441df 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "lint:e2e": "yarn --cwd tests/e2e lint", "package": "yarn package:dev", "package:prod": "yarn build:prod && electron-builder build -p never", - "package:stage": "yarn build:stage && cross-env DEBUG=\"@malept/flatpak-bundler\" electron-builder build -p never", + "package:stage": "yarn build:stage && electron-builder build -p never", "package:dev": "yarn build && cross-env DEBUG=electron-builder electron-builder build -p never", "package:win": "yarn build:prod && electron-builder build --win --x64 -p never", "package:mac": "yarn build:prod && electron-builder build --mac -p never", diff --git a/resources/icons/1024x1024.png b/resources/icons/1024x1024.png deleted file mode 100644 index 7c48857a60178f7daedab75432a46fb12c6c3358..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 102236 zcmeFYcT|&U+b^1gCP)#GCUsCmdXrwFBkCXmD$*28LMS5D&;^nJBSi#6X$lw|3o1bZ zD2M_Hf&@XqO4mdopn@hMog~k`_6>`+V#CeOOBg@H}_9dihfk3!Gys_Vb}&y@Qt8ukO{tt$Nb?J3xgSN75sxI-9#q9V4|q7qrP#zZU^nK z(UHcOpyBjrMMgx$+9f$^|611$d=~uLL|g0EBXQx5 z+PeiCYWcby(%KV^3)ZqSHZ{VUnwo1_+Zvl$?c8Z&W2j|rYHn&`YHea>YGiJ1XKH0< zX`%J6FKw_JE-1wAi1WUG?FD{v)IJ#(7h`8)l9-rioM>SjjSDp~v$eG~F*P?aH#Y)L z7{w+>#bJ_+qGGrHdj;pDPln*nggjiN{6!+B^tr5*!g685|WC3!XLm=h>K((Q(nSC!_xlOaJrd z|7`<6S~s_U_V{nl5*hi=7O`=A6Tmh8MUemY>DZ&mF~KHBf@7oOaoFI!3E-Gp1-FT@ z+k*?n#6{zdMn^~dn^A}UePk_jb7NDj9c~zGSd`!rdjIiCurnqu*il=+HzNVltd5%P zv;(X&H#ab~uroFN`%$;(ps=4R&-6&r_%!UnrIJ8A>+7>9)g+1Z$x+6LQVtc)yetOJd9n(f3I z*0g!C9&k-D9Ie;u0~q;N78scK-u8H2FUsKRPNQ z8229+h{516z#awTVjZav8Km6l0f}_CFIN%c^gN2=7@SN)|&byB$RnGDJE~&bdPN(s`-aUueq_R3J zl*2>3y*vg}eiZbkSZJm?b>^J!?ahhvNep3i><<$DvW~Fus>-`AnrW{h>z|tb7)jNC zsbsgyR7g0c;ObLWrR?8dMHMeli|U*H^+R5bFfr%XSFcg^|NjwC;(tJgLuqpg5DNMs zr574FK%8{APhq`HEvY>~{TzMF_GXo(4-6EGAU1@lN(U<$7oa8#KD zI6)Y{gF%9e_p-EGipBKV9Gj)bz0icyCluZ2LE_sFlM`NyJ*_T}DlgwW$WlX*^OGKo z9ZDHA41RQ@M!WU>{1aWXP;JE&XdGH6aOdKYc5p3iZEe^7!=EhC!spYy&}knVy^ZCo zPpmwGcz&%?V)u6$Dgk!4-YSLxwznp4rzys0CLJ{{RB|F5#j~Zo)n6h8(+7ztI6;gL z=QjCwi10<`(7Eu|{J1A3+PH6ef*o)?+pKdmaYZIUDpe>oS7d3hp_*_`ga=R2WcJ6m z5X)!;p@aq+@O8F$@*ESRx|90J^7 z2i5x1`*Sdz2z)D};~F7R#b zSl5Adswh5PYB{B66Q{nEq=M1qv-`_t3mT@LY(&c>WlD*0H+su*!pU|_l5YwzM0g@d zg1^`Ko}&)T+1a_2)YZ2%DE)KRL$FM)4om}ki8r|Csj1qgImsl=jVq`UriCslVT5C@1AC{-8cNv$(z+pV%M|J@);=pF*&+yFxZNJiUk*Y`6@E zp2)!nPeVkx@D){Wqn9U`yrTXaiZI!ta1U`95E0hvdF%q+f===|dp>cwy!8d5j@%PKNiX2AQ8sCRfP5pg0G#Ew+>Ju&9wW_1|nUJT0 zz~_01a&M*bgqj_98DBZ}Uw8R{I$QF+KCqpa*l_7<$6Ls&wT-#7xJc4P8Cwn=__0ZK z^GaiCYPys=*Pv8+MdP^6!~7Q#Hi0Jh z7^9x6TA8(6N_C?+(>A*b&jn@*t;c5&Izz-3goFvZED%5~-CtG61J!7;C2vkU;^f{S z&#Uo$3E3tfcoMFn5&qBG+tSvSi1w^m;W_lI5On^57*}z{(p#O9FQw?vc^C1hzx}cG z`Tx9*H-@)P!m_3Z8{W*|A2Ni1bPYWAJOkTZzV2pCf5W`b;4VEd=IriY9S@3Y7N<)wdP1)TPI;Up@jqAZ?>6a zx7U9Wv%H6aBH3s!HEMSTL@@N{i?@j`y?jX{)%=&yXstsiatL0=DD#7tsf8ki@^jtP z*1y(wXbxI%ty8nARy5IFa6nba&xtSYDtz0KS+nCGTTwI~c&=)p9uv$@q|i975!H;) z5f>=n?J&V}K>;qGwL5>XpQYmc<%fhTEHzt4>_c(CmFeS&#|FSwGze@(QN_K$awepp z_<4B`Zw-S_q`*#RoKTH^Bo^Z(R= zCp0XjW6Ao}EGud0qnv--k@Yj>jdPk7WOhnM)rylogPQFneE#0Fai82>6%F~>5$v=Q zjoQbukGM`FHI!sPPcFiN#t)y91INKtoyFa0pnY>n_-kFNxr6OGw=MA!rW!?E@(6nM z$*wbd1bZ^8iVw+$Z(Pxo%oL*1?}`StPH?T@4}cT`4iQbw^V-nX*}P$5B0giyXWPFm=kSZo8|O)Z zX*?A8XVQR<%+T3z%h#3ITeAfl2|rFFpuK<}nHJ%S)^1o4D&2G_1qhR&k-!NBT^2E1 zxf1BP#zqJ8d0Se1e4jzCbH(2E;W$~fQr|1Z^_rN%&i%fDOpAyV8md7BUZ1Xxrur99 ztkP%Q1WXye`e8?IPOtg#P6W?H*?YyY=OXMlt1(Mw!;^Otds}br^W{*}b^bb0*5Bd_ zR-oJ^c9^5fDlP~ENeFin7;ZmN>5a_&Eb|`GWyc_2#xk4rQ|x3K$=`ACkV1^iirOs1 zcp8EL4$#udfwN3Q5;ZCD8T)t9<6Sp=UiimY+&gx*>eI3i?<%^6TiAf6YI2tEbI+f6 z9OU6Ax0#;|B+@UKkWA$>N((P7!~2O&l(A^)BfbcFjs( zgs7czSdRvYat5{pUf-}pbc~2$iVx|>Snd?s^=|__a6Gdr&gm(FJHz@}!&*;lfQKEY z>7OyWIQ|0b1xE$CG@=595Y%90<(1H^gmi0vuM}d2z@d6XKLQTLbD@2}(yF>1Im%UA z5uJ_YtGOZ^dJuT2d3oEqCvt(uea{};7-1m{QJ;~L8}e8nWax-GPo*Xq7Fj(B5pn3zkJ7Ms zJ_>xhF-N`YK&N^{|E`p%WoLz7>LHMd`Pd%dfh3MImMy(MK^yG9FlZNr3{GC5_6C;+ zLZa(^#7M)>kbU#-90DLLp~p(R5+w(G@;%F6N?O$AzGb*!e$s931|;5vrq)yBdyU9G ze$K{a^l71klv8R%?A!vhk!;S?DV1DNbVbY|);i=!O!72AL^3 zw{^R#XrdKhGJPx!{hvyHR#-z0YG%Q%lYsdN=OefeQn^Z9+tZg5?53#YWbu z1FE9ZHI=7@oyq!4`quSTD%SzBvtK_j37>KZ){>F2aBOxSC=!sVrLfDL!?uu=OY!aK zDAa+kX+ZY3>rqpWez1;Up0#_CgJ*0@N7fYqv&5{dnQT@H5?v=8eVW)W?StSu8;8Np zhT!0euMeI1Gq=)2aO0>l2?JUDIS~YuRU$#9Wk6nrXyr1JluxYn-l@ukSz)^GUu*;B zhFZxLF>eJ9i8X(m9Em0+dLay*1M_N{w3V(AU`wOq%4~X7PO8JNC7ZWY3{cNjO*823 zOQb*+u(~>s48BV^*97+vX$u?7C_ zP=GgLm+?M^_wS;pV{W?&t&4sN(&pY5am~+vc}96IJ234@E$ey)p=dm`&(wyaYB)j= z;!0J>bF%%XMUv)`RX@c#TVJ!0_l0sQ%YLH`-l)vAm2Yag1er_HzU(%KfBfWKsNyZc(T&JwvWZ9=Iy-an@mNE}Y26Q} zKQX&)|N3NR8?9&I0gF=!)VS!zJG9I3yR%b)fK$UL&P;fgt@d=b4E(T`AuL~|vYw;TskZbM=_N{}kT%J%$Yr$T65+V(D<6^D z5NaxQhd{J=Hd%{lo`w$zSyGGZTMT{@4tF4*uHkFX&YsKRDvO z^*oUtz@Idnfzp)U5NY1cyoGx??T~vPZhWA)X=TrrNb* zHp}`sZH>r)lxQY>^chx4BIN+l4SkbK-oBRCJBSN)dUEKirkahIS&Dn$r|NGbk@D-2 zQxsPtZDUN%Mlmqf%UypnGer;I7EGP>3*_YY0)GZc|h&v^WEb= zrnfa(SuO*m!`#0X>a|(_lIBX3n+PmSN2IS zh9MnpH1_fL4pa%)R@Q~Nxu%hVw0MG8H)$}3>Ki`dO(;yl1Fj^?PQOW^Xn8t`9z&IT zrmJ=9xl7;N{i0!A^~aG@+qMS|T&U||U~Z1&wdl=vm|rfhs#|uoI%sY~zrW;gWv4?x zm~Wks+09iVb+Y)!9nH<<&#oB$hWq~OZff%6`;ctHSAvrLdq}ck4pOTX9x+7LE{|!? zcg__v4)6lL%(6WWTfEe<$t}vXYIu_{7hmM3(lfMtY|{K*>cTxu=^gWU@-VW@6LK!o z(}fE1?KlyyIYBo=9!M{(#0|}o(u=D-yG?M{er@QhKsgI+PA0H91#TSbaeO_S7k%h5 zH77Nv<36%X>pd%3PRqs>;XEh2v}60y*z+Y9I$qVb;LdYy%O(a%PbJ3v8numt{{!_E zyL{HE=x7F1ck9#W{plY!LUfWIy_UPMwwD;J!@OLvsVMfAz<#ct9@#}zh&TdO;gbKX z;t>3d>-&~_ei{2)l=AnZ`bawWzEkilJ34mSW^^aiZq>-pDR_LaY>i0`Qg?0K^&2(y z_IVK~?K0sBf?vWMz3pgoyFIj4d_vRbe7o-Nm&Gr*N}Q_RU)}rk2q+;i{F$vJyNb>;;DNMC=6z(~+UWyx%|*j>#g*fQ@y4CU2z|%yUC(iK z`kioU@X7uZbZ|y?zTjFIOX0xP7i0EvHRDH|r*HyyJAYcuyAjl6xYiAC_%*({kO}1? zq~yzCfFyQvkt){gC1E%>tiqz{WDPGmq^fF(?yPEIlzd{E{bQwLbjS5=#I_bb#(ZJ* zxG7tGr^XfL?KJfDd57}16OsDcOLT+#Ld0vG;QNgWU1VGR1(q>YIlJluu#9UUi4(Ev zW;-czkb0>4zgmX0=f$UMcmuL(!am#ALRBZMT`yl6a9?9Qx04<_Y5(?1_S)_pEuWH8 z>PWroUp?kLv+f;SP8=|kxN7hAbf3Di$Y8I1Pv;S@$ge5fNbO*|cvo%Qs6a|qlX=Id zwru>YiH1W45dvNJ@dbM#RVg#R?d^)dkSS#@oUJLs-@el2j*=OFUg1YyZfX|hS8Dv_ zjTg^j$ggf#8fl78Gg}sd)Z91|n(x{wts>qkkkZKfZ&QRw0 zMsor+xC`CzzZ&?4t69PzwXKIZJ3Ac`opEi&uX@K_E;py^#l8++8tZmfwLNe3it085 zvHg*+pl=EABAJiMXBM%|zY!uc%FRD?-k*2S7;dX97&+qseJI~)*0^5v-Vh1@vC$LOOsnt;U&5=&ky(g)Gr~g z%4xg02qORRyhzF$emwa(6}bp>?k6PbD(qm0)YYDM>H95hwGGwfbKs_H_q*wX73S*! zfuTbU!~3M|kAGA0M2-v&!OmEdMK-T0=Q;At{0a%$p2_>=)jVg!ORoPY$Zs5q-r{iK zOQYk7!*{5?M+60$DEmSYo($NqtWx7^qD%#c>g7~W|H=Ja?_%QDojdI-`+shuefkp; zzOU1Z%qglG66LDX4<5KYXFf1`FMe(I@mb`asjbgFxL=0%{+{=s>gb1t6C&RW!o3aX zy(#+a+m&0(Xhoi`1TsvqQYk7 z&&yJ;qMR}q?tcAAaUo_+@Aq{eqejEGHU+i6MH|75$8)JayCU;a&wGaRrXANy(WlyK zaOrC1;UzP#ZhU-o;KhNMAhk%pS*4jvJP*dL97A0Y@l9Es9tnfwnLnP;Ygn)AzmIp1 z!M<7yD+rX_Lt~E|TKQh7#eKcl&GIQtz26OS7vyyHUiCN(Iz399xOKIB%3;cUl=`B1 zU*MEug&Wh|^ObP+azPmDPJ7o!_mY*`3Dne?!vbpB6*>;rp;_EPiYLFmANaeq4oh+y z)p+>T`YGAflROuwt)BG8Ltk+vG(_+8W{Z@j?j^%U#y)ARHtQgV*_ztnIQG7f$u48jwOyGhOFpZGS1Qs{=GibSiBN3S1UJWxvn0y>-r{=>;zj)?O` zV_N*bV`tYiusmK`JruCB6XiVMhni=ruiyf&;I?nySUFZ~dm#6{Ur@B7?VOzMt7CAr z=D_?ptr9-}>=ImrKYs@|@biUSKr_=x2+K|~y_ei!{^`TRz|Csnw+~Pla9h18rPcuf zCo6ieI$Tm&zmhMFQO_Pn`!cL|cLK-o*HZ<2l*X+Y&-gXzKlGXy+m{~o;+1Sa%hAnV z-8wkXA^8UH89U-yCa+rz`zx}qbGDh*GE?Or^qcHrcTln|$h?1_0M{Eb{-)Js} z?Jq#y1+$FV)&4YwN>Xb8NXF^;!4MVBn|Qu5%9( zK2aH?xr7Rfhxigw-Ot(j@9!_GDy{yJXQ6w<_M6A9m;x7;U#)sc=Hgf^!M|zrs4)BF zC9k_T&W=9|jq-H9rzwyayIKm7iu^)&x@is>v;!gq%0qjd2cC|0;yX|*pN=x3aVLnz zF7Is~FX%G%<9MyDW=__@2@>Qf2Sd?W-WjT}bED4Hn1cw1t4AloPMr#^(D`~%n|)DA zWKOg84Eo%J8Zg$q@v_R^%lV*hkm4H#)e{Lq_;ekZRYM=KDYI#>v(vLL@px9sJ*cYD z-Z4b_y42!$*w)99L8WrGCIg}|NiCesCGUKUX0SgRE)$E(%YG!XPB$O1)u`X;fKq9A zCEQyaf2s7wU#M?03)=d)H-Zp-cz34A;nLx#ddCS;iKSJ{HAVA+IHR+r2NF zUGCxm5y{Q%QXY-ASPuxQcf0wg9qCb3qW7}>S-<%&(=~k7loO_w7osQs?C5}*RC=hu zmTkRNqFV+hK6Id7eSKKlx(tY9BUue_;#|opRk)+meI3|OZVP1*kDXg3{|*GDGV>{s zs`+hZE&Aa#iE1}c7q*@q`J%g4)){8i9{uq9oj!OKxzE;>b@9!n-W2QSWxXo5N<^SR zpDo@Qx;iI+QU*+}F;P>u1ZJ%Cg)%76nUdL$3#;a-fgAYNuVTw{{Iz_%S60t{;j5h7 zI!EtzIj45Bdw1DL^w#XAUPCja+oJEj*R6O9V(-%fwr_L{787lHIvf1NdP+)G>)&48 zKY7lDH6sLHS0I9%`SXjW9K$@K$YNoG7Ni>SkYQmcOA#lR^-G*FG168-pH^nXoMKPw z-q1B`vd?R~`XFM^&i%+S-`r`A@udmQ;^?@7tw$R3s(Ew?+egE@mNr>(MQk#wZ}&m* z%B>+Wqxymvb;4L66J9aCC<66SwE76`%nC^eWw8yWQ9^pP9xB_qsq1U|SxfxJ&Z9nO zZWlg!K0uyyQ?)%Q-&8!k=Uqlqp2?fH>*Hw515$6YZ)#F-wX0%pO6V;2O5YnB`l)ZN z7WWNTWnvUDrE7nt3o_F`fYWJ}*lI{2=g6MNd) zW^tcZhQ;185YnZ0{s_BrZe?A5AfC{Gc=mC5dt!V-Ag_d7JXY&MmHuWZqH!!xl2W~f$-1{@?;a+R@ zvQn#>-k$B3`ep(K;#5wFUDD^EkHpsBQ4!0IRVl1mXt_h>b9{#;y`2XH@wwBVp*zM= zPqUWZ(?wU>yZg9Q&`n+LA6!X%e<7zgxj}yIsV8Bb>TgGSENK=J0{o56hf3#!@q)?M z4AQfNv=-9|_BqHM_gJ!qpZ9Zn^^dDA!G%3Nw%-d409manBxIf+`O9ednFM$5l%v2v zvJGO+R5O0!k_EkBbnW{0Kn1wn7p0<&@m9Sm7hzqiJVOvw_R7%J*8^_6czNq!nfZ@7 z!-a^)yv+{Jf@iZAbQ)ifdM}S94}}kAx?pwqD*9siiQWwte+zYuTND?TEh1e9?u=W0 z$Qa$p-D33s%EPTn7%UUcE^pr8sq-ySxxOSP2Nvd1}JqlIwiD+l@eP?=E?bh#w;iphzeiJdgDEuA? zklJ?%Xc+27K|=~017_Js40XTn5%dvb?3}H04Nn{Qn_|e17nyM_n>bVR@dE{Z8N*Gz zvzJQSt#FlccO>`DW^$R?26io+&>a7Z{9>x+Yijv(oHQadgD~^?qL)~Z+cJJPP0)2qHBd*onwdw5p&rZmp=)=g7zyr-UP#TM!pj)W*$q(la`8#h~dY^H^RA1vY2FkZpWJMcWAZ zCfo$+^wEyD;x-(cRBxt<&vj(|VDPvzqgJyipI4iTuDz4AGpu|_Iz}gqHQfcfB+c79 zr3N$%m$K7W3njD_)X=0;Gppu71r483wKQATnB#x$sje#3^ep4|Z5yx1ZE{gd$gT1V zdi>3%e0gPNHNIVl{qCKKPxueBN^_Rkl^+AIlISD$JtczFSM{BPve$Wo{S_9vuiUxz zhR3Vl;k9vB1y@tQHFAgVI`=1Zcy&z{q>4}0_`{SB`1Zj~MR_wcqspjboT#*y3$ttH zakhWQ$59}d)25qMJ<>2F!<{_>aq;s{=)(7H->{rnYPG+`g!-G+Imok>y$TLYe*qK3dej?v}K`d)x+Ic(anF9FJ&PXUD@iS|&%BG*p`k~`8cN@9oH z*Wn?04R7`(jP-sn#BGv~yj=W&k=2B9UtY=c|I1UsV50$#{8n=;ND8XmI@t6sYd?_u z;Nh=FwzCda$VG06+5%Zj_;Sx2^_3CCoeR z*XK7m{ESqPopv!*hw^%ZK^zye-hQ zC0BUHyUXO7DsAVqA4k%ca&_P#qdok^X9x!4<>hh3)&oBqA-RQ`h{9191z%9C2!Am1 z@!|B31248EJqqeFd|c@geDfxVvB33$TJsxT^L2e`kece};reeWf9eQZ9R{^6|Atkf zQ)n%>GSj&0^z*qy^~X)!AcJwqHTL@ww{1|!;BgWEIVz|%$XXZ*Xh!4J9G2+{vu?q& zac$U}^EPT>9d};8NZ~7q@xB~3*7gy|Jxfl}xE54+kuTYdbQ6G=Qs20u8bPCoV)A-# z9x z7t~K&Ta?>Ya7WmcRVy<)=Nw5lyAPL*Ms7`MD!5)=CuujzvFJ^T&>G_=+?%f7Hs2K3 zyjW}&EGIsrc8Qt1^G>p@dcz5^jFzv&xvCdpe_T#_m+0?2r7kceX8>eK5JQr%aL6KJ zgt0|&p}ty!0AwXEbJ_pI-0WaY&AR=`faW8RRpa}838Cuyt4da8&W=k&DSw@bdu`%s z7^u1f=09*FLY(~IkuX#%RqLfC)KorQ3zHvpLOnEc9M~J=2mA}J6_GH8e2Q2zA`4nA z3xxE=HRPVzb>|>)Uy4)Z!ZPCpD9{|dY^4JVBOhDro9^%<8;RoP1Acc142^2^AEjQ5 zf!E8cm#*Oo-t|qk=eJDU7V_9`X`r*h?!!8^Pc{QwSU&KWWpr%&x`zsaKQrn_dMTR30~1vNX(cW(!#VLLm8f>mMBVh||*9{|#k_Yu?vuUt!oqyZ?Wpzizn7Etq{ z$q6Z(i8j>s>Ft~MSe`T~{i;i`#773E3^m&40o(7_r?WD+F_Wh@$}(R$=n>iIuc%q{ z^EblHZsc}*-ZJgLx8$}XKjX$ICqybUFO~NlbFDr5>J(D8)g5rlmSCy)lF^m5vKYRu zQC6>M1ojr>fjTceILr5BR@7UCtwH4tpr>BA2wY4-(LJ@45O60Z3!(TO=|dwm4ud`}gaU#9?Q$qHM;R~^vNx~n)=w_&rTl?br(Jy)aDk=ry0Qh2x}#KWKSxj)E(g1DUl~3UOm|%w=hDj} zkQJ@*MQ$;9KKQ{r`Q6p+NvGK5-dF2x1s=7Pv-(_bPy@NkB*?b*02;0)Nm<=zD{x#v zLJ?~SkC6!%dbtZJ>t`4B@in1}XdEj2sC$WwUEhXNmT&SEt!zDS5DuYzMQ;24QTi@0 z`D)$g{tI;vM6AipWQT~I=}NU*hr&_rE0I^v>a=D3Mi6wG9s#2`#XCyd0N@DeRnyU! zG%gCw;xo{?{EE*vP|l@qM~^@S&rd&B>*?ODG+MED>G4TV9@d!0iz=68>fPj@`*Z5d zXQ4G?zZq&`K~ak*wB6=bXKc%zkyD5_eR42evq2v}5Sr^I$d`cNzp-MF75n_iG{tppKzqEYAk+BufQQTb)P1`M>QyWK@vp?@Y{$ns z%Ip5rRIBYg9qoTQ`l!V{htAzz30_yXJr2=a32!ImOzZ?XOIzKOzI9c@YKHJt0Pv$e zabHITrc?Vs?0D>p$Gm64^T5FMZS%a-{ZYfOwpWNBN}m0}4iz$NmdNoJQ(PDq%`2Wy zjbYu#Ij9Xgpdv>i^wnP83UcI&KPry7=b;85Rt{j3N~<2xRmZLjKHayPQUBw3<#as( zsg0|%iLfLoEQD%bwh9dy%^*!co9w=o&9dHlOYE36u!y`5W7yO8sV6la`I&SR(|+$Z z@nH-v`3_+Si*k-K7BSThUY7wp7w>X`hDV6hbzcSAL;o%1(U0Y1TNARFPLt1$emI=6 zzwFyHx6VR;>(bX5e{2u69^7C5UtGZZOfd2F)a#sx5$E z*(r`N=x0{hAUHB<*hX`mdrMdZn6R8)+v92>%_DC#iG9c4H$B*}LtG1Vg2&2Tg0{?* zd#Cr<@9P95*XDf+2Av=~HQ#Ld1vG}ERCHidR(Bh!K|LP#tK+Z^xOt|Pj0Q2dwsl`y z%jnYD3!jRxmn45{t09>qS>^HpE`fD#K{02m)s#m{vW9p`Nb|^hF892`bDXRA!3lTOtL~@tXFlV$-*RHHHm~I6fb3yH%>YEbUGt|; z$1||xZU|nwx!k;sDpiK}*y~(+A!BQ+``T{2-X2=RF|n&|s#g!5%1G>fJxQvTTzj+C z-&wdAc44-TG))k*f5-M%r9L^p*9`39ZU^OFiH$4q4|TNXr3=h=xCK4aF>fl)5g)2J zmwoBn{!8Z$$J`%10Lfn_KLnsB5S&ig{qUtBJI}1M(Y$pJN(aA#4)DKYRGej^r}NY! z+!ojl*B6)?NN;!9wHGR7me4k2@Y>pJ5R3SEr(cUBoEKSMZXg2i^+rQDN6!9R zef`vla{!rl^I7p#e3^;}eaF!W6_JePIK9cL7a{=Yl?Y5x={x;BE&!wQoT@pWCo6#BiP*u0b;d#M?}aN z-PL8~p=HM&cP=WB$Ma}lq72pnYAdSY;H`d@GIi;b8Q1m0A^&DLvAUB0`ZfaU9H{k0 z0$r7$c1%EKdy)yB#sW~MbJR8bXB2YE1jM0OwTSSirw^RWJe$+I4yyRZuK9J}$3a_H z3N)Ewv(oV%{mVm#X(j+IIQwFlU*sb0Q1EW-bj_V4$`Q{L57o#WXzx{U%t3HW<11a> zKOJfAD?lXHHL;o8??Q=`ZrW!1Z$1bVIO?=66wvsEUlL39NvB`B@_6fU*yh-*V#^f| z=U>%_nL5qsVKk2yJ~G8u0(g^MR1=B@6pxM*$O8yU{0M6#i#TBFHgsCl2_1KPdLZWh zGdY;xN*@Y=CAq3k28}VRr;r6Ww@m+xjEeiZ`pM6j~RO>0=l6#jm{s6a)fuUzs^9osmdB3d%y=p!TJ$Q!EZT zelP%Wpl_f_bZJrdk9@&HtMUN6WScPdtE&lsyW!8aL&q5e?KesEV_v`=nAIUvPAUN9 zOEm?%?y;l=L<9nq;(e%M{s+A4YlllKKvDr*ed?zDD*Q4danrkM%RQDJJ|Hd0QMo7z z<^KI*jg4VBsuQtp@5_P8uh9W4!vJIl0G#DwzNtwE_NY=Nl)l4e=nApx^vK@8%QDqh zf0r#<29qt^V%UU`6CE(78^Ob0U?>iWnu_%J#AyOqnA)yi)IL}+mIC7a+WUt9=nce} zXp;tzl$u$QqZKytN!Ttf-pk3UD~tEe=k$KTJ0V|(`HQi<)T}Z4eI+xXK_chSRK8)L zsL>tp!YNA*_K9+WXJp7E^qKauF|7+IBye=#MQ3B@zT9%P54PSJGaqh$dUoLH{rM@- z1_!WrLF-lr7Dd8~pq{t`ymJi5kVQCNt6YT`Di=>1TK@7wz!IhHLb7xh_qR#jch8AU zUa5P85Ysigw37!c%5}Z!#{p^}u)_d@NGw^T3ZFphK%iQ7@ZNNkK)WN0&2I>s1y{Ph zmA+L{vs7T5{-za(uI^z3bLntA8_2+n+1bIe|>bN4s3 z_!wrN9nGLU5qOczjJ66rbS8HnD5}~e`~@l7v)KJSB>fE6r$LSYMm>sL%NjZfmk+Yc z-4sSnB4!`F;6}$bGkoC0zqgzQ#t$mTB!OWA4xtC+w&`v^hu5406Dmh+_dFkBppr)- z5FS2TK$(tB*4fnnj#}f1V6JVH)LBov42)O!+@DnTH$raJ(lVT|Pc;slbxLBf7BaVb zPjyvcI~aH|zuyjdFkji-oeSc5?U~=jiL-#CE2O|0UZILRPZLawsM59!$-VRhB?>S+ zv(=cqQd@N$C+%ZwvyP|s{QXJY8Q3wUg2j zodGbKfJ~z17jU=SRI;&8?Z4;iYZT~j?|j7~e*~&Kb5NYf@-!oVKi;){2}#RZNfYPF zt&mfrMM{?PfFxMHk%3%Q9u=Psy4%;}E#_TIT)q(4_*DQswLSndEED(s0I75dLv-I! zL4ii5!4rzHOgt+NoSI4Q<%C22dM< z9^H$13m$BjKoY_YEatXqi0o&nztLw_U3U|g3991fLef*4Eh=9X`FF!o1a z(!_7LQ6YC?ToeHgE(d6x=ZTf)YMPNb>vHz11ftY@g?Z)5EC|touW4I7-QHw_EKMMm zH!boAE3LdVCIsi&FMarmqM?WkAp(M*t^>v%qQFejKsJ4CjV=iSd|yA1R(yv3@;?!1 z;x0H*%f(m#lH`?FyDpi{AC6zI9Z9JJ(Da(rOdZ$>1SA6R)w+~SFCn$E!XNU`769=B zk~@Qa9`hPPBxTjjru^By={-G*A6ZNSqiM3O?<=L&Tw7E9PmV)0{2$rs-DGSXtgvA@ z5CwyzR#IBI5B&K@J3iPJQnNs(@-Jap_N>oJJ_U{6xN!l@vSBlMPINg~1#3p%U8V*s zDBJpA&3fvB05d(fl79K zK>zSK4bIKKrq4XiOd-dk#axx|YZ4NHhXlPIcJ%r7lc^_w1e$2oQlm`taS@yK_|pOh)N6J2EwW<f!vbO}Tjmw{y7ADf&e`uOY{+vZR>6c}X*R(IPs7%ZzkN=1F~k#D5;ME7i7PD>;rkPpEFxev%8l$wI-4H|zC!1GyU4(ym&2Gr)O?hY} zPUO3^X^thf_+Q01@=Yl7_v|I#a0M=L3K7u9g$i4It4*IuXL~m+UmT>Qr-Lamd=w4Y zKa4~hA%JCO-4c5wo$>^qm0HVq0x1$F`;)@AVIXu@{E`>uv)wq*@# zwv2{fT~SM6tco#BXo(}|iQj-7Zo3Xlt+XU9D>!KV5?>eCjQ~SE$~hfz z!T1(vCSKD~N3586Qw@^ybJOF&aQD8T3a1W;a9V9yA`!34BxkvN7;L8nGZ=p=gAtnjTdQjmEp%mJj z{`cEC6B@(6!C#UlQs%lL>6#QUZFlvMCf0)jBAzXEY%q8wTU0Agj5k!_AXGOzEPNos z-LfLZLHXu^;SXmTd{h5obPOIK(w{Wy!nQ>CG6LL5-$6Z&Kf{{bGxy93E{NoMEl+2L zz_^>~U(UTf-AWErs1-2u1Puvha2VJwp+e&y9p`$bTAqs&MIUBYaiB*3wM%>>!%KD| z`#ysd7JS_+R4`njX17xvX(5QzJShn-$~Txsq9nuZZ#(XC9_TdR22<cKY3ZlH=1`e|0?RBXt1P#fYbW zp?^Svqk*%XJZ9|CKWxFYCtEU?eZy(&bOx9ibBxGGXM2f)%j3IS@53INx3e5y7j7k! zAuw;dthtWC7XnWlAMq<#&Y3vW610I?bBZF=WM(C~uHX%S*&>$eV@wd@pL+m?&f4Ap z@Y9D=LcLu#p|_)?S7;rsOoKd_QS0FQ2++T}4`jVR)td`K30YZ-z$7G3QULpZA+n+? zn3!cE8x+$uiCFR$t$kqt#|B3?R3V?m2;|{_ctWAUy?S}t%^t&b#`yUAr_w-yp4pu z$v2pZr~k4#XWmE>V-I5#9@LJ6)RobsONCbu_J5|pxr()tNkVF%@0)#Ff7g`vYOx}$ ztWNUFy-X9e)V+kSy;2JGRr|&{WUbXJHy%bx+pz87`-{ zwO-~rG_7?jbTdZHmOw zzUae(2?82A8~cky1pY*v0OM)dAedLm@&fZ29UqLTq?hCIMXDdK%foa|F7LWzaY^`k zdF;vY=ZBxG#R(83$C9^~gvvQQSWYq&CuYtOE1&rd>I<%}U8YsszhPV>Mk$F{w>kBE zBxh~QaorWiQneM073m74mnZlEibX%?Ux~wZq&qarnF`q+Pdj`tw;g~lZ_F3Gv!nR= zM7&Ke(La=OT=g^Y=(O;|L}%#027zUr@E=1^cJ2c%T4DUdBBLQ0jzpKhs88<-;R_!~ z%w1Q3YY)GsN5d(hv*)G6!_>PYY+<;p^Hp>7$}rFAYPUJ>^!&r<>*8c#kC;-C7-C}A z8v;Q!0n9~$FI*>r;%$s(|5&D6hM52eSDEkc)g)O;-h&Nlm=gGzS>whjr(h|jJ@t!u z(2V=yVkmQ4uzQFRXk#S=G=LEN)ZqPjvM>md@7+h-=qN^i)N>yE<(I7Urgs@VfRV93 zo`_mmFsWeN3wojt=N~dA7&1Ilh`krrZzvL*Cb(tS9AU%JFp5cm=k(`?tVI|4Dfy98 z`T#9|Kclbeo^Z@SmL##lXY=Q?zZ7)pND|qoI$G*8QN>D)6YYDP26cN&@x?0?DA_uQ zV|}9^f*E15tEXqKz&^KHkQQ>&i+{G=Muym&+}fl(>huE?1@gz5j`Jf>8A&fY;~X3dJY z1`vx1R*J0s|6=JX9GdLj_BOf&6hu-|x?W0BK~X?bx?4(0x&|nUAc%CMbayE+L_!#$ zAR!2&r5qv6w(rc}w?E+7dCqg6JFn|HM>qxwthLR==zjpBpGqasu_?+

7VEj!+RM`fpnD?H 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 y0F_pZitd>0|R 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

>; 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 Date: Mon, 19 Dec 2022 13:09:02 +0100 Subject: [PATCH 097/107] add tests for prepopulated fields --- .../a-first-start-form/autodiscovery.e2e.ts | 7 +++--- .../database/connecting-to-the-db.e2e.ts | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts b/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts index 78b32bb623..6c13fcf002 100644 --- a/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts +++ b/tests/e2e/tests/critical-path/a-first-start-form/autodiscovery.e2e.ts @@ -20,7 +20,7 @@ test .after(async() => { // Delete all auto-discovered databases for(let i = 0; i < standalonePorts.length; i++) { - await myRedisDatabasePage.deleteDatabaseByName(`localhost:${standalonePorts[i]}`); + await myRedisDatabasePage.deleteDatabaseByName(`127.0.0.1:${standalonePorts[i]}`); } })('Verify that when users open application for the first time, they can see all auto-discovered Standalone DBs', async t => { // Check that standalone DBs have been added into the application @@ -29,11 +29,12 @@ test const name = await myRedisDatabasePage.dbNameList.nth(k).textContent; console.log(`AUTODISCOVERY ${k}: ${name}`); } + // Verify that user can see all the databases automatically discovered with 127.0.0.1 host instead of localhost for(let i = 0; i < standalonePorts.length; i++) { - await t.expect(myRedisDatabasePage.dbNameList.withExactText(`localhost:${standalonePorts[i]}`).exists).ok('Standalone DBs'); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(`127.0.0.1:${standalonePorts[i]}`).exists).ok('Standalone DBs'); } // Check that Sentinel and OSS cluster have not been added into the application for(let j = 0; j < otherPorts.length; j++) { - await t.expect(myRedisDatabasePage.dbNameList.withExactText(`localhost:${otherPorts[j]}`).exists).notOk('Sentinel and OSS DBs'); + await t.expect(myRedisDatabasePage.dbNameList.withExactText(`127.0.0.1:${otherPorts[j]}`).exists).notOk('Sentinel and OSS DBs'); } }); diff --git a/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts b/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts index 6353cc5c01..3ab582759d 100644 --- a/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts +++ b/tests/e2e/tests/critical-path/database/connecting-to-the-db.e2e.ts @@ -23,3 +23,26 @@ test await t.expect(addRedisDatabasePage.errorMessage.textContent).contains('Error', 'Error message not displayed', { timeout: 10000 }); await t.expect(addRedisDatabasePage.errorMessage.textContent).contains(errorMessage, 'Error message not displayed', { timeout: 10000 }); }); +test + .meta({ rte: rte.none })('Fields to add database prepopulation', async t => { + const defaultHost = '127.0.0.1'; + const defaultPort = '6379'; + const defaultSentinelPort = '26379'; + + await t + .click(addRedisDatabasePage.addDatabaseButton) + .click(addRedisDatabasePage.addDatabaseManually); + // Verify that the Host, Port, Database Alias values pre-populated by default for the manual flow + await t + .expect(addRedisDatabasePage.hostInput.value).eql(defaultHost, 'Default host not prepopulated') + .expect(addRedisDatabasePage.portInput.value).eql(defaultPort, 'Default port not prepopulated') + .expect(addRedisDatabasePage.databaseAliasInput.value).eql(`${defaultHost}:${defaultPort}`, 'Default db alias not prepopulated'); + // Verify that the Host, Port, Database Alias values pre-populated by default for Sentinel + await t + .click(addRedisDatabasePage.addAutoDiscoverDatabase) + .click(addRedisDatabasePage.redisSentinelType); + await t + .expect(addRedisDatabasePage.hostInput.value).eql(defaultHost, 'Default sentinel host not prepopulated') + .expect(addRedisDatabasePage.portInput.value).eql(defaultSentinelPort, 'Default sentinel port not prepopulated'); + + }); From 09e98fee92bb9ecc9b8552ec1af7e174704222f2 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Mon, 19 Dec 2022 16:47:18 +0100 Subject: [PATCH 098/107] updates for test-data folder structure --- .../{ => import-databases}/racompass-invalid.json | 0 .../{ => import-databases}/racompass-valid.json | 0 .../test-data/{ => import-databases}/rdm-full.json | 0 .../critical-path/database/import-databases.e2e.ts | 11 ++++++----- 4 files changed, 6 insertions(+), 5 deletions(-) rename tests/e2e/test-data/{ => import-databases}/racompass-invalid.json (100%) rename tests/e2e/test-data/{ => import-databases}/racompass-valid.json (100%) rename tests/e2e/test-data/{ => import-databases}/rdm-full.json (100%) diff --git a/tests/e2e/test-data/racompass-invalid.json b/tests/e2e/test-data/import-databases/racompass-invalid.json similarity index 100% rename from tests/e2e/test-data/racompass-invalid.json rename to tests/e2e/test-data/import-databases/racompass-invalid.json diff --git a/tests/e2e/test-data/racompass-valid.json b/tests/e2e/test-data/import-databases/racompass-valid.json similarity index 100% rename from tests/e2e/test-data/racompass-valid.json rename to tests/e2e/test-data/import-databases/racompass-valid.json diff --git a/tests/e2e/test-data/rdm-full.json b/tests/e2e/test-data/import-databases/rdm-full.json similarity index 100% rename from tests/e2e/test-data/rdm-full.json rename to tests/e2e/test-data/import-databases/rdm-full.json diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index 79812dd615..039eb1ca37 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -16,14 +16,14 @@ const racompassValidJson = 'racompass-valid.json'; const racompassInvalidJson = 'racompass-invalid.json'; const rdmFullJson = 'rdm-full.json'; const ardmValidAno = 'ardm-valid.ano'; -const listOfDB = JSON.parse(fs.readFileSync(path.join('test-data', 'rdm-full.json'), 'utf-8')); +const listOfDB = JSON.parse(fs.readFileSync(path.join('test-data', 'import-databases', 'rdm-full.json'), 'utf-8')); const dbSuccessNames = listOfDB.filter(element => element.result === 'success').map(item => item.name); const dbPartialNames = listOfDB.filter(element => element.result === 'partial').map(item => item.name); const dbFailedNames = listOfDB.filter(element => element.result === 'failed').map(item => item.name); const dbImportedNames = [...dbSuccessNames, ...dbPartialNames]; const rdmData = { type: 'rdm', - path: path.join('..', '..', '..', 'test-data', rdmFullJson), + path: path.join('..', '..', '..', 'test-data', 'import-databases', rdmFullJson), connectionType: 'Cluster', successNumber: dbSuccessNames.length, partialNumber: dbPartialNames.length, @@ -32,15 +32,16 @@ const rdmData = { const dbData = [ { type: 'racompass', - path: path.join('..', '..', '..', 'test-data', racompassValidJson), + path: path.join('..', '..', '..', 'test-data', 'import-databases', racompassValidJson), dbNames: ['racompassCluster', 'racompassDbWithIndex:8100 [1]'] }, { type: 'ardm', - path: path.join('..', '..', '..', 'test-data', ardmValidAno), + path: path.join('..', '..', '..', 'test-data', 'import-databases', ardmValidAno), dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] } ]; +const racompassInvalidJsonPath = path.join('..', '..', '..', 'test-data', 'import-databases', racompassInvalidJson); // List of all created databases to delete const databases = [ dbData[0].dbNames[0], @@ -79,7 +80,7 @@ test('Connection import from JSON', async t => { // Verify that user see the message when parse error appears await t - .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [path.join('..', '..', '..', 'test-data', racompassInvalidJson)]) + .setFilesToUpload(myRedisDatabasePage.importDatabaseInput, [racompassInvalidJsonPath]) .click(myRedisDatabasePage.submitImportBtn) .expect(myRedisDatabasePage.failedImportMessage.exists).ok('Failed to add database message not displayed') .expect(myRedisDatabasePage.failedImportMessage.textContent).contains(parseFailedMsg) From 1daa73d422fd5a5d03cfd560240a6fa95bbfedd5 Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 18:14:17 +0200 Subject: [PATCH 099/107] send START_EVENT* for electron builds only --- .../api/src/modules/server/server.service.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/redisinsight/api/src/modules/server/server.service.ts b/redisinsight/api/src/modules/server/server.service.ts index 75640d8923..d2efbb4093 100644 --- a/redisinsight/api/src/modules/server/server.service.ts +++ b/redisinsight/api/src/modules/server/server.service.ts @@ -52,15 +52,18 @@ export class ServerService implements OnApplicationBootstrap { appType: this.getAppType(SERVER_CONFIG.buildType), }); - this.eventEmitter.emit(AppAnalyticsEvents.Track, { - event: startEvent, - eventData: { - appVersion: SERVER_CONFIG.appVersion, - osPlatform: process.platform, - buildType: SERVER_CONFIG.buildType, - }, - nonTracking: true, - }); + // do not track start events for non-electron builds + if (SERVER_CONFIG?.buildType.toUpperCase() === 'ELECTRON') { + this.eventEmitter.emit(AppAnalyticsEvents.Track, { + event: startEvent, + eventData: { + appVersion: SERVER_CONFIG.appVersion, + osPlatform: process.platform, + buildType: SERVER_CONFIG.buildType, + }, + nonTracking: true, + }); + } } /** From 33a555340ec7039d2568b6ddbe5dca308d07cfd0 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 20 Dec 2022 09:41:44 +0100 Subject: [PATCH 100/107] added tests for certificates --- .../pageObjects/add-redis-database-page.ts | 11 +++-- .../pageObjects/my-redis-databases-page.ts | 49 +++++++++++++++++++ .../{ => import-databases}/ardm-valid.ano | 0 .../database/import-databases.e2e.ts | 49 ++++++++++++++----- 4 files changed, 93 insertions(+), 16 deletions(-) rename tests/e2e/test-data/{ => import-databases}/ardm-valid.ano (100%) diff --git a/tests/e2e/pageObjects/add-redis-database-page.ts b/tests/e2e/pageObjects/add-redis-database-page.ts index 22fcad871b..92a6f68350 100644 --- a/tests/e2e/pageObjects/add-redis-database-page.ts +++ b/tests/e2e/pageObjects/add-redis-database-page.ts @@ -48,6 +48,9 @@ export class AddRedisDatabasePage { buildFromSource = Selector('a').withExactText('Build from source'); buildFromDocker = Selector('a').withExactText('Docker'); buildFromHomebrew = Selector('a').withExactText('Homebrew'); + // DROPDOWNS + caCertField = Selector('[data-testid=select-ca-cert]', {timeout: 500}); + clientCertField = Selector('[data-testid=select-cert]', {timeout: 500}); /** * Adding a new redis database @@ -61,7 +64,7 @@ export class AddRedisDatabasePage { await t .typeText(this.hostInput, parameters.host, { replace: true, paste: true }) .typeText(this.portInput, parameters.port, { replace: true, paste: true }) - .typeText(this.databaseAliasInput, parameters.databaseName, { replace: true, paste: true }); + .typeText(this.databaseAliasInput, parameters.databaseName!, { replace: true, paste: true }); if (!!parameters.databaseUsername) { await t.typeText(this.usernameInput, parameters.databaseUsername, { replace: true, paste: true }); } @@ -82,7 +85,7 @@ export class AddRedisDatabasePage { await t .typeText(this.hostInput, parameters.host, { replace: true, paste: true }) .typeText(this.portInput, parameters.port, { replace: true, paste: true }) - .typeText(this.databaseAliasInput, parameters.databaseName, { replace: true, paste: true }); + .typeText(this.databaseAliasInput, parameters.databaseName!, { replace: true, paste: true }); if (!!parameters.databaseUsername) { await t.typeText(this.usernameInput, parameters.databaseUsername, { replace: true, paste: true }); } @@ -128,8 +131,8 @@ export class AddRedisDatabasePage { await t .typeText(this.hostInput, parameters.host, { replace: true, paste: true }) .typeText(this.portInput, parameters.port, { replace: true, paste: true }) - .typeText(this.usernameInput, parameters.databaseUsername, { replace: true, paste: true }) - .typeText(this.passwordInput, parameters.databasePassword, { replace: true, paste: true }); + .typeText(this.usernameInput, parameters.databaseUsername!, { replace: true, paste: true }) + .typeText(this.passwordInput, parameters.databasePassword!, { replace: true, paste: true }); } /** diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index c45ab329e1..a20bfe3d1a 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -199,4 +199,53 @@ export class MyRedisDatabasePage { await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) .notOk("Database status is still visible"); } + + /** + * Filter array with database objects by result field and return names + * @param listOfDb Actual databases list + * @param result The expected import result + */ + filterDatabaseListByResult(listOfDb: DatabasesForImport, result: string) { + return listOfDb.filter(element => element.result === result).map(item => item.name!); + } } + +/** + * Database for import parameters + * @param host Host of connection + * @param port Port of connection + * @param name The name of connection + * @param result The expected result of connection import + * @param username The username of connection + * @param auth Password of connection + * @param cluster Is the connection has cluster + * @param indName The name of coonection with index + * @param db The index of connection + * @param ssh_port The ssh port of connection + * @param timeout_connect The connect timeout of connection + * @param timeout_execute The execute timeout of connection + * @param other_field The test field + * @param ssl_ca_cert_path The CA certificate of connection by path + * @param ssl_local_cert_path The Client certificate of connection by path + * @param ssl_private_key_path The Client key of connection by path + * @param ssl Is the connection have ssl + */ +export type DatabasesForImport = { + host?: string, + port?: number | string, + name?: string, + result?: string, + username?: string, + auth?: string, + cluster?: boolean | string, + indName?: string, + db?: number, + ssh_port?: number, + timeout_connect?: number, + timeout_execute?: number, + other_field?: string, + ssl_ca_cert_path?: string, + ssl_local_cert_path?: string, + ssl_private_key_path?: string, + ssl?: boolean +}[]; diff --git a/tests/e2e/test-data/ardm-valid.ano b/tests/e2e/test-data/import-databases/ardm-valid.ano similarity index 100% rename from tests/e2e/test-data/ardm-valid.ano rename to tests/e2e/test-data/import-databases/ardm-valid.ano diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index 039eb1ca37..e0af002cb6 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -17,9 +17,9 @@ const racompassInvalidJson = 'racompass-invalid.json'; const rdmFullJson = 'rdm-full.json'; const ardmValidAno = 'ardm-valid.ano'; const listOfDB = JSON.parse(fs.readFileSync(path.join('test-data', 'import-databases', 'rdm-full.json'), 'utf-8')); -const dbSuccessNames = listOfDB.filter(element => element.result === 'success').map(item => item.name); -const dbPartialNames = listOfDB.filter(element => element.result === 'partial').map(item => item.name); -const dbFailedNames = listOfDB.filter(element => element.result === 'failed').map(item => item.name); +const dbSuccessNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'success'); +const dbPartialNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'partial'); +const dbFailedNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'failed'); const dbImportedNames = [...dbSuccessNames, ...dbPartialNames]; const rdmData = { type: 'rdm', @@ -50,18 +50,19 @@ const databases = [ dbData[1].dbNames[1] ]; const databasesToDelete = [...dbImportedNames, ...databases]; +const findImportedDbNameInList = async(dbName: string) => dbImportedNames.find(item => item === dbName)!; fixture `Import databases` - .meta({ type: 'critical_path', rte: rte.standalone }) + .meta({ type: 'critical_path', rte: rte.none }) .page(commonUrl) - .beforeEach(async() => { + .beforeEach(async () => { await acceptLicenseTerms(); }) - .afterEach(async() => { + .afterEach(async () => { // Delete databases deleteStandaloneDatabasesByNamesApi(databasesToDelete); - }); -test('Connection import from JSON', async t => { + }) +test('Connection import modal window', async t => { const tooltipText = 'Import Database Connections'; const defaultText = 'Select or drag and drop a file'; const parseFailedMsg = 'Failed to add database connections'; @@ -95,8 +96,9 @@ test('Connection import from JSON', async t => { await t.click(myRedisDatabasePage.removeImportedFileBtn); await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); +}); +test('Connection import from JSON', async t => { // Verify that user can import database with mandatory/optional fields - await t.click(myRedisDatabasePage.closeDialogBtn); await databasesActions.importDatabase(rdmData); // Fully imported table @@ -115,14 +117,37 @@ test('Connection import from JSON', async t => { await clickOnEditDatabaseByName(dbImportedNames[1]); // Verify username imported - await t.expect(addRedisDatabasePage.usernameInput.value).eql(listOfDB[1].username); + await t.expect(addRedisDatabasePage.usernameInput.value).eql(listOfDB[1].username, 'Username import incorrect'); // Verify password imported await t.click(addRedisDatabasePage.showPasswordBtn); - await t.expect(addRedisDatabasePage.passwordInput.value).eql(listOfDB[1].auth); + await t.expect(addRedisDatabasePage.passwordInput.value).eql(listOfDB[1].auth, 'Password import incorrect'); // Verify cluster connection type imported await clickOnEditDatabaseByName(dbImportedNames[2]); - await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType); + await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType, 'Connection type import incorrect'); + + // Verify that user can import database with CA certificate + await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+CaCert')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); + + // Verify that user can import database with Client certificate, Client private key + await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+clientCert+privateKey')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); + + // Verify that user can import database with all certificates + await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); + + // Verify that certificate not imported when any certificate field has not been parsed + await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmCaCertInvalidBody')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); + await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmInvalidClientCert')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); // Verify that user can import files from Racompass, ARDM, RDM for (const db of dbData) { From ad7cde621e38b7e4ef7a93ae8b8e23b43d49d4cd Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 20 Dec 2022 15:30:13 +0100 Subject: [PATCH 101/107] tests for import sertificates --- tests/e2e/common-actions/databases-actions.ts | 12 +- tests/e2e/helpers/api/api-database.ts | 8 +- tests/e2e/helpers/database.ts | 4 +- .../pageObjects/add-redis-database-page.ts | 1 + .../pageObjects/my-redis-databases-page.ts | 10 +- .../test-data/certs/certsByPath/caPath.crt | 3 + .../certs/certsByPath/caSameBody.crt | 3 + .../certs/certsByPath/clientPath.crt | 3 + .../certs/certsByPath/clientPath.key | 3 + .../certs/certsByPath/clientSameBody.crt | 3 + .../certs/certsByPath/clientSameBody.key | 3 + .../test-data/certs/sameNameCerts/caPath.crt | 3 + .../certs/sameNameCerts/clientPath.crt | 3 + .../certs/sameNameCerts/clientPath.key | 3 + .../test-data/import-databases/ardm-valid.ano | 2 +- .../import-databases/racompass-valid.json | 2 +- .../import-databases/rdm-certificates.json | 74 ++++++ .../database/import-databases.e2e.ts | 211 +++++++++++------- 18 files changed, 255 insertions(+), 96 deletions(-) create mode 100644 tests/e2e/test-data/certs/certsByPath/caPath.crt create mode 100644 tests/e2e/test-data/certs/certsByPath/caSameBody.crt create mode 100644 tests/e2e/test-data/certs/certsByPath/clientPath.crt create mode 100644 tests/e2e/test-data/certs/certsByPath/clientPath.key create mode 100644 tests/e2e/test-data/certs/certsByPath/clientSameBody.crt create mode 100644 tests/e2e/test-data/certs/certsByPath/clientSameBody.key create mode 100644 tests/e2e/test-data/certs/sameNameCerts/caPath.crt create mode 100644 tests/e2e/test-data/certs/sameNameCerts/clientPath.crt create mode 100644 tests/e2e/test-data/certs/sameNameCerts/clientPath.key create mode 100644 tests/e2e/test-data/import-databases/rdm-certificates.json diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index 08bea3bda9..277ffed1b6 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -1,4 +1,5 @@ import { t } from 'testcafe'; +import * as fs from 'fs'; import { MyRedisDatabasePage } from '../pageObjects'; const myRedisDatabasePage = new MyRedisDatabasePage(); @@ -26,6 +27,14 @@ export class DatabasesActions { .click(myRedisDatabasePage.submitImportBtn) .expect(myRedisDatabasePage.importDialogTitle.textContent).eql('Import Results', `Databases from ${fileParameters.type} not imported`); } + + /** + * Parse json for importing databases + * @param path The path to json file + */ + parseDbJsonByPath(path: string): any[] { + return JSON.parse(fs.readFileSync(path, 'utf-8')); + } } /** @@ -45,5 +54,6 @@ export type ImportDatabaseParameters = { userName?: string, password?: string, connectionType?: string, - fileName?: string + fileName?: string, + parsedJson?: any }; diff --git a/tests/e2e/helpers/api/api-database.ts b/tests/e2e/helpers/api/api-database.ts index 65b536167c..5288ef54af 100644 --- a/tests/e2e/helpers/api/api-database.ts +++ b/tests/e2e/helpers/api/api-database.ts @@ -62,7 +62,7 @@ export async function addNewOSSClusterDatabaseApi(databaseParameters: OSSCluster export async function discoverSentinelDatabaseApi(databaseParameters: SentinelParameters, primaryGroupsNumber?: number): Promise { let masters = databaseParameters.masters; if (primaryGroupsNumber) { - masters = databaseParameters.masters.slice(0, primaryGroupsNumber); + masters = databaseParameters.masters!.slice(0, primaryGroupsNumber); } const response = await request(endpoint).post('/redis-sentinel/databases') .send({ @@ -126,7 +126,7 @@ export async function getDatabaseByConnectionType(connectionType?: string): Prom export async function deleteAllDatabasesApi(): Promise { const allDatabases = await getAllDatabases(); if (allDatabases.length > 0) { - const databaseIds = []; + const databaseIds: string[] = []; for (let i = 0; i < allDatabases.length; i++) { const dbData = JSON.parse(JSON.stringify(allDatabases[i])); databaseIds.push(dbData.id); @@ -183,8 +183,8 @@ export async function deleteOSSClusterDatabaseApi(databaseParameters: OSSCluster * @param databaseParameters The database parameters */ export async function deleteAllSentinelDatabasesApi(databaseParameters: SentinelParameters): Promise { - for (let i = 0; i < databaseParameters.name.length; i++) { - const databaseId = await getDatabaseByName(databaseParameters.name[i]); + for (let i = 0; i < databaseParameters.name!.length; i++) { + const databaseId = await getDatabaseByName(databaseParameters.name![i]); const response = await request(endpoint).delete('/databases') .send({ 'ids': [`${databaseId}`] }).set('Accept', 'application/json'); await t.expect(response.status).eql(200, 'Delete Sentinel database request failed'); diff --git a/tests/e2e/helpers/database.ts b/tests/e2e/helpers/database.ts index 6e39f67541..851bf5c8ca 100644 --- a/tests/e2e/helpers/database.ts +++ b/tests/e2e/helpers/database.ts @@ -158,7 +158,7 @@ export async function acceptLicenseTermsAndAddSentinelDatabaseApi(databaseParame // Reload Page to see the database added through api await common.reloadPage(); // Connect to DB - await myRedisDatabasePage.clickOnDBByName(databaseParameters.name[1] ?? ''); + await myRedisDatabasePage.clickOnDBByName(databaseParameters.name![1] ?? ''); } /** @@ -273,7 +273,7 @@ export async function clickOnEditDatabaseByName(databaseName: string): Promise { +export async function deleteDatabaseByNameApi(databaseName: string): Promise { const databaseId = await getDatabaseByName(databaseName); const databaseDeleteBtn = Selector(`[data-testid=delete-instance-${databaseId}-icon]`); diff --git a/tests/e2e/pageObjects/add-redis-database-page.ts b/tests/e2e/pageObjects/add-redis-database-page.ts index 92a6f68350..fc71fb7a1d 100644 --- a/tests/e2e/pageObjects/add-redis-database-page.ts +++ b/tests/e2e/pageObjects/add-redis-database-page.ts @@ -44,6 +44,7 @@ export class AddRedisDatabasePage { primaryGroupNameInput = Selector('[data-testid=primary-group]'); masterGroupPassword = Selector('[data-testid=sentinel-master-password]'); connectionType = Selector('[data-testid=connection-type]'); + sentinelForm = Selector('[data-testid=form]'); //Links buildFromSource = Selector('a').withExactText('Build from source'); buildFromDocker = Selector('a').withExactText('Docker'); diff --git a/tests/e2e/pageObjects/my-redis-databases-page.ts b/tests/e2e/pageObjects/my-redis-databases-page.ts index a20bfe3d1a..23797c4bc8 100644 --- a/tests/e2e/pageObjects/my-redis-databases-page.ts +++ b/tests/e2e/pageObjects/my-redis-databases-page.ts @@ -188,16 +188,16 @@ export class MyRedisDatabasePage { * Verify database status is visible */ async verifyDatabaseStatusIsVisible(): Promise { - await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) - .ok("Database status is not visible"); + await t.expect(Selector('div').withAttribute('data-testid', /database-status-new-*/).visible) + .ok('Database status is not visible'); } /** * Verify database status is not visible */ async verifyDatabaseStatusIsNotVisible(): Promise { - await t.expect(Selector("div").withAttribute("data-testid", /database-status-new-*/).visible) - .notOk("Database status is still visible"); + await t.expect(Selector('div').withAttribute('data-testid', /database-status-new-*/).visible) + .notOk('Database status is still visible'); } /** @@ -205,7 +205,7 @@ export class MyRedisDatabasePage { * @param listOfDb Actual databases list * @param result The expected import result */ - filterDatabaseListByResult(listOfDb: DatabasesForImport, result: string) { + getDatabaseNamesFromListByResult(listOfDb: DatabasesForImport, result: string): string[] { return listOfDb.filter(element => element.result === result).map(item => item.name!); } } diff --git a/tests/e2e/test-data/certs/certsByPath/caPath.crt b/tests/e2e/test-data/certs/certsByPath/caPath.crt new file mode 100644 index 0000000000..a2e4e8dde6 --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/caPath.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedCACertificatePath +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/certsByPath/caSameBody.crt b/tests/e2e/test-data/certs/certsByPath/caSameBody.crt new file mode 100644 index 0000000000..a2e4e8dde6 --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/caSameBody.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedCACertificatePath +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/certsByPath/clientPath.crt b/tests/e2e/test-data/certs/certsByPath/clientPath.crt new file mode 100644 index 0000000000..7db9b4d04f --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/clientPath.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedClientCrtPath +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/certsByPath/clientPath.key b/tests/e2e/test-data/certs/certsByPath/clientPath.key new file mode 100644 index 0000000000..9df04e8724 --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/clientPath.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +mockedPrivateKeyPath +-----END PRIVATE KEY----- diff --git a/tests/e2e/test-data/certs/certsByPath/clientSameBody.crt b/tests/e2e/test-data/certs/certsByPath/clientSameBody.crt new file mode 100644 index 0000000000..7db9b4d04f --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/clientSameBody.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedClientCrtPath +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/certsByPath/clientSameBody.key b/tests/e2e/test-data/certs/certsByPath/clientSameBody.key new file mode 100644 index 0000000000..9df04e8724 --- /dev/null +++ b/tests/e2e/test-data/certs/certsByPath/clientSameBody.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +mockedPrivateKeyPath +-----END PRIVATE KEY----- diff --git a/tests/e2e/test-data/certs/sameNameCerts/caPath.crt b/tests/e2e/test-data/certs/sameNameCerts/caPath.crt new file mode 100644 index 0000000000..81c5565ebc --- /dev/null +++ b/tests/e2e/test-data/certs/sameNameCerts/caPath.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedCACertificatePath1 +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/sameNameCerts/clientPath.crt b/tests/e2e/test-data/certs/sameNameCerts/clientPath.crt new file mode 100644 index 0000000000..192ff845d1 --- /dev/null +++ b/tests/e2e/test-data/certs/sameNameCerts/clientPath.crt @@ -0,0 +1,3 @@ +-----BEGIN CERTIFICATE----- +mockedClientCrtPath1 +-----END CERTIFICATE----- diff --git a/tests/e2e/test-data/certs/sameNameCerts/clientPath.key b/tests/e2e/test-data/certs/sameNameCerts/clientPath.key new file mode 100644 index 0000000000..087fbe9c5b --- /dev/null +++ b/tests/e2e/test-data/certs/sameNameCerts/clientPath.key @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +mockedPrivateKeyPath1 +-----END PRIVATE KEY----- diff --git a/tests/e2e/test-data/import-databases/ardm-valid.ano b/tests/e2e/test-data/import-databases/ardm-valid.ano index 7c557d1989..cb5e8862bb 100644 --- a/tests/e2e/test-data/import-databases/ardm-valid.ano +++ b/tests/e2e/test-data/import-databases/ardm-valid.ano @@ -1 +1 @@ -W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9XQ== \ No newline at end of file +W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9LHsiaG9zdCI6Im9zcy1zZW50aW5lbCIsInBvcnQiOiIyNjM3OSIsIm5hbWUiOiJhcmRtU2VudGluZWwiLCJhdXRoIjoiZGVmYXVsdHBhc3MiLCJzZW50aW5lbE9wdGlvbnMiOnsibWFzdGVyTmFtZSI6InByaW1hcnktZ3JvdXAtMSIsIm5vZGVQYXNzd29yZCI6ImRlZmF1bHRwYXNzIn19XQ== \ No newline at end of file diff --git a/tests/e2e/test-data/import-databases/racompass-valid.json b/tests/e2e/test-data/import-databases/racompass-valid.json index dc4e32dcf5..7cac1af195 100644 --- a/tests/e2e/test-data/import-databases/racompass-valid.json +++ b/tests/e2e/test-data/import-databases/racompass-valid.json @@ -165,7 +165,7 @@ "port": 1111, "keyPrefix": null, "id": "f99a5d6d-daf4-489c-885b-6f8e411adbc9", - "cluster": true, + "type": "cluster", "name": "racompassCluster" } ] \ No newline at end of file diff --git a/tests/e2e/test-data/import-databases/rdm-certificates.json b/tests/e2e/test-data/import-databases/rdm-certificates.json new file mode 100644 index 0000000000..bd6f2daac9 --- /dev/null +++ b/tests/e2e/test-data/import-databases/rdm-certificates.json @@ -0,0 +1,74 @@ +[ + { + "host": "localhost", + "port": 8102, + "name": "theSameBody1", + "caCert": { + "name": "testCaName", + "certificate": "-----BEGIN CERTIFICATE-----mockedCACertificate1-----END CERTIFICATE-----" + }, + "clientCert": { + "name": "testClientCertName", + "certificate": "-----BEGIN CERTIFICATE-----mockedClientCertificate1-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----mockedClientKey1-----END PRIVATE KEY-----" + }, + "result": "success" + }, + { + "host": "localhost", + "port": 8101, + "name": "theSameBody2", + "caCert": { + "name": "testCaName2", + "certificate": "-----BEGIN CERTIFICATE-----mockedCACertificate1-----END CERTIFICATE-----" + }, + "clientCert": { + "name": "testClientCertName2", + "certificate": "-----BEGIN CERTIFICATE-----mockedClientCertificate1-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----mockedClientKey1-----END PRIVATE KEY-----" + }, + "result": "success" + }, + { + "host": "localhost", + "port": 8103, + "name": "theSameName", + "caCert": { + "name": "testCaName", + "certificate": "-----BEGIN CERTIFICATE-----mockedCACertificate2-----END CERTIFICATE-----" + }, + "clientCert": { + "name": "testClientCertName", + "certificate": "-----BEGIN CERTIFICATE-----mockedClientCertificate2-----END CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----mockedClientKey2-----END PRIVATE KEY-----" + }, + "result": "success" + }, + { + "host": "localhost", + "port": 8102, + "name": "theSameBody1Path", + "ssl_ca_cert_path": "/root/certs/certsByPath/caPath.crt", + "ssl_local_cert_path": "/root/certs/certsByPath/clientPath.crt", + "ssl_private_key_path": "/root/certs/certsByPath/clientPath.key", + "result": "success" + }, + { + "host": "localhost", + "port": 8101, + "name": "theSameBody2Path", + "ssl_ca_cert_path": "/root/certs/certsByPath/caSameBody.crt", + "ssl_local_cert_path": "/root/certs/certsByPath/clientSameBody.crt", + "ssl_private_key_path": "/root/certs/certsByPath/clientSameBody.key", + "result": "success" + }, + { + "host": "localhost", + "port": 8103, + "name": "theSameNamePath", + "ssl_ca_cert_path": "/root/certs/sameNameCerts/caPath.crt", + "ssl_local_cert_path": "/root/certs/sameNameCerts/clientPath.crt", + "ssl_private_key_path": "/root/certs/sameNameCerts/clientPath.key", + "result": "success" + } +] \ No newline at end of file diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index e0af002cb6..e9e32f44c6 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -1,4 +1,3 @@ -import * as fs from 'fs'; import * as path from 'path'; import { rte } from '../../../helpers/constants'; import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; @@ -15,19 +14,28 @@ const addRedisDatabasePage = new AddRedisDatabasePage(); const racompassValidJson = 'racompass-valid.json'; const racompassInvalidJson = 'racompass-invalid.json'; const rdmFullJson = 'rdm-full.json'; +const rdmCertsJson = 'rdm-certificates.json'; const ardmValidAno = 'ardm-valid.ano'; -const listOfDB = JSON.parse(fs.readFileSync(path.join('test-data', 'import-databases', 'rdm-full.json'), 'utf-8')); -const dbSuccessNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'success'); -const dbPartialNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'partial'); -const dbFailedNames = myRedisDatabasePage.filterDatabaseListByResult(listOfDB, 'failed'); -const dbImportedNames = [...dbSuccessNames, ...dbPartialNames]; +const racompassInvalidJsonPath = path.join('..', '..', '..', 'test-data', 'import-databases', racompassInvalidJson); +const rdmListOfDB = databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', rdmFullJson)); +const rdmCertsListOfDB = databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', rdmCertsJson)); +const rdmSuccessNames = myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'success'); +const rdmPartialNames = myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'partial'); +const rdmFailedNames = myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmListOfDB, 'failed'); +const rdmCertsNames = myRedisDatabasePage.getDatabaseNamesFromListByResult(rdmCertsListOfDB, 'success'); const rdmData = { type: 'rdm', path: path.join('..', '..', '..', 'test-data', 'import-databases', rdmFullJson), connectionType: 'Cluster', - successNumber: dbSuccessNames.length, - partialNumber: dbPartialNames.length, - failedNumber: dbFailedNames.length + successNumber: rdmSuccessNames.length, + partialNumber: rdmPartialNames.length, + failedNumber: rdmFailedNames.length, + dbImportedNames: [...rdmSuccessNames, ...rdmPartialNames] +}; +const rdmCertsData = { + type: 'rdm', + path: path.join('..', '..', '..', 'test-data', 'import-databases', rdmCertsJson), + parsedJson: databasesActions.parseDbJsonByPath(path.join('test-data', 'import-databases', rdmCertsJson)) }; const dbData = [ { @@ -38,30 +46,22 @@ const dbData = [ { type: 'ardm', path: path.join('..', '..', '..', 'test-data', 'import-databases', ardmValidAno), - dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername'] + dbNames: ['ardmNoName:12001', 'ardmWithPassAndUsername', 'ardmSentinel'] } ]; -const racompassInvalidJsonPath = path.join('..', '..', '..', 'test-data', 'import-databases', racompassInvalidJson); -// List of all created databases to delete -const databases = [ +const databasesToDelete = [ dbData[0].dbNames[0], dbData[0].dbNames[1].split(' ')[0], - dbData[1].dbNames[0], - dbData[1].dbNames[1] + ...dbData[1].dbNames ]; -const databasesToDelete = [...dbImportedNames, ...databases]; -const findImportedDbNameInList = async(dbName: string) => dbImportedNames.find(item => item === dbName)!; +const findImportedRdmDbNameInList = async(dbName: string): Promise => rdmData.dbImportedNames.find(item => item === dbName)!; fixture `Import databases` .meta({ type: 'critical_path', rte: rte.none }) .page(commonUrl) - .beforeEach(async () => { + .beforeEach(async() => { await acceptLicenseTerms(); - }) - .afterEach(async () => { - // Delete databases - deleteStandaloneDatabasesByNamesApi(databasesToDelete); - }) + }); test('Connection import modal window', async t => { const tooltipText = 'Import Database Connections'; const defaultText = 'Select or drag and drop a file'; @@ -95,64 +95,111 @@ test('Connection import modal window', async t => { // Click on remove button await t.click(myRedisDatabasePage.removeImportedFileBtn); await t.expect(myRedisDatabasePage.importDbDialog.textContent).contains(defaultText, 'File not removed from import input'); - }); -test('Connection import from JSON', async t => { - // Verify that user can import database with mandatory/optional fields - await databasesActions.importDatabase(rdmData); - - // Fully imported table - await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); - // Partially imported table - await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number'); - // Failed to import table - await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) - .contains(`${rdmData.failedNumber}`, 'Not correct import failed number'); - - // Verify that list of databases is reloaded when database added - await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(dbImportedNames); - - await clickOnEditDatabaseByName(dbImportedNames[1]); - // Verify username imported - await t.expect(addRedisDatabasePage.usernameInput.value).eql(listOfDB[1].username, 'Username import incorrect'); - // Verify password imported - await t.click(addRedisDatabasePage.showPasswordBtn); - await t.expect(addRedisDatabasePage.passwordInput.value).eql(listOfDB[1].auth, 'Password import incorrect'); - - // Verify cluster connection type imported - await clickOnEditDatabaseByName(dbImportedNames[2]); - await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType, 'Connection type import incorrect'); - - // Verify that user can import database with CA certificate - await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+CaCert')); - await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); - await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); - - // Verify that user can import database with Client certificate, Client private key - await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+clientCert+privateKey')); - await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); - await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); - - // Verify that user can import database with all certificates - await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey')); - await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); - await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); - - // Verify that certificate not imported when any certificate field has not been parsed - await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmCaCertInvalidBody')); - await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); - await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); - await clickOnEditDatabaseByName(await findImportedDbNameInList('rdmInvalidClientCert')); - await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); - await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); - - // Verify that user can import files from Racompass, ARDM, RDM - for (const db of dbData) { - await databasesActions.importDatabase(db); +test + .after(async() => { + // Delete databases + await deleteStandaloneDatabasesByNamesApi([...rdmData.dbImportedNames, ...databasesToDelete]); + })('Connection import from JSON', async t => { + // Verify that user can import database with mandatory/optional fields + await databasesActions.importDatabase(rdmData); + + // Fully imported table + await t.expect(myRedisDatabasePage.successResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.successNumber}`, 'Not correct successfully imported number'); + // Partially imported table + await t.expect(myRedisDatabasePage.partialResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.partialNumber}`, 'Not correct partially imported number'); + // Failed to import table + await t.expect(myRedisDatabasePage.failedResultsAccordion.find(myRedisDatabasePage.cssNumberOfDbs).textContent) + .contains(`${rdmData.failedNumber}`, 'Not correct import failed number'); + + // Verify that list of databases is reloaded when database added await t.click(myRedisDatabasePage.okDialogBtn); - await databasesActions.verifyDatabasesDisplayed(db.dbNames); - } -}); + await databasesActions.verifyDatabasesDisplayed(rdmData.dbImportedNames); + + await clickOnEditDatabaseByName(rdmData.dbImportedNames[1]); + // Verify username imported + await t.expect(addRedisDatabasePage.usernameInput.value).eql(rdmListOfDB[1].username, 'Username import incorrect'); + // Verify password imported + await t.click(addRedisDatabasePage.showPasswordBtn); + await t.expect(addRedisDatabasePage.passwordInput.value).eql(rdmListOfDB[1].auth, 'Password import incorrect'); + + // Verify cluster connection type imported + await clickOnEditDatabaseByName(rdmData.dbImportedNames[2]); + await t.expect(addRedisDatabasePage.connectionType.textContent).eql(rdmData.connectionType, 'Connection type import incorrect'); + + /* + Verify that user can import database with CA certificate + Verify that user can import database with certificates by an absolute folder path(CA certificate, Client certificate, Client private key) + Verify that user can see the certificate name as the certificate file name + */ + await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); + + // Verify that user can import database with Client certificate, Client private key + await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+clientCert+privateKey')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); + + // Verify that user can import database with all certificates + await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmHost+Port+Name+CaCert+clientCert+privateKey')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('ca', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('client', 'Client certificate import incorrect'); + + // Verify that certificate not imported when any certificate field has not been parsed + await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmCaCertInvalidBody')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); + await clickOnEditDatabaseByName(await findImportedRdmDbNameInList('rdmInvalidClientCert')); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('No CA Certificate', 'CA certificate was imported'); + await t.expect(addRedisDatabasePage.clientCertField.exists).notOk('Client certificate was imported'); + + // Verify that user can import files from Racompass, ARDM, RDM + for (const db of dbData) { + await databasesActions.importDatabase(db); + await t.click(myRedisDatabasePage.okDialogBtn); + await databasesActions.verifyDatabasesDisplayed(db.dbNames); + } + + // Verify that user can import Sentinel database connections by corresponding fields in JSON + await clickOnEditDatabaseByName(dbData[1].dbNames[2]); + await t.expect(addRedisDatabasePage.sentinelForm.textContent).contains('Sentinel', 'Sentinel connection type import incorrect'); + }); +test + .after(async() => { + // Delete databases + await deleteStandaloneDatabasesByNamesApi(rdmCertsNames); + })('Certificates import with/without path', async t => { + await databasesActions.importDatabase(rdmCertsData); + await t.click(myRedisDatabasePage.okDialogBtn); + + // Verify that when user imports a certificate and the same certificate body already exists, the existing certificate (with its name) is applied + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[0].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql(rdmCertsData.parsedJson[0].caCert.name, 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql(rdmCertsData.parsedJson[0].clientCert.name, 'Client certificate import incorrect'); + + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[1].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql(rdmCertsData.parsedJson[0].caCert.name, 'CA certificate name with the same body is incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql(rdmCertsData.parsedJson[0].clientCert.name, 'Client certificate name with the same body is incorrect'); + + // Verify that when user imports a certificate and the same certificate name exists but with a different body, the certificate imported with "({incremental_number})_certificate_name" name + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[2].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql(`1_${rdmCertsData.parsedJson[0].caCert.name}`, 'CA certificate name with the same body is incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql(`1_${rdmCertsData.parsedJson[0].clientCert.name}`, 'Client certificate name with the same body is incorrect'); + + // Verify that when user imports a certificate by path and the same certificate body already exists, the existing certificate (with its name) is applied + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[3].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('caPath', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect'); + + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[4].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('caPath', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('clientPath', 'Client certificate import incorrect'); + + // Verify that when user imports a certificate by path and the same certificate name exists but with a different body, the certificate imported with "({incremental_number})certificate_name" name + await clickOnEditDatabaseByName(rdmCertsData.parsedJson[5].name); + await t.expect(addRedisDatabasePage.caCertField.textContent).eql('1_caPath', 'CA certificate import incorrect'); + await t.expect(addRedisDatabasePage.clientCertField.textContent).eql('1_clientPath', 'Client certificate import incorrect'); + }); From 3b2af69d3a9e9482bccc2590a06a014a2fd66759 Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 20 Dec 2022 17:34:04 +0100 Subject: [PATCH 102/107] pr fix --- tests/e2e/common-actions/databases-actions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/e2e/common-actions/databases-actions.ts b/tests/e2e/common-actions/databases-actions.ts index 277ffed1b6..043966c937 100644 --- a/tests/e2e/common-actions/databases-actions.ts +++ b/tests/e2e/common-actions/databases-actions.ts @@ -46,6 +46,7 @@ export class DatabasesActions { * @param password The password of db * @param connectionType The connection type of db * @param fileName The file name + * @param parsedJson The parsed json content */ export type ImportDatabaseParameters = { path: string, From b5b40457cbbd6f13e43acc3dbf6b1c3ed87393ed Mon Sep 17 00:00:00 2001 From: vlad-dargel Date: Tue, 20 Dec 2022 17:49:30 +0100 Subject: [PATCH 103/107] add connection to sentinel --- tests/e2e/test-data/import-databases/ardm-valid.ano | 2 +- .../e2e/tests/critical-path/database/import-databases.e2e.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/e2e/test-data/import-databases/ardm-valid.ano b/tests/e2e/test-data/import-databases/ardm-valid.ano index cb5e8862bb..3a18087819 100644 --- a/tests/e2e/test-data/import-databases/ardm-valid.ano +++ b/tests/e2e/test-data/import-databases/ardm-valid.ano @@ -1 +1 @@ -W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9LHsiaG9zdCI6Im9zcy1zZW50aW5lbCIsInBvcnQiOiIyNjM3OSIsIm5hbWUiOiJhcmRtU2VudGluZWwiLCJhdXRoIjoiZGVmYXVsdHBhc3MiLCJzZW50aW5lbE9wdGlvbnMiOnsibWFzdGVyTmFtZSI6InByaW1hcnktZ3JvdXAtMSIsIm5vZGVQYXNzd29yZCI6ImRlZmF1bHRwYXNzIn19XQ== \ No newline at end of file +W3siaG9zdCI6ImxvY2FsaG9zdCIsInBvcnQiOiI2Mzc5IiwiYXV0aCI6InBhc3MiLCJ1c2VybmFtZSI6InVzZXJuYW1lVGVzdCIsIm5hbWUiOiJhcmRtV2l0aFBhc3NBbmRVc2VybmFtZSIsInNlcGFyYXRvciI6IjoiLCJjbHVzdGVyIjpmYWxzZSwia2V5IjoiMTY1MDM3MzUyNTY1MV9naHFwciIsIm9yZGVyIjowfSx7Imhvc3QiOiJhcmRtTm9OYW1lIiwicG9ydCI6IjEyMDAxIiwiYXV0aCI6IiIsInVzZXJuYW1lIjoiIiwic2VwYXJhdG9yIjoiOiIsImNsdXN0ZXIiOmZhbHNlLCJrZXkiOiIxNjUwODk3NjIxNzU0X2I1bjB2Iiwib3JkZXIiOjF9LHsiaG9zdCI6Im9zcy1zZW50aW5lbCIsInBvcnQiOiIyNjM3OSIsIm5hbWUiOiJhcmRtU2VudGluZWwiLCJhdXRoIjoicGFzc3dvcmQiLCJzZW50aW5lbE9wdGlvbnMiOnsibWFzdGVyTmFtZSI6InByaW1hcnktZ3JvdXAtMSIsIm5vZGVQYXNzd29yZCI6ImRlZmF1bHRwYXNzIn19XQ== \ No newline at end of file diff --git a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts index e9e32f44c6..ce46029b7f 100644 --- a/tests/e2e/tests/critical-path/database/import-databases.e2e.ts +++ b/tests/e2e/tests/critical-path/database/import-databases.e2e.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import { ClientFunction } from 'testcafe'; import { rte } from '../../../helpers/constants'; import { AddRedisDatabasePage, BrowserPage, MyRedisDatabasePage } from '../../../pageObjects'; import { commonUrl } from '../../../helpers/conf'; @@ -55,6 +56,8 @@ const databasesToDelete = [ ...dbData[1].dbNames ]; const findImportedRdmDbNameInList = async(dbName: string): Promise => rdmData.dbImportedNames.find(item => item === dbName)!; +// Returns the URL of the current web page +const getPageUrl = ClientFunction(() => window.location.href); fixture `Import databases` .meta({ type: 'critical_path', rte: rte.none }) @@ -166,6 +169,8 @@ test // Verify that user can import Sentinel database connections by corresponding fields in JSON await clickOnEditDatabaseByName(dbData[1].dbNames[2]); await t.expect(addRedisDatabasePage.sentinelForm.textContent).contains('Sentinel', 'Sentinel connection type import incorrect'); + await myRedisDatabasePage.clickOnDBByName(dbData[1].dbNames[2]); + await t.expect(getPageUrl()).contains('browser', 'Sentinel connection not opened'); }); test .after(async() => { From cb30d5edc5a38c9bab4895457f64ee777aefcecc Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 20 Dec 2022 20:50:41 +0200 Subject: [PATCH 104/107] possible solution for editing databases --- .../modules/database/database.controller.ts | 41 +- .../src/modules/database/database.service.ts | 3 +- .../database/dto/modify.database.dto.ts | 20 + .../database/dto/update.database.dto.ts | 96 ++- .../api/database/PATCH-databases-id.test.ts | 774 ++++++++++++++++++ .../ui/src/slices/instances/instances.ts | 2 +- 6 files changed, 929 insertions(+), 7 deletions(-) create mode 100644 redisinsight/api/src/modules/database/dto/modify.database.dto.ts create mode 100644 redisinsight/api/test/api/database/PATCH-databases-id.test.ts diff --git a/redisinsight/api/src/modules/database/database.controller.ts b/redisinsight/api/src/modules/database/database.controller.ts index b4f86075eb..532b512520 100644 --- a/redisinsight/api/src/modules/database/database.controller.ts +++ b/redisinsight/api/src/modules/database/database.controller.ts @@ -1,7 +1,17 @@ import { ApiTags } from '@nestjs/swagger'; import { Body, - ClassSerializerInterceptor, Controller, Delete, Get, Param, Post, Put, UseInterceptors, UsePipes, ValidationPipe, + ClassSerializerInterceptor, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Put, + UseInterceptors, + UsePipes, + ValidationPipe, } from '@nestjs/common'; import { ApiEndpoint } from 'src/decorators/api-endpoint.decorator'; import { Database } from 'src/modules/database/models/database'; @@ -16,6 +26,7 @@ import { DeleteDatabasesDto } from 'src/modules/database/dto/delete.databases.dt import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; import { ClientMetadataParam } from 'src/common/decorators'; import { ClientMetadata } from 'src/common/models'; +import { ModifyDatabaseDto } from 'src/modules/database/dto/modify.database.dto'; @ApiTags('Database') @Controller('databases') @@ -117,6 +128,34 @@ export class DatabaseController { return await this.service.update(id, database, true); } + @UseInterceptors(ClassSerializerInterceptor) + @UseInterceptors(new TimeoutInterceptor(ERROR_MESSAGES.CONNECTION_TIMEOUT)) + @Patch(':id') + @ApiEndpoint({ + description: 'Update database instance by id', + statusCode: 200, + responses: [ + { + status: 200, + description: 'Updated database instance\' response', + type: Database, + }, + ], + }) + @UsePipes( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + }), + ) + async modify( + @Param('id') id: string, + @Body() database: ModifyDatabaseDto, + ): Promise { + return await this.service.update(id, database, true); + } + @Delete('/:id') @ApiEndpoint({ statusCode: 200, diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 8c9800fc85..6f5aaacb7b 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -18,6 +18,7 @@ import { AppRedisInstanceEvents } from 'src/constants'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; import { ClientContext } from 'src/common/models'; +import { ModifyDatabaseDto } from 'src/modules/database/dto/modify.database.dto'; @Injectable() export class DatabaseService { @@ -118,7 +119,7 @@ export class DatabaseService { // todo: remove manualUpdate flag logic public async update( id: string, - dto: UpdateDatabaseDto, + dto: UpdateDatabaseDto | ModifyDatabaseDto, manualUpdate: boolean = true, ): Promise { this.logger.log(`Updating database: ${id}`); diff --git a/redisinsight/api/src/modules/database/dto/modify.database.dto.ts b/redisinsight/api/src/modules/database/dto/modify.database.dto.ts new file mode 100644 index 0000000000..a85e018b43 --- /dev/null +++ b/redisinsight/api/src/modules/database/dto/modify.database.dto.ts @@ -0,0 +1,20 @@ +import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; +import { PartialType } from '@nestjs/swagger'; +import { + IsInt, IsString, MaxLength, ValidateIf, +} from 'class-validator'; + +export class ModifyDatabaseDto extends PartialType(CreateDatabaseDto) { + @ValidateIf((object, value) => value !== undefined) + @IsString({ always: true }) + @MaxLength(500) + name: string; + + @ValidateIf((object, value) => value !== undefined) + @IsString({ always: true }) + host: string; + + @ValidateIf((object, value) => value !== undefined) + @IsInt({ always: true }) + port: number; +} diff --git a/redisinsight/api/src/modules/database/dto/update.database.dto.ts b/redisinsight/api/src/modules/database/dto/update.database.dto.ts index 1f50ed47f3..9946f1a5cf 100644 --- a/redisinsight/api/src/modules/database/dto/update.database.dto.ts +++ b/redisinsight/api/src/modules/database/dto/update.database.dto.ts @@ -1,10 +1,20 @@ -import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; -import { PartialType } from '@nestjs/swagger'; +import { ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { - IsInt, IsString, MaxLength, ValidateIf, + IsBoolean, + IsInt, IsNotEmpty, IsNotEmptyObject, IsOptional, IsString, MaxLength, Min, ValidateIf, ValidateNested, } from 'class-validator'; +import { CreateCaCertificateDto } from 'src/modules/certificate/dto/create.ca-certificate.dto'; +import { UseCaCertificateDto } from 'src/modules/certificate/dto/use.ca-certificate.dto'; +import { Expose, Type } from 'class-transformer'; +import { caCertTransformer } from 'src/modules/certificate/transformers/ca-cert.transformer'; +import { Default } from 'src/common/decorators'; +import { CreateClientCertificateDto } from 'src/modules/certificate/dto/create.client-certificate.dto'; +import { clientCertTransformer } from 'src/modules/certificate/transformers/client-cert.transformer'; +import { UseClientCertificateDto } from 'src/modules/certificate/dto/use.client-certificate.dto'; +import { SentinelMaster } from 'src/modules/redis-sentinel/models/sentinel-master'; +import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; -export class UpdateDatabaseDto extends PartialType(CreateDatabaseDto) { +export class UpdateDatabaseDto extends CreateDatabaseDto { @ValidateIf((object, value) => value !== undefined) @IsString({ always: true }) @MaxLength(500) @@ -17,4 +27,82 @@ export class UpdateDatabaseDto extends PartialType(CreateDatabaseDto) { @ValidateIf((object, value) => value !== undefined) @IsInt({ always: true }) port: number; + + @ApiPropertyOptional({ + description: 'Logical database number.', + type: Number, + }) + @IsInt() + @Min(0) + @IsOptional() + @Default(null) + db?: number; + + @ApiPropertyOptional({ + description: 'Use TLS to connect.', + type: Boolean, + }) + @IsBoolean() + @IsOptional() + @Default(false) + tls?: boolean; + + @ApiPropertyOptional({ + description: 'SNI servername', + type: String, + }) + @IsString() + @IsNotEmpty() + @IsOptional() + @Default(null) + tlsServername?: string; + + @ApiPropertyOptional({ + description: 'The certificate returned by the server needs to be verified.', + type: Boolean, + default: false, + }) + @IsOptional() + @IsBoolean({ always: true }) + @Default(false) + verifyServerCert?: boolean; + + @ApiPropertyOptional({ + description: 'CA Certificate', + oneOf: [ + { $ref: getSchemaPath(CreateCaCertificateDto) }, + { $ref: getSchemaPath(UseCaCertificateDto) }, + ], + }) + @IsOptional() + @IsNotEmptyObject() + @Type(caCertTransformer) + @ValidateNested() + @Default(null) + caCert?: CreateCaCertificateDto | UseCaCertificateDto; + + @ApiPropertyOptional({ + description: 'Client Certificate', + oneOf: [ + { $ref: getSchemaPath(CreateClientCertificateDto) }, + { $ref: getSchemaPath(UseCaCertificateDto) }, + ], + }) + @IsOptional() + @IsNotEmptyObject() + @Type(clientCertTransformer) + @ValidateNested() + @Default(null) + clientCert?: CreateClientCertificateDto | UseClientCertificateDto; + + @ApiPropertyOptional({ + description: 'Redis OSS Sentinel master group.', + type: SentinelMaster, + }) + @IsOptional() + @IsNotEmptyObject() + @Type(() => SentinelMaster) + @ValidateNested() + @Default(null) + sentinelMaster?: SentinelMaster; } diff --git a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts new file mode 100644 index 0000000000..2414aada9e --- /dev/null +++ b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts @@ -0,0 +1,774 @@ +import { + expect, + describe, + deps, + generateInvalidDataTestCases, + validateInvalidDataTestCase, + requirements, + getMainCheckFn, + _, it, validateApiCall, after +} from '../deps'; +import { Joi } from '../../helpers/test'; +import { databaseSchema } from './constants'; + +const { request, server, localDb, constants, rte } = deps; + +const endpoint = (id = constants.TEST_INSTANCE_ID) => request(server).patch(`/${constants.API.DATABASES}/${id}`); + +// input data schema +const dataSchema = Joi.object({ + name: Joi.string().max(500), + host: Joi.string(), + port: Joi.number().integer(), + db: Joi.number().integer().allow(null), + username: Joi.string().allow(null), + password: Joi.string().allow(null), + tls: Joi.boolean().allow(null), + tlsServername: Joi.string().allow(null), + verifyServerCert: Joi.boolean().allow(null), +}).messages({ + 'any.required': '{#label} should not be empty', +}).strict(true); + +const validInputData = { + name: constants.getRandomString(), + host: constants.getRandomString(), + port: 111, +}; + +const baseDatabaseData = { + name: 'someName', + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: constants.TEST_REDIS_USER || undefined, + password: constants.TEST_REDIS_PASSWORD || undefined, +} + +const responseSchema = databaseSchema.required().strict(true); + +const mainCheckFn = getMainCheckFn(endpoint); + +let oldDatabase; +let newDatabase; +describe(`PUT /databases/:id`, () => { + beforeEach(async () => await localDb.createDatabaseInstances()); + + describe('Validation', () => { + generateInvalidDataTestCases(dataSchema, validInputData).map( + validateInvalidDataTestCase(endpoint, dataSchema), + ); + + [ + { + name: 'should deprecate to pass both cert id and other cert fields', + data: { + ...validInputData, + caCert: { + id: 'id', + name: 'ca', + certificate: 'ca_certificate', + }, + clientCert: { + id: 'id', + name: 'client', + certificate: 'client_cert', + key: 'client_key', + }, + }, + statusCode: 400, + responseBody: { + statusCode: 400, + error: 'Bad Request', + }, + checkFn: ({ body }) => { + expect(body.message).to.contain('caCert.property name should not exist'); + expect(body.message).to.contain('caCert.property certificate should not exist'); + expect(body.message).to.contain('clientCert.property name should not exist'); + expect(body.message).to.contain('clientCert.property certificate should not exist'); + expect(body.message).to.contain('clientCert.property key should not exist'); + }, + }, + ].map(mainCheckFn); + }); + describe('Common', () => { + const newName = constants.getRandomString(); + + [ + { + name: 'Should change name (only) for existing database', + data: { + name: newName, + }, + responseSchema, + before: async () => { + oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(oldDatabase.name).to.not.eq(newName); + }, + responseBody: { + name: newName, + }, + after: async () => { + newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(newDatabase.name).to.eq(newName); + }, + }, + { + name: 'Should return 503 error if incorrect connection data provided', + data: { + name: 'new name', + port: 1111, + }, + statusCode: 503, + responseBody: { + statusCode: 503, + // message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`, + // todo: verify error handling because right now messages are different + error: 'Service Unavailable' + }, + after: async () => { + // check that instance wasn't changed + const newDb = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); + expect(newDb.name).to.not.eql('new name'); + expect(newDb.port).to.eql(constants.TEST_REDIS_PORT); + }, + }, + { + name: 'Should return Not Found Error', + endpoint: () => endpoint(constants.TEST_NOT_EXISTED_INSTANCE_ID), + data: { + name: 'new name', + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }, + statusCode: 404, + responseBody: { + statusCode: 404, + message: 'Invalid database instance id.', + error: 'Not Found' + }, + }, + ].map(mainCheckFn); + }); + describe('STANDALONE', () => { + requirements('rte.type=STANDALONE'); + describe('NO AUTH', function () { + requirements('!rte.tls', '!rte.pass'); + + [ + { + name: 'Should change host and port and recalculate data such as (provider, modules, etc...)', + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }, + responseSchema, + before: async () => { + oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); + expect(oldDatabase.name).to.eq(constants.TEST_INSTANCE_NAME_3); + expect(oldDatabase.modules).to.eq('[]'); + expect(oldDatabase.host).to.not.eq(constants.TEST_REDIS_HOST) + expect(oldDatabase.port).to.not.eq(constants.TEST_REDIS_PORT) + }, + responseBody: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: null, + password: null, + connectionType: constants.STANDALONE, + tls: false, + verifyServerCert: false, + tlsServername: null, + }, + after: async () => { + newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID_3); + expect(newDatabase).to.contain({ + ..._.omit(oldDatabase, ['modules', 'provider', 'lastConnection', 'new']), + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + }); + }, + }, + ].map(mainCheckFn); + + describe('Enterprise', () => { + requirements('rte.re'); + it('Should throw an error if db index specified', async () => { + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + db: constants.TEST_REDIS_DB_INDEX + }, + }); + }); + }); + describe('Oss', () => { + requirements('!rte.re'); + it('Update standalone with particular db index', async () => { + let addedId; + const dbName = constants.getRandomString(); + const cliUuid = constants.getRandomString(); + const browserKeyName = constants.getRandomString(); + const cliKeyName = constants.getRandomString(); + + await validateApiCall({ + endpoint, + data: { + db: constants.TEST_REDIS_DB_INDEX, + }, + responseSchema, + responseBody: { + db: constants.TEST_REDIS_DB_INDEX, + }, + checkFn: ({ body }) => { + addedId = body.id; + } + }); + + // Create string using Browser API to particular db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${addedId}/string`), + statusCode: 201, + data: { + keyName: browserKeyName, + value: 'somevalue' + }, + }); + + // Create client for CLI + await validateApiCall({ + endpoint: () => request(server).patch(`/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}`), + statusCode: 200, + }); + + // Create string using CLI API to 0 db index + await validateApiCall({ + endpoint: () => request(server).post(`/${constants.API.DATABASES}/${addedId}/cli/${cliUuid}/send-command`), + statusCode: 200, + data: { + command: `set ${cliKeyName} somevalue`, + }, + }); + + + // check data created by db index + await rte.data.executeCommand('select', `${constants.TEST_REDIS_DB_INDEX}`); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(1) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(1) + + // check data created by db index + await rte.data.executeCommand('select', '0'); + expect(await rte.data.executeCommand('exists', cliKeyName)).to.eql(0) + expect(await rte.data.executeCommand('exists', browserKeyName)).to.eql(0) + + // switch back to db index 0 + await validateApiCall({ + endpoint, + data: { + db: 0, + }, + responseSchema, + responseBody: { + db: 0, + }, + checkFn: ({ body }) => { + addedId = body.id; + } + }); + }); + }); + }); + describe('PASS', function () { + requirements('!rte.tls', 'rte.pass'); + it('Update standalone with password', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + password: constants.TEST_REDIS_PASSWORD, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + username: null, + password: constants.TEST_REDIS_PASSWORD, + connectionType: constants.STANDALONE, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + // todo: cover connection error for incorrect username/password + }); + describe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + it('update standalone instance using tls without CA verify', async () => { + const dbName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: false, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: false, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Update standalone instance using tls and verify and create CA certificate (new)', async () => { + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(localDb.encryptData(constants.TEST_REDIS_TLS_CA)); + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Should throw an error without CA cert when cert validation enabled', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + tls: true, + verifyServerCert: true, + caCert: null, + }, + responseBody: { + statusCode: 400, + // todo: verify error handling because right now messages are different + // message: 'Could not connect to', + error: 'Bad Request' + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + it('Should throw an error with invalid CA cert', async () => { + const dbName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + tls: true, + verifyServerCert: true, + caCert: { + name: dbName, + certificate: 'invalid' + }, + }, + responseBody: { + statusCode: 400, + // todo: verify error handling because right now messages are different + // message: 'Could not connect to', + error: 'Bad Request' + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + }); + describe('TLS AUTH', function () { + requirements('rte.tls', 'rte.tlsAuth'); + + let existingCACertId, existingClientCertId, existingCACertName, existingClientCertName; + + beforeEach(localDb.initAgreements); + after(localDb.initAgreements); + + // should be first test to not break other tests + it('Update standalone instance and verify users certs (new certificates !do not encrypt)', async () => { + await localDb.setAgreements({ + encryption: false, + }); + + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + const newClientCertName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.be.a('string'); + expect(body.clientCert.name).to.deep.eq(newClientCertName); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(constants.TEST_REDIS_TLS_CA); + + const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) + .findOneBy({ id: body.clientCert.id }); + + expect(clientPair.certificate).to.eql(constants.TEST_USER_TLS_CERT); + expect(clientPair.key).to.eql(constants.TEST_USER_TLS_KEY); + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Update standalone instance and verify users certs (new certificates)', async () => { + const dbName = constants.getRandomString(); + const newCaName = existingCACertName = constants.getRandomString(); + const newClientCertName = existingClientCertName = constants.getRandomString(); + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + const { body } = await validateApiCall({ + endpoint, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseSchema, + responseBody: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.id).to.be.a('string'); + expect(body.caCert.name).to.eq(newCaName); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.be.a('string'); + expect(body.clientCert.name).to.deep.eq(newClientCertName); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + + const ca: any = await (await localDb.getRepository(localDb.repositories.CA_CERT_REPOSITORY)) + .findOneBy({ id: body.caCert.id }); + + expect(ca.certificate).to.eql(localDb.encryptData(constants.TEST_REDIS_TLS_CA)); + + const clientPair: any = await (await localDb.getRepository(localDb.repositories.CLIENT_CERT_REPOSITORY)) + .findOneBy({ id: body.clientCert.id }); + + expect(clientPair.certificate).to.eql(localDb.encryptData(constants.TEST_USER_TLS_CERT)); + expect(clientPair.key).to.eql(localDb.encryptData(constants.TEST_USER_TLS_KEY)); + }, + }); + + // remember certificates ids + existingCACertId = body.caCert.id; + existingClientCertId = body.clientCert.id; + + expect(await localDb.getInstanceByName(dbName)).to.be.an('object'); + }); + it('Should update standalone instance with existing certificates', async () => { + const dbName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + data: { + tls: true, + verifyServerCert: true, + caCert: { + id: existingCACertId, + }, + clientCert: { + id: existingClientCertId, + }, + }, + responseSchema, + responseBody: { + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + connectionType: constants.STANDALONE, + tls: true, + verifyServerCert: true, + tlsServername: null, + }, + checkFn: async ({ body }) => { + expect(body.caCert.name).to.be.a('string'); + expect(body.caCert.id).to.eq(existingCACertId); + expect(body.caCert.certificate).to.be.undefined; + + expect(body.clientCert.id).to.deep.eq(existingClientCertId); + expect(body.clientCert.name).to.be.a('string'); + expect(body.clientCert.certificate).to.be.undefined; + expect(body.clientCert.key).to.be.undefined; + }, + }); + }); + it('Should throw an error if try to create client certificate with existing name', async () => { + const dbName = constants.getRandomString(); + const newCaName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: newCaName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: existingClientCertName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseBody: { + error: 'Bad Request', + message: 'This client certificate name is already in use.', + statusCode: 400, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + it('Should throw an error if try to create ca certificate with existing name', async () => { + const dbName = constants.getRandomString(); + const newClientName = constants.getRandomString(); + + // preconditions + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + + await validateApiCall({ + endpoint, + statusCode: 400, + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: existingCACertName, + certificate: constants.TEST_REDIS_TLS_CA, + }, + clientCert: { + name: newClientName, + certificate: constants.TEST_USER_TLS_CERT, + key: constants.TEST_USER_TLS_KEY, + }, + }, + responseBody: { + error: 'Bad Request', + message: 'This ca certificate name is already in use.', + statusCode: 400, + }, + }); + + expect(await localDb.getInstanceByName(dbName)).to.eql(null); + }); + }); + }); + describe('CLUSTER', () => { + requirements('rte.type=CLUSTER'); + describe('NO AUTH', function () { + requirements('!rte.tls', '!rte.pass'); + it('Update instance without pass', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + }, + responseSchema, + responseBody: { + name: dbName, + port: constants.TEST_REDIS_PORT, + connectionType: constants.CLUSTER, + nodes: rte.env.nodes, + }, + }); + }); + it('Should throw an error if db index specified', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + statusCode: 400, + data: { + name: dbName, + host: constants.TEST_REDIS_HOST, + port: constants.TEST_REDIS_PORT, + db: constants.TEST_REDIS_DB_INDEX + }, + }); + }); + }); + describe('TLS CA', function () { + requirements('rte.tls', '!rte.tlsAuth'); + it('Should create instance without CA tls', async () => { + const dbName = constants.getRandomString(); + + await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: false, + }, + responseSchema, + responseBody: { + name: dbName, + connectionType: constants.CLUSTER, + tls: true, + nodes: rte.env.nodes, + verifyServerCert: false, + }, + }); + }); + it('Should create instance tls and create new CA cert', async () => { + const dbName = constants.getRandomString(); + + const { body } = await validateApiCall({ + endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), + data: { + ...baseDatabaseData, + name: dbName, + tls: true, + verifyServerCert: true, + caCert: { + name: constants.getRandomString(), + certificate: constants.TEST_REDIS_TLS_CA, + }, + }, + responseSchema, + responseBody: { + name: dbName, + port: constants.TEST_REDIS_PORT, + connectionType: constants.CLUSTER, + nodes: rte.env.nodes, + tls: true, + verifyServerCert: true, + }, + }); + }); + // todo: Should throw an error without CA cert when cert validation enabled + // todo: Should throw an error with invalid CA cert + }); + }); +}); diff --git a/redisinsight/ui/src/slices/instances/instances.ts b/redisinsight/ui/src/slices/instances/instances.ts index 7fc52c2789..36f0bf7d9e 100644 --- a/redisinsight/ui/src/slices/instances/instances.ts +++ b/redisinsight/ui/src/slices/instances/instances.ts @@ -486,7 +486,7 @@ export function changeInstanceAliasAction( const { CancelToken } = axios sourceInstance = CancelToken.source() - const { status } = await apiService.put( + const { status } = await apiService.patch( `${ApiEndpoints.DATABASES}/${id}`, { name }, { cancelToken: sourceInstance.token } From c7e5106f32e6893f5523125bd8f546b229b1aff6 Mon Sep 17 00:00:00 2001 From: Artem Date: Tue, 20 Dec 2022 21:45:36 +0200 Subject: [PATCH 105/107] fix tests --- .../api/database/PUT-databases-id.test.ts | 20 ------------------- redisinsight/api/test/helpers/local-db.ts | 3 ++- .../slices/tests/instances/instances.spec.ts | 4 ++-- 3 files changed, 4 insertions(+), 23 deletions(-) diff --git a/redisinsight/api/test/api/database/PUT-databases-id.test.ts b/redisinsight/api/test/api/database/PUT-databases-id.test.ts index 95a09152cf..d53231eafd 100644 --- a/redisinsight/api/test/api/database/PUT-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PUT-databases-id.test.ts @@ -91,27 +91,7 @@ describe(`PUT /databases/:id`, () => { ].map(mainCheckFn); }); describe('Common', () => { - const newName = constants.getRandomString(); - [ - { - name: 'Should change name (only) for existing database', - data: { - name: newName, - }, - responseSchema, - before: async () => { - oldDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); - expect(oldDatabase.name).to.not.eq(newName); - }, - responseBody: { - name: newName, - }, - after: async () => { - newDatabase = await localDb.getInstanceById(constants.TEST_INSTANCE_ID); - expect(newDatabase.name).to.eq(newName); - }, - }, { name: 'Should return 503 error if incorrect connection data provided', data: { diff --git a/redisinsight/api/test/helpers/local-db.ts b/redisinsight/api/test/helpers/local-db.ts index 0a7ae6b936..c7c24bddfe 100644 --- a/redisinsight/api/test/helpers/local-db.ts +++ b/redisinsight/api/test/helpers/local-db.ts @@ -292,7 +292,8 @@ export const createDatabaseInstances = async () => { host: 'localhost', port: 3679, connectionType: 'STANDALONE', - ...instance + ...instance, + modules: '[]', }); } } diff --git a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts index 24a5cff3dc..769c3d1f36 100644 --- a/redisinsight/ui/src/slices/tests/instances/instances.spec.ts +++ b/redisinsight/ui/src/slices/tests/instances/instances.spec.ts @@ -1023,7 +1023,7 @@ describe('instances slice', () => { newName: 'newAlias', } const responsePayload = { status: 200, data } - apiService.put = jest.fn().mockResolvedValue(responsePayload) + apiService.patch = jest.fn().mockResolvedValue(responsePayload) // Act await store.dispatch( @@ -1047,7 +1047,7 @@ describe('instances slice', () => { data: { message: errorMessage }, }, } - apiService.put = jest.fn().mockRejectedValue(responsePayload) + apiService.patch = jest.fn().mockRejectedValue(responsePayload) // Act await store.dispatch( From 12eb753bef8329d8d23bc7d8c29d8e504ab6fa87 Mon Sep 17 00:00:00 2001 From: nmammadli Date: Wed, 21 Dec 2022 07:23:21 +0100 Subject: [PATCH 106/107] Add 'flushdb', change from big standalone to standalone in order to prevent flakiness --- .../tree-view/tree-view-improvements.e2e.ts | 68 +++++++++++-------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index fe3b036161..1afc20f00b 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -5,7 +5,6 @@ import { } from '../../../pageObjects'; import { commonUrl, - ossStandaloneBigConfig, ossStandaloneConfig } from '../../../helpers/conf'; import { KeyTypesTexts, rte } from '../../../helpers/constants'; @@ -19,27 +18,28 @@ const cliPage = new CliPage(); let keyNames: string[]; let keyName1: string; let keyName2: string; -let keynameSingle: string; +let keyNameSingle: string; let index: string; -fixture`Tree view navigations improvement tests` +fixture.only`Tree view navigations improvement tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl); test - .before(async () => { + .before(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .after(async () => { + .after(async() => { await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Tree view preselected folder', async t => { keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name - keynameSingle = common.generateWord(10); - keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; + keyNameSingle = common.generateWord(10); + keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; const commands = [ + 'flushdb', `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `HSET ${keyNames[2]} field value`, @@ -51,16 +51,16 @@ test await cliPage.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keynameSingle]); + await verifyKeysDisplayedInTheList([keyNameSingle]); await browserPage.openTreeFolders([await browserPage.getTextFromNthTreeElement(1)]); await browserPage.selectFilterGroupType(KeyTypesTexts.Set); // The folder without any namespaces is selected (if exists) when folder does not exist after search/filter - await verifyKeysDisplayedInTheList([keynameSingle]); + await verifyKeysDisplayedInTheList([keyNameSingle]); await t.click(browserPage.setDeleteButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keynameSingle]); + await verifyKeysDisplayedInTheList([keyNameSingle]); await verifyKeysNotDisplayedInTheList([`${keyNames[0]}:1`, `${keyNames[2]}:2`]); // switch between browser view and tree view @@ -82,32 +82,32 @@ test await cliPage.sendCommandsInCli(commands1); await t.click(browserPage.refreshKeysButton); - // Refreshed Tree view preselected folder + // Refreshed Tree view preselected folder await t.expect(firstTreeItemKeys.visible) .ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.selectFilterGroupType(KeyTypesTexts.Hash); await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected after searching with HASH'); - // Filterred Tree view preselected folder + // Filtered Tree view preselected folder await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.searchByKeyName('*'); - // Search capability Filterred Tree view preselected folder + // Search capability Filtered Tree view preselected folder await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await t.click(browserPage.clearFilterButton); - // Filterred Tree view preselected folder + // Filtered Tree view preselected folder await t.expect(firstTreeItemKeys.visible).ok('Folder is not selected'); await verifyKeysDisplayedInTheList([`${firstTreeItemText}:1`, `${firstTreeItemText}:2`]); await browserPage.selectFilterGroupType(KeyTypesTexts.Stream); - // Filterred Tree view preselected folder + // Filtered Tree view preselected folder await t.expect(browserPage.keyListTable.textContent).contains('No results found.', 'Key is not found message not displayed'); await t.click(browserPage.streamDeleteButton); // clear stream from filter - // Filterred Tree view preselected folder + // Filtered Tree view preselected folder await t.expect(browserPage.keyListTable.textContent).notContains('No results found.', 'Key is not found message still displayed'); await t.expect( firstTreeItemKeys.visible) @@ -115,15 +115,28 @@ test }); test - .before(async () => { - await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneBigConfig, ossStandaloneBigConfig.databaseName); + .before(async() => { + await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .after(async () => { + .after(async() => { await cliPage.sendCommandInCli(`FT.DROPINDEX ${index}`); - await deleteStandaloneDatabaseApi(ossStandaloneBigConfig); + await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Verify tree view navigation for index based search', async t => { + keyName1 = common.generateWord(10); // used to create index name + keyName2 = common.generateWord(10); // used to create index name + const subFolder1 = common.generateWord(10); // used to create index name + keyNames = [`${keyName1}:${subFolder1}:1`, `${keyName1}:${subFolder1}:2`, `${keyName2}:1:1`, `${keyName2}:1:2`]; + const commands = [ + 'flushdb', + `HSET ${keyNames[0]} field value`, + `HSET ${keyNames[1]} field value`, + `HSET ${keyNames[2]} field value`, + `HSET ${keyNames[3]} field value` + ]; + await cliPage.sendCommandsInCli(commands); + // generate index based on keyName - const folders = ['mobile', '2014']; + const folders = [keyName1, subFolder1]; index = await cliPage.createIndexwithCLI(folders.join(':')); await t.click(browserPage.redisearchModeBtn); // click redisearch button await browserPage.selectIndexByName(index); @@ -138,19 +151,20 @@ test }); test - .before(async () => { + .before(async() => { await acceptLicenseTermsAndAddDatabaseApi(ossStandaloneConfig, ossStandaloneConfig.databaseName); }) - .after(async () => { + .after(async() => { await t.click(browserPage.patternModeBtn); await browserPage.deleteKeysByNames(keyNames.slice(1)); await deleteStandaloneDatabaseApi(ossStandaloneConfig); })('Search capability Refreshed Tree view preselected folder', async t => { keyName1 = common.generateWord(10); // used to create index name keyName2 = common.generateWord(10); // used to create index name - keynameSingle = common.generateWord(10); - keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keynameSingle]; + keyNameSingle = common.generateWord(10); + keyNames = [`${keyName1}:1`, `${keyName1}:2`, `${keyName2}:1`, `${keyName2}:2`, keyNameSingle]; const commands = [ + 'flushdb', `HSET ${keyNames[0]} field value`, `HSET ${keyNames[1]} field value`, `RPUSH ${keyNames[2]} field`, @@ -160,7 +174,7 @@ test await cliPage.sendCommandsInCli(commands); await t.click(browserPage.treeViewButton); // The folder without any patterns selected and the list of keys is displayed when there is a folder without any patterns - await verifyKeysDisplayedInTheList([keynameSingle]); + await verifyKeysDisplayedInTheList([keyNameSingle]); await browserPage.openTreeFolders([keyName1]); // Type: hash await browserPage.openTreeFolders([keyName2]); // Type: list @@ -171,7 +185,7 @@ test await t.click(browserPage.hashDeleteButton); await cliPage.sendCommandsInCli([`DEL ${keyNames[0]}`]); await t.click(browserPage.refreshKeysButton); // refresh keys - // The previously selected folder is preselected when key does not exist after keys refresh + // The previously selected folder is preselected when key does not exist after keys refresh await verifyKeysDisplayedInTheList([keyNames[1]]); await verifyKeysNotDisplayedInTheList([keyNames[0], keyNames[2], keyNames[3], keyNames[4]]); From b88f0bbeb9e86a41871344a8a483ea0b728484dd Mon Sep 17 00:00:00 2001 From: nmammadli Date: Wed, 21 Dec 2022 07:53:35 +0100 Subject: [PATCH 107/107] delete .only --- .../tests/critical-path/tree-view/tree-view-improvements.e2e.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts index 1afc20f00b..63b2415041 100644 --- a/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts +++ b/tests/e2e/tests/critical-path/tree-view/tree-view-improvements.e2e.ts @@ -21,7 +21,7 @@ let keyName2: string; let keyNameSingle: string; let index: string; -fixture.only`Tree view navigations improvement tests` +fixture`Tree view navigations improvement tests` .meta({ type: 'critical_path', rte: rte.standalone }) .page(commonUrl); test