diff --git a/lib/Redis.ts b/lib/Redis.ts index 88febf6f..65abb4f9 100644 --- a/lib/Redis.ts +++ b/lib/Redis.ts @@ -189,115 +189,122 @@ class Redis extends Commander implements DataHandledable { const { options } = this; - this.condition = { - select: options.db, - auth: options.username - ? [options.username, options.password] - : options.password, - subscriber: false, - }; - - const _this = this; - asCallback( - this.connector.connect(function (type, err) { - _this.silentEmit(type, err); - }) as Promise, - function (err: Error | null, stream?: NetStream) { - if (err) { - _this.flushQueue(err); - _this.silentEmit("error", err); - reject(err); - _this.setStatus("end"); - return; - } - let CONNECT_EVENT = options.tls ? "secureConnect" : "connect"; - if ( - "sentinels" in options && - options.sentinels && - !options.enableTLSForSentinelMode - ) { - CONNECT_EVENT = "connect"; - } + this.resolvePassword().then((resolvedPassword) => { + this.condition = { + select: options.db, + auth: options.username + ? [options.username, resolvedPassword] + : resolvedPassword, + subscriber: false, + }; + + const _this = this; + asCallback( + this.connector.connect(function (type, err) { + _this.silentEmit(type, err); + }) as Promise, + function (err: Error | null, stream?: NetStream) { + if (err) { + _this.flushQueue(err); + _this.silentEmit("error", err); + reject(err); + _this.setStatus("end"); + return; + } + let CONNECT_EVENT = options.tls ? "secureConnect" : "connect"; + if ( + "sentinels" in options && + options.sentinels && + !options.enableTLSForSentinelMode + ) { + CONNECT_EVENT = "connect"; + } - _this.stream = stream; + _this.stream = stream; - if (options.noDelay) { - stream.setNoDelay(true); - } + if (options.noDelay) { + stream.setNoDelay(true); + } - // Node ignores setKeepAlive before connect, therefore we wait for the event: - // https://github.com/nodejs/node/issues/31663 - if (typeof options.keepAlive === "number") { - if (stream.connecting) { - stream.once(CONNECT_EVENT, () => { + // Node ignores setKeepAlive before connect, therefore we wait for the event: + // https://github.com/nodejs/node/issues/31663 + if (typeof options.keepAlive === "number") { + if (stream.connecting) { + stream.once(CONNECT_EVENT, () => { + stream.setKeepAlive(true, options.keepAlive); + }); + } else { stream.setKeepAlive(true, options.keepAlive); - }); - } else { - stream.setKeepAlive(true, options.keepAlive); + } } - } - if (stream.connecting) { - stream.once(CONNECT_EVENT, eventHandler.connectHandler(_this)); - - if (options.connectTimeout) { - /* - * Typically, Socket#setTimeout(0) will clear the timer - * set before. However, in some platforms (Electron 3.x~4.x), - * the timer will not be cleared. So we introduce a variable here. - * - * See https://github.com/electron/electron/issues/14915 - */ - let connectTimeoutCleared = false; - stream.setTimeout(options.connectTimeout, function () { - if (connectTimeoutCleared) { - return; - } - stream.setTimeout(0); - stream.destroy(); - - const err = new Error("connect ETIMEDOUT"); - // @ts-expect-error - err.errorno = "ETIMEDOUT"; - // @ts-expect-error - err.code = "ETIMEDOUT"; - // @ts-expect-error - err.syscall = "connect"; - eventHandler.errorHandler(_this)(err); - }); - stream.once(CONNECT_EVENT, function () { - connectTimeoutCleared = true; - stream.setTimeout(0); - }); + if (stream.connecting) { + stream.once(CONNECT_EVENT, eventHandler.connectHandler(_this)); + + if (options.connectTimeout) { + /* + * Typically, Socket#setTimeout(0) will clear the timer + * set before. However, in some platforms (Electron 3.x~4.x), + * the timer will not be cleared. So we introduce a variable here. + * + * See https://github.com/electron/electron/issues/14915 + */ + let connectTimeoutCleared = false; + stream.setTimeout(options.connectTimeout, function () { + if (connectTimeoutCleared) { + return; + } + stream.setTimeout(0); + stream.destroy(); + + const err = new Error("connect ETIMEDOUT"); + // @ts-expect-error + err.errorno = "ETIMEDOUT"; + // @ts-expect-error + err.code = "ETIMEDOUT"; + // @ts-expect-error + err.syscall = "connect"; + eventHandler.errorHandler(_this)(err); + }); + stream.once(CONNECT_EVENT, function () { + connectTimeoutCleared = true; + stream.setTimeout(0); + }); + } + } else if (stream.destroyed) { + const firstError = _this.connector.firstError; + if (firstError) { + process.nextTick(() => { + eventHandler.errorHandler(_this)(firstError); + }); + } + process.nextTick(eventHandler.closeHandler(_this)); + } else { + process.nextTick(eventHandler.connectHandler(_this)); } - } else if (stream.destroyed) { - const firstError = _this.connector.firstError; - if (firstError) { - process.nextTick(() => { - eventHandler.errorHandler(_this)(firstError); - }); + if (!stream.destroyed) { + stream.once("error", eventHandler.errorHandler(_this)); + stream.once("close", eventHandler.closeHandler(_this)); } - process.nextTick(eventHandler.closeHandler(_this)); - } else { - process.nextTick(eventHandler.connectHandler(_this)); - } - if (!stream.destroyed) { - stream.once("error", eventHandler.errorHandler(_this)); - stream.once("close", eventHandler.closeHandler(_this)); - } - const connectionReadyHandler = function () { - _this.removeListener("close", connectionCloseHandler); - resolve(); - }; - var connectionCloseHandler = function () { - _this.removeListener("ready", connectionReadyHandler); - reject(new Error(CONNECTION_CLOSED_ERROR_MSG)); - }; - _this.once("ready", connectionReadyHandler); - _this.once("close", connectionCloseHandler); - } - ); + const connectionReadyHandler = function () { + _this.removeListener("close", connectionCloseHandler); + resolve(); + }; + var connectionCloseHandler = function () { + _this.removeListener("ready", connectionReadyHandler); + reject(new Error(CONNECTION_CLOSED_ERROR_MSG)); + }; + _this.once("ready", connectionReadyHandler); + _this.once("close", connectionCloseHandler); + } + ); + }).catch((err) => { + this.flushQueue(err); + this.silentEmit("error", err); + reject(err); + this.setStatus("end"); + }); }); return asCallback(promise, callback); @@ -858,6 +865,17 @@ class Redis extends Commander implements DataHandledable { } }).catch(noop); } + + private async resolvePassword(): Promise { + const { password } = this.options; + if (!password) { + return null; + } + if (typeof password === "function") { + return await password(); + } + return password; + } } interface Redis extends EventEmitter { diff --git a/lib/cluster/util.ts b/lib/cluster/util.ts index 4b12b955..41f853ed 100644 --- a/lib/cluster/util.ts +++ b/lib/cluster/util.ts @@ -9,7 +9,7 @@ export interface RedisOptions { port: number; host: string; username?: string; - password?: string; + password?: string | (() => Promise | string); [key: string]: any; } diff --git a/lib/connectors/SentinelConnector/index.ts b/lib/connectors/SentinelConnector/index.ts index 0047a665..2993af6c 100644 --- a/lib/connectors/SentinelConnector/index.ts +++ b/lib/connectors/SentinelConnector/index.ts @@ -42,7 +42,7 @@ export interface SentinelConnectionOptions { role?: "master" | "slave"; tls?: ConnectionOptions; sentinelUsername?: string; - sentinelPassword?: string; + sentinelPassword?: string | (() => Promise | string); sentinels?: Array>; sentinelRetryStrategy?: (retryAttempts: number) => number | void | null; sentinelReconnectStrategy?: (retryAttempts: number) => number | void | null; @@ -294,15 +294,27 @@ export default class SentinelConnector extends AbstractConnector { return result; } - private connectToSentinel( + private async resolveSentinelPassword(): Promise { + const { sentinelPassword } = this.options; + if (!sentinelPassword) { + return null; + } + if (typeof sentinelPassword === "function") { + return await sentinelPassword(); + } + return sentinelPassword; + } + + private async connectToSentinel( endpoint: Partial, options?: Partial - ): RedisClient { + ): Promise { + const resolvedPassword = await this.resolveSentinelPassword(); const redis = new Redis({ port: endpoint.port || 26379, host: endpoint.host, username: this.options.sentinelUsername || null, - password: this.options.sentinelPassword || null, + password: resolvedPassword, family: endpoint.family || // @ts-expect-error @@ -324,7 +336,7 @@ export default class SentinelConnector extends AbstractConnector { private async resolve( endpoint: Partial ): Promise { - const client = this.connectToSentinel(endpoint); + const client = await this.connectToSentinel(endpoint); // ignore the errors since resolve* methods will handle them client.on("error", noop); @@ -357,7 +369,7 @@ export default class SentinelConnector extends AbstractConnector { break; } - const client = this.connectToSentinel(value, { + const client = await this.connectToSentinel(value, { lazyConnect: true, retryStrategy: this.options.sentinelReconnectStrategy, }); diff --git a/lib/redis/RedisOptions.ts b/lib/redis/RedisOptions.ts index a397c538..9ec53d80 100644 --- a/lib/redis/RedisOptions.ts +++ b/lib/redis/RedisOptions.ts @@ -52,8 +52,9 @@ export interface CommonRedisOptions extends CommanderOptions { /** * If set, client will send AUTH command with the value of this option when connected. + * Can be a string or a function that returns a string or Promise. */ - password?: string; + password?: string | (() => Promise | string); /** * Database index to use. diff --git a/test/functional/auth.ts b/test/functional/auth.ts index 08227f7f..4f0c18d0 100644 --- a/test/functional/auth.ts +++ b/test/functional/auth.ts @@ -1,282 +1,427 @@ import MockServer from "../helpers/mock_server"; -import { expect } from "chai"; +import {expect} from "chai"; import Redis from "../../lib/Redis"; import * as sinon from "sinon"; describe("auth", () => { - /* General non-Redis-version specific tests */ - it("should send auth before other commands", (done) => { - let authed = false; - new MockServer(17379, (argv) => { - if (argv[0] === "auth" && argv[1] === "pass") { - authed = true; - } else if (argv[0] === "get" && argv[1] === "foo") { - expect(authed).to.eql(true); - redis.disconnect(); - done(); - } + /* General non-Redis-version specific tests */ + it("should send auth before other commands", (done) => { + let authed = false; + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "pass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({port: 17379, password: "pass"}); + redis.get("foo").catch(() => { + }); }); - const redis = new Redis({ port: 17379, password: "pass" }); - redis.get("foo").catch(() => {}); - }); - it("should resend auth after reconnect", (done) => { - let begin = false; - let authed = false; - new MockServer(17379, (argv) => { - if (!begin) { - return; - } - if (argv[0] === "auth" && argv[1] === "pass") { - authed = true; - } else if (argv[0] === "get" && argv[1] === "foo") { - expect(authed).to.eql(true); - redis.disconnect(); - done(); - } + it("should resend auth after reconnect", (done) => { + let begin = false; + let authed = false; + new MockServer(17379, (argv) => { + if (!begin) { + return; + } + if (argv[0] === "auth" && argv[1] === "pass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({port: 17379, password: "pass"}); + redis.once("ready", () => { + begin = true; + redis.disconnect(true); + redis.get("foo").catch(() => { + }); + }); }); - const redis = new Redis({ port: 17379, password: "pass" }); - redis.once("ready", () => { - begin = true; - redis.disconnect(true); - redis.get("foo").catch(() => {}); - }); - }); - describe("auth:redis5-specific", () => { - it("should handle auth with Redis URL string (redis://:foo@bar.com/) correctly", (done) => { - const password = "pass"; - let redis; - new MockServer(17379, (argv) => { - if (argv[0] === "auth" && argv[1] === password) { - redis.disconnect(); - done(); - } - }); - redis = new Redis(`redis://:${password}@localhost:17379/`); - }); + describe("auth:redis5-specific", () => { + it("should handle auth with Redis URL string (redis://:foo@bar.com/) correctly", (done) => { + const password = "pass"; + let redis; + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === password) { + redis.disconnect(); + done(); + } + }); + redis = new Redis(`redis://:${password}@localhost:17379/`); + }); - it('should not emit "error" when the server doesn\'t need auth', (done) => { - new MockServer(17379, (argv) => { - if (argv[0] === "auth" && argv[1] === "pass") { - return new Error("ERR Client sent AUTH, but no password is set"); - } - }); - let errorEmitted = false; - const redis = new Redis({ port: 17379, password: "pass" }); - redis.on("error", () => { - errorEmitted = true; - }); - const stub = sinon.stub(console, "warn").callsFake((warn) => { - if (warn.indexOf("but a password was supplied") !== -1) { - stub.restore(); - setTimeout(() => { - expect(errorEmitted).to.eql(false); - redis.disconnect(); - done(); - }, 0); - } - }); - }); + it('should not emit "error" when the server doesn\'t need auth', (done) => { + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "pass") { + return new Error("ERR Client sent AUTH, but no password is set"); + } + }); + let errorEmitted = false; + const redis = new Redis({port: 17379, password: "pass"}); + redis.on("error", () => { + errorEmitted = true; + }); + const stub = sinon.stub(console, "warn").callsFake((warn) => { + if (warn.indexOf("but a password was supplied") !== -1) { + stub.restore(); + setTimeout(() => { + expect(errorEmitted).to.eql(false); + redis.disconnect(); + done(); + }, 0); + } + }); + }); - it('should emit "error" when the password is wrong', (done) => { - new MockServer(17379, (argv) => { - if (argv[0] === "auth" && argv[1] === "pass") { - return new Error("ERR invalid password"); - } - }); - const redis = new Redis({ port: 17379, password: "pass" }); - let pending = 2; - function check() { - if (!--pending) { - redis.disconnect(); - done(); - } - } - redis.on("error", (error) => { - expect(error).to.have.property("message", "ERR invalid password"); - check(); - }); - redis.get("foo", function (err, res) { - expect(err.message).to.eql("ERR invalid password"); - check(); - }); - }); + it('should emit "error" when the password is wrong', (done) => { + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "pass") { + return new Error("ERR invalid password"); + } + }); + const redis = new Redis({port: 17379, password: "pass"}); + let pending = 2; - it('should emit "error" when password is not provided', (done) => { - new MockServer(17379, (argv) => { - if (argv[0] === "info") { - return new Error("NOAUTH Authentication required."); - } - }); - const redis = new Redis({ port: 17379 }); - redis.on("error", (error) => { - expect(error).to.have.property( - "message", - "NOAUTH Authentication required." - ); - redis.disconnect(); - done(); - }); - }); + function check() { + if (!--pending) { + redis.disconnect(); + done(); + } + } - it('should emit "error" when username and password are set for a Redis 5 server', (done) => { - let username = "user"; - let password = "password"; + redis.on("error", (error) => { + expect(error).to.have.property("message", "ERR invalid password"); + check(); + }); + redis.get("foo", function (err, res) { + expect(err.message).to.eql("ERR invalid password"); + check(); + }); + }); - new MockServer(17379, (argv) => { - if ( - argv[0] === "auth" && - argv[1] === username && - argv[2] === password - ) { - return new Error("ERR wrong number of arguments for 'auth' command"); - } - }); + it('should emit "error" when password is not provided', (done) => { + new MockServer(17379, (argv) => { + if (argv[0] === "info") { + return new Error("NOAUTH Authentication required."); + } + }); + const redis = new Redis({port: 17379}); + redis.on("error", (error) => { + expect(error).to.have.property( + "message", + "NOAUTH Authentication required." + ); + redis.disconnect(); + done(); + }); + }); - const redis = new Redis({ port: 17379, username, password }); - const stub = sinon.stub(console, "warn").callsFake((warn) => { - if ( - warn.indexOf( - "You are probably passing both username and password to Redis version 5 or below" - ) !== -1 - ) { - stub.restore(); - setTimeout(() => { - redis.disconnect(); - done(); - }, 0); - } - }); - }); - }); + it('should emit "error" when username and password are set for a Redis 5 server', (done) => { + let username = "user"; + let password = "password"; - describe("auth:redis6-specific", () => { - /*Redis 6 specific tests */ - it("should handle username and password auth (Redis >=6) correctly", (done) => { - let username = "user"; - let password = "pass"; - let redis; - new MockServer(17379, (argv) => { - if ( - argv[0] === "auth" && - argv[1] === username && - argv[2] === password - ) { - redis.disconnect(); - done(); - } - }); - redis = new Redis({ port: 17379, username, password }); - }); + new MockServer(17379, (argv) => { + if ( + argv[0] === "auth" && + argv[1] === username && + argv[2] === password + ) { + return new Error("ERR wrong number of arguments for 'auth' command"); + } + }); - it("should handle auth with Redis URL string with username and password (Redis >=6) (redis://foo:bar@baz.com/) correctly", (done) => { - let username = "user"; - let password = "pass"; - let redis; - new MockServer(17379, (argv) => { - if ( - argv[0] === "auth" && - argv[1] === username && - argv[2] === password - ) { - redis.disconnect(); - done(); - } - }); - redis = new Redis( - `redis://user:pass@localhost:17379/?allowUsernameInURI=true` - ); + const redis = new Redis({port: 17379, username, password}); + const stub = sinon.stub(console, "warn").callsFake((warn) => { + if ( + warn.indexOf( + "You are probably passing both username and password to Redis version 5 or below" + ) !== -1 + ) { + stub.restore(); + setTimeout(() => { + redis.disconnect(); + done(); + }, 0); + } + }); + }); }); - it('should not emit "error" when the Redis >=6 server doesn\'t need auth', (done) => { - new MockServer(17379, (argv) => { - if (argv[0] === "auth" && argv[1] === "pass") { - return new Error( - "ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?" - ); - } - }); - let errorEmited = false; - const redis = new Redis({ port: 17379, password: "pass" }); - redis.on("error", () => { - errorEmited = true; - }); - const stub = sinon.stub(console, "warn").callsFake((warn) => { - if (warn.indexOf("`default` user does not require a password") !== -1) { - stub.restore(); - setTimeout(() => { - expect(errorEmited).to.eql(false); - redis.disconnect(); - done(); - }, 0); - } - }); - }); + describe("auth:redis6-specific", () => { + /*Redis 6 specific tests */ + it("should handle username and password auth (Redis >=6) correctly", (done) => { + let username = "user"; + let password = "pass"; + let redis; + new MockServer(17379, (argv) => { + if ( + argv[0] === "auth" && + argv[1] === username && + argv[2] === password + ) { + redis.disconnect(); + done(); + } + }); + redis = new Redis({port: 17379, username, password}); + }); - it('should emit "error" when passing username but not password to Redis >=6 instance', (done) => { - let username = "user"; - let password = "pass"; - let redis; - new MockServer(17379, (argv) => { - if (argv[0] === "auth") { - if (argv[1] === username && argv[2] === password) { - return "OK"; - } else { - return new Error("WRONGPASS invalid username-password pair"); - } - } - }); - redis = new Redis({ port: 17379, username }); - redis.on("error", (error) => { - expect(error).to.have.property( - "message", - "WRONGPASS invalid username-password pair" - ); - redis.disconnect(); - done(); - }); - }); + it("should handle auth with Redis URL string with username and password (Redis >=6) (redis://foo:bar@baz.com/) correctly", (done) => { + let username = "user"; + let password = "pass"; + let redis; + new MockServer(17379, (argv) => { + if ( + argv[0] === "auth" && + argv[1] === username && + argv[2] === password + ) { + redis.disconnect(); + done(); + } + }); + redis = new Redis( + `redis://user:pass@localhost:17379/?allowUsernameInURI=true` + ); + }); + + it('should not emit "error" when the Redis >=6 server doesn\'t need auth', (done) => { + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "pass") { + return new Error( + "ERR AUTH called without any password configured for the default user. Are you sure your configuration is correct?" + ); + } + }); + let errorEmited = false; + const redis = new Redis({port: 17379, password: "pass"}); + redis.on("error", () => { + errorEmited = true; + }); + const stub = sinon.stub(console, "warn").callsFake((warn) => { + if (warn.indexOf("`default` user does not require a password") !== -1) { + stub.restore(); + setTimeout(() => { + expect(errorEmited).to.eql(false); + redis.disconnect(); + done(); + }, 0); + } + }); + }); - it('should emit "error" when the password is wrong', (done) => { - let username = "user"; - let password = "pass"; - let redis; - new MockServer(17379, (argv) => { - if (argv[0] === "auth") { - if (argv[1] === username && argv[2] === password) { - return "OK"; - } else { - return new Error("WRONGPASS invalid username-password pair"); - } - } - }); - redis = new Redis({ port: 17379, username, password: "notpass" }); - redis.on("error", (error) => { - expect(error).to.have.property( - "message", - "WRONGPASS invalid username-password pair" - ); - redis.disconnect(); - done(); - }); + it('should emit "error" when passing username but not password to Redis >=6 instance', (done) => { + let username = "user"; + let password = "pass"; + let redis; + new MockServer(17379, (argv) => { + if (argv[0] === "auth") { + if (argv[1] === username && argv[2] === password) { + return "OK"; + } else { + return new Error("WRONGPASS invalid username-password pair"); + } + } + }); + redis = new Redis({port: 17379, username}); + redis.on("error", (error) => { + expect(error).to.have.property( + "message", + "WRONGPASS invalid username-password pair" + ); + redis.disconnect(); + done(); + }); + }); + + it('should emit "error" when the password is wrong', (done) => { + let username = "user"; + let password = "pass"; + let redis; + new MockServer(17379, (argv) => { + if (argv[0] === "auth") { + if (argv[1] === username && argv[2] === password) { + return "OK"; + } else { + return new Error("WRONGPASS invalid username-password pair"); + } + } + }); + redis = new Redis({port: 17379, username, password: "notpass"}); + redis.on("error", (error) => { + expect(error).to.have.property( + "message", + "WRONGPASS invalid username-password pair" + ); + redis.disconnect(); + done(); + }); + }); + + it('should emit "error" when password is required but not provided', (done) => { + new MockServer(17379, (argv) => { + if (argv[0] === "info") { + return new Error("NOAUTH Authentication required."); + } + }); + const redis = new Redis({port: 17379}); + redis.on("error", (error) => { + expect(error).to.have.property( + "message", + "NOAUTH Authentication required." + ); + redis.disconnect(); + done(); + }); + }); }); - it('should emit "error" when password is required but not provided', (done) => { - new MockServer(17379, (argv) => { - if (argv[0] === "info") { - return new Error("NOAUTH Authentication required."); - } - }); - const redis = new Redis({ port: 17379 }); - redis.on("error", (error) => { - expect(error).to.have.property( - "message", - "NOAUTH Authentication required." - ); - redis.disconnect(); - done(); - }); + describe("password function support", () => { + it("should support static string password (baseline)", (done) => { + let authed = false; + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "staticpass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({port: 17379, password: "staticpass"}); + redis.get("foo").catch(() => { + }); + }); + + it("should support sync function password", (done) => { + let authed = false; + let callCount = 0; + const passwordFunction = () => { + callCount++; + return "syncpass"; + }; + + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "syncpass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + expect(callCount).to.eql(1); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({port: 17379, password: passwordFunction}); + redis.get("foo").catch(() => { + }); + }); + + it("should support async function password", (done) => { + let authed = false; + let callCount = 0; + const passwordFunction = async (): Promise => { + callCount++; + return new Promise(resolve => resolve("asyncpass")); + }; + + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "asyncpass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + expect(callCount).to.eql(1); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({port: 17379, password: passwordFunction}); + redis.get("foo").catch(() => { + }); + }); + + it("should call password function on each reconnect", (done) => { + let callCount = 0; + const passwordFunction = () => { + callCount++; + return "reconnectpass"; + }; + + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "reconnectpass") { + if (callCount >= 2) { + expect(callCount).to.be.at.least(2); + redis.disconnect(); + done(); + } + } + }); + + const redis = new Redis({port: 17379, password: passwordFunction}); + redis.once("ready", () => { + redis.disconnect(true); + redis.connect(); + }); + }); + + it("should support sync function password with username", (done) => { + let authed = false; + let callCount = 0; + const passwordFunction = () => { + callCount++; + return "userpass"; + }; + + new MockServer(17379, (argv) => { + if (argv[0] === "auth" && argv[1] === "testuser" && argv[2] === "userpass") { + authed = true; + } else if (argv[0] === "get" && argv[1] === "foo") { + expect(authed).to.eql(true); + expect(callCount).to.eql(1); + redis.disconnect(); + done(); + } + }); + const redis = new Redis({ + port: 17379, + username: "testuser", + password: passwordFunction + }); + redis.get("foo").catch(() => { + }); + }); + + it("should handle password function errors gracefully", (done) => { + const passwordFunction = () => { + throw new Error("Password retrieval failed"); + }; + + const redis = new Redis({port: 17379, password: passwordFunction}); + redis.on("error", (error) => { + expect(error.message).to.eql("Password retrieval failed"); + redis.disconnect(); + done(); + }); + }); + + it("should handle async password function errors gracefully", (done) => { + const passwordFunction = async () => { + throw new Error("Async password retrieval failed"); + }; + + const redis = new Redis({port: 17379, password: passwordFunction}); + redis.on("error", (error) => { + expect(error.message).to.eql("Async password retrieval failed"); + redis.disconnect(); + done(); + }); + }); }); - }); }); diff --git a/test/functional/sentinel.ts b/test/functional/sentinel.ts index 25a15479..6e8cf087 100644 --- a/test/functional/sentinel.ts +++ b/test/functional/sentinel.ts @@ -802,4 +802,101 @@ describe("sentinel", () => { }); }); }); + + describe("sentinel password function support", () => { + it("should support static string sentinelPassword (baseline)", (done) => { + let sentinelAuthed = false; + const sentinel = new MockServer(27379, (argv) => { + if (argv[0] === "auth" && argv[1] === "staticsentinelpass") { + sentinelAuthed = true; + } else if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { + expect(sentinelAuthed).to.eql(true); + sentinel.disconnect(); + redis.disconnect(); + done(); + return ["127.0.0.1", "17380"]; + } + }); + + const redis = new Redis({ + sentinelPassword: "staticsentinelpass", + sentinels: [{ host: "127.0.0.1", port: 27379 }], + name: "master", + }); + }); + + it("should support sync function sentinelPassword", (done) => { + let sentinelAuthed = false; + let callCount = 0; + const passwordFunction = () => { + callCount++; + return "syncsentinelpass"; + }; + + const sentinel = new MockServer(27379, (argv) => { + if (argv[0] === "auth" && argv[1] === "syncsentinelpass") { + sentinelAuthed = true; + } else if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { + expect(sentinelAuthed).to.eql(true); + expect(callCount).to.eql(1); + sentinel.disconnect(); + redis.disconnect(); + done(); + return ["127.0.0.1", "17380"]; + } + }); + + const redis = new Redis({ + sentinelPassword: passwordFunction, + sentinels: [{ host: "127.0.0.1", port: 27379 }], + name: "master", + }); + }); + + it("should support async function sentinelPassword", (done) => { + let sentinelAuthed = false; + let callCount = 0; + const passwordFunction = async () => { + callCount++; + return new Promise(resolve => setTimeout(() => resolve("asyncsentinelpass"), 10)); + }; + + const sentinel = new MockServer(27379, (argv) => { + if (argv[0] === "auth" && argv[1] === "asyncsentinelpass") { + sentinelAuthed = true; + } else if (argv[0] === "sentinel" && argv[1] === "get-master-addr-by-name") { + expect(sentinelAuthed).to.eql(true); + expect(callCount).to.eql(1); + sentinel.disconnect(); + redis.disconnect(); + done(); + return ["127.0.0.1", "17380"]; + } + }); + + const redis = new Redis({ + sentinelPassword: passwordFunction, + sentinels: [{ host: "127.0.0.1", port: 27379 }], + name: "master", + }); + }); + + it("should handle sentinelPassword function errors gracefully", (done) => { + const passwordFunction = () => { + throw new Error("Sentinel password retrieval failed"); + }; + + const redis = new Redis({ + sentinelPassword: passwordFunction, + sentinels: [{ host: "127.0.0.1", port: 27379 }], + name: "master", + }); + + redis.on("error", (error) => { + expect(error.message).to.include("Sentinel password retrieval failed"); + redis.disconnect(); + done(); + }); + }); + }); });