diff --git a/packages/nodes-base/credentials/Redis.credentials.ts b/packages/nodes-base/credentials/Redis.credentials.ts index 194c2f59e3..9d9bbab394 100644 --- a/packages/nodes-base/credentials/Redis.credentials.ts +++ b/packages/nodes-base/credentials/Redis.credentials.ts @@ -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', diff --git a/packages/nodes-base/nodes/Redis/Redis.node.ts b/packages/nodes-base/nodes/Redis/Redis.node.ts index ccd826eb89..014c28d3d6 100644 --- a/packages/nodes-base/nodes/Redis/Redis.node.ts +++ b/packages/nodes-base/nodes/Redis/Redis.node.ts @@ -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('redis'); const client = setupRedisClient(credentials); await client.connect(); diff --git a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts index 34b4bb8d62..680d19b026 100644 --- a/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts +++ b/packages/nodes-base/nodes/Redis/RedisTrigger.node.ts @@ -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 { - const credentials = await this.getCredentials('redis'); + const credentials = await this.getCredentials('redis'); const channels = (this.getNodeParameter('channels') as string).split(','); const options = this.getNodeParameter('options') as Options; diff --git a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts b/packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts similarity index 83% rename from packages/nodes-base/nodes/Redis/test/Redis.node.test.ts rename to packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts index 6339459eb1..789aef5242 100644 --- a/packages/nodes-base/nodes/Redis/test/Redis.node.test.ts +++ b/packages/nodes-base/nodes/Redis/__tests__/Redis.node.test.ts @@ -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(); +const mockClient = mock(); 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({}); + const credentials = mock({ + 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', () => { diff --git a/packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts b/packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts similarity index 97% rename from packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts rename to packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts index eadad5fe90..97227fbe04 100644 --- a/packages/nodes-base/nodes/Redis/test/RedisTrigger.node.test.ts +++ b/packages/nodes-base/nodes/Redis/__tests__/RedisTrigger.node.test.ts @@ -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(); + const mockRedisClient = mock(); return { setupRedisClient: jest.fn().mockReturnValue(mockRedisClient), }; diff --git a/packages/nodes-base/nodes/Redis/types.ts b/packages/nodes-base/nodes/Redis/types.ts new file mode 100644 index 0000000000..63e873acce --- /dev/null +++ b/packages/nodes-base/nodes/Redis/types.ts @@ -0,0 +1,12 @@ +import type { createClient } from 'redis'; + +export type RedisClient = ReturnType; + +export type RedisCredential = { + host: string; + port: number; + ssl?: boolean; + database: number; + user?: string; + password?: string; +}; diff --git a/packages/nodes-base/nodes/Redis/utils.ts b/packages/nodes-base/nodes/Redis/utils.ts index effdd99778..4364bab892 100644 --- a/packages/nodes-base/nodes/Redis/utils.ts +++ b/packages/nodes-base/nodes/Redis/utils.ts @@ -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; +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 { - 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,