From 0a2de093c01689b8f179b3f4413a4ce29ccf279a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Thu, 2 May 2024 17:35:41 +0200 Subject: [PATCH] feat(Redis Node): Add support for TLS (#9266) --- .../credentials/Redis.credentials.ts | 6 + .../nodes/Redis/test/Redis.node.test.ts | 251 +++++++++++------- packages/nodes-base/nodes/Redis/utils.ts | 5 +- 3 files changed, 158 insertions(+), 104 deletions(-) diff --git a/packages/nodes-base/credentials/Redis.credentials.ts b/packages/nodes-base/credentials/Redis.credentials.ts index 2833ca24e1..194c2f59e3 100644 --- a/packages/nodes-base/credentials/Redis.credentials.ts +++ b/packages/nodes-base/credentials/Redis.credentials.ts @@ -35,5 +35,11 @@ export class Redis implements ICredentialType { type: 'number', default: 0, }, + { + displayName: 'SSL', + name: 'ssl', + type: 'boolean', + default: false, + }, ]; } diff --git a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts b/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts index 892165169a..029b85f4d4 100644 --- a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts +++ b/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts @@ -1,36 +1,79 @@ import { mock } from 'jest-mock-extended'; import type { RedisClientType } from '@redis/client'; import type { IExecuteFunctions } from 'n8n-workflow'; -import { Redis } from '../Redis.node'; const mockClient = mock(); -jest.mock('redis', () => ({ - createClient: () => mockClient, -})); +const createClient = jest.fn().mockReturnValue(mockClient); +jest.mock('redis', () => ({ createClient })); + +import { Redis } from '../Redis.node'; +import { setupRedisClient } from '../utils'; describe('Redis Node', () => { - const mockCredential = { - host: 'redis', - port: 1234, - database: 0, - password: 'random', - }; - const node = new Redis(); - const thisArg = mock({}); - thisArg.getCredentials.calledWith('redis').mockResolvedValue(mockCredential); beforeEach(() => jest.clearAllMocks()); - afterEach(() => { - expect(mockClient.connect).toHaveBeenCalled(); - expect(mockClient.ping).toHaveBeenCalled(); - expect(mockClient.quit).toHaveBeenCalled(); + describe('setupRedisClient', () => { + it('should not configure TLS by default', () => { + setupRedisClient({ + host: 'redis.domain', + port: 1234, + database: 0, + }); + expect(createClient).toHaveBeenCalledWith({ + database: 0, + password: undefined, + socket: { + host: 'redis.domain', + port: 1234, + tls: false, + }, + }); + }); + + it('should configure TLS', () => { + setupRedisClient({ + host: 'redis.domain', + port: 1234, + database: 0, + ssl: true, + }); + expect(createClient).toHaveBeenCalledWith({ + database: 0, + password: undefined, + socket: { + host: 'redis.domain', + port: 1234, + tls: true, + }, + }); + }); }); - it('info operation', async () => { - thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info'); - mockClient.info.mockResolvedValue(` + describe('operations', () => { + const thisArg = mock({}); + + const mockCredential = { + host: 'redis', + port: 1234, + database: 0, + password: 'random', + }; + + thisArg.getCredentials.calledWith('redis').mockResolvedValue(mockCredential); + + afterEach(() => { + expect(createClient).toHaveBeenCalled(); + expect(mockClient.connect).toHaveBeenCalled(); + expect(mockClient.ping).toHaveBeenCalled(); + expect(mockClient.quit).toHaveBeenCalled(); + }); + + describe('info operation', () => { + it('should return info', async () => { + thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info'); + mockClient.info.mockResolvedValue(` # Server redis_version:6.2.14 redis_git_sha1:00000000 @@ -55,104 +98,108 @@ connected_slaves:0 master_failover_state:no-failover `); - const output = await node.execute.call(thisArg); + const output = await node.execute.call(thisArg); - expect(mockClient.info).toHaveBeenCalled(); - expect(output[0][0].json).toEqual({ - redis_version: 6.2, - redis_git_sha1: 0, - redis_git_dirty: 0, - redis_mode: 'standalone', - arch_bits: 64, - tcp_port: 6379, - uptime_in_seconds: 429905, - uptime_in_days: 4, - connected_clients: 1, - cluster_connections: 0, - max_clients: 10000, - used_memory: 876648, - role: 'master', - connected_slaves: 0, - master_failover_state: 'no-failover', - }); - }); - - it('delete operation', async () => { - thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); - thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('delete'); - thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1'); - mockClient.del.calledWith('key1').mockResolvedValue(1); - - const output = await node.execute.call(thisArg); - expect(mockClient.del).toHaveBeenCalledWith('key1'); - expect(output[0][0].json).toEqual({ x: 1 }); - }); - - describe('get operation', () => { - beforeEach(() => { - thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); - thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('get'); - thisArg.getNodeParameter.calledWith('options', 0).mockReturnValue({ dotNotation: true }); - thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1'); - thisArg.getNodeParameter.calledWith('propertyName', 0).mockReturnValue('x.y'); + expect(mockClient.info).toHaveBeenCalled(); + expect(output[0][0].json).toEqual({ + redis_version: 6.2, + redis_git_sha1: 0, + redis_git_dirty: 0, + redis_mode: 'standalone', + arch_bits: 64, + tcp_port: 6379, + uptime_in_seconds: 429905, + uptime_in_days: 4, + connected_clients: 1, + cluster_connections: 0, + max_clients: 10000, + used_memory: 876648, + role: 'master', + connected_slaves: 0, + master_failover_state: 'no-failover', + }); + }); }); - it('keyType = automatic', async () => { - thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic'); - mockClient.type.calledWith('key1').mockResolvedValue('string'); - mockClient.get.calledWith('key1').mockResolvedValue('value'); + describe('delete operation', () => { + it('should delete', async () => { + thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); + thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('delete'); + thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1'); + mockClient.del.calledWith('key1').mockResolvedValue(1); - const output = await node.execute.call(thisArg); - expect(mockClient.type).toHaveBeenCalledWith('key1'); - expect(mockClient.get).toHaveBeenCalledWith('key1'); - expect(output[0][0].json).toEqual({ x: { y: 'value' } }); + const output = await node.execute.call(thisArg); + expect(mockClient.del).toHaveBeenCalledWith('key1'); + expect(output[0][0].json).toEqual({ x: 1 }); + }); }); - it('keyType = hash', async () => { - thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('hash'); - mockClient.hGetAll.calledWith('key1').mockResolvedValue({ - field1: '1', - field2: '2', + describe('get operation', () => { + beforeEach(() => { + thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); + thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('get'); + thisArg.getNodeParameter.calledWith('options', 0).mockReturnValue({ dotNotation: true }); + thisArg.getNodeParameter.calledWith('key', 0).mockReturnValue('key1'); + thisArg.getNodeParameter.calledWith('propertyName', 0).mockReturnValue('x.y'); }); - const output = await node.execute.call(thisArg); - expect(mockClient.hGetAll).toHaveBeenCalledWith('key1'); - expect(output[0][0].json).toEqual({ - x: { - y: { - field1: '1', - field2: '2', + it('keyType = automatic', async () => { + thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic'); + mockClient.type.calledWith('key1').mockResolvedValue('string'); + mockClient.get.calledWith('key1').mockResolvedValue('value'); + + const output = await node.execute.call(thisArg); + expect(mockClient.type).toHaveBeenCalledWith('key1'); + expect(mockClient.get).toHaveBeenCalledWith('key1'); + expect(output[0][0].json).toEqual({ x: { y: 'value' } }); + }); + + it('keyType = hash', async () => { + thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('hash'); + mockClient.hGetAll.calledWith('key1').mockResolvedValue({ + field1: '1', + field2: '2', + }); + + const output = await node.execute.call(thisArg); + expect(mockClient.hGetAll).toHaveBeenCalledWith('key1'); + expect(output[0][0].json).toEqual({ + x: { + y: { + field1: '1', + field2: '2', + }, }, - }, + }); }); }); - }); - describe('keys operation', () => { - beforeEach(() => { - thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); - thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('keys'); - thisArg.getNodeParameter.calledWith('keyPattern', 0).mockReturnValue('key*'); - mockClient.keys.calledWith('key*').mockResolvedValue(['key1', 'key2']); - }); + describe('keys operation', () => { + beforeEach(() => { + thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); + thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('keys'); + thisArg.getNodeParameter.calledWith('keyPattern', 0).mockReturnValue('key*'); + mockClient.keys.calledWith('key*').mockResolvedValue(['key1', 'key2']); + }); - it('getValues = false', async () => { - thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(false); + it('getValues = false', async () => { + thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(false); - const output = await node.execute.call(thisArg); - expect(mockClient.keys).toHaveBeenCalledWith('key*'); - expect(output[0][0].json).toEqual({ keys: ['key1', 'key2'] }); - }); + const output = await node.execute.call(thisArg); + expect(mockClient.keys).toHaveBeenCalledWith('key*'); + expect(output[0][0].json).toEqual({ keys: ['key1', 'key2'] }); + }); - it('getValues = true', async () => { - thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true); - mockClient.type.mockResolvedValue('string'); - mockClient.get.calledWith('key1').mockResolvedValue('value1'); - mockClient.get.calledWith('key2').mockResolvedValue('value2'); + it('getValues = true', async () => { + thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true); + mockClient.type.mockResolvedValue('string'); + mockClient.get.calledWith('key1').mockResolvedValue('value1'); + mockClient.get.calledWith('key2').mockResolvedValue('value2'); - const output = await node.execute.call(thisArg); - expect(mockClient.keys).toHaveBeenCalledWith('key*'); - expect(output[0][0].json).toEqual({ key1: 'value1', key2: 'value2' }); + const output = await node.execute.call(thisArg); + expect(mockClient.keys).toHaveBeenCalledWith('key*'); + expect(output[0][0].json).toEqual({ key1: 'value1', key2: 'value2' }); + }); }); }); }); diff --git a/packages/nodes-base/nodes/Redis/utils.ts b/packages/nodes-base/nodes/Redis/utils.ts index 2e59eb67e4..effdd99778 100644 --- a/packages/nodes-base/nodes/Redis/utils.ts +++ b/packages/nodes-base/nodes/Redis/utils.ts @@ -8,14 +8,15 @@ import type { } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow'; -import { createClient } from 'redis'; +import { type RedisClientOptions, createClient } from 'redis'; export type RedisClientType = ReturnType; export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType { - const redisOptions = { + const redisOptions: RedisClientOptions = { socket: { host: credentials.host as string, port: credentials.port as number, + tls: credentials.ssl === true, }, database: credentials.database as number, password: (credentials.password as string) || undefined,