fix(Redis Node): Add support for username auth (#12274)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2024-12-19 10:58:26 +01:00 committed by GitHub
parent 38c5ed2932
commit 64c0414ef2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 118 additions and 25 deletions

View file

@ -17,6 +17,13 @@ export class Redis implements ICredentialType {
}, },
default: '', default: '',
}, },
{
displayName: 'User',
name: 'user',
type: 'string',
default: '',
hint: 'Leave blank for password-only auth',
},
{ {
displayName: 'Host', displayName: 'Host',
name: 'host', name: 'host',

View file

@ -15,6 +15,7 @@ import {
getValue, getValue,
setValue, setValue,
} from './utils'; } from './utils';
import type { RedisCredential } from './types';
export class Redis implements INodeType { export class Redis implements INodeType {
description: INodeTypeDescription = { description: INodeTypeDescription = {
@ -512,7 +513,7 @@ export class Redis implements INodeType {
// have a parameter field for a path. Because it is not possible to set // have a parameter field for a path. Because it is not possible to set
// array, object via parameter directly (should maybe be possible?!?!) // array, object via parameter directly (should maybe be possible?!?!)
// Should maybe have a parameter which is JSON. // Should maybe have a parameter which is JSON.
const credentials = await this.getCredentials('redis'); const credentials = await this.getCredentials<RedisCredential>('redis');
const client = setupRedisClient(credentials); const client = setupRedisClient(credentials);
await client.connect(); await client.connect();

View file

@ -7,6 +7,7 @@ import type {
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow'; import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { redisConnectionTest, setupRedisClient } from './utils'; import { redisConnectionTest, setupRedisClient } from './utils';
import type { RedisCredential } from './types';
interface Options { interface Options {
jsonParseBody: boolean; jsonParseBody: boolean;
@ -74,7 +75,7 @@ export class RedisTrigger implements INodeType {
}; };
async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> { async trigger(this: ITriggerFunctions): Promise<ITriggerResponse> {
const credentials = await this.getCredentials('redis'); const credentials = await this.getCredentials<RedisCredential>('redis');
const channels = (this.getNodeParameter('channels') as string).split(','); const channels = (this.getNodeParameter('channels') as string).split(',');
const options = this.getNodeParameter('options') as Options; const options = this.getNodeParameter('options') as Options;

View file

@ -1,18 +1,24 @@
import type { RedisClientType } from '@redis/client';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { NodeOperationError, type IExecuteFunctions } from 'n8n-workflow'; import type {
ICredentialsDecrypted,
ICredentialTestFunctions,
IExecuteFunctions,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
const mockClient = mock<RedisClientType>(); const mockClient = mock<RedisClient>();
const createClient = jest.fn().mockReturnValue(mockClient); const createClient = jest.fn().mockReturnValue(mockClient);
jest.mock('redis', () => ({ createClient })); jest.mock('redis', () => ({ createClient }));
import { Redis } from '../Redis.node'; import { Redis } from '../Redis.node';
import { setupRedisClient } from '../utils'; import { redisConnectionTest, setupRedisClient } from '../utils';
import type { RedisClient } from '../types';
describe('Redis Node', () => { describe('Redis Node', () => {
const node = new Redis(); const node = new Redis();
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks();
createClient.mockReturnValue(mockClient); createClient.mockReturnValue(mockClient);
}); });
@ -27,7 +33,6 @@ describe('Redis Node', () => {
}); });
expect(createClient).toHaveBeenCalledWith({ expect(createClient).toHaveBeenCalledWith({
database: 0, database: 0,
password: undefined,
socket: { socket: {
host: 'redis.domain', host: 'redis.domain',
port: 1234, port: 1234,
@ -45,7 +50,6 @@ describe('Redis Node', () => {
}); });
expect(createClient).toHaveBeenCalledWith({ expect(createClient).toHaveBeenCalledWith({
database: 0, database: 0,
password: undefined,
socket: { socket: {
host: 'redis.domain', host: 'redis.domain',
port: 1234, port: 1234,
@ -53,6 +57,75 @@ describe('Redis Node', () => {
}, },
}); });
}); });
it('should set user on auth', () => {
setupRedisClient({
host: 'redis.domain',
port: 1234,
database: 0,
user: 'test_user',
password: 'test_password',
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
username: 'test_user',
password: 'test_password',
socket: {
host: 'redis.domain',
port: 1234,
tls: false,
},
});
});
});
describe('redisConnectionTest', () => {
const thisArg = mock<ICredentialTestFunctions>({});
const credentials = mock<ICredentialsDecrypted>({
data: {
host: 'localhost',
port: 6379,
user: 'username',
password: 'password',
database: 0,
},
});
const redisOptions = {
socket: {
host: 'localhost',
port: 6379,
tls: false,
},
database: 0,
username: 'username',
password: 'password',
};
it('should return success when connection is established', async () => {
const result = await redisConnectionTest.call(thisArg, credentials);
expect(result).toEqual({
status: 'OK',
message: 'Connection successful!',
});
expect(createClient).toHaveBeenCalledWith(redisOptions);
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).toHaveBeenCalled();
});
it('should return error when connection fails', async () => {
mockClient.connect.mockRejectedValue(new Error('Connection failed'));
const result = await redisConnectionTest.call(thisArg, credentials);
expect(result).toEqual({
status: 'Error',
message: 'Connection failed',
});
expect(createClient).toHaveBeenCalledWith(redisOptions);
expect(mockClient.connect).toHaveBeenCalled();
expect(mockClient.ping).not.toHaveBeenCalled();
});
}); });
describe('operations', () => { describe('operations', () => {

View file

@ -3,10 +3,11 @@ import { captor, mock } from 'jest-mock-extended';
import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject, ITriggerFunctions } from 'n8n-workflow';
import { RedisTrigger } from '../RedisTrigger.node'; import { RedisTrigger } from '../RedisTrigger.node';
import { type RedisClientType, setupRedisClient } from '../utils'; import { setupRedisClient } from '../utils';
import type { RedisClient } from '../types';
jest.mock('../utils', () => { jest.mock('../utils', () => {
const mockRedisClient = mock<RedisClientType>(); const mockRedisClient = mock<RedisClient>();
return { return {
setupRedisClient: jest.fn().mockReturnValue(mockRedisClient), setupRedisClient: jest.fn().mockReturnValue(mockRedisClient),
}; };

View file

@ -0,0 +1,12 @@
import type { createClient } from 'redis';
export type RedisClient = ReturnType<typeof createClient>;
export type RedisCredential = {
host: string;
port: number;
ssl?: boolean;
database: number;
user?: string;
password?: string;
};

View file

@ -1,5 +1,4 @@
import type { import type {
ICredentialDataDecryptedObject,
ICredentialTestFunctions, ICredentialTestFunctions,
ICredentialsDecrypted, ICredentialsDecrypted,
IDataObject, IDataObject,
@ -7,29 +6,28 @@ import type {
INodeCredentialTestResult, INodeCredentialTestResult,
} 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'; import type { RedisCredential, RedisClient } from './types';
export type RedisClientType = ReturnType<typeof createClient>;
export function setupRedisClient(credentials: ICredentialDataDecryptedObject): RedisClientType { export function setupRedisClient(credentials: RedisCredential): RedisClient {
const redisOptions: RedisClientOptions = { return createClient({
socket: { socket: {
host: credentials.host as string, host: credentials.host,
port: credentials.port as number, port: credentials.port,
tls: credentials.ssl === true, tls: credentials.ssl === true,
}, },
database: credentials.database as number, database: credentials.database,
password: (credentials.password as string) || undefined, username: credentials.user || undefined,
}; password: credentials.password || undefined,
});
return createClient(redisOptions);
} }
export async function redisConnectionTest( export async function redisConnectionTest(
this: ICredentialTestFunctions, this: ICredentialTestFunctions,
credential: ICredentialsDecrypted, credential: ICredentialsDecrypted,
): Promise<INodeCredentialTestResult> { ): Promise<INodeCredentialTestResult> {
const credentials = credential.data as ICredentialDataDecryptedObject; const credentials = credential.data as RedisCredential;
try { try {
const client = setupRedisClient(credentials); const client = setupRedisClient(credentials);
@ -88,7 +86,7 @@ export function convertInfoToObject(stringData: string): IDataObject {
return returnData; return returnData;
} }
export async function getValue(client: RedisClientType, keyName: string, type?: string) { export async function getValue(client: RedisClient, keyName: string, type?: string) {
if (type === undefined || type === 'automatic') { if (type === undefined || type === 'automatic') {
// Request the type first // Request the type first
type = await client.type(keyName); type = await client.type(keyName);
@ -107,7 +105,7 @@ export async function getValue(client: RedisClientType, keyName: string, type?:
export async function setValue( export async function setValue(
this: IExecuteFunctions, this: IExecuteFunctions,
client: RedisClientType, client: RedisClient,
keyName: string, keyName: string,
value: string | number | object | string[] | number[], value: string | number | object | string[] | number[],
expire: boolean, expire: boolean,