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: '',
},
{
displayName: 'User',
name: 'user',
type: 'string',
default: '',
hint: 'Leave blank for password-only auth',
},
{
displayName: 'Host',
name: 'host',

View file

@ -15,6 +15,7 @@ import {
getValue,
setValue,
} from './utils';
import type { RedisCredential } from './types';
export class Redis implements INodeType {
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
// array, object via parameter directly (should maybe be possible?!?!)
// 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);
await client.connect();

View file

@ -7,6 +7,7 @@ import type {
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import { redisConnectionTest, setupRedisClient } from './utils';
import type { RedisCredential } from './types';
interface Options {
jsonParseBody: boolean;
@ -74,7 +75,7 @@ export class RedisTrigger implements INodeType {
};
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 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 { 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);
jest.mock('redis', () => ({ createClient }));
import { Redis } from '../Redis.node';
import { setupRedisClient } from '../utils';
import { redisConnectionTest, setupRedisClient } from '../utils';
import type { RedisClient } from '../types';
describe('Redis Node', () => {
const node = new Redis();
beforeEach(() => {
jest.clearAllMocks();
createClient.mockReturnValue(mockClient);
});
@ -27,7 +33,6 @@ describe('Redis Node', () => {
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
password: undefined,
socket: {
host: 'redis.domain',
port: 1234,
@ -45,7 +50,6 @@ describe('Redis Node', () => {
});
expect(createClient).toHaveBeenCalledWith({
database: 0,
password: undefined,
socket: {
host: 'redis.domain',
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', () => {

View file

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