diff --git a/packages/backend/test/misc/FakeRedis.ts b/packages/backend/test/misc/FakeRedis.ts new file mode 100644 index 0000000000..c9384490fe --- /dev/null +++ b/packages/backend/test/misc/FakeRedis.ts @@ -0,0 +1,145 @@ +/* + * SPDX-FileCopyrightText: hazelnoot and other Sharkey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Redis from 'ioredis'; + +export type RedisKey = Redis.RedisKey; +export type RedisString = Buffer | string; +export type RedisNumber = string | number; +export type RedisValue = RedisKey | RedisString | RedisNumber; +export type RedisCallback = Redis.Callback; + +export type Ok = 'OK'; +export const ok = 'OK' as const; + +export type FakeRedis = Redis.Redis; +export interface FakeRedisConstructor { + new (): FakeRedis; +} + +/** + * Fake implementation of Redis that pretends to connect but throws on any operation. + */ +export const FakeRedis: FakeRedisConstructor = createFakeRedis(); + +function createFakeRedis(): FakeRedisConstructor { + class FakeRedis implements Partial { + async connect(callback?: RedisCallback): Promise { + // no-op + callback?.(null); + } + + async hello(...callbacks: (undefined | string | number | Buffer | RedisCallback)[]): Promise { + // no-op + const callback = callbacks.find(c => typeof(c) === 'function'); + callback?.(null, []); + return []; + } + + async auth(...callbacks: (undefined | string | Buffer | RedisCallback)[]): Promise { + const callback = callbacks.find(c => typeof(c) === 'function'); + callback?.(null, ok); + return ok; + } + + async quit(callback?: RedisCallback) { + // no-op + callback?.(null, ok); + return ok; + } + + async save(callback?: RedisCallback) { + // no-op + callback?.(null, ok); + return ok; + } + + async sync(callback?: RedisCallback) { + // no-op + callback?.(null, ok); + return ok; + } + + disconnect(): void { + // no-op + } + + end(): void { + // no-op + } + } + + const fakeProto = FakeRedis.prototype as Partial; + const redisProto = Redis.Redis.prototype as Partial; + + // Override all methods and accessors from Redis + for (const [key, property] of allProps(redisProto)) { + // Skip anything already defined + if (Reflect.has(fakeProto, key)) { + continue; + } + + if (property.get || property.set) { + // Stub accessors + Reflect.defineProperty(fakeProto, key, { + ...property, + get: property.get ? stub(property.get.name || key) : undefined, + set: property.set ? stub(property.set.name || key) : undefined, + }); + } else if (property.value && typeof(property.value) === 'function') { + // Stub methods + Reflect.defineProperty(fakeProto, key, { + ...property, + value: stub(property.value.name || key), + }); + } + } + + // Fixup protoype + Reflect.setPrototypeOf(fakeProto, redisProto); + + // test + const test = new FakeRedis(); + if (!(test instanceof Redis.Redis)) { + throw new Error('failed to extend'); + } + + return FakeRedis as FakeRedisConstructor; +} + +function *allProps(obj: object | null): Generator<[PropertyKey, PropertyDescriptor]> { + while (obj != null) { + for (const key of Reflect.ownKeys(obj)) { + const prop = Reflect.getOwnPropertyDescriptor(obj, key); + if (prop) { + yield [key, prop]; + } + } + + obj = Object.getPrototypeOf(obj); + } +} + +function stub(name: PropertyKey) { + if (typeof(name) === 'symbol') { + name = `[symbol.${name.description || ''}]`; + } else if (typeof(name) === 'number') { + name = String(name); + } + + const stub = () => { + throw new Error(`Not Implemented: MockRedis does not support ${name}`); + }; + + // Make the stub match the original name + Object.defineProperty(stub, 'name', { + writable: false, + enumerable: false, + configurable: true, + value: name, + }); + + return stub; +}