feat(Redis Node): Add support for TLS (#9266)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-05-02 17:35:41 +02:00 committed by GitHub
parent 30c8efc4cc
commit 0a2de093c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 158 additions and 104 deletions

View file

@ -35,5 +35,11 @@ export class Redis implements ICredentialType {
type: 'number', type: 'number',
default: 0, default: 0,
}, },
{
displayName: 'SSL',
name: 'ssl',
type: 'boolean',
default: false,
},
]; ];
} }

View file

@ -1,36 +1,79 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { RedisClientType } from '@redis/client'; import type { RedisClientType } from '@redis/client';
import type { IExecuteFunctions } from 'n8n-workflow'; import type { IExecuteFunctions } from 'n8n-workflow';
import { Redis } from '../Redis.node';
const mockClient = mock<RedisClientType>(); const mockClient = mock<RedisClientType>();
jest.mock('redis', () => ({ const createClient = jest.fn().mockReturnValue(mockClient);
createClient: () => mockClient, jest.mock('redis', () => ({ createClient }));
}));
import { Redis } from '../Redis.node';
import { setupRedisClient } from '../utils';
describe('Redis Node', () => { describe('Redis Node', () => {
const mockCredential = {
host: 'redis',
port: 1234,
database: 0,
password: 'random',
};
const node = new Redis(); const node = new Redis();
const thisArg = mock<IExecuteFunctions>({});
thisArg.getCredentials.calledWith('redis').mockResolvedValue(mockCredential);
beforeEach(() => jest.clearAllMocks()); beforeEach(() => jest.clearAllMocks());
afterEach(() => { describe('setupRedisClient', () => {
expect(mockClient.connect).toHaveBeenCalled(); it('should not configure TLS by default', () => {
expect(mockClient.ping).toHaveBeenCalled(); setupRedisClient({
expect(mockClient.quit).toHaveBeenCalled(); 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 () => { describe('operations', () => {
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('info'); const thisArg = mock<IExecuteFunctions>({});
mockClient.info.mockResolvedValue(`
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 # Server
redis_version:6.2.14 redis_version:6.2.14
redis_git_sha1:00000000 redis_git_sha1:00000000
@ -55,104 +98,108 @@ connected_slaves:0
master_failover_state:no-failover master_failover_state:no-failover
`); `);
const output = await node.execute.call(thisArg); const output = await node.execute.call(thisArg);
expect(mockClient.info).toHaveBeenCalled(); expect(mockClient.info).toHaveBeenCalled();
expect(output[0][0].json).toEqual({ expect(output[0][0].json).toEqual({
redis_version: 6.2, redis_version: 6.2,
redis_git_sha1: 0, redis_git_sha1: 0,
redis_git_dirty: 0, redis_git_dirty: 0,
redis_mode: 'standalone', redis_mode: 'standalone',
arch_bits: 64, arch_bits: 64,
tcp_port: 6379, tcp_port: 6379,
uptime_in_seconds: 429905, uptime_in_seconds: 429905,
uptime_in_days: 4, uptime_in_days: 4,
connected_clients: 1, connected_clients: 1,
cluster_connections: 0, cluster_connections: 0,
max_clients: 10000, max_clients: 10000,
used_memory: 876648, used_memory: 876648,
role: 'master', role: 'master',
connected_slaves: 0, connected_slaves: 0,
master_failover_state: 'no-failover', 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');
}); });
it('keyType = automatic', async () => { describe('delete operation', () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic'); it('should delete', async () => {
mockClient.type.calledWith('key1').mockResolvedValue('string'); thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
mockClient.get.calledWith('key1').mockResolvedValue('value'); 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); const output = await node.execute.call(thisArg);
expect(mockClient.type).toHaveBeenCalledWith('key1'); expect(mockClient.del).toHaveBeenCalledWith('key1');
expect(mockClient.get).toHaveBeenCalledWith('key1'); expect(output[0][0].json).toEqual({ x: 1 });
expect(output[0][0].json).toEqual({ x: { y: 'value' } }); });
}); });
it('keyType = hash', async () => { describe('get operation', () => {
thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('hash'); beforeEach(() => {
mockClient.hGetAll.calledWith('key1').mockResolvedValue({ thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
field1: '1', thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('get');
field2: '2', 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); it('keyType = automatic', async () => {
expect(mockClient.hGetAll).toHaveBeenCalledWith('key1'); thisArg.getNodeParameter.calledWith('keyType', 0).mockReturnValue('automatic');
expect(output[0][0].json).toEqual({ mockClient.type.calledWith('key1').mockResolvedValue('string');
x: { mockClient.get.calledWith('key1').mockResolvedValue('value');
y: {
field1: '1', const output = await node.execute.call(thisArg);
field2: '2', 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', () => { describe('keys operation', () => {
beforeEach(() => { beforeEach(() => {
thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]); thisArg.getInputData.mockReturnValue([{ json: { x: 1 } }]);
thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('keys'); thisArg.getNodeParameter.calledWith('operation', 0).mockReturnValue('keys');
thisArg.getNodeParameter.calledWith('keyPattern', 0).mockReturnValue('key*'); thisArg.getNodeParameter.calledWith('keyPattern', 0).mockReturnValue('key*');
mockClient.keys.calledWith('key*').mockResolvedValue(['key1', 'key2']); mockClient.keys.calledWith('key*').mockResolvedValue(['key1', 'key2']);
}); });
it('getValues = false', async () => { it('getValues = false', async () => {
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(false); thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(false);
const output = await node.execute.call(thisArg); const output = await node.execute.call(thisArg);
expect(mockClient.keys).toHaveBeenCalledWith('key*'); expect(mockClient.keys).toHaveBeenCalledWith('key*');
expect(output[0][0].json).toEqual({ keys: ['key1', 'key2'] }); expect(output[0][0].json).toEqual({ keys: ['key1', 'key2'] });
}); });
it('getValues = true', async () => { it('getValues = true', async () => {
thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true); thisArg.getNodeParameter.calledWith('getValues', 0).mockReturnValue(true);
mockClient.type.mockResolvedValue('string'); mockClient.type.mockResolvedValue('string');
mockClient.get.calledWith('key1').mockResolvedValue('value1'); mockClient.get.calledWith('key1').mockResolvedValue('value1');
mockClient.get.calledWith('key2').mockResolvedValue('value2'); mockClient.get.calledWith('key2').mockResolvedValue('value2');
const output = await node.execute.call(thisArg); const output = await node.execute.call(thisArg);
expect(mockClient.keys).toHaveBeenCalledWith('key*'); expect(mockClient.keys).toHaveBeenCalledWith('key*');
expect(output[0][0].json).toEqual({ key1: 'value1', key2: 'value2' }); expect(output[0][0].json).toEqual({ key1: 'value1', key2: 'value2' });
});
}); });
}); });
}); });

View file

@ -8,14 +8,15 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow'; import { NodeOperationError } from 'n8n-workflow';
import { createClient } from 'redis'; import { type RedisClientOptions, createClient } from 'redis';
export type RedisClientType = ReturnType<typeof createClient>; export type RedisClientType = ReturnType<typeof createClient>;
export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType { export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType {
const redisOptions = { const redisOptions: RedisClientOptions = {
socket: { socket: {
host: credentials.host as string, host: credentials.host as string,
port: credentials.port as number, port: credentials.port as number,
tls: credentials.ssl === true,
}, },
database: credentials.database as number, database: credentials.database as number,
password: (credentials.password as string) || undefined, password: (credentials.password as string) || undefined,