From 25e1e92f06d6af56b4110f051de13af218f5d8bb Mon Sep 17 00:00:00 2001 From: Artem Date: Mon, 19 Dec 2022 09:15:18 +0200 Subject: [PATCH 1/2] 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 2/2] 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(),