@fastify/redis
Version:
Plugin to share a common Redis connection across Fastify.
580 lines (452 loc) • 13.8 kB
JavaScript
'use strict'
const whyIsNodeRunning = require('why-is-node-running')
const { test } = require('node:test')
const proxyquire = require('proxyquire')
const Fastify = require('fastify')
const Redis = require('ioredis')
const fastifyRedis = require('..')
test.beforeEach(async () => {
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
await fastify.redis.flushall()
await fastify.close()
})
test('fastify.redis should exist', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
t.assert.ok(fastify.redis)
await fastify.close()
})
test('fastify.redis should support url', async (t) => {
t.plan(4)
const fastify = Fastify()
const fastifyRedis = proxyquire('..', {
ioredis: function Redis (path, options) {
t.assert.deepStrictEqual(path, 'redis://127.0.0.1')
t.assert.deepStrictEqual(Object.keys(options).sort(), ['clientInfoTag', 'otherOption'])
t.assert.deepStrictEqual(options.otherOption, 'foo')
t.assert.match(options.clientInfoTag, /^fastify-redis_v\d+\.\d+\.\d+$/)
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function (name, handler) {
if (name === 'ready') {
handler(null, 'ready')
}
return this
}
this.status = 'ready'
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis, {
url: 'redis://127.0.0.1',
otherOption: 'foo'
})
await fastify.ready()
await fastify.close()
})
test('fastify.redis should be the redis client', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
await fastify.redis.set('key', 'value')
const val = await fastify.redis.get('key')
t.assert.deepStrictEqual(val, 'value')
await fastify.close()
})
test('fastify.redis.test namespace should exist', async (t) => {
t.plan(2)
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1',
namespace: 'test'
})
await fastify.ready()
t.assert.ok(fastify.redis)
t.assert.ok(fastify.redis.test)
await fastify.close()
})
test('fastify.redis.test should be the redis client', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1',
namespace: 'test'
})
await fastify.ready()
await fastify.redis.test.set('key_namespace', 'value_namespace')
const val = await fastify.redis.test.get('key_namespace')
t.assert.deepStrictEqual(val, 'value_namespace')
await fastify.close()
})
test('promises support', async (t) => {
t.plan(1)
const fastify = Fastify()
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
await fastify.redis.set('key', 'value')
const val = await fastify.redis.get('key')
t.assert.deepStrictEqual(val, 'value')
await fastify.close()
})
test('custom ioredis client that is already connected', async (t) => {
t.plan(3)
const fastify = Fastify()
const Redis = require('ioredis')
const redis = new Redis({ host: 'localhost', port: 6379 })
await redis.set('key', 'value')
const val = await redis.get('key')
t.assert.deepStrictEqual(val, 'value')
fastify.register(fastifyRedis, {
client: redis,
lazyConnect: false
})
await fastify.ready()
t.assert.deepStrictEqual(fastify.redis, redis)
await fastify.redis.set('key2', 'value2')
const val2 = await fastify.redis.get('key2')
t.assert.deepStrictEqual(val2, 'value2')
await fastify.close()
await fastify.redis.quit()
})
test('If closeClient is enabled, close the client.', async (t) => {
t.plan(4)
const fastify = Fastify()
const Redis = require('ioredis')
const redis = new Redis({ host: 'localhost', port: 6379 })
await redis.set('key', 'value')
const val = await redis.get('key')
t.assert.deepStrictEqual(val, 'value')
fastify.register(fastifyRedis, {
client: redis,
closeClient: true
})
await fastify.ready()
t.assert.deepStrictEqual(fastify.redis, redis)
await fastify.redis.set('key2', 'value2')
const val2 = await fastify.redis.get('key2')
t.assert.deepStrictEqual(val2, 'value2')
const originalQuit = fastify.redis.quit
fastify.redis.quit = (callback) => {
t.assert.ok('redis client closed')
originalQuit.call(fastify.redis, callback)
}
await fastify.close()
})
test('If closeClient is enabled, close the client namespace.', async (t) => {
t.plan(4)
const fastify = Fastify()
const Redis = require('ioredis')
const redis = new Redis({ host: 'localhost', port: 6379 })
await redis.set('key', 'value')
const val = await redis.get('key')
t.assert.deepStrictEqual(val, 'value')
fastify.register(fastifyRedis, {
client: redis,
namespace: 'foo',
closeClient: true
})
await fastify.ready()
t.assert.deepStrictEqual(fastify.redis.foo, redis)
await fastify.redis.foo.set('key2', 'value2')
const val2 = await fastify.redis.foo.get('key2')
t.assert.deepStrictEqual(val2, 'value2')
const originalQuit = fastify.redis.foo.quit
fastify.redis.foo.quit = (callback) => {
t.assert.ok('redis client closed')
originalQuit.call(fastify.redis.foo, callback)
}
await fastify.close()
})
test('fastify.redis.test should throw with duplicate connection namespaces', async (t) => {
t.plan(1)
const namespace = 'test'
const fastify = Fastify()
t.after(() => fastify.close())
fastify
.register(fastifyRedis, {
host: '127.0.0.1',
namespace
})
.register(fastifyRedis, {
host: '127.0.0.1',
namespace
})
await t.assert.rejects(fastify.ready(), new Error(`Redis '${namespace}' instance namespace has already been registered`))
})
test('Should throw when trying to register multiple instances without giving a namespace', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
fastify
.register(fastifyRedis, {
host: '127.0.0.1'
})
.register(fastifyRedis, {
host: '127.0.0.1'
})
await t.assert.rejects(fastify.ready(), new Error('@fastify/redis has already been registered'))
})
test('Should not throw within different contexts', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
fastify.register(function (instance, _options, next) {
instance.register(fastifyRedis, {
host: '127.0.0.1'
})
next()
})
fastify.register(function (instance, _options, next) {
instance
.register(fastifyRedis, {
host: '127.0.0.1',
namespace: 'test1'
})
.register(fastifyRedis, {
host: '127.0.0.1',
namespace: 'test2'
})
next()
})
await fastify.ready()
t.assert.ok(fastify)
})
// Skipped because it makes TAP crash
test('Should throw when trying to connect on an invalid host', { skip: true }, async (t) => {
t.plan(1)
const fastify = Fastify({ pluginTimeout: 20000 })
t.after(() => fastify.close())
fastify
.register(fastifyRedis, {
host: 'invalid_host'
})
await t.assert.rejects(fastify.ready())
})
test('Should successfully create a Redis client when registered with a `url` option and without a `client` option in a namespaced instance', async t => {
t.plan(2)
const fastify = Fastify()
t.after(() => fastify.close())
await fastify.register(fastifyRedis, {
url: 'redis://127.0.0.1',
namespace: 'test'
})
await fastify.ready()
t.assert.ok(fastify.redis)
t.assert.ok(fastify.redis.test)
})
test('Should be able to register multiple namespaced @fastify/redis instances', async t => {
t.plan(3)
const fastify = Fastify()
t.after(() => fastify.close())
await fastify.register(fastifyRedis, {
url: 'redis://127.0.0.1',
namespace: 'one'
})
await fastify.register(fastifyRedis, {
url: 'redis://127.0.0.1',
namespace: 'two'
})
await fastify.ready()
t.assert.ok(fastify.redis)
t.assert.ok(fastify.redis.one)
t.assert.ok(fastify.redis.two)
})
test('Should throw when @fastify/redis is initialized with an option that makes Redis throw', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
// This will throw a `TypeError: this.options.Connector is not a constructor`
fastify.register(fastifyRedis, {
Connector: 'should_fail'
})
await t.assert.rejects(fastify.ready())
})
test('Should throw when @fastify/redis is initialized with a namespace and an option that makes Redis throw', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
// This will throw a `TypeError: this.options.Connector is not a constructor`
fastify.register(fastifyRedis, {
Connector: 'should_fail',
namespace: 'fail'
})
await t.assert.rejects(fastify.ready())
})
test('catch .ping() errors', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
const fastifyRedis = proxyquire('..', {
ioredis: function Redis () {
this.ping = () => {
return Promise.reject(new Redis.ReplyError('ping error'))
}
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function () {
return this
}
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis)
await t.assert.rejects(fastify.ready(), new Redis.ReplyError('ping error'))
})
test('Should propagate SELF_SIGNED_CERT_IN_CHAIN error', async (t) => {
t.plan(1)
const fastify = Fastify()
t.after(() => fastify.close())
const fastifyRedis = proxyquire('..', {
ioredis: function Redis () {
this.ping = () => {
const error = new Error('self signed certificate in certificate chain')
error.code = 'SELF_SIGNED_CERT_IN_CHAIN'
return Promise.reject(error)
}
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function () {
return this
}
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis)
const error = new Error('self signed certificate in certificate chain')
error.code = 'SELF_SIGNED_CERT_IN_CHAIN'
await t.assert.rejects(fastify.ready(), error)
})
test('should use default clientInfoTag when not provided', async (t) => {
t.plan(1)
const fastify = Fastify()
const fastifyRedis = proxyquire('..', {
ioredis: function Redis (options) {
t.assert.match(options.clientInfoTag, /^fastify-redis_v\d+\.\d+\.\d+$/)
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function (name, handler) {
if (name === 'ready') {
handler(null, 'ready')
}
return this
}
this.status = 'ready'
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
await fastify.close()
})
test('should use custom clientInfoTag when provided', async (t) => {
t.plan(1)
const fastify = Fastify()
const fastifyRedis = proxyquire('..', {
ioredis: function Redis (options) {
t.assert.deepStrictEqual(options.clientInfoTag, 'my-custom-app')
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function (name, handler) {
if (name === 'ready') {
handler(null, 'ready')
}
return this
}
this.status = 'ready'
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis, {
host: '127.0.0.1',
clientInfoTag: 'my-custom-app'
})
await fastify.ready()
await fastify.close()
})
test('should not set clientInfoTag when custom client is provided', async (t) => {
t.plan(1)
const fastify = Fastify()
const redis = new Redis({ host: 'localhost', port: 6379 })
fastify.register(fastifyRedis, {
client: redis,
closeClient: true
})
await fastify.ready()
// Custom client should be used as-is without modification
t.assert.deepStrictEqual(fastify.redis, redis)
await fastify.close()
})
test('should use default clientInfoTag with url option', async (t) => {
t.plan(1)
const fastify = Fastify()
const fastifyRedis = proxyquire('..', {
ioredis: function Redis (_url, options) {
t.assert.match(options.clientInfoTag, /^fastify-redis_v\d+\.\d+\.\d+$/)
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function (name, handler) {
if (name === 'ready') {
handler(null, 'ready')
}
return this
}
this.status = 'ready'
this.off = function () { return this }
return this
}
})
fastify.register(fastifyRedis, {
url: 'redis://127.0.0.1'
})
await fastify.ready()
await fastify.close()
})
test('should fallback to fastify-redis when package.json version is unavailable', async (t) => {
t.plan(1)
const fastify = Fastify()
const fastifyRedis = proxyquire('..', {
ioredis: function Redis (options) {
t.assert.deepStrictEqual(options.clientInfoTag, 'fastify-redis')
this.quit = () => {}
this.info = cb => cb(null, 'info')
this.on = function (name, handler) {
if (name === 'ready') {
handler(null, 'ready')
}
return this
}
this.status = 'ready'
this.off = function () { return this }
return this
},
'./package.json': null
})
fastify.register(fastifyRedis, {
host: '127.0.0.1'
})
await fastify.ready()
await fastify.close()
})
setInterval(() => {
whyIsNodeRunning()
}, 5000).unref()