From b9e8265c258b80a12f98f43da86b78b039045b24 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Sat, 24 May 2025 19:12:32 +0300 Subject: [PATCH 1/4] RI-7101 introduce redis connection errors and single handling mechanism --- redisinsight/api/package.json | 6 +- .../api/src/constants/custom-error-codes.ts | 12 +- .../api/src/constants/error-messages.ts | 3 +- .../src/modules/browser/keys/keys.service.ts | 1 + .../modules/database/database.service.spec.ts | 12 +- .../src/modules/database/database.service.ts | 23 ++- .../providers/database.client.factory.spec.ts | 15 +- .../providers/database.client.factory.ts | 11 +- .../database/providers/database.factory.ts | 8 +- .../redis-sentinel.service.spec.ts | 9 +- .../redis-sentinel/redis-sentinel.service.ts | 11 +- .../ioredis.redis.connection.strategy.ts | 21 ++- .../connection/redis.connection.strategy.ts | 38 ++++- .../redis/exceptions/connection/index.ts | 8 + ...s-connection-auth-unsupported.exception.ts | 21 +++ ...ion-cluster-nodes-unavailable.exception.ts | 21 +++ .../redis-connection-failed.exception.ts | 31 ++++ ...nection-incorrect-certificate.exception.ts | 21 +++ ...tion-sentinel-master-required.exception.ts | 21 +++ .../redis-connection-timeout.exception.ts | 21 +++ ...redis-connection-unauthorized.exception.ts | 21 +++ .../redis-connection-unavailable.exception.ts | 21 +++ .../api/src/utils/catch-redis-errors.spec.ts | 142 +++++++++++++++++ .../api/src/utils/catch-redis-errors.ts | 61 ++++---- .../api/database/PATCH-databases-id.test.ts | 14 +- .../database/POST-databases-clone-id.test.ts | 10 +- .../database/POST-databases-test-id.test.ts | 10 +- .../GET-databases-id-plugins-commands.test.ts | 7 +- redisinsight/api/yarn.lock | 143 ++++++------------ redisinsight/ui/src/services/apiService.ts | 5 +- 30 files changed, 533 insertions(+), 215 deletions(-) create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/index.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-auth-unsupported.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-cluster-nodes-unavailable.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-incorrect-certificate.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-sentinel-master-required.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-timeout.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unauthorized.exception.ts create mode 100644 redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unavailable.exception.ts create mode 100644 redisinsight/api/src/utils/catch-redis-errors.spec.ts diff --git a/redisinsight/api/package.json b/redisinsight/api/package.json index 167800402a..fd26838d51 100644 --- a/redisinsight/api/package.json +++ b/redisinsight/api/package.json @@ -132,8 +132,8 @@ "jest-junit": "^16.0.0", "jest-when": "^3.2.1", "joi": "^17.4.0", - "mocha": "^11.1.0", - "mocha-junit-reporter": "^2.0.0", + "mocha": "^11.4.0", + "mocha-junit-reporter": "^2.2.1", "mocha-multi-reporters": "^1.5.1", "nock": "^13.3.0", "nyc": "^15.1.0", @@ -143,7 +143,7 @@ "supertest": "^4.0.2", "ts-jest": "^29.2.5", "ts-loader": "^6.2.1", - "ts-mocha": "^8.0.0", + "ts-mocha": "^11.1.0", "ts-node": "^10.9.2", "tsconfig-paths": "^3.9.0", "tsconfig-paths-webpack-plugin": "^3.3.0", diff --git a/redisinsight/api/src/constants/custom-error-codes.ts b/redisinsight/api/src/constants/custom-error-codes.ts index b8a85c3e27..29c96930f2 100644 --- a/redisinsight/api/src/constants/custom-error-codes.ts +++ b/redisinsight/api/src/constants/custom-error-codes.ts @@ -1,7 +1,17 @@ export enum CustomErrorCodes { - // General [10000, 10999] + // General [10000, 10899] WindowUnauthorized = 10_001, + // Redis Connection [10900, 10999] + RedisConnectionFailed = 10_900, + RedisConnectionTimeout = 10_901, + RedisConnectionUnauthorized = 10_902, + RedisConnectionClusterNodesUnavailable = 10_903, + RedisConnectionUnavailable = 10_904, + RedisConnectionAuthUnsupported = 10_905, + RedisConnectionSentinelMasterRequired = 10_906, + RedisConnectionIncorrectCertificate = 10_907, + // Cloud API [11001, 11099] CloudApiInternalServerError = 11_000, CloudApiUnauthorized = 11_001, diff --git a/redisinsight/api/src/constants/error-messages.ts b/redisinsight/api/src/constants/error-messages.ts index 98b16d7473..e333e034b4 100644 --- a/redisinsight/api/src/constants/error-messages.ts +++ b/redisinsight/api/src/constants/error-messages.ts @@ -108,8 +108,9 @@ export default { UNDEFINED_WINDOW_ID: 'Undefined window id.', LIBRARY_NOT_EXIST: 'This library does not exist.', - CLOUD_CAPI_KEY_UNAUTHORIZED: 'Unable to authorize such CAPI key', + REDIS_CONNECTION_FAILED: 'Unable to connect to the Redis database', + CLOUD_CAPI_KEY_UNAUTHORIZED: 'Unable to authorize such CAPI key', CLOUD_OAUTH_CANCELED: 'Authorization request was canceled.', CLOUD_OAUTH_MISCONFIGURATION: 'Authorization server misconfiguration.', CLOUD_OAUTH_GITHUB_EMAIL_PERMISSION: diff --git a/redisinsight/api/src/modules/browser/keys/keys.service.ts b/redisinsight/api/src/modules/browser/keys/keys.service.ts index 13ba2dd454..c629561083 100644 --- a/redisinsight/api/src/modules/browser/keys/keys.service.ts +++ b/redisinsight/api/src/modules/browser/keys/keys.service.ts @@ -80,6 +80,7 @@ export class KeysService { plainToInstance(GetKeysWithDetailsResponse, nodeResult), ); } catch (error) { + console.log('___ERROR', error); this.logger.error( `Failed to get keys with details info. ${error.message}.`, clientMetadata, diff --git a/redisinsight/api/src/modules/database/database.service.spec.ts b/redisinsight/api/src/modules/database/database.service.spec.ts index d152b42a73..44cd9ec8d7 100644 --- a/redisinsight/api/src/modules/database/database.service.spec.ts +++ b/redisinsight/api/src/modules/database/database.service.spec.ts @@ -1,7 +1,6 @@ import { InternalServerErrorException, NotFoundException, - ServiceUnavailableException, } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -33,11 +32,14 @@ import { DatabaseRepository } from 'src/modules/database/repositories/database.r import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; import { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto'; -import { RedisErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { Compressor } from 'src/modules/database/entities/database.entity'; import { RedisClientFactory } from 'src/modules/redis/redis.client.factory'; import { RedisClientStorage } from 'src/modules/redis/redis.client.storage'; +import { + RedisConnectionSentinelMasterRequiredException, + RedisConnectionUnavailableException, +} from 'src/modules/redis/exceptions/connection'; import { ExportDatabase } from './models/export-database'; const updateDatabaseTests = [ @@ -347,7 +349,7 @@ describe('DatabaseService', () => { }); it('should successfully test valid sentinel config (without sentinelMaster)', async () => { databaseFactory.createDatabaseModel.mockRejectedValueOnce( - new Error(RedisErrorCodes.SentinelParamsRequired), + new RedisConnectionSentinelMasterRequiredException(), ); expect( await service.testConnection(mockSessionMetadata, mockDatabase), @@ -355,12 +357,12 @@ describe('DatabaseService', () => { }); it('should throw connection error', async () => { databaseFactory.createDatabaseModel.mockRejectedValueOnce( - new Error(RedisErrorCodes.ConnectionRefused), + new RedisConnectionUnavailableException(), ); await expect( service.testConnection(mockSessionMetadata, mockDatabase), - ).rejects.toThrow(ServiceUnavailableException); + ).rejects.toThrow(RedisConnectionUnavailableException); }); it('should not call get database by id', async () => { const spy = jest.spyOn(service as any, 'get'); diff --git a/redisinsight/api/src/modules/database/database.service.ts b/redisinsight/api/src/modules/database/database.service.ts index 28ff6782dc..26c4b89235 100644 --- a/redisinsight/api/src/modules/database/database.service.ts +++ b/redisinsight/api/src/modules/database/database.service.ts @@ -9,16 +9,12 @@ import { Database } from 'src/modules/database/models/database'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; import { DatabaseAnalytics } from 'src/modules/database/database.analytics'; -import { - catchRedisConnectionError, - classToClass, - getRedisConnectionException, -} from 'src/utils'; +import { classToClass } from 'src/utils'; import { CreateDatabaseDto } from 'src/modules/database/dto/create.database.dto'; import { DatabaseInfoProvider } from 'src/modules/database/providers/database-info.provider'; import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; import { UpdateDatabaseDto } from 'src/modules/database/dto/update.database.dto'; -import { AppRedisInstanceEvents, RedisErrorCodes } from 'src/constants'; +import { AppRedisInstanceEvents } from 'src/constants'; import { EventEmitter2 } from '@nestjs/event-emitter'; import { DeleteDatabasesResponse } from 'src/modules/database/dto/delete.databases.response'; import { ClientContext, SessionMetadata } from 'src/common/models'; @@ -31,6 +27,7 @@ import { RedisClientFactory, } from 'src/modules/redis/redis.client.factory'; import { RedisClientStorage } from 'src/modules/redis/redis.client.storage'; +import { RedisConnectionSentinelMasterRequiredException } from 'src/modules/redis/exceptions/connection'; @Injectable() export class DatabaseService { @@ -221,11 +218,9 @@ export class DatabaseService { } catch (error) { this.logger.error('Failed to add database.', error, sessionMetadata); - const exception = getRedisConnectionException(error, dto); - - this.analytics.sendInstanceAddFailedEvent(sessionMetadata, exception); + this.analytics.sendInstanceAddFailedEvent(sessionMetadata, error); - throw exception; + throw error; } } @@ -279,7 +274,8 @@ export class DatabaseService { error, sessionMetadata, ); - throw catchRedisConnectionError(error, database); + + throw error; } } @@ -317,12 +313,13 @@ export class DatabaseService { return; } catch (error) { // don't throw an error to support sentinel autodiscovery flow - if (error.message === RedisErrorCodes.SentinelParamsRequired) { + if (error instanceof RedisConnectionSentinelMasterRequiredException) { return; } this.logger.error('Connection test failed', error, sessionMetadata); - throw catchRedisConnectionError(error, database); + + throw error; } } diff --git a/redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts b/redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts index e131fe4744..2bd7b99c94 100644 --- a/redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts +++ b/redisinsight/api/src/modules/database/providers/database.client.factory.spec.ts @@ -1,4 +1,3 @@ -import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { mockCommonClientMetadata, @@ -6,7 +5,6 @@ import { mockDatabaseAnalytics, mockDatabaseRepository, mockDatabaseService, - mockRedisNoAuthError, MockType, mockRedisClientFactory, mockStandaloneRedisClient, @@ -16,7 +14,6 @@ import { import { DatabaseAnalytics } from 'src/modules/database/database.analytics'; import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseRepository } from 'src/modules/database/repositories/database.repository'; -import ERROR_MESSAGES from 'src/constants/error-messages'; import { DatabaseClientFactory } from 'src/modules/database/providers/database.client.factory'; import { RedisClientStorage } from 'src/modules/redis/redis.client.storage'; import { RedisClientFactory } from 'src/modules/redis/redis.client.factory'; @@ -31,6 +28,10 @@ import { } from 'src/__mocks__/redis-client'; import { RedisClient } from 'src/modules/redis/client'; import { ConnectionType } from 'src/modules/database/entities/database.entity'; +import { + RedisConnectionTimeoutException, + RedisConnectionUnauthorizedException, +} from 'src/modules/redis/exceptions/connection'; describe('DatabaseClientFactory', () => { let service: DatabaseClientFactory; @@ -246,17 +247,17 @@ describe('DatabaseClientFactory', () => { }, ); }); - it('should throw Unauthorized error in case of NOAUTH', async () => { + it('should throw original error', async () => { jest .spyOn(redisClientFactory, 'createClient') - .mockRejectedValue(mockRedisNoAuthError); + .mockRejectedValue(new RedisConnectionTimeoutException()); await expect( service.createClient(mockCommonClientMetadata), - ).rejects.toThrow(UnauthorizedException); + ).rejects.toThrow(RedisConnectionTimeoutException); expect(analytics.sendConnectionFailedEvent).toHaveBeenCalledWith( mockSessionMetadata, mockDatabase, - new UnauthorizedException(ERROR_MESSAGES.AUTHENTICATION_FAILED()), + new RedisConnectionTimeoutException(), ); }); }); diff --git a/redisinsight/api/src/modules/database/providers/database.client.factory.ts b/redisinsight/api/src/modules/database/providers/database.client.factory.ts index 3b9cc6cd71..eb75aa1316 100644 --- a/redisinsight/api/src/modules/database/providers/database.client.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.client.factory.ts @@ -155,17 +155,14 @@ export class DatabaseClientFactory { return client; } catch (error) { this.logger.error('Failed to create database client', error); - const exception = getRedisConnectionException( - error, - database, - database.name, - ); + this.analytics.sendConnectionFailedEvent( clientMetadata.sessionMetadata, database, - exception, + error, ); - throw exception; + + throw error; } } diff --git a/redisinsight/api/src/modules/database/providers/database.factory.ts b/redisinsight/api/src/modules/database/providers/database.factory.ts index 5c938af2b8..e6b6740933 100644 --- a/redisinsight/api/src/modules/database/providers/database.factory.ts +++ b/redisinsight/api/src/modules/database/providers/database.factory.ts @@ -4,7 +4,7 @@ import { ConnectionType, HostingProvider, } from 'src/modules/database/entities/database.entity'; -import { catchRedisConnectionError, getHostingProvider } from 'src/utils'; +import { getHostingProvider } from 'src/utils'; import { Database } from 'src/modules/database/models/database'; import { ClientContext, SessionMetadata } from 'src/common/models'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -156,7 +156,8 @@ export class DatabaseFactory { return model; } catch (error) { this.logger.error('Failed to add oss cluster.', error, sessionMetadata); - throw catchRedisConnectionError(error, database); + + throw error; } } @@ -209,7 +210,8 @@ export class DatabaseFactory { error, sessionMetadata, ); - throw catchRedisConnectionError(error, database); + + throw error; } } } 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 6d38541213..4f77b17152 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 @@ -9,8 +9,6 @@ jest.doMock( ); import { Test, TestingModule } from '@nestjs/testing'; -import { BadRequestException } from '@nestjs/common'; -import ERROR_MESSAGES from 'src/constants/error-messages'; import { mockConstantsProvider, mockDatabaseFactory, @@ -30,6 +28,9 @@ import { DatabaseService } from 'src/modules/database/database.service'; import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; import { RedisClientFactory } from 'src/modules/redis/redis.client.factory'; import { ConstantsProvider } from 'src/modules/constants/providers/constants.provider'; +import { + RedisConnectionIncorrectCertificateException, +} from 'src/modules/redis/exceptions/connection'; describe('RedisSentinelService', () => { let service: RedisSentinelService; @@ -89,7 +90,7 @@ describe('RedisSentinelService', () => { redisClientFactory .getConnectionStrategy() .createStandaloneClient.mockRejectedValue( - new Error(ERROR_MESSAGES.NO_CONNECTION_TO_REDIS_DB), + new RedisConnectionIncorrectCertificateException(), ); await expect( @@ -97,7 +98,7 @@ describe('RedisSentinelService', () => { mockSessionMetadata, mockSentinelDatabaseWithTlsAuth, ), - ).rejects.toThrow(BadRequestException); + ).rejects.toThrow(RedisConnectionIncorrectCertificateException); }); }); }); 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 3563b659e0..0628526a9d 100644 --- a/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts +++ b/redisinsight/api/src/modules/redis-sentinel/redis-sentinel.service.ts @@ -1,4 +1,4 @@ -import { HttpException, Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { v4 as uuidv4 } from 'uuid'; import { CreateSentinelDatabaseResponse } from 'src/modules/redis-sentinel/dto/create.sentinel.database.response'; import { CreateSentinelDatabasesDto } from 'src/modules/redis-sentinel/dto/create.sentinel.databases.dto'; @@ -9,7 +9,6 @@ import { SessionMetadata, } 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'; import { RedisSentinelAnalytics } from 'src/modules/redis-sentinel/redis-sentinel.analytics'; import { DatabaseFactory } from 'src/modules/database/providers/database.factory'; @@ -113,7 +112,7 @@ export class RedisSentinelService { error, sessionMetadata, ); - throw getRedisConnectionException(error, connectionOptions); + throw error; } } @@ -153,12 +152,12 @@ export class RedisSentinelService { await client.disconnect(); } catch (error) { - const exception: HttpException = getRedisConnectionException(error, dto); this.redisSentinelAnalytics.sendGetSentinelMastersFailedEvent( sessionMetadata, - exception, + error, ); - throw exception; + + throw error; } return result; } diff --git a/redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.ts b/redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.ts index 1622860b00..5e5b6d631d 100644 --- a/redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.ts +++ b/redisinsight/api/src/modules/redis/connection/ioredis.redis.connection.strategy.ts @@ -17,6 +17,8 @@ import { } from 'src/modules/redis/client'; import { discoverClusterNodes } from 'src/modules/redis/utils'; import { SshTunnel } from 'src/modules/ssh/models/ssh-tunnel'; +import { getRedisConnectionException } from 'src/utils'; +import { ReplyError } from 'src/models'; const REDIS_CLIENTS_CONFIG = serverConfig.get('redis_clients'); @@ -203,13 +205,13 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { // cover cases when we are connecting to sentinel as to standalone to discover master groups db: config.db > 0 && !database.sentinelMaster ? config.db : 0, }); - connection.on('error', (e): void => { + connection.on('error', (e: ReplyError): void => { this.logger.error( 'Failed connection to the redis database.', e, clientMetadata, ); - reject(e); + reject(getRedisConnectionException(e, database)); }); connection.on('end', (): void => { this.logger.warn( @@ -245,6 +247,8 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { } }); } catch (e) { + this.addConnectionError(e); + tnl?.close?.(); throw e; } @@ -317,7 +321,10 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { e, clientMetadata, ); - reject(!isEmpty(e.lastNodeError) ? e.lastNodeError : e); + reject(getRedisConnectionException( + !isEmpty(e.lastNodeError) ? e.lastNodeError : e as ReplyError, + database, + )); }); cluster.on('end', (): void => { this.logger.warn( @@ -347,6 +354,8 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { } }); } catch (e) { + this.addConnectionError(e); + tnls.forEach((tnl) => tnl?.close?.()); standaloneClient?.disconnect?.().catch(); throw e; @@ -373,13 +382,13 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { return new Promise((resolve, reject) => { try { const client = new Redis(config); - client.on('error', (e): void => { + client.on('error', (e: ReplyError): void => { this.logger.error( 'Failed connection to the redis oss sentinel', e, clientMetadata, ); - reject(e); + reject(getRedisConnectionException(e, database)); }); client.on('end', (): void => { this.logger.error( @@ -405,6 +414,8 @@ export class IoredisRedisConnectionStrategy extends RedisConnectionStrategy { ); }); } catch (e) { + this.addConnectionError(e); + reject(e); } }); diff --git a/redisinsight/api/src/modules/redis/connection/redis.connection.strategy.ts b/redisinsight/api/src/modules/redis/connection/redis.connection.strategy.ts index fd9ee6d6fc..7a403684da 100644 --- a/redisinsight/api/src/modules/redis/connection/redis.connection.strategy.ts +++ b/redisinsight/api/src/modules/redis/connection/redis.connection.strategy.ts @@ -1,16 +1,20 @@ import { ClientMetadata } from 'src/common/models'; import { Database } from 'src/modules/database/models/database'; import { SshTunnelProvider } from 'src/modules/ssh/ssh-tunnel.provider'; -import { Injectable, Logger } from '@nestjs/common'; +import { HttpException, Injectable, Logger } from '@nestjs/common'; import { IRedisConnectionOptions } from 'src/modules/redis/redis.client.factory'; import { RedisClient } from 'src/modules/redis/client'; -import { CONNECTION_NAME_GLOBAL_PREFIX } from 'src/constants'; +import { CONNECTION_NAME_GLOBAL_PREFIX, CustomErrorCodes } from 'src/constants'; @Injectable() export abstract class RedisConnectionStrategy { protected logger = new Logger(this.constructor.name); - constructor(protected readonly sshTunnelProvider: SshTunnelProvider) {} + protected connectionErrors: {}; + + constructor(protected readonly sshTunnelProvider: SshTunnelProvider) { + this.resetConnectionErrors(); + } /** * Try to create standalone redis connection @@ -70,4 +74,32 @@ export abstract class RedisConnectionStrategy { .join('-') .toLowerCase(); } + + private resetConnectionErrors() { + this.connectionErrors = { + [CustomErrorCodes.RedisConnectionFailed]: 0, + [CustomErrorCodes.RedisConnectionTimeout]: 0, + [CustomErrorCodes.RedisConnectionUnauthorized]: 0, + [CustomErrorCodes.RedisConnectionClusterNodesUnavailable]: 0, + [CustomErrorCodes.RedisConnectionUnavailable]: 0, + }; + } + + protected addConnectionError(error: HttpException) { + const errorCode = error.getResponse?.()?.['errorCode']; + + if (this.connectionErrors[errorCode] !== undefined) { + this.connectionErrors[errorCode] += 1; + } + } + + public getConnectionErrorsAndReset() { + const connectionErrors = { + ...this.connectionErrors, + }; + + this.resetConnectionErrors(); + + return connectionErrors; + } } diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/index.ts b/redisinsight/api/src/modules/redis/exceptions/connection/index.ts new file mode 100644 index 0000000000..28089bceec --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/index.ts @@ -0,0 +1,8 @@ +export * from './redis-connection-auth-unsupported.exception'; +export * from './redis-connection-cluster-nodes-unavailable.exception'; +export * from './redis-connection-failed.exception'; +export * from './redis-connection-incorrect-certificate.exception'; +export * from './redis-connection-sentinel-master-required.exception'; +export * from './redis-connection-timeout.exception'; +export * from './redis-connection-unauthorized.exception'; +export * from './redis-connection-unavailable.exception'; diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-auth-unsupported.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-auth-unsupported.exception.ts new file mode 100644 index 0000000000..7c70635409 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-auth-unsupported.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionAuthUnsupportedException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.COMMAND_NOT_SUPPORTED('auth'), + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionAuthUnsupportedException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionAuthUnsupported, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-cluster-nodes-unavailable.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-cluster-nodes-unavailable.exception.ts new file mode 100644 index 0000000000..46d0ce5d87 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-cluster-nodes-unavailable.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionClusterNodesUnavailableException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.DB_CLUSTER_CONNECT_FAILED, + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionClusterNodesUnavailableException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionClusterNodesUnavailable, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts new file mode 100644 index 0000000000..fac0b408f4 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts @@ -0,0 +1,31 @@ +import { HttpException, HttpExceptionOptions } from '@nestjs/common'; +import { isString } from 'lodash'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; + +// The HTTP 424 Failed Dependency client error response status code indicates +// that the method could not be performed on the resource because the requested action +// depended on another action, and that action failed. +export const RedisConnectionFailedStatusCode = 424; + +export class RedisConnectionFailedException extends HttpException { + constructor( + message: string | Record = ERROR_MESSAGES.REDIS_CONNECTION_FAILED, + options?: HttpExceptionOptions, + ) { + let response: Record; + + if (isString(message)) { + response = { + message, + error: 'RedisConnectionFailedException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionFailed, + }; + } else { + response = message; + } + + super(response, RedisConnectionFailedStatusCode, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-incorrect-certificate.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-incorrect-certificate.exception.ts new file mode 100644 index 0000000000..b00db3b6df --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-incorrect-certificate.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionIncorrectCertificateException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.INCORRECT_CERTIFICATES('this host'), + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionIncorrectCertificateException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionIncorrectCertificate, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-sentinel-master-required.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-sentinel-master-required.exception.ts new file mode 100644 index 0000000000..8cd38b2536 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-sentinel-master-required.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionSentinelMasterRequiredException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.SENTINEL_MASTER_NAME_REQUIRED, + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionSentinelMasterRequiredException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionSentinelMasterRequired, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-timeout.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-timeout.exception.ts new file mode 100644 index 0000000000..bc6abcfefb --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-timeout.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionTimeoutException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.CONNECTION_TIMEOUT, + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionTimeoutException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionTimeout, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unauthorized.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unauthorized.exception.ts new file mode 100644 index 0000000000..3be9d0d6c3 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unauthorized.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionUnauthorizedException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.AUTHENTICATION_FAILED(), + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionUnauthorizedException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionUnauthorized, + }, options); + } +} diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unavailable.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unavailable.exception.ts new file mode 100644 index 0000000000..01791d3bb4 --- /dev/null +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-unavailable.exception.ts @@ -0,0 +1,21 @@ +import { HttpExceptionOptions } from '@nestjs/common'; +import { CustomErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { + RedisConnectionFailedException, + RedisConnectionFailedStatusCode, +} from 'src/modules/redis/exceptions/connection/redis-connection-failed.exception'; + +export class RedisConnectionUnavailableException extends RedisConnectionFailedException { + constructor( + message: string = ERROR_MESSAGES.INCORRECT_DATABASE_URL('this host'), + options?: HttpExceptionOptions, + ) { + super({ + message, + error: 'RedisConnectionUnavailableException', + statusCode: RedisConnectionFailedStatusCode, + errorCode: CustomErrorCodes.RedisConnectionUnavailable, + }, options); + } +} diff --git a/redisinsight/api/src/utils/catch-redis-errors.spec.ts b/redisinsight/api/src/utils/catch-redis-errors.spec.ts new file mode 100644 index 0000000000..1fb4a4ad30 --- /dev/null +++ b/redisinsight/api/src/utils/catch-redis-errors.spec.ts @@ -0,0 +1,142 @@ +import { + RedisConnectionAuthUnsupportedException, + RedisConnectionClusterNodesUnavailableException, + RedisConnectionFailedException, RedisConnectionIncorrectCertificateException, + RedisConnectionSentinelMasterRequiredException, + RedisConnectionTimeoutException, + RedisConnectionUnauthorizedException, + RedisConnectionUnavailableException, +} from 'src/modules/redis/exceptions/connection'; +import { getRedisConnectionException } from 'src/utils/catch-redis-errors'; +import { ReplyError } from 'src/models'; +import { CertificatesErrorCodes, RedisErrorCodes } from 'src/constants'; +import ERROR_MESSAGES from 'src/constants/error-messages'; +import { BadRequestException } from '@nestjs/common'; + +describe('catch-redis-errors', () => { + describe('getRedisConnectionExceptions', () => { + const database = { host: '127.0.0.1', port: 6379 }; + + const urlPlaceholder = `${database.host}:${database.port}`; + + it.each([ + { + error: new BadRequestException(), + output: new BadRequestException(), + }, + { + error: new Error('unknown error'), + output: new RedisConnectionFailedException('unknown error'), + }, + { + error: new Error(RedisErrorCodes.SentinelParamsRequired), + output: new RedisConnectionSentinelMasterRequiredException(), + }, + { + error: new Error(RedisErrorCodes.Timeout), + output: new RedisConnectionTimeoutException(), + }, + { + error: new Error('connection timed out'), + output: new RedisConnectionTimeoutException(), + }, + { + error: new Error(RedisErrorCodes.InvalidPassword), + output: new RedisConnectionUnauthorizedException(), + }, + { + error: new Error(RedisErrorCodes.AuthRequired), + output: new RedisConnectionUnauthorizedException(), + }, + { + error: new Error('ERR invalid password'), + output: new RedisConnectionUnauthorizedException(), + }, + { + error: new Error('ERR unknown command \'auth\''), + output: new RedisConnectionAuthUnsupportedException(), + }, + { + error: new Error(RedisErrorCodes.ClusterAllFailedError), + output: new RedisConnectionClusterNodesUnavailableException(), + }, + { + error: new Error(RedisErrorCodes.ConnectionRefused), + output: new RedisConnectionUnavailableException( + ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder), + ), + }, + { + error: new Error(RedisErrorCodes.ConnectionNotFound), + output: new RedisConnectionUnavailableException( + ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder), + ), + }, + { + error: new Error(RedisErrorCodes.DNSTimeoutError), + output: new RedisConnectionUnavailableException( + ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder), + ), + }, + { + error: { message: 'some message', code: RedisErrorCodes.ConnectionReset }, + output: new RedisConnectionUnavailableException( + ERROR_MESSAGES.INCORRECT_DATABASE_URL(urlPlaceholder), + ), + }, + { + error: { message: 'some message', code: CertificatesErrorCodes.IncorrectCertificates }, + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: { message: 'some message', code: CertificatesErrorCodes.DepthZeroSelfSignedCert }, + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: { message: 'some message', code: CertificatesErrorCodes.SelfSignedCertInChain }, + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: { message: 'some message', code: CertificatesErrorCodes.OSSLError }, + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: new Error('SSL error'), + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: new Error(CertificatesErrorCodes.OSSLError), + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: new Error(CertificatesErrorCodes.IncorrectCertificates), + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + { + error: new Error('ERR unencrypted connection is prohibited'), + output: new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES(urlPlaceholder), + ), + }, + ])('should handle %j', ({ error, output }) => { + expect(getRedisConnectionException( + error as ReplyError, + database, + )).toEqual(output); + }); + }); +}); diff --git a/redisinsight/api/src/utils/catch-redis-errors.ts b/redisinsight/api/src/utils/catch-redis-errors.ts index 1118eec36c..cfa4710060 100644 --- a/redisinsight/api/src/utils/catch-redis-errors.ts +++ b/redisinsight/api/src/utils/catch-redis-errors.ts @@ -1,21 +1,26 @@ import { - BadRequestException, ConflictException, ForbiddenException, - GatewayTimeoutException, HttpException, - HttpStatus, InternalServerErrorException, - MethodNotAllowedException, NotFoundException, ServiceUnavailableException, - UnauthorizedException, } from '@nestjs/common'; import { ReplyError } from 'src/models'; import { RedisErrorCodes, CertificatesErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; import { EncryptionServiceErrorException } from 'src/modules/encryption/exceptions'; import { RedisClientCommandReply } from 'src/modules/redis/client'; +import { + RedisConnectionAuthUnsupportedException, + RedisConnectionClusterNodesUnavailableException, + RedisConnectionFailedException, + RedisConnectionTimeoutException, + RedisConnectionUnauthorizedException, + RedisConnectionUnavailableException, + RedisConnectionSentinelMasterRequiredException, + RedisConnectionIncorrectCertificateException, +} from 'src/modules/redis/exceptions/connection'; export const isCertError = (error: ReplyError): boolean => { try { @@ -46,21 +51,14 @@ export const getRedisConnectionException = ( if (error?.message) { if (error.message.includes(RedisErrorCodes.SentinelParamsRequired)) { - return new HttpException( - { - statusCode: HttpStatus.BAD_REQUEST, - error: RedisErrorCodes.SentinelParamsRequired, - message: ERROR_MESSAGES.SENTINEL_MASTER_NAME_REQUIRED, - }, - HttpStatus.BAD_REQUEST, - ); + return new RedisConnectionSentinelMasterRequiredException(undefined, { cause: error }); } if ( error.message.includes(RedisErrorCodes.Timeout) || error.message.includes('timed out') ) { - return new GatewayTimeoutException(ERROR_MESSAGES.CONNECTION_TIMEOUT); + return new RedisConnectionTimeoutException(undefined, { cause: error }); } if ( @@ -68,19 +66,15 @@ export const getRedisConnectionException = ( error.message.includes(RedisErrorCodes.AuthRequired) || error.message === 'ERR invalid password' ) { - return new UnauthorizedException(ERROR_MESSAGES.AUTHENTICATION_FAILED()); + return new RedisConnectionUnauthorizedException(undefined, { cause: error }); } if (error.message === "ERR unknown command 'auth'") { - return new MethodNotAllowedException( - ERROR_MESSAGES.COMMAND_NOT_SUPPORTED('auth'), - ); + return new RedisConnectionAuthUnsupportedException(undefined, { cause: error }); } if (error.message.includes(RedisErrorCodes.ClusterAllFailedError)) { - return new ServiceUnavailableException( - ERROR_MESSAGES.DB_CLUSTER_CONNECT_FAILED, - ); + return new RedisConnectionClusterNodesUnavailableException(undefined, { cause: error }); } if ( @@ -89,29 +83,25 @@ export const getRedisConnectionException = ( error.message.includes(RedisErrorCodes.DNSTimeoutError) || error?.code === RedisErrorCodes.ConnectionReset ) { - return new ServiceUnavailableException( + return new RedisConnectionUnavailableException( ERROR_MESSAGES.INCORRECT_DATABASE_URL( errorPlaceholder || `${host}:${port}`, ), + { cause: error }, ); } + if (isCertError(error)) { - const message = ERROR_MESSAGES.INCORRECT_CERTIFICATES( - errorPlaceholder || `${host}:${port}`, + return new RedisConnectionIncorrectCertificateException( + ERROR_MESSAGES.INCORRECT_CERTIFICATES( + errorPlaceholder || `${host}:${port}`, + ), + { cause: error }, ); - return new BadRequestException(message); } } - // todo: Move to other place after refactoring - if (error instanceof EncryptionServiceErrorException) { - return error; - } - - if (error?.message) { - return new BadRequestException(error.message); - } - return new InternalServerErrorException(); + return new RedisConnectionFailedException(error?.message, { cause: error }); }; export const catchRedisConnectionError = ( @@ -128,7 +118,8 @@ export const catchAclError = (error: ReplyError): HttpException => { error instanceof EncryptionServiceErrorException || error instanceof NotFoundException || error instanceof ConflictException || - error instanceof ServiceUnavailableException + error instanceof ServiceUnavailableException || + error instanceof RedisConnectionFailedException ) { throw error; } diff --git a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts index c230a02901..53053e959a 100644 --- a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts @@ -179,7 +179,7 @@ describe(`PATCH /databases/:id`, () => { request(server).get( `/${constants.API.DATABASES}/${oldDatabase.id}/connect`, ), - statusCode: 503, + statusCode: 424, }); }, responseBody: { @@ -195,18 +195,18 @@ describe(`PATCH /databases/:id`, () => { }, }, { - name: 'Should return 503 error if incorrect connection data provided', + name: 'Should return 424 error if incorrect connection data provided', data: { name: 'new name', port: 1111, ssh: false, }, - statusCode: 503, + statusCode: 424, 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', + statusCode: 424, + message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, after: async () => { // check that instance wasn't changed diff --git a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts index 125d8c16ef..611c16ec9e 100644 --- a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts @@ -121,12 +121,12 @@ describe(`POST /databases/clone/:id`, () => { port: 1111, ssh: false, }, - statusCode: 503, + statusCode: 424, 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', + statusCode: 424, + message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, after: async () => { expect(await localDb.getInstanceByName('new name')).to.eq(null); diff --git a/redisinsight/api/test/api/database/POST-databases-test-id.test.ts b/redisinsight/api/test/api/database/POST-databases-test-id.test.ts index 252e6ae877..bfedf0be4b 100644 --- a/redisinsight/api/test/api/database/POST-databases-test-id.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-test-id.test.ts @@ -155,12 +155,12 @@ describe(`POST /databases/test/:id`, () => { port: 1111, ssh: false, }, - statusCode: 503, + statusCode: 424, 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', + statusCode: 424, + message: `Could not connect to ${constants.TEST_REDIS_HOST}:1111, please check the connection details.`, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, after: async () => { // check that instance wasn't changed diff --git a/redisinsight/api/test/api/plugins/GET-databases-id-plugins-commands.test.ts b/redisinsight/api/test/api/plugins/GET-databases-id-plugins-commands.test.ts index 6f103cb9a7..23a89be788 100644 --- a/redisinsight/api/test/api/plugins/GET-databases-id-plugins-commands.test.ts +++ b/redisinsight/api/test/api/plugins/GET-databases-id-plugins-commands.test.ts @@ -34,10 +34,11 @@ describe('GET /databases/:instanceId/plugins/commands', () => { { endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), name: 'Should not connect to a database due to misconfiguration', - statusCode: 503, + statusCode: 424, responseBody: { - statusCode: 503, - error: 'Service Unavailable', + statusCode: 424, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, }, { diff --git a/redisinsight/api/yarn.lock b/redisinsight/api/yarn.lock index ad8c5145b9..22e21fb560 100644 --- a/redisinsight/api/yarn.lock +++ b/redisinsight/api/yarn.lock @@ -2484,7 +2484,7 @@ ajv@^8.0.0, ajv@^8.0.1: require-from-string "^2.0.2" uri-js "^4.2.2" -ansi-colors@4.1.3, ansi-colors@^4.1.1, ansi-colors@^4.1.3: +ansi-colors@4.1.3, ansi-colors@^4.1.1: version "4.1.3" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== @@ -2545,7 +2545,7 @@ any-promise@^1.0.0: resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@^3.0.3: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -2654,11 +2654,6 @@ array.prototype.flatmap@^1.3.1: es-abstract "^1.20.4" es-shim-unscopables "^1.0.0" -arrify@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" - integrity sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA== - asn1@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d" @@ -2806,11 +2801,6 @@ bignumber.js@^9.0.0: resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c" integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug== -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -2890,7 +2880,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.3, braces@~3.0.2: +braces@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -2961,7 +2951,7 @@ buffer-equal-constant-time@1.0.1: resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA== -buffer-from@^1.0.0, buffer-from@^1.1.0: +buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== @@ -3152,21 +3142,6 @@ chokidar@4.0.3, chokidar@^4.0.1: dependencies: readdirp "^4.0.1" -chokidar@^3.5.3: - version "3.6.0" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" - integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - chownr@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -3784,20 +3759,15 @@ diff-sequences@^29.6.3: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== -diff@^3.1.0: - version "3.5.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" - integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== - diff@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.2.0.tgz#26ded047cd1179b78b9537d5ef725503ce1ae531" - integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A== +diff@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-7.0.0.tgz#3fb34d387cd76d803f6eebea67b921dab0182a9a" + integrity sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw== dir-glob@^3.0.1: version "3.0.1" @@ -4845,7 +4815,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@^2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -4981,7 +4951,7 @@ github-from-package@0.0.0: resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw== -glob-parent@^5.1.2, glob-parent@~5.1.2: +glob-parent@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== @@ -5428,13 +5398,6 @@ is-bigint@^1.0.1: dependencies: has-bigints "^1.0.1" -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - is-boolean-object@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" @@ -5499,7 +5462,7 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== -is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -6778,14 +6741,14 @@ mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@^0.5.1, mkdirp@^0.5.4: +mkdirp@^0.5.4: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== dependencies: minimist "^1.2.6" -mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -6795,14 +6758,19 @@ mkdirp@^2.1.3: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19" integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A== -mocha-junit-reporter@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.0.tgz#2663aaf25a98989ac9080c92b19e54209e539f67" - integrity sha512-W83Ddf94nfLiTBl24aS8IVyFvO8aRDLlCvb+cKb/VEaN5dEbcqu3CXiTe8MQK2DvzS7oKE1RsFTxzN302GGbDQ== +mkdirp@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + +mocha-junit-reporter@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/mocha-junit-reporter/-/mocha-junit-reporter-2.2.1.tgz#739f5595d0f051d07af9d74e32c416e13a41cde5" + integrity sha512-iDn2tlKHn8Vh8o4nCzcUVW4q7iXp7cC4EB78N0cDHIobLymyHNwe0XG8HEHHjc3hJlXm0Vy6zcrxaIhnI2fWmw== dependencies: debug "^4.3.4" md5 "^2.3.0" - mkdirp "~1.0.4" + mkdirp "^3.0.0" strip-ansi "^6.0.1" xml "^1.0.1" @@ -6814,16 +6782,15 @@ mocha-multi-reporters@^1.5.1: debug "^4.1.1" lodash "^4.17.15" -mocha@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.1.0.tgz#20d7c6ac4d6d6bcb60a8aa47971fca74c65c3c66" - integrity sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg== +mocha@^11.4.0: + version "11.4.0" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-11.4.0.tgz#6e873ee0beed4475e06f782bc9dd076670f932fd" + integrity sha512-O6oi5Y9G6uu8f9iqXR6iKNLWHLRex3PKbmHynfpmUnMJJGrdgXh8ZmS85Ei5KR2Gnl+/gQ9s+Ktv5CqKybNw4A== dependencies: - ansi-colors "^4.1.3" browser-stdout "^1.3.1" - chokidar "^3.5.3" + chokidar "^4.0.1" debug "^4.3.5" - diff "^5.2.0" + diff "^7.0.0" escape-string-regexp "^4.0.0" find-up "^5.0.0" glob "^10.4.5" @@ -6832,6 +6799,7 @@ mocha@^11.1.0: log-symbols "^4.1.0" minimatch "^5.1.6" ms "^2.1.3" + picocolors "^1.1.1" serialize-javascript "^6.0.2" strip-json-comments "^3.1.1" supports-color "^8.1.1" @@ -7056,7 +7024,7 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-path@^3.0.0, normalize-path@~3.0.0: +normalize-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7492,12 +7460,17 @@ picocolors@^1.1.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59" integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw== +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + picomatch@4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== @@ -7818,13 +7791,6 @@ readdirp@^4.0.1: resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - readline-sync@^1.4.9: version "1.4.10" resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" @@ -8360,7 +8326,7 @@ source-map-support@0.5.13: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.19, source-map-support@^0.5.6, source-map-support@~0.5.20: +source-map-support@^0.5.19, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -8937,28 +8903,10 @@ ts-loader@^6.2.1: micromatch "^4.0.0" semver "^6.0.0" -ts-mocha@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-8.0.0.tgz#962d0fa12eeb6468aa1a6b594bb3bbc818da3ef0" - integrity sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA== - dependencies: - ts-node "7.0.1" - optionalDependencies: - tsconfig-paths "^3.5.0" - -ts-node@7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-7.0.1.tgz#9562dc2d1e6d248d24bc55f773e3f614337d9baf" - integrity sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw== - dependencies: - arrify "^1.0.0" - buffer-from "^1.1.0" - diff "^3.1.0" - make-error "^1.1.1" - minimist "^1.2.0" - mkdirp "^0.5.1" - source-map-support "^0.5.6" - yn "^2.0.0" +ts-mocha@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/ts-mocha/-/ts-mocha-11.1.0.tgz#d8336ec0146bd6f36cca2555f4cfc7df85bd1586" + integrity sha512-yT7FfzNRCu8ZKkYvAOiH01xNma/vLq6Vit7yINKYFNVP8e5UyrYXSOMIipERTpzVKJQ4Qcos5bQo1tNERNZevQ== ts-node@^10.9.2: version "10.9.2" @@ -9007,7 +8955,7 @@ tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2: minimist "^1.2.6" strip-bom "^3.0.0" -tsconfig-paths@^3.14.1, tsconfig-paths@^3.5.0, tsconfig-paths@^3.9.0: +tsconfig-paths@^3.14.1, tsconfig-paths@^3.9.0: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088" integrity sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g== @@ -9748,11 +9696,6 @@ yn@3.1.1: resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== -yn@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" - integrity sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ== - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" diff --git a/redisinsight/ui/src/services/apiService.ts b/redisinsight/ui/src/services/apiService.ts index 04633170be..c5698dda44 100644 --- a/redisinsight/ui/src/services/apiService.ts +++ b/redisinsight/ui/src/services/apiService.ts @@ -93,9 +93,10 @@ export const connectivityErrorsInterceptor = (error: AxiosError) => { } if ( - response?.status === 503 && + (response?.status === 503 && (responseData.code === 'serviceUnavailable' || - responseData.error === 'Service Unavailable') + responseData.error === 'Service Unavailable')) + || (response?.status === 424 && responseData.error?.startsWith?.('RedisConnection') ) ) { store?.dispatch(setConnectivityError(ApiErrors.ConnectionLost)) } From 2934ac23747d8a2612c91bbf8ee4581c378be7e6 Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Sat, 24 May 2025 19:15:10 +0300 Subject: [PATCH 2/4] RI-7101 remove console.log --- redisinsight/api/src/modules/browser/keys/keys.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/redisinsight/api/src/modules/browser/keys/keys.service.ts b/redisinsight/api/src/modules/browser/keys/keys.service.ts index c629561083..13ba2dd454 100644 --- a/redisinsight/api/src/modules/browser/keys/keys.service.ts +++ b/redisinsight/api/src/modules/browser/keys/keys.service.ts @@ -80,7 +80,6 @@ export class KeysService { plainToInstance(GetKeysWithDetailsResponse, nodeResult), ); } catch (error) { - console.log('___ERROR', error); this.logger.error( `Failed to get keys with details info. ${error.message}.`, clientMetadata, From 3631a6d7a7e3307e4cbbd4a05378f72e4184ad57 Mon Sep 17 00:00:00 2001 From: Artsiom Kharuzhenka Date: Mon, 26 May 2025 20:27:34 +0300 Subject: [PATCH 3/4] RI-7101 fix tests (#4579) * RI-7101 fix tests * RI-7101 fix tests * RI-7101 fix re tests --- .../GET-databases-id-cluster_details.test.ts | 7 ++++--- .../database-import/POST-databases-import.test.ts | 6 +++--- .../api/database/GET-databases-id-connect.test.ts | 7 ++++--- .../test/api/database/GET-databases-id-info.test.ts | 7 ++++--- .../api/database/GET-databases-id-overview.test.ts | 7 ++++--- .../api/test/api/database/PATCH-databases-id.test.ts | 10 ++++++---- .../api/database/POST-databases-clone-id.test.ts | 8 ++++---- .../test/api/database/POST-databases-test-id.test.ts | 12 ++++++------ .../test/api/database/POST-databases-test.test.ts | 2 +- .../api/test/api/database/POST-databases.test.ts | 2 +- redisinsight/api/test/test-runs/start-test-run.sh | 12 +++++++++++- 11 files changed, 48 insertions(+), 32 deletions(-) diff --git a/redisinsight/api/test/api/cluster-monitor/GET-databases-id-cluster_details.test.ts b/redisinsight/api/test/api/cluster-monitor/GET-databases-id-cluster_details.test.ts index 23239091f3..f91a4e58d1 100644 --- a/redisinsight/api/test/api/cluster-monitor/GET-databases-id-cluster_details.test.ts +++ b/redisinsight/api/test/api/cluster-monitor/GET-databases-id-cluster_details.test.ts @@ -73,10 +73,11 @@ describe('GET /databases/:id/cluster-details', () => { { endpoint: () => endpoint(constants.TEST_INSTANCE_ID_4), name: 'Should not connect to a database due to misconfiguration', - statusCode: 503, + statusCode: 424, responseBody: { - statusCode: 503, - error: 'Service Unavailable', + statusCode: 424, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, }, { 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 0b516f0e6a..d381bdef3a 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 @@ -1354,7 +1354,7 @@ describe('POST /databases/import', () => { }, }); - await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE', 424); }); it('Import standalone with CA + CLIENT tls partial with no cert name (format 0)', async () => { await validateApiCall({ @@ -1406,7 +1406,7 @@ describe('POST /databases/import', () => { }, }); - await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE', 424); }); it('Import standalone with CA + CLIENT tls (format 1)', async () => { await validateApiCall({ @@ -1481,7 +1481,7 @@ describe('POST /databases/import', () => { }, }); - await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE'); + await validatePartialImportedDatabase(name, 'STANDALONE', 'STANDALONE', 424); }); it('Import standalone with CA + CLIENT tls (format 2)', async () => { await validateApiCall({ diff --git a/redisinsight/api/test/api/database/GET-databases-id-connect.test.ts b/redisinsight/api/test/api/database/GET-databases-id-connect.test.ts index df0b2f8605..bcd3303041 100644 --- a/redisinsight/api/test/api/database/GET-databases-id-connect.test.ts +++ b/redisinsight/api/test/api/database/GET-databases-id-connect.test.ts @@ -24,10 +24,11 @@ describe(`GET /databases/:id/connect`, () => { { endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), name: 'Should not connect to a database due to misconfiguration', - statusCode: 503, + statusCode: 424, responseBody: { - statusCode: 503, - error: 'Service Unavailable', + statusCode: 424, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, }, { diff --git a/redisinsight/api/test/api/database/GET-databases-id-info.test.ts b/redisinsight/api/test/api/database/GET-databases-id-info.test.ts index b8972efd85..220763b6d3 100644 --- a/redisinsight/api/test/api/database/GET-databases-id-info.test.ts +++ b/redisinsight/api/test/api/database/GET-databases-id-info.test.ts @@ -58,10 +58,11 @@ describe(`GET /databases/:id/info`, () => { { endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), name: 'Should not get info due to misconfiguration', - statusCode: 503, + statusCode: 424, responseBody: { - statusCode: 503, - error: 'Service Unavailable', + statusCode: 424, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, }, { diff --git a/redisinsight/api/test/api/database/GET-databases-id-overview.test.ts b/redisinsight/api/test/api/database/GET-databases-id-overview.test.ts index 229cf1fd2d..53bfd41eca 100644 --- a/redisinsight/api/test/api/database/GET-databases-id-overview.test.ts +++ b/redisinsight/api/test/api/database/GET-databases-id-overview.test.ts @@ -46,10 +46,11 @@ describe(`GET /${constants.API.DATABASES}/:id/overview`, () => { { endpoint: () => endpoint(constants.TEST_INSTANCE_ID_2), name: 'Should not connect to a database due to misconfiguration', - statusCode: 503, + statusCode: 424, responseBody: { - statusCode: 503, - error: 'Service Unavailable', + statusCode: 424, + error: 'RedisConnectionUnavailableException', + errorCode: 10904, }, }, { diff --git a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts index 53053e959a..4fe0e898ad 100644 --- a/redisinsight/api/test/api/database/PATCH-databases-id.test.ts +++ b/redisinsight/api/test/api/database/PATCH-databases-id.test.ts @@ -458,7 +458,7 @@ describe(`PATCH /databases/:id`, () => { it('Should throw an error if db index specified', async () => { await validateApiCall({ endpoint, - statusCode: 400, + statusCode: 424, data: { db: constants.TEST_REDIS_DB_INDEX, }, @@ -1005,7 +1005,7 @@ describe(`PATCH /databases/:id`, () => { await validateApiCall({ endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), - statusCode: 400, + statusCode: 424, data: { name: dbName, host: constants.TEST_REDIS_HOST, @@ -1062,9 +1062,11 @@ describe(`PATCH /databases/:id`, () => { data: { caCert: null, }, - statusCode: 400, + statusCode: 424, responseBody: { - error: 'Bad Request', + error: 'RedisConnectionIncorrectCertificateException', + statusCode: 424, + errorCode: 10907 }, }); }); diff --git a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts index 611c16ec9e..2f29bf753c 100644 --- a/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-clone-id.test.ts @@ -115,7 +115,7 @@ describe(`POST /databases/clone/:id`, () => { const dbName = constants.getRandomString(); [ { - name: 'Should return 503 error if incorrect connection data provided', + name: 'Should return 424 error if incorrect connection data provided', data: { name: 'new name', port: 1111, @@ -212,7 +212,7 @@ describe(`POST /databases/clone/:id`, () => { it('Should throw an error if db index specified', async () => { await validateApiCall({ endpoint, - statusCode: 400, + statusCode: 424, data: { db: constants.TEST_REDIS_DB_INDEX, }, @@ -759,7 +759,7 @@ describe(`POST /databases/clone/:id`, () => { await validateApiCall({ endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), - statusCode: 400, + statusCode: 424, data: { name: dbName, host: constants.TEST_REDIS_HOST, @@ -802,7 +802,7 @@ describe(`POST /databases/clone/:id`, () => { data: { caCert: null, }, - statusCode: 503, + statusCode: 424, }); }); it('Should throw an error without invalid cert', async () => { diff --git a/redisinsight/api/test/api/database/POST-databases-test-id.test.ts b/redisinsight/api/test/api/database/POST-databases-test-id.test.ts index bfedf0be4b..56ad6c2c26 100644 --- a/redisinsight/api/test/api/database/POST-databases-test-id.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-test-id.test.ts @@ -149,7 +149,7 @@ describe(`POST /databases/test/:id`, () => { }, }, { - name: 'Should return 503 error if incorrect connection data provided', + name: 'Should return 424 error if incorrect connection data provided', data: { name: 'new name', port: 1111, @@ -227,7 +227,7 @@ describe(`POST /databases/test/:id`, () => { it('Should throw an error if db index specified', async () => { await validateApiCall({ endpoint, - statusCode: 400, + statusCode: 424, data: { db: constants.TEST_REDIS_DB_INDEX, }, @@ -527,7 +527,7 @@ describe(`POST /databases/test/:id`, () => { await validateApiCall({ endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), - statusCode: 400, + statusCode: 424, data: { name: dbName, host: constants.TEST_REDIS_HOST, @@ -544,7 +544,7 @@ describe(`POST /databases/test/:id`, () => { await validateApiCall({ endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), - statusCode: 503, + statusCode: 424, data: { name: dbName, tls: true, @@ -557,7 +557,7 @@ describe(`POST /databases/test/:id`, () => { await validateApiCall({ endpoint: () => endpoint(constants.TEST_INSTANCE_ID_3), - statusCode: 503, + statusCode: 424, data: { name: dbName, tls: true, @@ -575,7 +575,7 @@ describe(`POST /databases/test/:id`, () => { data: { caCert: null, }, - statusCode: 503, + statusCode: 424, }); }); it('Should throw an error without invalid cert', async () => { diff --git a/redisinsight/api/test/api/database/POST-databases-test.test.ts b/redisinsight/api/test/api/database/POST-databases-test.test.ts index a0c463f245..327f8e0efc 100644 --- a/redisinsight/api/test/api/database/POST-databases-test.test.ts +++ b/redisinsight/api/test/api/database/POST-databases-test.test.ts @@ -385,7 +385,7 @@ describe('POST /databases/test', () => { it('Should throw an error if db index specified', async () => { await validateApiCall({ endpoint, - statusCode: 400, + statusCode: 424, data: { name: dbName, host: constants.TEST_REDIS_HOST, diff --git a/redisinsight/api/test/api/database/POST-databases.test.ts b/redisinsight/api/test/api/database/POST-databases.test.ts index 2f1939e075..0e2ce144cf 100644 --- a/redisinsight/api/test/api/database/POST-databases.test.ts +++ b/redisinsight/api/test/api/database/POST-databases.test.ts @@ -1240,7 +1240,7 @@ describe('POST /databases', () => { await validateApiCall({ endpoint, - statusCode: 400, + statusCode: 424, data: { name: dbName, host: constants.TEST_REDIS_HOST, diff --git a/redisinsight/api/test/test-runs/start-test-run.sh b/redisinsight/api/test/test-runs/start-test-run.sh index 8c552eaf9a..2585c8f027 100755 --- a/redisinsight/api/test/test-runs/start-test-run.sh +++ b/redisinsight/api/test/test-runs/start-test-run.sh @@ -8,6 +8,7 @@ helpFunction() printf "Some of the required parameters are empty\n\n" printf "Usage: %s -r RTE [-t local]\n" "$0" printf " -r - (required) Redis Test Environment (RTE). Should match any service name from redis.docker-compose.yml\n" + printf " -f - (optional) force rebuild api image\n" printf " -t - Backend build type. \t local - (default) run server using source code \t docker - run server on built docker container @@ -16,11 +17,12 @@ helpFunction() } # required params -while getopts "r:t:" opt +while getopts "r:t:f" opt do case "$opt" in r ) RTE="$OPTARG" ;; t ) BUILD="$OPTARG" ;; + f ) FORCE_REBUILD=true ;; ? ) helpFunction ;; # Print helpFunction in case parameter is non-existent esac done @@ -54,6 +56,14 @@ eval "ID=$ID RTE=$RTE docker compose \ -f $BASEDIR/$RTE/docker-compose.yml \ --env-file $BASEDIR/$BUILD.build.env build --no-cache redis" +if [ "$FORCE_REBUILD" = true ]; then + echo "Force rebuilding api and test containers" + eval "ID=$ID RTE=$RTE docker compose \ + -f $BASEDIR/$BUILD.build.yml \ + -f $BASEDIR/$RTE/docker-compose.yml \ + --env-file $BASEDIR/$BUILD.build.env build --no-cache test app" +fi + echo "Test run is starting... ${RTE}" eval "ID=$ID RTE=$RTE docker compose -p $ID \ -f $BASEDIR/$BUILD.build.yml \ From bbd76fc256f489c3e9ebe49fc01aa568e079be8c Mon Sep 17 00:00:00 2001 From: ArtemHoruzhenko Date: Fri, 6 Jun 2025 15:44:32 +0300 Subject: [PATCH 4/4] RI-7101 resolve PR comments --- .../redis-connection-failed.exception.ts | 4 +- redisinsight/ui/src/services/apiService.ts | 25 +++++-- .../ui/src/services/tests/apiService.spec.ts | 66 +++++++++++++++++++ 3 files changed, 87 insertions(+), 8 deletions(-) diff --git a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts index fac0b408f4..e3730e5bd8 100644 --- a/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts +++ b/redisinsight/api/src/modules/redis/exceptions/connection/redis-connection-failed.exception.ts @@ -1,4 +1,4 @@ -import { HttpException, HttpExceptionOptions } from '@nestjs/common'; +import { HttpException, HttpExceptionOptions, HttpStatus } from '@nestjs/common'; import { isString } from 'lodash'; import { CustomErrorCodes } from 'src/constants'; import ERROR_MESSAGES from 'src/constants/error-messages'; @@ -6,7 +6,7 @@ import ERROR_MESSAGES from 'src/constants/error-messages'; // The HTTP 424 Failed Dependency client error response status code indicates // that the method could not be performed on the resource because the requested action // depended on another action, and that action failed. -export const RedisConnectionFailedStatusCode = 424; +export const RedisConnectionFailedStatusCode = HttpStatus.FAILED_DEPENDENCY; export class RedisConnectionFailedException extends HttpException { constructor( diff --git a/redisinsight/ui/src/services/apiService.ts b/redisinsight/ui/src/services/apiService.ts index c5698dda44..134cec838e 100644 --- a/redisinsight/ui/src/services/apiService.ts +++ b/redisinsight/ui/src/services/apiService.ts @@ -84,6 +84,24 @@ export const hostedAuthInterceptor = (error: AxiosError) => { return Promise.reject(error) } +export const isConnectivityError = ( + status?: number, + data?: { code?: string; error?: string } +): boolean => { + if (!status || !data) { + return false + } + + switch (status) { + case 424: + return !!data.error?.startsWith?.('RedisConnection') + case 503: + return data.code === 'serviceUnavailable' || data.error === 'Service Unavailable' + default: + return false + } +} + export const connectivityErrorsInterceptor = (error: AxiosError) => { const { response } = error const responseData = response?.data as { @@ -92,12 +110,7 @@ export const connectivityErrorsInterceptor = (error: AxiosError) => { error?: string } - if ( - (response?.status === 503 && - (responseData.code === 'serviceUnavailable' || - responseData.error === 'Service Unavailable')) - || (response?.status === 424 && responseData.error?.startsWith?.('RedisConnection') ) - ) { + if (isConnectivityError(response?.status, responseData)) { store?.dispatch(setConnectivityError(ApiErrors.ConnectionLost)) } diff --git a/redisinsight/ui/src/services/tests/apiService.spec.ts b/redisinsight/ui/src/services/tests/apiService.spec.ts index 66e0b152e7..81761ea8eb 100644 --- a/redisinsight/ui/src/services/tests/apiService.spec.ts +++ b/redisinsight/ui/src/services/tests/apiService.spec.ts @@ -2,6 +2,7 @@ import { cloneDeep } from 'lodash' import { sessionStorageService } from 'uiSrc/services' import { cloudAuthInterceptor, + isConnectivityError, requestInterceptor, } from 'uiSrc/services/apiService' import { ApiEndpoints } from 'uiSrc/constants' @@ -80,3 +81,68 @@ describe('cloudAuthInterceptor', () => { } }) }) + +describe('isConnectivityError', () => { + it.each<{apiResponse: any, result: boolean}>([ + { + apiResponse: undefined, + result: false, + }, + { + apiResponse: { + status: 424, + data: { + error: 'RedisConnectionFailedException', + }, + }, + result: true, + }, + { + apiResponse: { + status: 500, + data: { + error: 'RedisConnectionFailedException', + }, + }, + result: false, + }, + { + apiResponse: { + status: 503, + data: { + error: 'Service Unavailable', + }, + }, + result: true, + }, + { + apiResponse: { + status: 401, + data: { + error: 'Service Unavailable', + }, + }, + result: false, + }, + { + apiResponse: { + status: 503, + data: { + code: 'serviceUnavailable', + }, + }, + result: true, + }, + { + apiResponse: { + status: 400, + data: { + code: 'serviceUnavailable', + }, + }, + result: false, + } + ])('test %j', ({ apiResponse, result }) => { + expect(isConnectivityError(apiResponse?.status, apiResponse?.data)).toEqual(result) + }) +})