import { randomUUIDv7, RedisClient, spawn } from "bun"; import { beforeAll, beforeEach, describe, expect, test } from "bun:test"; import { bunExe, bunRun } from "harness"; import { join } from "node:path"; import { ctx as _ctx, awaitableCounter, ConnectionType, createClient, DEFAULT_REDIS_URL, expectType, isEnabled, randomCoinFlip, setupDockerContainer, TLS_REDIS_OPTIONS, TLS_REDIS_URL, } from "./test-utils"; import type { RedisTestStartMessage } from "./valkey.failing-subscriber"; import type { Message } from "./valkey.failing-subscriber-no-ipc"; for (const connectionType of [ConnectionType.TLS, ConnectionType.TCP]) { const ctx = { ..._ctx, redis: connectionType ? _ctx.redis : (_ctx.redisTLS as RedisClient) }; describe.skipIf(!isEnabled)(`Valkey Redis Client (${connectionType})`, () => { beforeAll(async () => { await setupDockerContainer(); if (!ctx.redis) { ctx.redis = createClient(connectionType); } }); beforeEach(async () => { if (!ctx.redis) { ctx.redis = createClient(connectionType); } await ctx.redis.connect(); await ctx.redis.send("FLUSHALL", ["SYNC"]); }); describe("Basic Operations", () => { test("should keep process alive when connecting", async () => { const result = bunRun(join(import.meta.dir, "valkey.connecting.fixture.ts"), { "BUN_VALKEY_URL": connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, "BUN_VALKEY_TLS": connectionType === ConnectionType.TLS ? JSON.stringify(TLS_REDIS_OPTIONS.tlsPaths) : "", }); expect(result.stdout).toContain(`connected`); }); test("should set and get strings", async () => { const redis = ctx.redis; const testKey = "greeting"; const testValue = "Hello from Bun Redis!"; const setResult = await redis.set(testKey, testValue); expect(setResult).toMatchInlineSnapshot(`"OK"`); const setResult2 = await redis.set(testKey, testValue, "GET"); expect(setResult2).toMatchInlineSnapshot(`"${testValue}"`); const getValue = await redis.get(testKey); expect(getValue).toMatchInlineSnapshot(`"${testValue}"`); }); test("should test key existence", async () => { const redis = ctx.redis; await redis.set("greeting", "test existence"); const exists = await redis.exists("greeting"); expect(exists).toBeDefined(); expect(exists).toBe(true); const randomKey = "nonexistent-key-" + randomUUIDv7(); const notExists = await redis.exists(randomKey); expect(notExists).toBeDefined(); expect(notExists).toBe(false); }); test("should increment and decrement counters", async () => { const redis = ctx.redis; const counterKey = "counter"; await redis.set(counterKey, "10"); const incrementedValue = await redis.incr(counterKey); expect(incrementedValue).toBeDefined(); expect(typeof incrementedValue).toBe("number"); expect(incrementedValue).toBe(11); const decrementedValue = await redis.decr(counterKey); expect(decrementedValue).toBeDefined(); expect(typeof decrementedValue).toBe("number"); expect(decrementedValue).toBe(10); }); test("should increment by specified amount with INCRBY", async () => { const redis = ctx.redis; const counterKey = "incrby-counter"; await redis.set(counterKey, "5"); const result1 = await redis.incrby(counterKey, 10); expect(result1).toBe(15); const result2 = await redis.incrby(counterKey, -3); expect(result2).toBe(12); const result3 = await redis.incrby("new-incrby-key", 5); expect(result3).toBe(5); }); test("should increment by float amount with INCRBYFLOAT", async () => { const redis = ctx.redis; const floatKey = "float-counter"; await redis.set(floatKey, "10.5"); const result1 = await redis.incrbyfloat(floatKey, 2.3); expect(result1).toBe("12.8"); const result2 = await redis.incrbyfloat(floatKey, -0.8); expect(result2).toBe("12"); const result3 = await redis.incrbyfloat("new-float-key", 3.14); expect(result3).toBe("3.14"); }); test("should decrement by specified amount with DECRBY", async () => { const redis = ctx.redis; const counterKey = "decrby-counter"; await redis.set(counterKey, "20"); const result1 = await redis.decrby(counterKey, 5); expect(result1).toBe(15); const result2 = await redis.decrby(counterKey, 10); expect(result2).toBe(5); const result3 = await redis.decrby("new-decrby-key", 3); expect(result3).toBe(-3); }); test("should rename a key with RENAME", async () => { const redis = ctx.redis; const oldKey = "old-key"; const newKey = "new-key"; const value = "test-value"; await redis.set(oldKey, value); const result = await redis.rename(oldKey, newKey); expect(result).toBe("OK"); const newValue = await redis.get(newKey); expect(newValue).toBe(value); const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); test("should rename a key with RENAME overwriting existing key", async () => { const redis = ctx.redis; const oldKey = "old-key-overwrite"; const newKey = "new-key-overwrite"; await redis.set(oldKey, "old-value"); await redis.set(newKey, "existing-value"); const result = await redis.rename(oldKey, newKey); expect(result).toBe("OK"); const newValue = await redis.get(newKey); expect(newValue).toBe("old-value"); const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); test("should rename a key only if new key does not exist with RENAMENX", async () => { const redis = ctx.redis; const oldKey = "old-key-nx"; const newKey = "new-key-nx"; const value = "test-value"; await redis.set(oldKey, value); const result1 = await redis.renamenx(oldKey, newKey); expect(result1).toBe(1); const newValue = await redis.get(newKey); expect(newValue).toBe(value); const oldValue = await redis.get(oldKey); expect(oldValue).toBeNull(); }); test("should not rename if new key exists with RENAMENX", async () => { const redis = ctx.redis; const oldKey = "old-key-nx-fail"; const newKey = "new-key-nx-fail"; await redis.set(oldKey, "old-value"); await redis.set(newKey, "existing-value"); const result = await redis.renamenx(oldKey, newKey); expect(result).toBe(0); const oldValue = await redis.get(oldKey); expect(oldValue).toBe("old-value"); const newValue = await redis.get(newKey); expect(newValue).toBe("existing-value"); }); test("should set multiple keys with MSET", async () => { const redis = ctx.redis; const result = await redis.mset("mset-key1", "value1", "mset-key2", "value2", "mset-key3", "value3"); expect(result).toBe("OK"); const value1 = await redis.get("mset-key1"); expect(value1).toBe("value1"); const value2 = await redis.get("mset-key2"); expect(value2).toBe("value2"); const value3 = await redis.get("mset-key3"); expect(value3).toBe("value3"); }); test("should set multiple keys only if none exist with MSETNX", async () => { const redis = ctx.redis; const result1 = await redis.msetnx("msetnx-key1", "value1", "msetnx-key2", "value2"); expect(result1).toBe(1); const value1 = await redis.get("msetnx-key1"); expect(value1).toBe("value1"); const value2 = await redis.get("msetnx-key2"); expect(value2).toBe("value2"); const result2 = await redis.msetnx("msetnx-key1", "newvalue", "msetnx-key3", "value3"); expect(result2).toBe(0); const unchangedValue = await redis.get("msetnx-key1"); expect(unchangedValue).toBe("value1"); const nonExistentKey = await redis.get("msetnx-key3"); expect(nonExistentKey).toBeNull(); }); test("should manage key expiration", async () => { const redis = ctx.redis; const tempKey = "temporary"; await redis.set(tempKey, "will expire"); const result = await redis.expire(tempKey, 60); expect(result).toMatchInlineSnapshot(`1`); const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); }); test("should set key with expiration using SETEX", async () => { const redis = ctx.redis; const key = "setex-test-key"; const value = "test-value"; const result = await redis.setex(key, 10, value); expect(result).toBe("OK"); const getValue = await redis.get(key); expect(getValue).toBe(value); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(10); }); test("should set key with expiration using PSETEX", async () => { const redis = ctx.redis; const key = "psetex-test-key"; const value = "test-value"; const result = await redis.psetex(key, 5000, value); expect(result).toBe("OK"); const getValue = await redis.get(key); expect(getValue).toBe(value); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5000); }); test("should set expiration with EXPIREAT using Unix timestamp", async () => { const redis = ctx.redis; const key = "expireat-test-key"; await redis.set(key, "test-value"); const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.expireat(key, futureTimestamp); expect(result).toBe(1); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); }); test("should return 0 for EXPIREAT on non-existent key", async () => { const redis = ctx.redis; const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.expireat("nonexistent-expireat-key", futureTimestamp); expect(result).toBe(0); }); test("should set expiration with PEXPIRE in milliseconds", async () => { const redis = ctx.redis; const key = "pexpire-test-key"; await redis.set(key, "test-value"); const result = await redis.pexpire(key, 5000); expect(result).toBe(1); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5050); }); test("should return 0 for PEXPIRE on non-existent key", async () => { const redis = ctx.redis; const result = await redis.pexpire("nonexistent-pexpire-key", 5000); expect(result).toBe(0); }); test("should set expiration with PEXPIREAT using Unix timestamp in milliseconds", async () => { const redis = ctx.redis; const key = "pexpireat-test-key"; await redis.set(key, "test-value"); const futureTimestampMs = Date.now() + 5000; const result = await redis.pexpireat(key, futureTimestampMs); expect(result).toBe(1); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5050); }); test("should return 0 for PEXPIREAT on non-existent key", async () => { const redis = ctx.redis; const futureTimestampMs = Date.now() + 5000; const result = await redis.pexpireat("nonexistent-pexpireat-key", futureTimestampMs); expect(result).toBe(0); }); test("should determine the type of a key with TYPE", async () => { const redis = ctx.redis; await redis.set("string-key", "value"); const stringType = await redis.type("string-key"); expect(stringType).toBe("string"); await redis.lpush("list-key", "value"); const listType = await redis.type("list-key"); expect(listType).toBe("list"); await redis.sadd("set-key", "value"); const setType = await redis.type("set-key"); expect(setType).toBe("set"); await redis.send("HSET", ["hash-key", "field", "value"]); const hashType = await redis.type("hash-key"); expect(hashType).toBe("hash"); const noneType = await redis.type("nonexistent-key"); expect(noneType).toBe("none"); }); test("should update last access time with TOUCH", async () => { const redis = ctx.redis; await redis.set("touch-key1", "value1"); await redis.set("touch-key2", "value2"); const touchedCount = await redis.touch("touch-key1", "touch-key2"); expect(touchedCount).toBe(2); const mixedCount = await redis.touch("touch-key1", "nonexistent-key"); expect(mixedCount).toBe(1); const noneCount = await redis.touch("nonexistent-key1", "nonexistent-key2"); expect(noneCount).toBe(0); }); test("should get and set bits", async () => { const redis = ctx.redis; const bitKey = "mybitkey"; const oldValue = await redis.setbit(bitKey, 7, 1); expect(oldValue).toBe(0); const bitValue = await redis.getbit(bitKey, 7); expect(bitValue).toBe(1); const unsetBit = await redis.getbit(bitKey, 100); expect(unsetBit).toBe(0); const oldValue2 = await redis.setbit(bitKey, 7, 0); expect(oldValue2).toBe(1); const bitValue2 = await redis.getbit(bitKey, 7); expect(bitValue2).toBe(0); }); test("should handle multiple bit operations", async () => { const redis = ctx.redis; const bitKey = "multibit"; await redis.setbit(bitKey, 0, 1); await redis.setbit(bitKey, 3, 1); await redis.setbit(bitKey, 7, 1); expect(await redis.getbit(bitKey, 0)).toBe(1); expect(await redis.getbit(bitKey, 1)).toBe(0); expect(await redis.getbit(bitKey, 2)).toBe(0); expect(await redis.getbit(bitKey, 3)).toBe(1); expect(await redis.getbit(bitKey, 4)).toBe(0); expect(await redis.getbit(bitKey, 5)).toBe(0); expect(await redis.getbit(bitKey, 6)).toBe(0); expect(await redis.getbit(bitKey, 7)).toBe(1); const count = await redis.bitcount(bitKey); expect(count).toBe(3); }); test("should get range of string", async () => { const redis = ctx.redis; const key = "rangetest"; await redis.set(key, "Hello World"); const result1 = await redis.getrange(key, 0, 4); expect(result1).toBe("Hello"); const result2 = await redis.getrange(key, 6, 10); expect(result2).toBe("World"); const result3 = await redis.getrange(key, -5, -1); expect(result3).toBe("World"); const result4 = await redis.getrange(key, 0, -1); expect(result4).toBe("Hello World"); }); test("should set range of string", async () => { const redis = ctx.redis; const key = "setrangetest"; await redis.set(key, "Hello World"); const newLength = await redis.setrange(key, 6, "Redis"); expect(newLength).toBe(11); const result = await redis.get(key); expect(result).toBe("Hello Redis"); const key2 = "newkey"; const newLength2 = await redis.setrange(key2, 5, "Redis"); expect(newLength2).toBeGreaterThanOrEqual(10); }); test("should append to string with APPEND", async () => { const redis = ctx.redis; const key = "append-test"; const len1 = await redis.append(key, "Hello"); expect(len1).toBe(5); const len2 = await redis.append(key, " World"); expect(len2).toBe(11); const value = await redis.get(key); expect(value).toBe("Hello World"); }); test("should delete keys with DEL", async () => { const redis = ctx.redis; await redis.set("del-key1", "value1"); await redis.set("del-key2", "value2"); await redis.set("del-key3", "value3"); const count1 = await redis.del("del-key1"); expect(count1).toBe(1); const value1 = await redis.get("del-key1"); expect(value1).toBeNull(); const count2 = await redis.del("del-key2", "del-key3"); expect(count2).toBe(2); const count3 = await redis.del("nonexistent"); expect(count3).toBe(0); }); test("should serialize key with DUMP", async () => { const redis = ctx.redis; const key = "dump-test"; await redis.set(key, "test-value"); const serialized = await redis.dump(key); expect(serialized).toBeDefined(); expect(serialized).not.toBeNull(); const empty = await redis.dump("nonexistent"); expect(empty).toBeNull(); }); test("should get value as Buffer with getBuffer", async () => { const redis = ctx.redis; const key = "getbuffer-test"; await redis.set(key, "test-value"); const buffer = await redis.getBuffer(key); expect(buffer).toBeInstanceOf(Buffer); expect(buffer?.toString()).toBe("test-value"); const empty = await redis.getBuffer("nonexistent"); expect(empty).toBeNull(); }); test("should get and delete with GETDEL", async () => { const redis = ctx.redis; const key = "getdel-test"; await redis.set(key, "test-value"); const value = await redis.getdel(key); expect(value).toBe("test-value"); const deleted = await redis.get(key); expect(deleted).toBeNull(); const empty = await redis.getdel("nonexistent"); expect(empty).toBeNull(); }); test("should get and set expiration with GETEX", async () => { const redis = ctx.redis; const key = "getex-test"; await redis.set(key, "test-value"); const value1 = await redis.getex(key, "EX", 60); expect(value1).toBe("test-value"); const ttl1 = await redis.ttl(key); expect(ttl1).toBeGreaterThan(0); expect(ttl1).toBeLessThanOrEqual(60); const value2 = await redis.getex(key, "PX", 5000); expect(value2).toBe("test-value"); const pttl = await redis.pttl(key); expect(pttl).toBeGreaterThan(0); expect(pttl).toBeLessThanOrEqual(5000); const empty = await redis.getex("nonexistent", "EX", 60); expect(empty).toBeNull(); }); test("should get old value and set new with GETSET", async () => { const redis = ctx.redis; const key = "getset-test"; const old1 = await redis.getset(key, "value1"); expect(old1).toBeNull(); const old2 = await redis.getset(key, "value2"); expect(old2).toBe("value1"); const current = await redis.get(key); expect(current).toBe("value2"); }); test("should get string length with STRLEN", async () => { const redis = ctx.redis; const key = "strlen-test"; const len1 = await redis.strlen(key); expect(len1).toBe(0); await redis.set(key, "Hello"); const len2 = await redis.strlen(key); expect(len2).toBe(5); await redis.set(key, "Hello World"); const len3 = await redis.strlen(key); expect(len3).toBe(11); }); test("should get substring with SUBSTR", async () => { const redis = ctx.redis; const key = "substr-test"; await redis.set(key, "Hello World"); const result = await redis.substr(key, 0, 4); expect(result).toBe("Hello"); }); test("should get expiration time with EXPIRETIME", async () => { const redis = ctx.redis; const key = "expiretime-test"; await redis.set(key, "value"); const futureTs = Math.floor(Date.now() / 1000) + 60; await redis.expireat(key, futureTs); const expireTime = await redis.expiretime(key); expect(expireTime).toBeGreaterThan(0); expect(expireTime).toBeLessThanOrEqual(futureTs); const key2 = "no-expire"; await redis.set(key2, "value"); const noExpire = await redis.expiretime(key2); expect(noExpire).toBe(-1); const nonExist = await redis.expiretime("nonexistent"); expect(nonExist).toBe(-2); }); test("should get expiration time in ms with PEXPIRETIME", async () => { const redis = ctx.redis; const key = "pexpiretime-test"; await redis.set(key, "value"); const futureTs = Date.now() + 5000; await redis.pexpireat(key, futureTs); const pexpireTime = await redis.pexpiretime(key); expect(pexpireTime).toBeGreaterThan(0); expect(pexpireTime).toBeLessThanOrEqual(futureTs); const key2 = "no-expire"; await redis.set(key2, "value"); const noExpire = await redis.pexpiretime(key2); expect(noExpire).toBe(-1); const nonExist = await redis.pexpiretime("nonexistent"); expect(nonExist).toBe(-2); }); test("should remove expiration with PERSIST", async () => { const redis = ctx.redis; const key = "persist-test"; await redis.set(key, "value"); await redis.expire(key, 60); const ttlBefore = await redis.ttl(key); expect(ttlBefore).toBeGreaterThan(0); const result = await redis.persist(key); expect(result).toBe(1); const ttlAfter = await redis.ttl(key); expect(ttlAfter).toBe(-1); const result2 = await redis.persist(key); expect(result2).toBe(0); const result3 = await redis.persist("nonexistent"); expect(result3).toBe(0); }); test("should get multiple values with MGET", async () => { const redis = ctx.redis; await redis.set("mget-key1", "value1"); await redis.set("mget-key2", "value2"); await redis.set("mget-key3", "value3"); const values = await redis.mget("mget-key1", "mget-key2", "mget-key3"); expect(values).toEqual(["value1", "value2", "value3"]); const mixed = await redis.mget("mget-key1", "nonexistent", "mget-key2"); expect(mixed).toEqual(["value1", null, "value2"]); const allNull = await redis.mget("none1", "none2", "none3"); expect(allNull).toEqual([null, null, null]); }); test("should set only if not exists with SETNX", async () => { const redis = ctx.redis; const key = "setnx-test"; const result1 = await redis.setnx(key, "value1"); expect(result1).toBe(1); const value1 = await redis.get(key); expect(value1).toBe("value1"); const result2 = await redis.setnx(key, "value2"); expect(result2).toBe(0); const value2 = await redis.get(key); expect(value2).toBe("value1"); }); test("should add to HyperLogLog with PFADD", async () => { const redis = ctx.redis; const key = "pfadd-test"; const result1 = await redis.pfadd(key, "element1"); expect(result1).toBe(1); const result2 = await redis.pfadd(key, "element2"); expect(result2).toBe(1); const result3 = await redis.pfadd(key, "element1"); expect(result3).toBe(0); }); test("should implement TTL command correctly for different cases", async () => { const redis = ctx.redis; const tempKey = "ttl-test-key"; await redis.set(tempKey, "ttl test value"); await redis.expire(tempKey, 60); const ttl = await redis.ttl(tempKey); expectType(ttl, "number"); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); const permanentKey = "permanent-key"; await redis.set(permanentKey, "no expiry"); const noExpiry = await redis.ttl(permanentKey); expect(noExpiry).toMatchInlineSnapshot(`-1`); const nonExistentKey = "non-existent-" + randomUUIDv7(); const noKey = await redis.ttl(nonExistentKey); expect(noKey).toMatchInlineSnapshot(`-2`); }); test("should copy a key to a new key with COPY", async () => { const redis = ctx.redis; const sourceKey = "copy-source"; const destKey = "copy-dest"; await redis.set(sourceKey, "Hello World"); const result = await redis.copy(sourceKey, destKey); expect(result).toBe(1); const sourceValue = await redis.get(sourceKey); const destValue = await redis.get(destKey); expect(sourceValue).toBe("Hello World"); expect(destValue).toBe("Hello World"); const result2 = await redis.copy(sourceKey, destKey); expect(result2).toBe(0); }); test("should copy a key with REPLACE option", async () => { const redis = ctx.redis; const sourceKey = "copy-replace-source"; const destKey = "copy-replace-dest"; await redis.set(sourceKey, "New Value"); await redis.set(destKey, "Old Value"); const result = await redis.copy(sourceKey, destKey, "REPLACE"); expect(result).toBe(1); const destValue = await redis.get(destKey); expect(destValue).toBe("New Value"); }); test("should unlink one or more keys asynchronously with UNLINK", async () => { const redis = ctx.redis; await redis.set("unlink-key1", "value1"); await redis.set("unlink-key2", "value2"); await redis.set("unlink-key3", "value3"); const result = await redis.unlink("unlink-key1", "unlink-key2", "unlink-key3"); expect(result).toBe(3); expect(await redis.get("unlink-key1")).toBeNull(); expect(await redis.get("unlink-key2")).toBeNull(); expect(await redis.get("unlink-key3")).toBeNull(); }); test("should unlink with non-existent keys", async () => { const redis = ctx.redis; await redis.set("unlink-exists", "value"); const result = await redis.unlink("unlink-exists", "unlink-nonexist1", "unlink-nonexist2"); expect(result).toBe(1); expect(await redis.get("unlink-exists")).toBeNull(); }); test("should return a random key with RANDOMKEY", async () => { const redis = ctx.redis; const emptyResult = await redis.randomkey(); expect(emptyResult).toBeNull(); await redis.set("random-key1", "value1"); await redis.set("random-key2", "value2"); await redis.set("random-key3", "value3"); const randomKey = await redis.randomkey(); expect(randomKey).toBeDefined(); expect(randomKey).not.toBeNull(); expect(["random-key1", "random-key2", "random-key3"]).toContain(randomKey); const value = await redis.get(randomKey!); expect(value).toBeDefined(); }); test("should iterate keys with SCAN", async () => { const redis = ctx.redis; const testKeys = ["scan-test:1", "scan-test:2", "scan-test:3", "scan-test:4", "scan-test:5"]; for (const key of testKeys) { await redis.set(key, "value"); } let cursor = "0"; const foundKeys: string[] = []; do { const [nextCursor, keys] = await redis.scan(cursor); foundKeys.push(...keys); cursor = nextCursor; } while (cursor !== "0"); for (const testKey of testKeys) { expect(foundKeys).toContain(testKey); } }); test("should iterate keys with SCAN and MATCH pattern", async () => { const redis = ctx.redis; await redis.set("user:1", "alice"); await redis.set("user:2", "bob"); await redis.set("post:1", "hello"); await redis.set("post:2", "world"); let cursor = "0"; const userKeys: string[] = []; do { const [nextCursor, keys] = await redis.scan(cursor, "MATCH", "user:*"); userKeys.push(...keys); cursor = nextCursor; } while (cursor !== "0"); expect(userKeys).toContain("user:1"); expect(userKeys).toContain("user:2"); expect(userKeys).not.toContain("post:1"); expect(userKeys).not.toContain("post:2"); }); test("should reject invalid object argument in SCAN", async () => { const redis = ctx.redis; expect(async () => { await redis.scan({} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); }); test("should reject invalid array argument in SCAN", async () => { const redis = ctx.redis; expect(async () => { await redis.scan([] as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); }); test("should reject invalid null argument in SCAN", async () => { const redis = ctx.redis; expect(async () => { await redis.scan(null as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'scan'."`); }); test("should reject invalid source key in COPY", async () => { const redis = ctx.redis; expect(async () => { await redis.copy({} as any, "dest"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); }); test("should reject invalid destination key in COPY", async () => { const redis = ctx.redis; expect(async () => { await redis.copy("source", [] as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'copy'."`); }); test("should reject invalid option in COPY", async () => { const redis = ctx.redis; await redis.set("copy-invalid-opt-source", "value"); expect(async () => { await redis.copy("copy-invalid-opt-source", "copy-invalid-opt-dest", "NOTVALID" as any); }).toThrowErrorMatchingInlineSnapshot(`"ERR syntax error"`); }); test("should reject invalid old key in RENAME", async () => { const redis = ctx.redis; expect(async () => { await redis.rename({} as any, "newkey"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'rename'."`); }); test("should reject invalid new key in RENAME", async () => { const redis = ctx.redis; expect(async () => { await redis.rename("oldkey", null as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected newkey to be a string or buffer for 'rename'."`); }); test("should reject invalid key in GETRANGE", async () => { const redis = ctx.redis; expect(async () => { await redis.getrange({} as any, 0, 5); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'getrange'."`); }); test("should reject invalid key in SETRANGE", async () => { const redis = ctx.redis; expect(async () => { await redis.setrange(undefined as any, 0, "value"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setrange'."`); }); test("should reject invalid key in INCRBY", async () => { const redis = ctx.redis; expect(async () => { await redis.incrby([] as any, 10); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'incrby'."`); }); test("should reject invalid value in MSET", async () => { const redis = ctx.redis; expect(async () => { await redis.mset("key", {} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'mset'."`); }); test("should reject invalid value in MSETNX", async () => { const redis = ctx.redis; expect(async () => { await redis.msetnx("key1", "value1", "key2", [] as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'msetnx'."`); }); test("should reject invalid key in SETBIT", async () => { const redis = ctx.redis; expect(async () => { await redis.setbit({} as any, 0, 1); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setbit'."`); }); test("should reject invalid key in SETEX", async () => { const redis = ctx.redis; expect(async () => { await redis.setex(null as any, 10, "value"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'setex'."`); }); test("should reject invalid key in PSETEX", async () => { const redis = ctx.redis; expect(async () => { await redis.psetex([] as any, 1000, "value"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'psetex'."`); }); test("should reject invalid key in UNLINK", async () => { const redis = ctx.redis; expect(async () => { await redis.unlink({} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); }); test("should reject invalid additional key in UNLINK", async () => { const redis = ctx.redis; expect(async () => { await redis.unlink("valid-key", [] as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'unlink'."`); }); test("should reject invalid key in TOUCH", async () => { const redis = ctx.redis; expect(async () => { await redis.touch(null as any); }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); }); test("should reject invalid additional key in TOUCH", async () => { const redis = ctx.redis; expect(async () => { await redis.touch("valid-key", {} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'touch'."`); }); test("should reject invalid key in EXPIREAT", async () => { const redis = ctx.redis; expect(async () => { await redis.expireat({} as any, 1234567890); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'expireat'."`); }); test("should reject invalid key in PEXPIRE", async () => { const redis = ctx.redis; expect(async () => { await redis.pexpire([] as any, 5000); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpire'."`); }); test("should reject invalid key in PEXPIREAT", async () => { const redis = ctx.redis; expect(async () => { await redis.pexpireat(null as any, 1234567890000); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'pexpireat'."`); }); }); describe("String commands", () => { test("should append value to key with APPEND", async () => { const redis = ctx.redis; const key = "append-test"; const length1 = await redis.append(key, "Hello"); expect(length1).toBe(5); const value1 = await redis.get(key); expect(value1).toBe("Hello"); const length2 = await redis.append(key, " World"); expect(length2).toBe(11); const value2 = await redis.get(key); expect(value2).toBe("Hello World"); }); test("should delete one or more keys with DEL", async () => { const redis = ctx.redis; await redis.set("del-test-1", "value1"); await redis.set("del-test-2", "value2"); await redis.set("del-test-3", "value3"); const result1 = await redis.del("del-test-1"); expect(result1).toBe(1); expect(await redis.get("del-test-1")).toBeNull(); const result2 = await redis.del("del-test-2", "del-test-3"); expect(result2).toBe(2); expect(await redis.get("del-test-2")).toBeNull(); expect(await redis.get("del-test-3")).toBeNull(); const result3 = await redis.del("del-test-nonexistent"); expect(result3).toBe(0); }); test("should serialize key with DUMP", async () => { const redis = ctx.redis; const key = "dump-test"; await redis.set(key, "Hello World"); const serialized = await redis.dump(key); expect(serialized).toBeDefined(); expect(serialized).not.toBeNull(); expect(typeof serialized === "string" || Buffer.isBuffer(serialized)).toBe(true); const nonExistent = await redis.dump("dump-test-nonexistent"); expect(nonExistent).toBeNull(); }); test("should get value as Buffer with getBuffer", async () => { const redis = ctx.redis; const key = "getbuffer-test"; await redis.set(key, "Hello Buffer"); const buffer = await redis.getBuffer(key); expect(buffer).toBeDefined(); expect(buffer).not.toBeNull(); expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer!.toString()).toBe("Hello Buffer"); const nonExistent = await redis.getBuffer("getbuffer-nonexistent"); expect(nonExistent).toBeNull(); }); test("should get and delete key with GETDEL", async () => { const redis = ctx.redis; const key = "getdel-test"; await redis.set(key, "Delete me"); const value = await redis.getdel(key); expect(value).toBe("Delete me"); const check = await redis.get(key); expect(check).toBeNull(); const nonExistent = await redis.getdel("getdel-nonexistent"); expect(nonExistent).toBeNull(); }); test("should get key with expiration using GETEX with EX", async () => { const redis = ctx.redis; const key = "getex-ex-test"; await redis.set(key, "Expire me"); const value = await redis.getex(key, "EX", 10); expect(value).toBe("Expire me"); const check = await redis.get(key); expect(check).toBe("Expire me"); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(10); }); test("should get key with expiration using GETEX with PX", async () => { const redis = ctx.redis; const key = "getex-px-test"; await redis.set(key, "Expire me"); const value = await redis.getex(key, "PX", 5000); expect(value).toBe("Expire me"); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(5); }); test("should get key with expiration using GETEX with EXAT", async () => { const redis = ctx.redis; const key = "getex-exat-test"; await redis.set(key, "Expire at timestamp"); const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const value = await redis.getex(key, "EXAT", futureTimestamp); expect(value).toBe("Expire at timestamp"); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); }); test("should get key with expiration using GETEX with PXAT", async () => { const redis = ctx.redis; const key = "getex-pxat-test"; await redis.set(key, "Expire at timestamp"); const futureTimestamp = Date.now() + 60000; const value = await redis.getex(key, "PXAT", futureTimestamp); expect(value).toBe("Expire at timestamp"); const ttl = await redis.ttl(key); expect(ttl).toBeGreaterThan(0); expect(ttl).toBeLessThanOrEqual(60); }); test("should persist key expiration using GETEX with PERSIST", async () => { const redis = ctx.redis; const key = "getex-persist-test"; await redis.set(key, "Remove expiration", "EX", 100); const ttlBefore = await redis.ttl(key); expect(ttlBefore).toBeGreaterThan(0); const value = await redis.getex(key, "PERSIST"); expect(value).toBe("Remove expiration"); const ttlAfter = await redis.ttl(key); expect(ttlAfter).toBe(-1); }); test("should get non-existent key with GETEX", async () => { const redis = ctx.redis; const value = await redis.getex("getex-nonexistent", "EX", 10); expect(value).toBeNull(); }); test("should get and set in one operation with GETSET", async () => { const redis = ctx.redis; const key = "getset-test"; const oldValue1 = await redis.getset(key, "new value"); expect(oldValue1).toBeNull(); const check1 = await redis.get(key); expect(check1).toBe("new value"); const oldValue2 = await redis.getset(key, "newer value"); expect(oldValue2).toBe("new value"); const check2 = await redis.get(key); expect(check2).toBe("newer value"); }); test("should get string length with STRLEN", async () => { const redis = ctx.redis; const key = "strlen-test"; const length1 = await redis.strlen("strlen-nonexistent"); expect(length1).toBe(0); await redis.set(key, "Hello World"); const length2 = await redis.strlen(key); expect(length2).toBe(11); await redis.set(key, "Hi"); const length3 = await redis.strlen(key); expect(length3).toBe(2); }); }); describe("List Operations", () => { test("should get list length with LLEN", async () => { const redis = ctx.redis; const key = "llen-test"; const len1 = await redis.llen(key); expect(len1).toBe(0); await redis.lpush(key, "one", "two", "three"); const len2 = await redis.llen(key); expect(len2).toBe(3); await redis.lpop(key); const len3 = await redis.llen(key); expect(len3).toBe(2); }); test("should pop left with LPOP", async () => { const redis = ctx.redis; const key = "lpop-test"; await redis.lpush(key, "three", "two", "one"); const elem1 = await redis.lpop(key); expect(elem1).toBe("one"); const elem2 = await redis.lpop(key, 2); expect(elem2).toEqual(["two", "three"]); const empty = await redis.lpop(key); expect(empty).toBeNull(); }); test("should push to existing list with LPUSHX", async () => { const redis = ctx.redis; const key = "lpushx-test"; const len1 = await redis.lpushx(key, "value"); expect(len1).toBe(0); await redis.lpush(key, "one"); const len2 = await redis.lpushx(key, "two"); expect(len2).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["two", "one"]); }); test("should pop right with RPOP", async () => { const redis = ctx.redis; const key = "rpop-test"; await redis.rpush(key, "one", "two", "three"); const elem1 = await redis.rpop(key); expect(elem1).toBe("three"); const elem2 = await redis.rpop(key, 2); expect(elem2).toEqual(["two", "one"]); const empty = await redis.rpop(key); expect(empty).toBeNull(); }); test("should push to existing list with RPUSHX", async () => { const redis = ctx.redis; const key = "rpushx-test"; const len1 = await redis.rpushx(key, "value"); expect(len1).toBe(0); await redis.rpush(key, "one"); const len2 = await redis.rpushx(key, "two"); expect(len2).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["one", "two"]); }); test("should get range of elements with LRANGE", async () => { const redis = ctx.redis; const key = "lrange-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["one", "two", "three"]); const firstTwo = await redis.lrange(key, 0, 1); expect(firstTwo).toEqual(["one", "two"]); const lastTwo = await redis.lrange(key, -2, -1); expect(lastTwo).toEqual(["two", "three"]); const middle = await redis.lrange(key, 1, 1); expect(middle).toEqual(["two"]); const outOfRange = await redis.lrange(key, 10, 20); expect(outOfRange).toEqual([]); const nonExistent = await redis.lrange("nonexistent-list", 0, -1); expect(nonExistent).toEqual([]); }); test("should get element at index with LINDEX", async () => { const redis = ctx.redis; const key = "lindex-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const first = await redis.lindex(key, 0); expect(first).toBe("one"); const second = await redis.lindex(key, 1); expect(second).toBe("two"); const third = await redis.lindex(key, 2); expect(third).toBe("three"); const last = await redis.lindex(key, -1); expect(last).toBe("three"); const secondLast = await redis.lindex(key, -2); expect(secondLast).toBe("two"); const outOfRange = await redis.lindex(key, 10); expect(outOfRange).toBeNull(); const outOfRangeNeg = await redis.lindex(key, -10); expect(outOfRangeNeg).toBeNull(); const nonExistent = await redis.lindex("nonexistent-list", 0); expect(nonExistent).toBeNull(); }); test("should set element at index with LSET", async () => { const redis = ctx.redis; const key = "lset-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const result1 = await redis.lset(key, 0, "zero"); expect(result1).toBe("OK"); const first = await redis.lindex(key, 0); expect(first).toBe("zero"); const result2 = await redis.lset(key, -1, "last"); expect(result2).toBe("OK"); const last = await redis.lindex(key, -1); expect(last).toBe("last"); const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["zero", "two", "last"]); }); test("should handle LSET errors", async () => { const redis = ctx.redis; await redis.lpush("lset-error-test", "value"); expect(async () => { await redis.lset("lset-error-test", 10, "newvalue"); }).toThrow(/index out of range/i); expect(async () => { await redis.lset("nonexistent-list", 0, "value"); }).toThrow(/no such key/i); await redis.set("string-key", "value"); expect(async () => { await redis.lset("string-key", 0, "value"); }).toThrow(/wrong.*type|WRONGTYPE/i); }); test("should handle LRANGE with various ranges", async () => { const redis = ctx.redis; const key = "lrange-advanced"; for (let i = 5; i >= 1; i--) { await redis.lpush(key, String(i)); } const fullList = await redis.lrange(key, 0, -1); expect(fullList).toEqual(["1", "2", "3", "4", "5"]); const invalid = await redis.lrange(key, 3, 1); expect(invalid).toEqual([]); const mixed = await redis.lrange(key, -3, 4); expect(mixed).toEqual(["3", "4", "5"]); const bothNeg = await redis.lrange(key, -4, -2); expect(bothNeg).toEqual(["2", "3", "4"]); }); test("should handle LINDEX and LSET with numbers", async () => { const redis = ctx.redis; const key = "list-numbers"; await redis.lpush(key, "100"); await redis.lpush(key, "200"); await redis.lpush(key, "300"); const elem = await redis.lindex(key, 1); expect(elem).toBe("200"); await redis.lset(key, 1, "250"); const updated = await redis.lindex(key, 1); expect(updated).toBe("250"); }); test("should insert element before pivot with LINSERT", async () => { const redis = ctx.redis; const key = "linsert-before-test"; await redis.lpush(key, "World"); await redis.lpush(key, "Hello"); const result = await redis.linsert(key, "BEFORE", "World", "There"); expect(result).toBe(3); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["Hello", "There", "World"]); }); test("should insert element after pivot with LINSERT", async () => { const redis = ctx.redis; const key = "linsert-after-test"; await redis.lpush(key, "World"); await redis.lpush(key, "Hello"); const result = await redis.linsert(key, "AFTER", "Hello", "Beautiful"); expect(result).toBe(3); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["Hello", "Beautiful", "World"]); }); test("should handle LINSERT when pivot not found", async () => { const redis = ctx.redis; const key = "linsert-notfound-test"; await redis.lpush(key, "value1"); await redis.lpush(key, "value2"); const result = await redis.linsert(key, "BEFORE", "nonexistent", "newvalue"); expect(result).toBe(-1); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["value2", "value1"]); }); test("should handle LINSERT on non-existent key", async () => { const redis = ctx.redis; const result = await redis.linsert("nonexistent-list", "BEFORE", "pivot", "element"); expect(result).toBe(0); }); test("should remove elements from head with LREM", async () => { const redis = ctx.redis; const key = "lrem-positive-test"; await redis.rpush(key, "hello"); await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); const result = await redis.lrem(key, 2, "hello"); expect(result).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["world", "hello"]); }); test("should remove elements from tail with LREM", async () => { const redis = ctx.redis; const key = "lrem-negative-test"; await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); await redis.rpush(key, "hello"); const result = await redis.lrem(key, -2, "hello"); expect(result).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["hello", "world"]); }); test("should remove all occurrences with LREM count=0", async () => { const redis = ctx.redis; const key = "lrem-all-test"; await redis.rpush(key, "hello"); await redis.rpush(key, "world"); await redis.rpush(key, "hello"); await redis.rpush(key, "foo"); await redis.rpush(key, "hello"); const result = await redis.lrem(key, 0, "hello"); expect(result).toBe(3); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["world", "foo"]); }); test("should handle LREM when element not found", async () => { const redis = ctx.redis; const key = "lrem-notfound-test"; await redis.rpush(key, "value1"); await redis.rpush(key, "value2"); const result = await redis.lrem(key, 1, "nonexistent"); expect(result).toBe(0); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["value1", "value2"]); }); test("should trim list to range with LTRIM", async () => { const redis = ctx.redis; const key = "ltrim-test"; await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); await redis.rpush(key, "four"); const result = await redis.ltrim(key, 1, 2); expect(result).toBe("OK"); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["two", "three"]); }); test("should handle LTRIM with negative indexes", async () => { const redis = ctx.redis; const key = "ltrim-negative-test"; await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); await redis.rpush(key, "four"); await redis.rpush(key, "five"); const result = await redis.ltrim(key, -3, -1); expect(result).toBe("OK"); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["three", "four", "five"]); }); test("should handle LTRIM with out of range indexes", async () => { const redis = ctx.redis; const key = "ltrim-outofrange-test"; await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); const result = await redis.ltrim(key, 0, 100); expect(result).toBe("OK"); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["one", "two", "three"]); }); test("should empty list with LTRIM when stop < start", async () => { const redis = ctx.redis; const key = "ltrim-empty-test"; await redis.rpush(key, "one"); await redis.rpush(key, "two"); await redis.rpush(key, "three"); const result = await redis.ltrim(key, 2, 0); expect(result).toBe("OK"); const list = await redis.lrange(key, 0, -1); expect(list).toEqual([]); }); test("should block and pop element with BLPOP", async () => { const redis = ctx.redis; const key = "blpop-test"; await redis.lpush(key, "value1"); const result = await redis.blpop(key, 0.1); expect(result).toEqual([key, "value1"]); const timeout = await redis.blpop(key, 0.1); expect(timeout).toBeNull(); }); test("should block and pop element with BRPOP", async () => { const redis = ctx.redis; const key = "brpop-test"; await redis.lpush(key, "value2"); await redis.lpush(key, "value1"); const result = await redis.brpop(key, 0.1); expect(result).toEqual([key, "value2"]); await redis.brpop(key, 0.1); const timeout = await redis.brpop(key, 0.1); expect(timeout).toBeNull(); }); test("should pop from first non-empty list with BLPOP", async () => { const redis = ctx.redis; const key1 = "blpop-list1"; const key2 = "blpop-list2"; await redis.lpush(key2, "value2"); const result = await redis.blpop(key1, key2, 0.1); expect(result).toEqual([key2, "value2"]); }); test("should pop elements with LMPOP LEFT", async () => { const redis = ctx.redis; const key = "lmpop-left-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const result = await redis.lmpop(1, key, "LEFT"); expect(result).toEqual([key, ["one"]]); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["two", "three"]); }); test("should pop elements with LMPOP RIGHT", async () => { const redis = ctx.redis; const key = "lmpop-right-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const result = await redis.lmpop(1, key, "RIGHT"); expect(result).toEqual([key, ["three"]]); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["one", "two"]); }); test("should pop multiple elements with LMPOP COUNT", async () => { const redis = ctx.redis; const key = "lmpop-count-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const result = await redis.lmpop(1, key, "LEFT", "COUNT", 2); expect(result).toEqual([key, ["one", "two"]]); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["three"]); }); test("should return null for LMPOP on empty list", async () => { const redis = ctx.redis; const result = await redis.lmpop(1, "nonexistent-list", "LEFT"); expect(result).toBeNull(); }); test("should pop from first non-empty list with LMPOP", async () => { const redis = ctx.redis; const key1 = "lmpop-empty"; const key2 = "lmpop-full"; await redis.lpush(key2, "value"); const result = await redis.lmpop(2, key1, key2, "LEFT"); expect(result).toEqual([key2, ["value"]]); }); test("should find position of element with LPOS", async () => { const redis = ctx.redis; const key = "lpos-test"; await redis.lpush(key, "d"); await redis.lpush(key, "b"); await redis.lpush(key, "c"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); const pos1 = await redis.lpos(key, "b"); expect(pos1).toBe(1); const pos2 = await redis.lpos(key, "a"); expect(pos2).toBe(0); const pos3 = await redis.lpos(key, "d"); expect(pos3).toBe(4); const pos4 = await redis.lpos(key, "x"); expect(pos4).toBeNull(); }); test("should find position with RANK option in LPOS", async () => { const redis = ctx.redis; const key = "lpos-rank-test"; await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); const first = await redis.lpos(key, "b"); expect(first).toBe(0); const second = await redis.lpos(key, "b", "RANK", 2); expect(second).toBe(2); const third = await redis.lpos(key, "b", "RANK", 3); expect(third).toBe(4); const fourth = await redis.lpos(key, "b", "RANK", 4); expect(fourth).toBeNull(); const fromEnd = await redis.lpos(key, "b", "RANK", -1); expect(fromEnd).toBe(4); const fromEnd2 = await redis.lpos(key, "b", "RANK", -2); expect(fromEnd2).toBe(2); }); test("should find multiple positions with COUNT option in LPOS", async () => { const redis = ctx.redis; const key = "lpos-count-test"; await redis.lpush(key, "c"); await redis.lpush(key, "b"); await redis.lpush(key, "b"); await redis.lpush(key, "a"); await redis.lpush(key, "b"); const all = await redis.lpos(key, "b", "COUNT", 0); expect(all).toEqual([0, 2, 3]); const first2 = await redis.lpos(key, "b", "COUNT", 2); expect(first2).toEqual([0, 2]); const more = await redis.lpos(key, "b", "COUNT", 10); expect(more).toEqual([0, 2, 3]); const none = await redis.lpos(key, "x", "COUNT", 5); expect(none).toEqual([]); }); test("should find position with MAXLEN option in LPOS", async () => { const redis = ctx.redis; const key = "lpos-maxlen-test"; for (let i = 5; i >= 1; i--) { await redis.lpush(key, String(i)); } await redis.lpush(key, "target"); const found = await redis.lpos(key, "target", "MAXLEN", 6); expect(found).toBe(0); const notFound = await redis.lpos(key, "5", "MAXLEN", 3); expect(notFound).toBeNull(); const found3 = await redis.lpos(key, "3", "MAXLEN", 10); expect(found3).toBe(3); }); test("should move element from source to destination with LMOVE", async () => { const redis = ctx.redis; const source = "lmove-source"; const dest = "lmove-dest"; await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); const result1 = await redis.lmove(source, dest, "LEFT", "RIGHT"); expect(result1).toBe("one"); const sourceList1 = await redis.lrange(source, 0, -1); expect(sourceList1).toEqual(["two", "three"]); const destList1 = await redis.lrange(dest, 0, -1); expect(destList1).toEqual(["one"]); const result2 = await redis.lmove(source, dest, "RIGHT", "LEFT"); expect(result2).toBe("three"); const sourceList2 = await redis.lrange(source, 0, -1); expect(sourceList2).toEqual(["two"]); const destList2 = await redis.lrange(dest, 0, -1); expect(destList2).toEqual(["three", "one"]); }); test("should handle all LMOVE direction combinations", async () => { const redis = ctx.redis; await redis.lpush("src1", "b", "a"); const res1 = await redis.lmove("src1", "dst1", "LEFT", "LEFT"); expect(res1).toBe("a"); expect(await redis.lrange("dst1", 0, -1)).toEqual(["a"]); await redis.lpush("src2", "b", "a"); const res2 = await redis.lmove("src2", "dst2", "LEFT", "RIGHT"); expect(res2).toBe("a"); expect(await redis.lrange("dst2", 0, -1)).toEqual(["a"]); await redis.lpush("src3", "b", "a"); const res3 = await redis.lmove("src3", "dst3", "RIGHT", "LEFT"); expect(res3).toBe("b"); expect(await redis.lrange("dst3", 0, -1)).toEqual(["b"]); await redis.lpush("src4", "b", "a"); const res4 = await redis.lmove("src4", "dst4", "RIGHT", "RIGHT"); expect(res4).toBe("b"); expect(await redis.lrange("dst4", 0, -1)).toEqual(["b"]); }); test("should return null for LMOVE on empty source", async () => { const redis = ctx.redis; const result = await redis.lmove("empty-source", "some-dest", "LEFT", "RIGHT"); expect(result).toBeNull(); const destList = await redis.lrange("some-dest", 0, -1); expect(destList).toEqual([]); }); test("should handle LMOVE to same list", async () => { const redis = ctx.redis; const key = "circular-list"; await redis.lpush(key, "c", "b", "a"); const result = await redis.lmove(key, key, "LEFT", "RIGHT"); expect(result).toBe("a"); expect(await redis.lrange(key, 0, -1)).toEqual(["b", "c", "a"]); }); test("should pop from source and push to dest with RPOPLPUSH", async () => { const redis = ctx.redis; const source = "rpoplpush-source"; const dest = "rpoplpush-dest"; await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); const result = await redis.rpoplpush(source, dest); expect(result).toBe("three"); const sourceList = await redis.lrange(source, 0, -1); expect(sourceList).toEqual(["one", "two"]); const destList = await redis.lrange(dest, 0, -1); expect(destList).toEqual(["three"]); const result2 = await redis.rpoplpush(source, dest); expect(result2).toBe("two"); const sourceList2 = await redis.lrange(source, 0, -1); expect(sourceList2).toEqual(["one"]); const destList2 = await redis.lrange(dest, 0, -1); expect(destList2).toEqual(["two", "three"]); }); test("should return null for RPOPLPUSH on empty source", async () => { const redis = ctx.redis; const result = await redis.rpoplpush("empty-source", "some-dest"); expect(result).toBeNull(); }); test("should handle RPOPLPUSH to same list (circular)", async () => { const redis = ctx.redis; const key = "circular-rpoplpush"; await redis.lpush(key, "c", "b", "a"); const result = await redis.rpoplpush(key, key); expect(result).toBe("c"); expect(await redis.lrange(key, 0, -1)).toEqual(["c", "a", "b"]); }); test("should block and move element with BLMOVE", async () => { const redis = ctx.redis; const source = "blmove-source"; const dest = "blmove-dest"; await redis.lpush(source, "three"); await redis.lpush(source, "two"); await redis.lpush(source, "one"); const result = await redis.blmove(source, dest, "RIGHT", "LEFT", 0.1); expect(result).toBe("three"); const sourceRemaining = await redis.lrange(source, 0, -1); expect(sourceRemaining).toEqual(["one", "two"]); const destElements = await redis.lrange(dest, 0, -1); expect(destElements).toEqual(["three"]); const result2 = await redis.blmove(source, dest, "LEFT", "RIGHT", 0.1); expect(result2).toBe("one"); const finalSource = await redis.lrange(source, 0, -1); expect(finalSource).toEqual(["two"]); const finalDest = await redis.lrange(dest, 0, -1); expect(finalDest).toEqual(["three", "one"]); }); test("should timeout and return null with BLMOVE on empty list", async () => { const redis = ctx.redis; const result = await redis.blmove("empty-source", "dest", "LEFT", "RIGHT", 0.1); expect(result).toBeNull(); }); test("should block and pop multiple elements with BLMPOP", async () => { const redis = ctx.redis; const key = "blmpop-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const result = await redis.blmpop(0.1, 1, key, "LEFT"); expect(result).toEqual([key, ["one"]]); const result2 = await redis.blmpop(0.1, 1, key, "RIGHT", "COUNT", 2); expect(result2).toEqual([key, ["three", "two"]]); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual([]); }); test("should pop from first non-empty list with BLMPOP", async () => { const redis = ctx.redis; const key1 = "blmpop-empty"; const key2 = "blmpop-full"; await redis.lpush(key2, "value"); const result = await redis.blmpop(0.1, 2, key1, key2, "LEFT"); expect(result).toEqual([key2, ["value"]]); }); test("should timeout and return null with BLMPOP on empty lists", async () => { const redis = ctx.redis; const result = await redis.blmpop(0.1, 2, "empty-list1", "empty-list2", "LEFT"); expect(result).toBeNull(); }); test("should block and move element with BRPOPLPUSH", async () => { const redis = ctx.redis; const source = "brpoplpush-source"; const dest = "brpoplpush-dest"; await redis.lpush(source, "value2"); await redis.lpush(source, "value1"); const result = await redis.brpoplpush(source, dest, 0.1); expect(result).toBe("value2"); const sourceRemaining = await redis.lrange(source, 0, -1); expect(sourceRemaining).toEqual(["value1"]); const destElements = await redis.lrange(dest, 0, -1); expect(destElements).toEqual(["value2"]); const result2 = await redis.brpoplpush(source, dest, 0.1); expect(result2).toBe("value1"); const finalSource = await redis.lrange(source, 0, -1); expect(finalSource).toEqual([]); const finalDest = await redis.lrange(dest, 0, -1); expect(finalDest).toEqual(["value1", "value2"]); }); test("should timeout and return null with BRPOPLPUSH on empty list", async () => { const redis = ctx.redis; const result = await redis.brpoplpush("empty-source", "dest", 0.1); expect(result).toBeNull(); }); test("should get list length with LLEN", async () => { const redis = ctx.redis; const key = "llen-test"; const empty = await redis.llen(key); expect(empty).toBe(0); await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const length = await redis.llen(key); expect(length).toBe(3); const nonExistent = await redis.llen("nonexistent-list"); expect(nonExistent).toBe(0); }); test("should pop element from head with LPOP", async () => { const redis = ctx.redis; const key = "lpop-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const first = await redis.lpop(key); expect(first).toBe("one"); const second = await redis.lpop(key); expect(second).toBe("two"); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["three"]); const last = await redis.lpop(key); expect(last).toBe("three"); const empty = await redis.lpop(key); expect(empty).toBeNull(); const nonExistent = await redis.lpop("nonexistent-list"); expect(nonExistent).toBeNull(); }); test("should pop element from tail with RPOP", async () => { const redis = ctx.redis; const key = "rpop-test"; await redis.lpush(key, "three"); await redis.lpush(key, "two"); await redis.lpush(key, "one"); const first = await redis.rpop(key); expect(first).toBe("three"); const second = await redis.rpop(key); expect(second).toBe("two"); const remaining = await redis.lrange(key, 0, -1); expect(remaining).toEqual(["one"]); const last = await redis.rpop(key); expect(last).toBe("one"); const empty = await redis.rpop(key); expect(empty).toBeNull(); const nonExistent = await redis.rpop("nonexistent-list"); expect(nonExistent).toBeNull(); }); test("should push element to head only if key exists with LPUSHX", async () => { const redis = ctx.redis; const key = "lpushx-test"; const nonExistent = await redis.lpushx(key, "value"); expect(nonExistent).toBe(0); const exists = await redis.exists(key); expect(exists).toBe(false); await redis.lpush(key, "initial"); const result = await redis.lpushx(key, "new"); expect(result).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["new", "initial"]); const result2 = await redis.lpushx(key, "newer"); expect(result2).toBe(3); const finalList = await redis.lrange(key, 0, -1); expect(finalList).toEqual(["newer", "new", "initial"]); }); test("should push element to tail only if key exists with RPUSHX", async () => { const redis = ctx.redis; const key = "rpushx-test"; const nonExistent = await redis.rpushx(key, "value"); expect(nonExistent).toBe(0); const exists = await redis.exists(key); expect(exists).toBe(false); await redis.lpush(key, "initial"); const result = await redis.rpushx(key, "new"); expect(result).toBe(2); const list = await redis.lrange(key, 0, -1); expect(list).toEqual(["initial", "new"]); const result2 = await redis.rpushx(key, "newer"); expect(result2).toBe(3); const finalList = await redis.lrange(key, 0, -1); expect(finalList).toEqual(["initial", "new", "newer"]); }); }); describe("Set Operations", () => { test("should get set cardinality with SCARD", async () => { const redis = ctx.redis; const key = "scard-test"; const count1 = await redis.scard(key); expect(count1).toBe(0); await redis.sadd(key, "one", "two", "three"); const count2 = await redis.scard(key); expect(count2).toBe(3); await redis.sadd(key, "two"); const count3 = await redis.scard(key); expect(count3).toBe(3); }); test("should get set difference with SDIFF", async () => { const redis = ctx.redis; const key1 = "sdiff-test1"; const key2 = "sdiff-test2"; await redis.sadd(key1, "a", "b", "c", "d"); await redis.sadd(key2, "c", "d", "e"); const diff = await redis.sdiff(key1, key2); expect(diff.sort()).toEqual(["a", "b"]); const diff2 = await redis.sdiff(key1, "nonexistent"); expect(diff2.sort()).toEqual(["a", "b", "c", "d"]); }); test("should check set membership with SISMEMBER", async () => { const redis = ctx.redis; const key = "sismember-test"; await redis.sadd(key, "one", "two", "three"); const result1 = await redis.sismember(key, "one"); expect(result1).toBe(true); const result2 = await redis.sismember(key, "nonexistent"); expect(result2).toBe(false); const result3 = await redis.sismember("nonexistent", "member"); expect(result3).toBe(false); }); test("should move member between sets with SMOVE", async () => { const redis = ctx.redis; const source = "smove-source"; const dest = "smove-dest"; await redis.sadd(source, "one", "two", "three"); await redis.sadd(dest, "four"); const result1 = await redis.smove(source, dest, "one"); expect(result1).toBe(true); const sourceMembers = await redis.smembers(source); expect(sourceMembers.sort()).toEqual(["three", "two"]); const destMembers = await redis.smembers(dest); expect(destMembers.sort()).toEqual(["four", "one"]); const result2 = await redis.smove(source, dest, "nonexistent"); expect(result2).toBe(false); }); test("should pop random member with SPOP", async () => { const redis = ctx.redis; const key = "spop-test"; await redis.sadd(key, "one", "two", "three", "four"); const popped1 = await redis.spop(key); expect(["one", "two", "three", "four"]).toContain(popped1); const remaining1 = await redis.scard(key); expect(remaining1).toBe(3); const popped2 = await redis.spop(key, 2); expect(Array.isArray(popped2)).toBe(true); expect(popped2).toBeDefined(); expect(popped2!.length).toBe(2); const remaining2 = await redis.scard(key); expect(remaining2).toBe(1); await redis.spop(key); const empty = await redis.spop(key); expect(empty).toBeNull(); }); test("should publish to sharded channel with SPUBLISH", async () => { const redis = ctx.redis; const result = await redis.spublish("test-channel", "test-message"); expect(typeof result).toBe("number"); expect(result).toBeGreaterThanOrEqual(0); }); test("should get random member with SRANDMEMBER", async () => { const redis = ctx.redis; const key = "srandmember-test"; await redis.sadd(key, "one", "two", "three"); const member1 = await redis.srandmember(key); expect(["one", "two", "three"]).toContain(member1); const members = await redis.srandmember(key, 2); expect(Array.isArray(members)).toBe(true); expect(members!.length).toBeLessThanOrEqual(2); const count = await redis.scard(key); expect(count).toBe(3); const empty = await redis.srandmember("nonexistent"); expect(empty).toBeNull(); }); test("should remove members with SREM", async () => { const redis = ctx.redis; const key = "srem-test"; await redis.sadd(key, "one", "two", "three", "four"); const count1 = await redis.srem(key, "one"); expect(count1).toBe(1); const count2 = await redis.srem(key, "two", "three"); expect(count2).toBe(2); const remaining = await redis.smembers(key); expect(remaining).toEqual(["four"]); const count3 = await redis.srem(key, "nonexistent"); expect(count3).toBe(0); }); test("should get set union with SUNION", async () => { const redis = ctx.redis; const key1 = "sunion-test1"; const key2 = "sunion-test2"; await redis.sadd(key1, "a", "b", "c"); await redis.sadd(key2, "c", "d", "e"); const union = await redis.sunion(key1, key2); expect(union.sort()).toEqual(["a", "b", "c", "d", "e"]); const union2 = await redis.sunion(key1, "nonexistent"); expect(union2.sort()).toEqual(["a", "b", "c"]); }); test("should store set union with SUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "sunionstore-test1"; const key2 = "sunionstore-test2"; const dest = "sunionstore-dest"; await redis.sadd(key1, "a", "b", "c"); await redis.sadd(key2, "c", "d", "e"); const count = await redis.sunionstore(dest, key1, key2); expect(count).toBe(5); const stored = await redis.smembers(dest); expect(stored.sort()).toEqual(["a", "b", "c", "d", "e"]); await redis.sadd(dest, "z"); const count2 = await redis.sunionstore(dest, key1, key2); expect(count2).toBe(5); const stored2 = await redis.smembers(dest); expect(stored2.sort()).toEqual(["a", "b", "c", "d", "e"]); expect(stored2).not.toContain("z"); }); test("should return intersection of two sets with SINTER", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); const result = await redis.sinter(key1, key2); expect(result.sort()).toEqual(["b", "c"]); }); test("should return intersection of multiple sets with SINTER", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; const key3 = "set3"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); await redis.sadd(key3, "c"); await redis.sadd(key3, "d"); await redis.sadd(key3, "e"); const result = await redis.sinter(key1, key2, key3); expect(result).toEqual(["c"]); }); test("should return empty array when sets have no intersection with SINTER", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); const result = await redis.sinter(key1, key2); expect(result).toEqual([]); }); test("should return empty array when one set does not exist with SINTER", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "nonexistent"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); const result = await redis.sinter(key1, key2); expect(result).toEqual([]); }); test("should store intersection in destination with SINTERSTORE", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; const dest = "dest-set"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(2); const members = await redis.smembers(dest); expect(members.sort()).toEqual(["b", "c"]); }); test("should overwrite existing destination with SINTERSTORE", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; const dest = "dest-set"; await redis.sadd(dest, "old"); await redis.sadd(dest, "data"); await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(1); const members = await redis.smembers(dest); expect(members).toEqual(["b"]); }); test("should return 0 when storing empty intersection with SINTERSTORE", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; const dest = "dest-set"; await redis.sadd(key1, "a"); await redis.sadd(key2, "b"); const count = await redis.sinterstore(dest, key1, key2); expect(count).toBe(0); const members = await redis.smembers(dest); expect(members).toEqual([]); }); test("should return cardinality of intersection with SINTERCARD", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); const count = await redis.sintercard(2, key1, key2); expect(count).toBe(2); }); test("should return 0 for empty intersection with SINTERCARD", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; await redis.sadd(key1, "a"); await redis.sadd(key2, "b"); const count = await redis.sintercard(2, key1, key2); expect(count).toBe(0); }); test("should support LIMIT option with SINTERCARD", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key1, "d"); await redis.sadd(key2, "a"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); const count = await redis.sintercard(2, key1, key2, "LIMIT", 2); expect(count).toBe(2); }); test("should throw error when SINTER receives no keys", async () => { const redis = ctx.redis; expect(async () => { // @ts-expect-error await redis.sinter(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinter' command"`); }); test("should throw error when SINTERSTORE receives no keys", async () => { const redis = ctx.redis; expect(async () => { // @ts-expect-error await redis.sinterstore(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sinterstore' command"`); }); test("should throw error when SINTERCARD receives no keys", async () => { const redis = ctx.redis; expect(async () => { // @ts-expect-error await redis.sintercard(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sintercard' command"`); }); test("should store set difference with SDIFFSTORE", async () => { const redis = ctx.redis; const key1 = "set1"; const key2 = "set2"; const key3 = "set3"; const dest = "diff-result"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key1, "d"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key3, "d"); const count1 = await redis.sdiffstore(dest, key1, key2); expect(count1).toBe(2); const members1 = await redis.smembers(dest); expect(members1.sort()).toEqual(["a", "d"]); const count2 = await redis.sdiffstore(dest, key1, key2, key3); expect(count2).toBe(1); const members2 = await redis.smembers(dest); expect(members2).toEqual(["a"]); }); test("should throw error with SDIFFSTORE on invalid arguments", async () => { const redis = ctx.redis; expect(async () => { await (redis as any).sdiffstore(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sdiffstore' command"`); }); test("should check multiple members with SMISMEMBER", async () => { const redis = ctx.redis; const key = "test-set"; await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); const result = await redis.smismember(key, "a", "b", "d", "e"); expect(result).toEqual([1, 1, 0, 0]); const result2 = await redis.smismember(key, "c"); expect(result2).toEqual([1]); const result3 = await redis.smismember("nonexistent", "a", "b"); expect(result3).toEqual([0, 0]); }); test("should throw error with SMISMEMBER on invalid arguments", async () => { const redis = ctx.redis; expect(async () => { await (redis as any).smismember(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'smismember' command"`); }); test("should scan set members with SSCAN", async () => { const redis = ctx.redis; const key = "scan-set"; for (let i = 0; i < 20; i++) { await redis.sadd(key, `member${i}`); } let cursor = "0"; const allMembers: string[] = []; do { const [nextCursor, members] = await redis.sscan(key, cursor); allMembers.push(...members); cursor = nextCursor; } while (cursor !== "0"); expect(allMembers.length).toBe(20); expect(new Set(allMembers).size).toBe(20); for (let i = 0; i < 20; i++) { expect(allMembers).toContain(`member${i}`); } }); test("should scan set with MATCH pattern using SSCAN", async () => { const redis = ctx.redis; const key = "scan-pattern-set"; await redis.sadd(key, "user:1"); await redis.sadd(key, "user:2"); await redis.sadd(key, "user:3"); await redis.sadd(key, "admin:1"); await redis.sadd(key, "admin:2"); const [cursor, members] = await redis.sscan(key, "0", "MATCH", "user:*"); let allUserMembers: string[] = [...members]; let scanCursor = cursor; while (scanCursor !== "0") { const [nextCursor, nextMembers] = await redis.sscan(key, scanCursor, "MATCH", "user:*"); allUserMembers.push(...nextMembers); scanCursor = nextCursor; } const userMembers = allUserMembers.filter(m => m.startsWith("user:")); expect(userMembers.length).toBeGreaterThanOrEqual(0); }); test("should scan empty set with SSCAN", async () => { const redis = ctx.redis; const key = "empty-scan-set"; const [cursor, members] = await redis.sscan(key, "0"); expect(cursor).toBe("0"); expect(members).toEqual([]); }); test("should throw error with SSCAN on invalid arguments", async () => { const redis = ctx.redis; expect(async () => { await (redis as any).sscan(); }).toThrowErrorMatchingInlineSnapshot(`"ERR wrong number of arguments for 'sscan' command"`); }); test("should get cardinality of set with SCARD", async () => { const redis = ctx.redis; const key = "scard-test"; const emptyCount = await redis.scard(key); expect(emptyCount).toBe(0); await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); const count = await redis.scard(key); expect(count).toBe(3); await redis.sadd(key, "a"); const sameCount = await redis.scard(key); expect(sameCount).toBe(3); }); test("should get difference of sets with SDIFF", async () => { const redis = ctx.redis; const key1 = "sdiff-test1"; const key2 = "sdiff-test2"; const key3 = "sdiff-test3"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key1, "d"); await redis.sadd(key2, "b"); await redis.sadd(key2, "c"); await redis.sadd(key3, "d"); const diff1 = await redis.sdiff(key1, key2); expect(diff1.sort()).toEqual(["a", "d"]); const diff2 = await redis.sdiff(key1, key2, key3); expect(diff2.sort()).toEqual(["a"]); const diff3 = await redis.sdiff(key1, "nonexistent"); expect(diff3.sort()).toEqual(["a", "b", "c", "d"]); const diff4 = await redis.sdiff(key2, key1); expect(diff4).toEqual([]); }); test("should check if member exists in set with SISMEMBER", async () => { const redis = ctx.redis; const key = "sismember-test"; await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); const exists1 = await redis.sismember(key, "a"); expect(exists1).toBe(true); const exists2 = await redis.sismember(key, "b"); expect(exists2).toBe(true); const notExists = await redis.sismember(key, "z"); expect(notExists).toBe(false); const notExistsSet = await redis.sismember("nonexistent", "a"); expect(notExistsSet).toBe(false); }); test("should move member between sets with SMOVE", async () => { const redis = ctx.redis; const source = "smove-source"; const dest = "smove-dest"; await redis.sadd(source, "a"); await redis.sadd(source, "b"); await redis.sadd(source, "c"); await redis.sadd(dest, "x"); await redis.sadd(dest, "y"); const moved = await redis.smove(source, dest, "b"); expect(moved).toBe(true); const sourceMembers = await redis.smembers(source); expect(sourceMembers.sort()).toEqual(["a", "c"]); const destMembers = await redis.smembers(dest); expect(destMembers.sort()).toEqual(["b", "x", "y"]); const notMoved = await redis.smove(source, dest, "z"); expect(notMoved).toBe(false); const notMoved2 = await redis.smove("nonexistent", dest, "a"); expect(notMoved2).toBe(false); }); test("should remove and return random member with SPOP", async () => { const redis = ctx.redis; const key = "spop-test"; await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); const popped = await redis.spop(key); expect(popped).toBeDefined(); expect(["a", "b", "c", "d", "e"]).toContain(popped); const remaining = await redis.scard(key); expect(remaining).toBe(4); const poppedMultiple = await redis.spop(key, 2); expect(Array.isArray(poppedMultiple)).toBe(true); expect(poppedMultiple!.length).toBe(2); poppedMultiple!.forEach(member => { expect(["a", "b", "c", "d", "e"]).toContain(member); }); const remainingAfter = await redis.scard(key); expect(remainingAfter).toBe(2); await redis.spop(key, 10); const emptyPop = await redis.spop(key); expect(emptyPop).toBeNull(); }); test("should publish to sharded channel with SPUBLISH", async () => { const redis = ctx.redis; const channel = "spublish-channel"; const count = await redis.spublish(channel, "test message"); expect(typeof count).toBe("number"); expect(count).toBe(0); }); test("should get random member without removing with SRANDMEMBER", async () => { const redis = ctx.redis; const key = "srandmember-test"; await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); const random = await redis.srandmember(key); expect(random).toBeDefined(); expect(["a", "b", "c", "d", "e"]).toContain(random); const count = await redis.scard(key); expect(count).toBe(5); const randomMultiple = await redis.srandmember(key, 3); expect(Array.isArray(randomMultiple)).toBe(true); expect(randomMultiple!.length).toBe(3); randomMultiple!.forEach(member => { expect(["a", "b", "c", "d", "e"]).toContain(member); }); const countAfter = await redis.scard(key); expect(countAfter).toBe(5); const tooMany = await redis.srandmember(key, 10); expect(Array.isArray(tooMany)).toBe(true); expect(tooMany!.length).toBe(5); const withDuplicates = await redis.srandmember(key, -10); expect(withDuplicates!.length).toBe(10); withDuplicates!.forEach(member => { expect(["a", "b", "c", "d", "e"]).toContain(member); }); const emptyRandom = await redis.srandmember("nonexistent"); expect(emptyRandom).toBeNull(); }); test("should remove members from set with SREM", async () => { const redis = ctx.redis; const key = "srem-test"; await redis.sadd(key, "a"); await redis.sadd(key, "b"); await redis.sadd(key, "c"); await redis.sadd(key, "d"); await redis.sadd(key, "e"); const removed1 = await redis.srem(key, "a"); expect(removed1).toBe(1); const members1 = await redis.smembers(key); expect(members1.sort()).toEqual(["b", "c", "d", "e"]); const removed2 = await redis.srem(key, "b", "c", "z"); expect(removed2).toBe(2); const members2 = await redis.smembers(key); expect(members2.sort()).toEqual(["d", "e"]); const removed3 = await redis.srem(key, "nonexistent"); expect(removed3).toBe(0); const removed4 = await redis.srem("nonexistent", "a"); expect(removed4).toBe(0); }); test("should get union of sets with SUNION", async () => { const redis = ctx.redis; const key1 = "sunion-test1"; const key2 = "sunion-test2"; const key3 = "sunion-test3"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); await redis.sadd(key2, "e"); await redis.sadd(key3, "e"); await redis.sadd(key3, "f"); await redis.sadd(key3, "g"); const union1 = await redis.sunion(key1, key2); expect(union1.sort()).toEqual(["a", "b", "c", "d", "e"]); const union2 = await redis.sunion(key1, key2, key3); expect(union2.sort()).toEqual(["a", "b", "c", "d", "e", "f", "g"]); const union3 = await redis.sunion(key1, "nonexistent"); expect(union3.sort()).toEqual(["a", "b", "c"]); const union4 = await redis.sunion("nonexistent1", "nonexistent2"); expect(union4).toEqual([]); }); test("should store union of sets with SUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "sunionstore-test1"; const key2 = "sunionstore-test2"; const key3 = "sunionstore-test3"; const dest = "sunionstore-dest"; await redis.sadd(key1, "a"); await redis.sadd(key1, "b"); await redis.sadd(key1, "c"); await redis.sadd(key2, "c"); await redis.sadd(key2, "d"); await redis.sadd(key2, "e"); await redis.sadd(key3, "e"); await redis.sadd(key3, "f"); const count1 = await redis.sunionstore(dest, key1, key2); expect(count1).toBe(5); const members1 = await redis.smembers(dest); expect(members1.sort()).toEqual(["a", "b", "c", "d", "e"]); const count2 = await redis.sunionstore(dest, key1, key2, key3); expect(count2).toBe(6); const members2 = await redis.smembers(dest); expect(members2.sort()).toEqual(["a", "b", "c", "d", "e", "f"]); const count3 = await redis.sunionstore(dest, key1, "nonexistent"); expect(count3).toBe(3); const members3 = await redis.smembers(dest); expect(members3.sort()).toEqual(["a", "b", "c"]); const count4 = await redis.sunionstore(dest, "nonexistent1", "nonexistent2"); expect(count4).toBe(0); const members4 = await redis.smembers(dest); expect(members4).toEqual([]); }); }); describe("Sorted Set Operations", () => { test("should get cardinality with ZCARD", async () => { const redis = ctx.redis; const key = "zcard-test"; const count1 = await redis.zcard(key); expect(count1).toBe(0); await redis.zadd(key, 1, "one", 2, "two", 3, "three"); const count2 = await redis.zcard(key); expect(count2).toBe(3); await redis.zadd(key, 4, "one"); const count3 = await redis.zcard(key); expect(count3).toBe(3); }); test("should pop max with ZPOPMAX", async () => { const redis = ctx.redis; const key = "zpopmax-test"; await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); const result1 = await redis.zpopmax(key); expect(result1).toEqual(["four", 4]); const result2 = await redis.zpopmax(key, 2); expect(result2).toEqual([ ["three", 3], ["two", 2], ]); const remaining = await redis.zcard(key); expect(remaining).toBe(1); await redis.zpopmax(key); const empty = await redis.zpopmax(key); expect(empty).toEqual([]); }); test("should pop min with ZPOPMIN", async () => { const redis = ctx.redis; const key = "zpopmin-test"; await redis.zadd(key, 1, "one", 2, "two", 3, "three", 4, "four"); const result1 = await redis.zpopmin(key); expect(result1).toEqual(["one", 1]); const result2 = await redis.zpopmin(key, 2); expect(result2).toEqual([ ["two", 2], ["three", 3], ]); const remaining = await redis.zcard(key); expect(remaining).toBe(1); await redis.zpopmin(key); const empty = await redis.zpopmin(key); expect(empty).toEqual([]); }); test("should get random member with ZRANDMEMBER", async () => { const redis = ctx.redis; const key = "zrandmember-test"; await redis.zadd(key, 1, "one", 2, "two", 3, "three"); const result1 = await redis.zrandmember(key); expect(result1).toBeDefined(); expect(["one", "two", "three"]).toContain(result1); const result2 = await redis.zrandmember(key, 2); expect(Array.isArray(result2)).toBe(true); expect(result2!.length).toBeLessThanOrEqual(2); result2!.forEach((member: string) => { expect(["one", "two", "three"]).toContain(member); }); const result3 = await redis.zrandmember(key, 1, "WITHSCORES"); expect<([string, number][] | null)[]>([[["one", 1]], [["two", 2]], [["three", 3]]]).toContainEqual(result3); const emptyKey = "zrandmember-empty-" + randomUUIDv7(); const empty = await redis.zrandmember(emptyKey); expect(empty).toBeNull(); }); test("should get rank with ZRANK", async () => { const redis = ctx.redis; const key = "zrank-test"; await redis.zadd(key, 1, "one", 2, "two", 3, "three"); const rank1 = await redis.zrank(key, "one"); expect(rank1).toBe(0); const rank2 = await redis.zrank(key, "two"); expect(rank2).toBe(1); const rank3 = await redis.zrank(key, "three"); expect(rank3).toBe(2); const rank4 = await redis.zrank(key, "nonexistent"); expect(rank4).toBeNull(); }); test("should get reverse rank with ZREVRANK", async () => { const redis = ctx.redis; const key = "zrevrank-test"; await redis.zadd(key, 1, "one", 2, "two", 3, "three"); const rank1 = await redis.zrevrank(key, "three"); expect(rank1).toBe(0); const rank2 = await redis.zrevrank(key, "two"); expect(rank2).toBe(1); const rank3 = await redis.zrevrank(key, "one"); expect(rank3).toBe(2); const rank4 = await redis.zrevrank(key, "nonexistent"); expect(rank4).toBeNull(); }); test("should increment score with ZINCRBY", async () => { const redis = ctx.redis; const key = "zincrby-test"; await redis.send("ZADD", [key, "1.0", "member1", "2.0", "member2"]); const newScore1 = await redis.zincrby(key, 2.5, "member1"); expect(newScore1).toBe(3.5); const newScore2 = await redis.zincrby(key, -1.5, "member2"); expect(newScore2).toBe(0.5); const newScore3 = await redis.zincrby(key, 5, "member3"); expect(newScore3).toBe(5); }); test("should count members in score range with ZCOUNT", async () => { const redis = ctx.redis; const key = "zcount-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const count1 = await redis.zcount(key, "-inf", "+inf"); expect(count1).toBe(5); const count2 = await redis.zcount(key, 2, 4); expect(count2).toBe(3); const count3 = await redis.zcount(key, 1, 3); expect(count3).toBe(3); const count4 = await redis.zcount(key, 10, 20); expect(count4).toBe(0); }); test("should count members in lexicographical range with ZLEXCOUNT", async () => { const redis = ctx.redis; const key = "zlexcount-test"; await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); const count1 = await redis.zlexcount(key, "-", "+"); expect(count1).toBe(5); const count2 = await redis.zlexcount(key, "[banana", "[date"); expect(count2).toBe(3); const count3 = await redis.zlexcount(key, "(banana", "(date"); expect(count3).toBe(1); const count4 = await redis.zlexcount(key, "[zebra", "[zoo"); expect(count4).toBe(0); }); test("should compute difference between sorted sets with ZDIFF", async () => { const redis = ctx.redis; const key1 = "zdiff-test1"; const key2 = "zdiff-test2"; const key3 = "zdiff-test3"; await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three", "4", "four"]); await redis.send("ZADD", [key2, "1", "one", "2", "two"]); await redis.send("ZADD", [key3, "3", "three"]); const diff1 = await redis.zdiff(2, key1, key2); expect(diff1).toEqual(["three", "four"]); const diff2 = await redis.zdiff(3, key1, key2, key3); expect(diff2).toEqual(["four"]); const diff3 = await redis.zdiff(2, key1, key2, "WITHSCORES"); expect(diff3).toEqual([ ["three", 3], ["four", 4], ]); const diff4 = await redis.zdiff(2, key1, "nonexistent"); expect(diff4.length).toBe(4); expect(diff4).toEqual(["one", "two", "three", "four"]); const diff5 = await redis.zdiff(2, key2, key1); expect(diff5).toEqual([]); }); test("should store difference between sorted sets with ZDIFFSTORE", async () => { const redis = ctx.redis; const key1 = "zdiffstore-test1"; const key2 = "zdiffstore-test2"; const dest = "zdiffstore-dest"; await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); await redis.send("ZADD", [key2, "1", "one"]); const count = await redis.zdiffstore(dest, 2, key1, key2); expect(count).toBe(2); const members = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(members).toEqual(["two", "three"]); const membersWithScores = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(membersWithScores).toEqual([ ["two", 2], ["three", 3], ]); const count2 = await redis.zdiffstore(dest, 2, key2, key1); expect(count2).toBe(0); const finalCount = await redis.send("ZCARD", [dest]); expect(finalCount).toBe(0); }); test("should count intersection with ZINTERCARD", async () => { const redis = ctx.redis; const key1 = "zintercard-test1"; const key2 = "zintercard-test2"; const key3 = "zintercard-test3"; await redis.send("ZADD", [key1, "1", "one", "2", "two", "3", "three"]); await redis.send("ZADD", [key2, "1", "one", "2", "two", "4", "four"]); await redis.send("ZADD", [key3, "1", "one", "5", "five"]); const count1 = await redis.zintercard(2, key1, key2); expect(count1).toBe(2); const count2 = await redis.zintercard(3, key1, key2, key3); expect(count2).toBe(1); const count3 = await redis.zintercard(2, key1, key2, "LIMIT", 1); expect(count3).toBe(1); const count4 = await redis.zintercard(2, key1, key3); expect(count4).toBe(1); const count5 = await redis.zintercard(2, key1, "nonexistent"); expect(count5).toBe(0); }); test("should reject invalid arguments in ZDIFF", async () => { const redis = ctx.redis; expect(async () => { await redis.zdiff({} as any, "key1"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zdiff'."`); }); test("should reject invalid arguments in ZDIFFSTORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zdiffstore("dest", {} as any, "key1"); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zdiffstore'."`, ); }); test("should reject invalid arguments in ZINTERCARD", async () => { const redis = ctx.redis; expect(async () => { await redis.zintercard({} as any, "key1"); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zintercard'."`, ); }); test("should remove members by rank with ZREMRANGEBYRANK", async () => { const redis = ctx.redis; const key = "zremrangebyrank-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const removed1 = await redis.zremrangebyrank(key, 0, 1); expect(removed1).toBe(2); const remaining = await redis.send("ZCARD", [key]); expect(remaining).toBe(3); const removed2 = await redis.zremrangebyrank(key, -1, -1); expect(removed2).toBe(1); const final = await redis.send("ZCARD", [key]); expect(final).toBe(2); }); test("should remove members by score range with ZREMRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zremrangebyscore-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const removed1 = await redis.zremrangebyscore(key, 2, 4); expect(removed1).toBe(3); const remaining = await redis.send("ZCARD", [key]); expect(remaining).toBe(2); const removed2 = await redis.zremrangebyscore(key, "-inf", "+inf"); expect(removed2).toBe(2); }); test("should remove members by lexicographical range with ZREMRANGEBYLEX", async () => { const redis = ctx.redis; const key = "zremrangebylex-test"; await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); const removed1 = await redis.zremrangebylex(key, "[banana", "[date"); expect(removed1).toBe(3); const remaining = await redis.send("ZCARD", [key]); expect(remaining).toBe(2); const removed2 = await redis.zremrangebylex(key, "-", "+"); expect(removed2).toBe(2); }); test("should reject invalid key in ZINCRBY", async () => { const redis = ctx.redis; expect(async () => { await redis.zincrby({} as any, 1, "member"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zincrby'."`); }); test("should reject invalid key in ZCOUNT", async () => { const redis = ctx.redis; expect(async () => { await redis.zcount([] as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zcount'."`); }); test("should reject invalid key in ZLEXCOUNT", async () => { const redis = ctx.redis; expect(async () => { await redis.zlexcount(null as any, "[a", "[z"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zlexcount'."`); }); test("should reject invalid key in ZREMRANGEBYRANK", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebyrank({} as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyrank'."`); }); test("should reject invalid key in ZREMRANGEBYSCORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebyscore([] as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebyscore'."`); }); test("should reject invalid key in ZREMRANGEBYLEX", async () => { const redis = ctx.redis; expect(async () => { await redis.zremrangebylex(null as any, "[a", "[z"); }).toThrowErrorMatchingInlineSnapshot(`"Expected key to be a string or buffer for 'zremrangebylex'."`); }); test("should remove one or more members with ZREM", async () => { const redis = ctx.redis; const key = "zrem-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); const removed1 = await redis.zrem(key, "two"); expect(removed1).toBe(1); const removed2 = await redis.zrem(key, "one", "three"); expect(removed2).toBe(2); const removed3 = await redis.zrem(key, "nonexistent"); expect(removed3).toBe(0); const removed4 = await redis.zrem(key, "four", "nothere"); expect(removed4).toBe(1); }); test("should get scores with ZMSCORE", async () => { const redis = ctx.redis; const key = "zmscore-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const scores1 = await redis.zmscore(key, "two"); expect(scores1).toEqual([2.7]); const scores2 = await redis.zmscore(key, "one", "three"); expect(scores2).toEqual([1.5, 3.9]); const scores3 = await redis.zmscore(key, "one", "nonexistent", "three"); expect(scores3).toEqual([1.5, null, 3.9]); const scores4 = await redis.zmscore(key, "nothere", "alsonothere"); expect(scores4).toEqual([null, null]); }); test("should reject invalid key in ZREM", async () => { const redis = ctx.redis; expect(async () => { await redis.zrem({} as any, "member"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrem'."`); }); test("should reject invalid key in ZMSCORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zmscore([] as any, "member"); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zmscore'."`, ); }); test("should add members to sorted set with ZADD", async () => { const redis = ctx.redis; const key = "zadd-basic-test"; const added1 = await redis.zadd(key, "1", "one"); expect(added1).toBe(1); const added2 = await redis.zadd(key, "2", "two", "3", "three"); expect(added2).toBe(2); const added3 = await redis.zadd(key, "1.5", "one"); expect(added3).toBe(0); const score = await redis.zscore(key, "one"); expect(score).toBe(1.5); }); test("should add members with NX option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-nx-test"; await redis.zadd(key, "1", "one"); const added1 = await redis.zadd(key, "NX", "2", "one"); expect(added1).toBe(0); const score1 = await redis.zscore(key, "one"); expect(score1).toBe(1); const added2 = await redis.zadd(key, "NX", "2", "two"); expect(added2).toBe(1); const score2 = await redis.zscore(key, "two"); expect(score2).toBe(2); }); test("should update members with XX option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-xx-test"; await redis.zadd(key, "1", "one"); const updated1 = await redis.zadd(key, "XX", "2", "one"); expect(updated1).toBe(0); const score1 = await redis.zscore(key, "one"); expect(score1).toBe(2); const added = await redis.zadd(key, "XX", "3", "three"); expect(added).toBe(0); const score2 = await redis.zscore(key, "three"); expect(score2).toBeNull(); }); test("should return changed count with CH option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-ch-test"; await redis.zadd(key, "1", "one", "2", "two"); const changed = await redis.zadd(key, "CH", "1.5", "one", "3", "three"); expect(changed).toBe(2); }); test("should increment score with INCR option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-incr-test"; await redis.zadd(key, "1", "one"); const newScore = await redis.zadd(key, "INCR", "2.5", "one"); expect(newScore).toBe(3.5); const score = await redis.zscore(key, "one"); expect(score).toBe(3.5); }); test("should handle GT option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-gt-test"; await redis.zadd(key, "5", "one"); const updated1 = await redis.zadd(key, "GT", "3", "one"); expect(updated1).toBe(0); const score1 = await redis.zscore(key, "one"); expect(score1).toBe(5); const updated2 = await redis.zadd(key, "GT", "7", "one"); expect(updated2).toBe(0); const score2 = await redis.zscore(key, "one"); expect(score2).toBe(7); }); test("should handle LT option in ZADD", async () => { const redis = ctx.redis; const key = "zadd-lt-test"; await redis.zadd(key, "5", "one"); const updated1 = await redis.zadd(key, "LT", "7", "one"); expect(updated1).toBe(0); const score1 = await redis.zscore(key, "one"); expect(score1).toBe(5); const updated2 = await redis.zadd(key, "LT", "3", "one"); expect(updated2).toBe(0); const score2 = await redis.zscore(key, "one"); expect(score2).toBe(3); }); test("should iterate sorted set with ZSCAN", async () => { const redis = ctx.redis; const key = "zscan-test"; await redis.zadd(key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"); let cursor = "0"; const allElements: string[] = []; do { const [nextCursor, elements] = await redis.zscan(key, cursor); allElements.push(...elements); cursor = nextCursor; } while (cursor !== "0"); expect(allElements.length).toBe(10); const members = allElements.filter((_, index) => index % 2 === 0); expect(members).toContain("one"); expect(members).toContain("two"); expect(members).toContain("three"); expect(members).toContain("four"); expect(members).toContain("five"); }); test("should iterate sorted set with ZSCAN and MATCH", async () => { const redis = ctx.redis; const key = "zscan-match-test"; await redis.zadd(key, "1", "user:1", "2", "user:2", "3", "post:1", "4", "post:2"); let cursor = "0"; const userElements: string[] = []; do { const [nextCursor, elements] = await redis.zscan(key, cursor, "MATCH", "user:*"); userElements.push(...elements); cursor = nextCursor; } while (cursor !== "0"); const members = userElements.filter((_, index) => index % 2 === 0); expect(members).toContain("user:1"); expect(members).toContain("user:2"); expect(members).not.toContain("post:1"); expect(members).not.toContain("post:2"); }); test("should iterate sorted set with ZSCAN and COUNT", async () => { const redis = ctx.redis; const key = "zscan-count-test"; const promises: Promise[] = []; for (let i = 0; i < 100; i++) { promises.push(redis.zadd(key, String(i), `member:${i}`)); } await Promise.all(promises); let cursor = "0"; const allElements: string[] = []; do { const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); allElements.push(...elements); cursor = nextCursor; } while (cursor !== "0"); expect(allElements.length).toBe(200); const members = allElements.filter((_, index) => index % 2 === 0); expect(members.length).toBe(100); for (let i = 0; i < 100; i++) { expect(members).toContain(`member:${i}`); } cursor = 0 as any; const allElements2: string[] = []; do { const [nextCursor, elements] = await redis.zscan(key, cursor, "COUNT", "10"); allElements2.push(...elements); cursor = nextCursor; } while (cursor !== "0"); expect(allElements2.length).toBe(200); }); test("should reject invalid key in ZADD", async () => { const redis = ctx.redis; expect(async () => { await redis.zadd({} as any, "1", "member"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zadd'."`); }); test("should reject invalid key in ZSCAN", async () => { const redis = ctx.redis; expect(async () => { await redis.zscan([] as any, 0); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zscan'."`); }); test("should return range of members with ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-basic-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const all = await redis.zrange(key, 0, -1); expect(all).toEqual(["one", "two", "three", "four", "five"]); const first3 = await redis.zrange(key, 0, 2); expect(first3).toEqual(["one", "two", "three"]); const last2 = await redis.zrange(key, -2, -1); expect(last2).toEqual(["four", "five"]); }); test("should return members with scores using WITHSCORES option in ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-withscores-test"; await redis.send("ZADD", [key, "1", "one", "2.5", "two", "3", "three"]); const result = await redis.zrange(key, 0, -1, "WITHSCORES"); expect(result).toEqual([ ["one", 1], ["two", 2.5], ["three", 3], ]); }); test("should return members by score range with BYSCORE option in ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-byscore-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const range1 = await redis.zrange(key, "2", "4", "BYSCORE"); expect(range1).toEqual(["two", "three", "four"]); const range2 = await redis.zrange(key, "(2", "4", "BYSCORE"); expect(range2).toEqual(["three", "four"]); const all = await redis.zrange(key, "-inf", "+inf", "BYSCORE"); expect(all).toEqual(["one", "two", "three", "four", "five"]); }); test("should return members in reverse order with REV option in ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-rev-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); const reversed = await redis.zrange(key, 0, -1, "REV"); expect(reversed).toEqual(["three", "two", "one"]); const top2 = await redis.zrange(key, 0, 1, "REV"); expect(top2).toEqual(["three", "two"]); }); test("should support LIMIT option with BYSCORE in ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-limit-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result = await redis.zrange(key, "1", "5", "BYSCORE", "LIMIT", "1", "2"); expect(result).toEqual(["two", "three"]); }); test("should return members by lexicographical range with BYLEX option in ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-bylex-test"; await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date"]); const range1 = await redis.zrange(key, "[banana", "[cherry", "BYLEX"); expect(range1).toEqual(["banana", "cherry"]); const range2 = await redis.zrange(key, "(banana", "(date", "BYLEX"); expect(range2).toEqual(["cherry"]); }); test("should return members in reverse order with ZREVRANGE", async () => { const redis = ctx.redis; const key = "zrevrange-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const all = await redis.zrevrange(key, 0, -1); expect(all).toEqual(["five", "four", "three", "two", "one"]); const top3 = await redis.zrevrange(key, 0, 2); expect(top3).toEqual(["five", "four", "three"]); const last2 = await redis.zrevrange(key, -2, -1); expect(last2).toEqual(["two", "one"]); }); test("should return members with scores using WITHSCORES option in ZREVRANGE", async () => { const redis = ctx.redis; const key = "zrevrange-withscores-test"; await redis.send("ZADD", [key, "1.5", "one", "2", "two", "3.7", "three"]); const result = await redis.zrevrange(key, 0, -1, "WITHSCORES"); expect(result).toEqual([ ["three", 3.7], ["two", 2], ["one", 1.5], ]); }); test("should handle empty sorted set with ZRANGE", async () => { const redis = ctx.redis; const key = "zrange-empty-test"; const result = await redis.zrange(key, 0, -1); expect(result).toEqual([]); }); test("should handle empty sorted set with ZREVRANGE", async () => { const redis = ctx.redis; const key = "zrevrange-empty-test"; const result = await redis.zrevrange(key, 0, -1); expect(result).toEqual([]); }); test("should reject invalid key in ZRANGE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrange({} as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zrange'."`); }); test("should reject invalid key in ZREVRANGE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrevrange([] as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrevrange'."`, ); }); test("should return members by score range with ZRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrangebyscore-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const all = await redis.zrangebyscore(key, "-inf", "+inf"); expect(all).toEqual(["one", "two", "three", "four", "five"]); const range1 = await redis.zrangebyscore(key, 2, 4); expect(range1).toEqual(["two", "three", "four"]); const range2 = await redis.zrangebyscore(key, "(2", 4); expect(range2).toEqual(["three", "four"]); const range3 = await redis.zrangebyscore(key, 2, "(4"); expect(range3).toEqual(["two", "three"]); const range4 = await redis.zrangebyscore(key, "(2", "(4"); expect(range4).toEqual(["three"]); }); test("should support WITHSCORES option with ZRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrangebyscore-withscores-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const result = await redis.zrangebyscore(key, 1, 3, "WITHSCORES"); expect(result).toEqual([ ["one", 1.5], ["two", 2.7], ]); }); test("should support LIMIT option with ZRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrangebyscore-limit-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const limited1 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 0, 2); expect(limited1).toEqual(["one", "two"]); const limited2 = await redis.zrangebyscore(key, "-inf", "+inf", "LIMIT", 1, 2); expect(limited2).toEqual(["two", "three"]); const limited3 = await redis.zrangebyscore(key, 2, 5, "LIMIT", 1, 2); expect(limited3).toEqual(["three", "four"]); }); test("should support WITHSCORES with ZRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrangebyscore-withscores-only-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES"); expect(result).toEqual([ ["one", 1], ["two", 2], ["three", 3], ["four", 4], ]); }); test("should support LIMIT and WITHSCORES together with ZRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrangebyscore-combined-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four"]); const result = await redis.zrangebyscore(key, "-inf", "+inf", "WITHSCORES", "LIMIT", 1, 2); expect(result).toEqual([ ["two", 2], ["three", 3], ]); }); test("should return members by score range in reverse with ZREVRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrevrangebyscore-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const all = await redis.zrevrangebyscore(key, "+inf", "-inf"); expect(all).toEqual(["five", "four", "three", "two", "one"]); const range1 = await redis.zrevrangebyscore(key, 4, 2); expect(range1).toEqual(["four", "three", "two"]); const range2 = await redis.zrevrangebyscore(key, "(4", "(2"); expect(range2).toEqual(["three"]); const range3 = await redis.zrevrangebyscore(key, 4, "(2"); expect(range3).toEqual(["four", "three"]); }); test("should support WITHSCORES option with ZREVRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrevrangebyscore-withscores-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const result = await redis.zrevrangebyscore(key, 3, 1, "WITHSCORES"); expect(result).toEqual([ ["two", 2.7], ["one", 1.5], ]); }); test("should support LIMIT option with ZREVRANGEBYSCORE", async () => { const redis = ctx.redis; const key = "zrevrangebyscore-limit-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const limited1 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 0, 2); expect(limited1).toEqual(["five", "four"]); const limited2 = await redis.zrevrangebyscore(key, "+inf", "-inf", "LIMIT", 1, 2); expect(limited2).toEqual(["four", "three"]); }); test("should return members by lexicographical range with ZRANGEBYLEX", async () => { const redis = ctx.redis; const key = "zrangebylex-test"; await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); const all = await redis.zrangebylex(key, "-", "+"); expect(all).toEqual(["apple", "banana", "cherry", "date", "elderberry"]); const range1 = await redis.zrangebylex(key, "[banana", "[date"); expect(range1).toEqual(["banana", "cherry", "date"]); const range2 = await redis.zrangebylex(key, "(banana", "(date"); expect(range2).toEqual(["cherry"]); const range3 = await redis.zrangebylex(key, "[banana", "(date"); expect(range3).toEqual(["banana", "cherry"]); const range4 = await redis.zrangebylex(key, "-", "[cherry"); expect(range4).toEqual(["apple", "banana", "cherry"]); const range5 = await redis.zrangebylex(key, "[cherry", "+"); expect(range5).toEqual(["cherry", "date", "elderberry"]); }); test("should support LIMIT option with ZRANGEBYLEX", async () => { const redis = ctx.redis; const key = "zrangebylex-limit-test"; await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); const limited1 = await redis.zrangebylex(key, "-", "+", "LIMIT", 0, 3); expect(limited1).toEqual(["a", "b", "c"]); const limited2 = await redis.zrangebylex(key, "-", "+", "LIMIT", 2, 3); expect(limited2).toEqual(["c", "d", "e"]); const limited3 = await redis.zrangebylex(key, "-", "+", "LIMIT", 5, 10); expect(limited3).toEqual(["f", "g"]); }); test("should get cardinality of sorted set with ZCARD", async () => { const redis = ctx.redis; const key = "zcard-test"; const count0 = await redis.zcard(key); expect(count0).toBe(0); await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); const count1 = await redis.zcard(key); expect(count1).toBe(3); await redis.send("ZADD", [key, "4", "four", "5", "five"]); const count2 = await redis.zcard(key); expect(count2).toBe(5); }); test("should pop member with lowest score using ZPOPMIN", async () => { const redis = ctx.redis; const key = "zpopmin-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result1 = await redis.zpopmin(key); expect(result1).toEqual(["one", 1]); const result2 = await redis.zpopmin(key); expect(result2).toEqual(["two", 2]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(3); }); test("should pop multiple members with ZPOPMIN using COUNT", async () => { const redis = ctx.redis; const key = "zpopmin-count-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result = await redis.zpopmin(key, 3); expect(result).toEqual([ ["one", 1], ["two", 2], ["three", 3], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should return empty array when ZPOPMIN on empty set", async () => { const redis = ctx.redis; const emptyKey = "zpopmin-empty-test"; const result = await redis.zpopmin(emptyKey); expect(result).toEqual([]); }); test("should return empty array when ZPOPMIN on non-existent key", async () => { const redis = ctx.redis; const nonExistentKey = "zpopmin-nonexistent-" + randomUUIDv7(); const result = await redis.zpopmin(nonExistentKey); expect(result).toEqual([]); }); test("should handle ties in score with ZPOPMIN", async () => { const redis = ctx.redis; const key = "zpopmin-tie-test"; await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); const result = await redis.zpopmin(key, 2); expect(result).toEqual([ ["a", 1], ["b", 1], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should pop member with highest score using ZPOPMAX", async () => { const redis = ctx.redis; const key = "zpopmax-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result1 = await redis.zpopmax(key); expect(result1).toEqual(["five", 5]); const result2 = await redis.zpopmax(key); expect(result2).toEqual(["four", 4]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(3); }); test("should pop multiple members with ZPOPMAX using COUNT", async () => { const redis = ctx.redis; const key = "zpopmax-count-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result = await redis.zpopmax(key, 3); expect(result).toEqual([ ["five", 5], ["four", 4], ["three", 3], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should return empty array when ZPOPMAX on empty set", async () => { const redis = ctx.redis; const emptyKey = "zpopmax-empty-test"; const result = await redis.zpopmax(emptyKey); expect(result).toEqual([]); }); test("should return empty array when ZPOPMAX on non-existent key", async () => { const redis = ctx.redis; const nonExistentKey = "zpopmax-nonexistent-" + randomUUIDv7(); const result = await redis.zpopmax(nonExistentKey); expect(result).toEqual([]); }); test("should handle ties in score with ZPOPMAX", async () => { const redis = ctx.redis; const key = "zpopmax-tie-test"; await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); const result = await redis.zpopmax(key, 2); expect(result).toEqual([ ["d", 2], ["c", 2], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should get random member with ZRANDMEMBER", async () => { const redis = ctx.redis; const key = "zrandmember-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result = await redis.zrandmember(key); expect(result).toBeDefined(); expect(typeof result).toBe("string"); expect(["one", "two", "three", "four", "five"]).toContain(result); }); test("should return null when ZRANDMEMBER on empty set", async () => { const redis = ctx.redis; const emptyKey = "zrandmember-empty-test"; const result = await redis.zrandmember(emptyKey); expect(result).toBeNull(); }); test("should get multiple random members with ZRANDMEMBER", async () => { const redis = ctx.redis; const key = "zrandmember-count-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); const result = await redis.zrandmember(key, 2); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result!.length).toBe(2); for (const member of result!) { expect(["one", "two", "three"]).toContain(member); } const count = await redis.send("ZCARD", [key]); expect(count).toBe(3); }); test("should get random members with scores using WITHSCORES in ZRANDMEMBER", async () => { const redis = ctx.redis; const key = "zrandmember-withscores-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const result = await redis.zrandmember(key, 2, "WITHSCORES"); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result!.length).toBe(2); for (const item of result!) { expect(Array.isArray(item)).toBe(true); expect(item.length).toBe(2); expect(typeof item[0]).toBe("string"); expect(typeof item[1]).toBe("number"); expect(["one", "two", "three"]).toContain(item[0]); } }); test("should allow negative count for ZRANDMEMBER to allow duplicates", async () => { const redis = ctx.redis; const key = "zrandmember-negative-test"; await redis.send("ZADD", [key, "1", "one", "2", "two"]); const result = await redis.zrandmember(key, -5); expect(result).toBeDefined(); expect(Array.isArray(result)).toBe(true); expect(result!.length).toBe(5); for (const member of result!) { expect(["one", "two"]).toContain(member); } }); test("should get rank of member with ZRANK", async () => { const redis = ctx.redis; const key = "zrank-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const rank1 = await redis.zrank(key, "one"); expect(rank1).toBe(0); const rank2 = await redis.zrank(key, "three"); expect(rank2).toBe(2); const rank3 = await redis.zrank(key, "five"); expect(rank3).toBe(4); const rank4 = await redis.zrank(key, "nonexistent"); expect(rank4).toBeNull(); }); test("should get rank with score using WITHSCORE in ZRANK", async () => { const redis = ctx.redis; const key = "zrank-withscore-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const result = await redis.zrank(key, "two", "WITHSCORE"); expect(result).toEqual([1, 2.7]); const result2 = await redis.zrank(key, "one", "WITHSCORE"); expect(result2).toEqual([0, 1.5]); const result3 = await redis.zrank(key, "nonexistent", "WITHSCORE"); expect(result3).toBeNull(); }); test("should handle ties in score with ZRANK", async () => { const redis = ctx.redis; const key = "zrank-tie-test"; await redis.send("ZADD", [key, "1", "a", "1", "b", "1", "c", "2", "d"]); const rankA = await redis.zrank(key, "a"); expect(rankA).toBe(0); const rankB = await redis.zrank(key, "b"); expect(rankB).toBe(1); const rankC = await redis.zrank(key, "c"); expect(rankC).toBe(2); const rankD = await redis.zrank(key, "d"); expect(rankD).toBe(3); }); test("should get reverse rank of member with ZREVRANK", async () => { const redis = ctx.redis; const key = "zrevrank-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const rank1 = await redis.zrevrank(key, "five"); expect(rank1).toBe(0); const rank2 = await redis.zrevrank(key, "three"); expect(rank2).toBe(2); const rank3 = await redis.zrevrank(key, "one"); expect(rank3).toBe(4); const rank4 = await redis.zrevrank(key, "nonexistent"); expect(rank4).toBeNull(); }); test("should get reverse rank with score using WITHSCORE in ZREVRANK", async () => { const redis = ctx.redis; const key = "zrevrank-withscore-test"; await redis.send("ZADD", [key, "1.5", "one", "2.7", "two", "3.9", "three"]); const result = await redis.zrevrank(key, "two", "WITHSCORE"); expect(result).toEqual([1, 2.7]); const result2 = await redis.zrevrank(key, "three", "WITHSCORE"); expect(result2).toEqual([0, 3.9]); const result3 = await redis.zrevrank(key, "nonexistent", "WITHSCORE"); expect(result3).toBeNull(); }); test("should handle ties in score with ZREVRANK", async () => { const redis = ctx.redis; const key = "zrevrank-tie-test"; await redis.send("ZADD", [key, "1", "a", "2", "b", "2", "c", "2", "d"]); const rankD = await redis.zrevrank(key, "d"); expect(rankD).toBe(0); const rankC = await redis.zrevrank(key, "c"); expect(rankC).toBe(1); const rankB = await redis.zrevrank(key, "b"); expect(rankB).toBe(2); const rankA = await redis.zrevrank(key, "a"); expect(rankA).toBe(3); }); test("should reject invalid key in ZRANGEBYSCORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrangebyscore({} as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrangebyscore'."`, ); }); test("should reject invalid key in ZREVRANGEBYSCORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrevrangebyscore([] as any, 10, 0); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrevrangebyscore'."`, ); }); test("should reject invalid key in ZRANGEBYLEX", async () => { const redis = ctx.redis; expect(async () => { await redis.zrangebylex(null as any, "-", "+"); }).toThrowErrorMatchingInlineSnapshot(`"The "key" argument must be specified"`); }); test("should return members in reverse lexicographical order with ZREVRANGEBYLEX", async () => { const redis = ctx.redis; const key = "zrevrangebylex-test"; await redis.send("ZADD", [key, "0", "apple", "0", "banana", "0", "cherry", "0", "date", "0", "elderberry"]); const all = await redis.zrevrangebylex(key, "+", "-"); expect(all).toEqual(["elderberry", "date", "cherry", "banana", "apple"]); const range1 = await redis.zrevrangebylex(key, "[date", "[banana"); expect(range1).toEqual(["date", "cherry", "banana"]); const range2 = await redis.zrevrangebylex(key, "(date", "(banana"); expect(range2).toEqual(["cherry"]); const range3 = await redis.zrevrangebylex(key, "[elderberry", "(cherry"); expect(range3).toEqual(["elderberry", "date"]); }); test("should support LIMIT option with ZREVRANGEBYLEX", async () => { const redis = ctx.redis; const key = "zrevrangebylex-limit-test"; await redis.send("ZADD", [key, "0", "a", "0", "b", "0", "c", "0", "d", "0", "e", "0", "f", "0", "g"]); const limited1 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "0", "3"); expect(limited1).toEqual(["g", "f", "e"]); const limited2 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "2", "3"); expect(limited2).toEqual(["e", "d", "c"]); const limited3 = await redis.zrevrangebylex(key, "+", "-", "LIMIT", "5", "10"); expect(limited3).toEqual(["b", "a"]); }); test("should store range of members with ZRANGESTORE", async () => { const redis = ctx.redis; const source = "zrangestore-source"; const dest = "zrangestore-dest"; await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const count1 = await redis.zrangestore(dest, source, 1, 3); expect(count1).toBe(3); const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["two", "three", "four"]); }); test("should store range with BYSCORE option in ZRANGESTORE", async () => { const redis = ctx.redis; const source = "zrangestore-byscore-source"; const dest = "zrangestore-byscore-dest"; await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const count = await redis.zrangestore(dest, source, "2", "4", "BYSCORE"); expect(count).toBe(3); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["two", 2], ["three", 3], ["four", 4], ]); }); test("should store range in reverse order with REV option in ZRANGESTORE", async () => { const redis = ctx.redis; const source = "zrangestore-rev-source"; const dest = "zrangestore-rev-dest"; await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three"]); const count = await redis.zrangestore(dest, source, "0", "-1", "REV"); expect(count).toBe(3); const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["one", "two", "three"]); }); test("should support LIMIT option with ZRANGESTORE", async () => { const redis = ctx.redis; const source = "zrangestore-limit-source"; const dest = "zrangestore-limit-dest"; await redis.send("ZADD", [source, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const count = await redis.zrangestore(dest, source, "-inf", "+inf", "BYSCORE", "LIMIT", "1", "2"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["two", "three"]); }); test("should reject invalid key in ZREVRANGEBYLEX", async () => { const redis = ctx.redis; expect(async () => { await redis.zrevrangebylex({} as any, "+", "-"); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrevrangebylex'."`, ); }); test("should reject invalid destination in ZRANGESTORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrangestore([] as any, "source", 0, 10); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, ); }); test("should reject invalid source in ZRANGESTORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zrangestore("dest", null as any, 0, 10); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zrangestore'."`, ); }); test("should compute intersection with ZINTER", async () => { const redis = ctx.redis; const key1 = "zinter-test-1"; const key2 = "zinter-test-2"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); const result1 = await redis.zinter(2, key1, key2); expect(result1).toEqual(["b", "c"]); const result2 = await redis.zinter(2, key1, key2, "WITHSCORES"); expect(result2).toEqual([ ["b", 3], ["c", 5], ]); }); test("should compute intersection with WEIGHTS in ZINTER", async () => { const redis = ctx.redis; const key1 = "zinter-weights-1"; const key2 = "zinter-weights-2"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); const result = await redis.zinter(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); expect(result).toEqual([ ["b", 7], ["c", 12], ]); }); test("should compute intersection with AGGREGATE in ZINTER", async () => { const redis = ctx.redis; const key1 = "zinter-agg-1"; const key2 = "zinter-agg-2"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); const result1 = await redis.zinter(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); expect(result1).toEqual([ ["b", 1], ["c", 2], ]); const result2 = await redis.zinter(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); expect(result2).toEqual([ ["b", 2], ["c", 3], ]); }); test("should handle empty intersection with ZINTER", async () => { const redis = ctx.redis; const key1 = "zinter-empty-1"; const key2 = "zinter-empty-2"; await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "1", "c", "2", "d"); const result = await redis.zinter(2, key1, key2); expect(result).toEqual([]); }); test("should store intersection with ZINTERSTORE", async () => { const redis = ctx.redis; const key1 = "zinterstore-test-1"; const key2 = "zinterstore-test-2"; const dest = "zinterstore-dest"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "1", "b", "2", "c", "3", "d"); const count = await redis.zinterstore(dest, 2, key1, key2); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["b", 3], ["c", 5], ]); }); test("should store intersection with WEIGHTS in ZINTERSTORE", async () => { const redis = ctx.redis; const key1 = "zinterstore-weights-1"; const key2 = "zinterstore-weights-2"; const dest = "zinterstore-weights-dest"; await redis.zadd(key1, "1", "x", "2", "y"); await redis.zadd(key2, "2", "x", "3", "y"); const count = await redis.zinterstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["x", 8], ["y", 13], ]); }); test("should store intersection with AGGREGATE in ZINTERSTORE", async () => { const redis = ctx.redis; const key1 = "zinterstore-agg-1"; const key2 = "zinterstore-agg-2"; const destMin = "zinterstore-agg-min"; const destMax = "zinterstore-agg-max"; await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); const count1 = await redis.zinterstore(destMin, 2, key1, key2, "AGGREGATE", "MIN"); expect(count1).toBe(2); const storedMin = await redis.send("ZRANGE", [destMin, "0", "-1", "WITHSCORES"]); expect(storedMin).toEqual([ ["m", 1], ["n", 1], ]); const count2 = await redis.zinterstore(destMax, 2, key1, key2, "AGGREGATE", "MAX"); expect(count2).toBe(2); const storedMax = await redis.send("ZRANGE", [destMax, "0", "-1", "WITHSCORES"]); expect(storedMax).toEqual([ ["m", 2], ["n", 3], ]); }); test("should handle empty result with ZINTERSTORE", async () => { const redis = ctx.redis; const key1 = "zinterstore-empty-1"; const key2 = "zinterstore-empty-2"; const dest = "zinterstore-empty-dest"; await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "1", "c", "2", "d"); const count = await redis.zinterstore(dest, 2, key1, key2); expect(count).toBe(0); const exists = await redis.exists(dest); expect(exists).toBe(false); }); test("should compute union with ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-test-1"; const key2 = "zunion-test-2"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); const result1 = await redis.zunion(2, key1, key2); expect(result1).toEqual(["a", "b", "d", "c"]); const result2 = await redis.zunion(2, key1, key2, "WITHSCORES"); expect(result2).toEqual([ ["a", 1], ["b", 6], ["d", 6], ["c", 8], ]); }); test("should compute union with WEIGHTS in ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-weights-1"; const key2 = "zunion-weights-2"; await redis.zadd(key1, "1", "x", "2", "y", "3", "z"); await redis.zadd(key2, "2", "y", "3", "z", "4", "w"); const result = await redis.zunion(2, key1, key2, "WEIGHTS", "2", "3", "WITHSCORES"); expect(result).toEqual([ ["x", 2], ["y", 10], ["w", 12], ["z", 15], ]); }); test("should compute union with AGGREGATE MIN in ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-min-1"; const key2 = "zunion-min-2"; await redis.zadd(key1, "1", "p", "3", "q"); await redis.zadd(key2, "2", "p", "1", "q"); const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MIN", "WITHSCORES"); expect(result).toEqual([ ["p", 1], ["q", 1], ]); }); test("should compute union with AGGREGATE MAX in ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-max-1"; const key2 = "zunion-max-2"; await redis.zadd(key1, "1", "r", "3", "s"); await redis.zadd(key2, "2", "r", "1", "s"); const result = await redis.zunion(2, key1, key2, "AGGREGATE", "MAX", "WITHSCORES"); expect(result).toEqual([ ["r", 2], ["s", 3], ]); }); test("should compute union with single set in ZUNION", async () => { const redis = ctx.redis; const key = "zunion-single"; await redis.zadd(key, "1", "one", "2", "two", "3", "three"); const result = await redis.zunion(1, key); expect(result).toEqual(["one", "two", "three"]); }); test("should compute union with three sets in ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-three-1"; const key2 = "zunion-three-2"; const key3 = "zunion-three-3"; await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "2", "b", "3", "c"); await redis.zadd(key3, "3", "c", "4", "d"); const result = await redis.zunion(3, key1, key2, key3, "WITHSCORES"); expect(result).toEqual([ ["a", 1], ["b", 4], ["d", 4], ["c", 6], ]); }); test("should handle empty set in ZUNION", async () => { const redis = ctx.redis; const key1 = "zunion-empty-1"; const key2 = "zunion-empty-2"; await redis.zadd(key1, "1", "a", "2", "b"); const result = await redis.zunion(2, key1, key2); expect(result).toEqual(["a", "b"]); }); test("should store union with ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-test-1"; const key2 = "zunionstore-test-2"; const dest = "zunionstore-dest"; await redis.zadd(key1, "1", "a", "2", "b", "3", "c"); await redis.zadd(key2, "4", "b", "5", "c", "6", "d"); const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(4); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["a", 1], ["b", 6], ["d", 6], ["c", 8], ]); }); test("should store union with WEIGHTS in ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-weights-1"; const key2 = "zunionstore-weights-2"; const dest = "zunionstore-weights-dest"; await redis.zadd(key1, "1", "x", "2", "y"); await redis.zadd(key2, "2", "x", "3", "y"); const count = await redis.zunionstore(dest, 2, key1, key2, "WEIGHTS", "2", "3"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["x", 8], ["y", 13], ]); }); test("should store union with AGGREGATE MIN in ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-agg-min-1"; const key2 = "zunionstore-agg-min-2"; const dest = "zunionstore-agg-min-dest"; await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MIN"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["m", 1], ["n", 1], ]); }); test("should store union with AGGREGATE MAX in ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-agg-max-1"; const key2 = "zunionstore-agg-max-2"; const dest = "zunionstore-agg-max-dest"; await redis.zadd(key1, "1", "m", "3", "n"); await redis.zadd(key2, "2", "m", "1", "n"); const count = await redis.zunionstore(dest, 2, key1, key2, "AGGREGATE", "MAX"); expect(count).toBe(2); const stored = await redis.send("ZRANGE", [dest, "0", "-1", "WITHSCORES"]); expect(stored).toEqual([ ["m", 2], ["n", 3], ]); }); test("should overwrite existing destination with ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-overwrite-1"; const key2 = "zunionstore-overwrite-2"; const dest = "zunionstore-overwrite-dest"; await redis.zadd(dest, "100", "old"); await redis.zadd(key1, "1", "a", "2", "b"); await redis.zadd(key2, "3", "c"); const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(3); const stored = await redis.send("ZRANGE", [dest, "0", "-1"]); expect(stored).toEqual(["a", "b", "c"]); expect(stored).not.toContain("old"); }); test("should handle empty sets with ZUNIONSTORE", async () => { const redis = ctx.redis; const key1 = "zunionstore-empty-1"; const key2 = "zunionstore-empty-2"; const dest = "zunionstore-empty-dest"; const count = await redis.zunionstore(dest, 2, key1, key2); expect(count).toBe(0); const exists = await redis.exists(dest); expect(exists).toBe(false); }); test("should reject invalid numkeys in ZUNION", async () => { const redis = ctx.redis; expect(async () => { await redis.zunion(-1, "key1"); }).toThrowErrorMatchingInlineSnapshot(`"ERR at least 1 input key is needed for 'zunion' command"`); }); test("should reject invalid key in ZUNION", async () => { const redis = ctx.redis; expect(async () => { await redis.zunion(1, {} as any); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zunion'."`); }); test("should reject invalid destination in ZUNIONSTORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zunionstore([] as any, 2, "key1", "key2"); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, ); }); test("should reject invalid source key in ZUNIONSTORE", async () => { const redis = ctx.redis; expect(async () => { await redis.zunionstore("dest", 2, "key1", null as any); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'zunionstore'."`, ); }); test("should pop members with MIN option using ZMPOP", async () => { const redis = ctx.redis; const key = "zmpop-min-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result1 = await redis.zmpop(1, key, "MIN"); expect(result1).toEqual([key, [["one", 1]]]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(4); }); test("should pop members with MAX option using ZMPOP", async () => { const redis = ctx.redis; const key = "zmpop-max-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result1 = await redis.zmpop(1, key, "MAX"); expect(result1).toEqual([key, [["five", 5]]]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(4); }); test("should pop multiple members with COUNT option using ZMPOP", async () => { const redis = ctx.redis; const key = "zmpop-count-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three", "4", "four", "5", "five"]); const result = await redis.zmpop(1, key, "MIN", "COUNT", 3); expect(result).toBeDefined(); expect(result).not.toBeNull(); expect(result![0]).toBe(key); expect(result![1]).toEqual([ ["one", 1], ["two", 2], ["three", 3], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should return null when ZMPOP on empty set", async () => { const redis = ctx.redis; const emptyKey = "zmpop-empty-test"; const result = await redis.zmpop(1, emptyKey, "MIN"); expect(result).toBeNull(); }); test("should pop from first non-empty set with ZMPOP", async () => { const redis = ctx.redis; const key1 = "zmpop-multi-test1"; const key2 = "zmpop-multi-test2"; const key3 = "zmpop-multi-test3"; await redis.send("ZADD", [key2, "1", "one", "2", "two"]); const result = await redis.zmpop(3, key1, key2, key3, "MIN"); expect(result).toEqual([key2, [["one", 1]]]); }); test("should block and pop with BZMPOP", async () => { const redis = ctx.redis; const key = "bzmpop-test"; await redis.send("ZADD", [key, "1", "one", "2", "two"]); const result = await redis.bzmpop(0.1, 1, key, "MIN"); expect(result).toEqual([key, [["one", 1]]]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(1); }); test("should timeout with BZMPOP on empty set", async () => { const redis = ctx.redis; const emptyKey = "bzmpop-timeout-test"; const result = await redis.bzmpop(0.1, 1, emptyKey, "MIN"); expect(result).toBeNull(); }); test("should block and pop multiple members with BZMPOP COUNT", async () => { const redis = ctx.redis; const key = "bzmpop-count-test"; await redis.send("ZADD", [key, "1", "one", "2", "two", "3", "three"]); const result = await redis.bzmpop(0.5, 1, key, "MAX", "COUNT", 2); expect(result).toBeDefined(); expect(result).not.toBeNull(); expect(result![0]).toBe(key); expect(result![1]).toEqual([ ["three", 3], ["two", 2], ]); const count = await redis.send("ZCARD", [key]); expect(count).toBe(1); }); test("should reject invalid arguments in ZMPOP", async () => { const redis = ctx.redis; expect(async () => { await redis.zmpop({} as any, "key1", "MIN"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'zmpop'."`); }); test("should reject invalid arguments in BZMPOP", async () => { const redis = ctx.redis; expect(async () => { await redis.bzmpop(1, {} as any, "key1", "MIN"); }).toThrowErrorMatchingInlineSnapshot(`"Expected additional arguments to be a string or buffer for 'bzmpop'."`); }); test("should pop member with highest score using ZPOPMAX", async () => { const redis = ctx.redis; const key = "zpopmax-test"; await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); const result = await redis.zpopmax(key); expect(result).toBeDefined(); expect(result).not.toBeNull(); expect(Array.isArray(result)).toBe(true); expect(result).toHaveLength(2); expect(result![0]).toBe("three"); expect(result![1]).toBe(3); const count = await redis.zcard(key); expect(count).toBe(2); }); test("should pop member with lowest score using ZPOPMIN", async () => { const redis = ctx.redis; const key = "zpopmin-test"; await redis.zadd(key, 1.0, "one", 2.0, "two", 3.0, "three"); const result = await redis.zpopmin(key); expect(result).toBeDefined(); expect(result).not.toBeNull(); expect(Array.isArray(result)).toBe(true); expect(result).toHaveLength(2); expect(result![0]).toBe("one"); expect(result![1]).toBe(1); const count = await redis.zcard(key); expect(count).toBe(2); }); test("should return empty array for ZPOPMAX on empty set", async () => { const redis = ctx.redis; const key = "zpopmax-empty-test"; const result = await redis.zpopmax(key); expect(result).toEqual([]); }); test("should return empty array for ZPOPMIN on empty set", async () => { const redis = ctx.redis; const key = "zpopmin-empty-test"; const result = await redis.zpopmin(key); expect(result).toEqual([]); }); test("should block and pop lowest score with BZPOPMIN", async () => { const redis = ctx.redis; const key = "bzpopmin-test"; await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); const result = await redis.bzpopmin(key, 0.1); expect(result).toBeDefined(); expect(result).toHaveLength(3); expect(result![0]).toBe(key); expect(result![1]).toBe("one"); expect(result![2]).toBe(1); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should timeout with BZPOPMIN when no elements available", async () => { const redis = ctx.redis; const key = "bzpopmin-empty-test"; const result = await redis.bzpopmin(key, 0.1); expect(result).toBeNull(); }); test("should block and pop highest score with BZPOPMAX", async () => { const redis = ctx.redis; const key = "bzpopmax-test"; await redis.send("ZADD", [key, "1.0", "one", "2.0", "two", "3.0", "three"]); const result = await redis.bzpopmax(key, 0.1); expect(result).toBeDefined(); expect(result).toHaveLength(3); expect(result![0]).toBe(key); expect(result![1]).toBe("three"); expect(result![2]).toBe(3); const count = await redis.send("ZCARD", [key]); expect(count).toBe(2); }); test("should timeout with BZPOPMAX when no elements available", async () => { const redis = ctx.redis; const key = "bzpopmax-empty-test"; const result = await redis.bzpopmax(key, 0.1); expect(result).toBeNull(); }); test("should work with multiple keys in BZPOPMIN", async () => { const redis = ctx.redis; const key1 = "bzpopmin-multi-1"; const key2 = "bzpopmin-multi-2"; await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); const result = await redis.bzpopmin(key1, key2, 0.1); expect(result).toBeDefined(); expect(result![0]).toBe(key2); expect(result![1]).toBe("five"); expect(result![2]).toBe(5); }); test("should work with multiple keys in BZPOPMAX", async () => { const redis = ctx.redis; const key1 = "bzpopmax-multi-1"; const key2 = "bzpopmax-multi-2"; await redis.send("ZADD", [key2, "5.0", "five", "6.0", "six"]); const result = await redis.bzpopmax(key1, key2, 0.5); expect(result).toBeDefined(); expect(result![0]).toBe(key2); expect(result![1]).toBe("six"); expect(result![2]).toBe(6); }); test("should reject invalid arguments in BZPOPMIN", async () => { const redis = ctx.redis; expect(async () => { await redis.bzpopmin({} as any, 1); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'bzpopmin'."`, ); }); test("should reject invalid arguments in BZPOPMAX", async () => { const redis = ctx.redis; expect(async () => { await redis.bzpopmax([] as any, 1); }).toThrowErrorMatchingInlineSnapshot( `"Expected additional arguments to be a string or buffer for 'bzpopmax'."`, ); }); }); describe("Hash Operations", () => { test("should increment hash field by integer with HINCRBY", async () => { const redis = ctx.redis; const key = "hincrby-test"; const val1 = await redis.hincrby(key, "field1", 5); expect(val1).toBe(5); const val2 = await redis.hincrby(key, "field1", 3); expect(val2).toBe(8); const val3 = await redis.hincrby(key, "field1", -2); expect(val3).toBe(6); }); test("should increment hash field by float with HINCRBYFLOAT", async () => { const redis = ctx.redis; const key = "hincrbyfloat-test"; const val1 = await redis.hincrbyfloat(key, "field1", 2.5); expect(val1).toBe("2.5"); const val2 = await redis.hincrbyfloat(key, "field1", 1.3); expect(Number.parseFloat(val2)).toBeCloseTo(3.8); const val3 = await redis.hincrbyfloat(key, "field1", -0.8); expect(Number.parseFloat(val3)).toBeCloseTo(3.0); }); test("should get all hash keys with HKEYS", async () => { const redis = ctx.redis; const key = "hkeys-test"; const keys1 = await redis.hkeys(key); expect(keys1).toEqual([]); await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const keys2 = await redis.hkeys(key); expect(keys2.sort()).toEqual(["field1", "field2", "field3"]); }); test("should get hash length with HLEN", async () => { const redis = ctx.redis; const key = "hlen-test"; const len1 = await redis.hlen(key); expect(len1).toBe(0); await redis.hset(key, "field1", "value1", "field2", "value2"); const len2 = await redis.hlen(key); expect(len2).toBe(2); await redis.hset(key, "field3", "value3"); const len3 = await redis.hlen(key); expect(len3).toBe(3); }); test("should get multiple hash values with HMGET where only two arguments are passed because the second is an array", async () => { const redis = ctx.redis; const key = "hmget-test"; await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const values = await redis.hmget(key, ["field1", "field2", "field3"]); expect(values).toEqual(["value1", "value2", "value3"]); const mixed = await redis.hmget(key, ["field1", "nonexistent", "field2"]); expect(mixed).toEqual(["value1", null, "value2"]); }); test("should get multiple hash values with HMGET", async () => { const redis = ctx.redis; const key = "hmget-test"; await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const values = await redis.hmget(key, "field1", "field2", "field3"); expect(values).toEqual(["value1", "value2", "value3"]); const mixed = await redis.hmget(key, "field1", "nonexistent", "field2"); expect(mixed).toEqual(["value1", null, "value2"]); }); test("should get all hash values with HVALS", async () => { const redis = ctx.redis; const key = "hvals-test"; const vals1 = await redis.hvals(key); expect(vals1).toEqual([]); await redis.hset(key, "field1", "value1", "field2", "value2", "field3", "value3"); const vals2 = await redis.hvals(key); expect(vals2.sort()).toEqual(["value1", "value2", "value3"]); }); test("should get hash field string length with HSTRLEN", async () => { const redis = ctx.redis; const key = "hstrlen-test"; await redis.hset(key, "field1", "Hello", "field2", "World!"); const len1 = await redis.hstrlen(key, "field1"); expect(len1).toBe(5); const len2 = await redis.hstrlen(key, "field2"); expect(len2).toBe(6); const len3 = await redis.hstrlen(key, "nonexistent"); expect(len3).toBe(0); }); test("should set hash field expiration with HEXPIRE", async () => { const redis = ctx.redis; const key = "hexpire-test"; await redis.hset(key, "field1", "value1", "field2", "value2"); const result = await redis.hexpire(key, 60, "FIELDS", 1, "field1"); expect(result).toEqual([1]); const ttl = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttl[0]).toBeGreaterThan(0); expect(ttl[0]).toBeLessThanOrEqual(60); }); test("should set hash field expiration at timestamp with HEXPIREAT", async () => { const redis = ctx.redis; const key = "hexpireat-test"; await redis.hset(key, "field1", "value1"); const futureTs = Math.floor(Date.now() / 1000) + 60; const result = await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); expect(result).toEqual([1]); const ttl = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttl[0]).toBeGreaterThan(0); }); test("should get hash field expiration time with HEXPIRETIME", async () => { const redis = ctx.redis; const key = "hexpiretime-test"; await redis.hset(key, "field1", "value1"); const futureTs = Math.floor(Date.now() / 1000) + 60; await redis.hexpireat(key, futureTs, "FIELDS", 1, "field1"); const expireTime = await redis.hexpiretime(key, "FIELDS", 1, "field1"); expect(expireTime[0]).toBeGreaterThan(0); expect(expireTime[0]).toBeLessThanOrEqual(futureTs); }); test("should remove hash field expiration with HPERSIST", async () => { const redis = ctx.redis; const key = "hpersist-test"; await redis.hset(key, "field1", "value1"); await redis.hexpire(key, 60, "FIELDS", 1, "field1"); const ttlBefore = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttlBefore[0]).toBeGreaterThan(0); const result = await redis.hpersist(key, "FIELDS", 1, "field1"); expect(result).toEqual([1]); const ttlAfter = await redis.httl(key, "FIELDS", 1, "field1"); expect(ttlAfter[0]).toBe(-1); }); test("should set hash field expiration at timestamp in ms with HPEXPIREAT", async () => { const redis = ctx.redis; const key = "hpexpireat-test"; await redis.hset(key, "field1", "value1"); const futureTs = Date.now() + 5000; const result = await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); expect(result).toEqual([1]); const pttl = await redis.hpttl(key, "FIELDS", 1, "field1"); expect(pttl[0]).toBeGreaterThan(0); }); test("should get hash field expiration time in ms with HPEXPIRETIME", async () => { const redis = ctx.redis; const key = "hpexpiretime-test"; await redis.hset(key, "field1", "value1"); const futureTs = Date.now() + 5000; await redis.hpexpireat(key, futureTs, "FIELDS", 1, "field1"); const pexpireTime = await redis.hpexpiretime(key, "FIELDS", 1, "field1"); expect(pexpireTime[0]).toBeGreaterThan(0); expect(pexpireTime[0]).toBeLessThanOrEqual(futureTs); }); test("should set hash fields using object syntax", async () => { const redis = ctx.redis; const key = "hash-object-test"; const result = await redis.hset(key, { field1: "value1", field2: "value2", field3: "value3" }); expect(result).toBe(3); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); const value2 = await redis.hget(key, "field2"); expect(value2).toBe("value2"); const value3 = await redis.hget(key, "field3"); expect(value3).toBe("value3"); }); test("should set hash fields using variadic syntax", async () => { const redis = ctx.redis; const key = "hash-variadic-test"; const result = await redis.hset(key, "field1", "value1", "field2", "value2"); expect(result).toBe(2); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); const value2 = await redis.hget(key, "field2"); expect(value2).toBe("value2"); }); test("should set single hash field", async () => { const redis = ctx.redis; const key = "hash-single-test"; const result = await redis.hset(key, "field1", "value1"); expect(result).toBe(1); const value = await redis.hget(key, "field1"); expect(value).toBe("value1"); }); test("should update existing hash fields", async () => { const redis = ctx.redis; const key = "hash-update-test"; const result1 = await redis.hset(key, { field1: "value1", field2: "value2" }); expect(result1).toBe(2); const result2 = await redis.hset(key, { field1: "new-value1", field3: "value3" }); expect(result2).toBe(1); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("new-value1"); const value3 = await redis.hget(key, "field3"); expect(value3).toBe("value3"); }); test("should work with HMSET using object syntax", async () => { const redis = ctx.redis; const key = "hmset-object-test"; const result = await redis.hmset(key, { field1: "value1", field2: "value2" }); expect(result).toBe("OK"); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); const value2 = await redis.hget(key, "field2"); expect(value2).toBe("value2"); }); test("should work with HMSET using variadic syntax", async () => { const redis = ctx.redis; const key = "hmset-variadic-test"; const result = await redis.hmset(key, "field1", "value1", "field2", "value2"); expect(result).toBe("OK"); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); }); test("should work with HMSET using array syntax", async () => { const redis = ctx.redis; const key = "hmset-array-test"; const result = await redis.hmset(key, ["field1", "value1", "field2", "value2"]); expect(result).toBe("OK"); const value1 = await redis.hget(key, "field1"); expect(value1).toBe("value1"); }); test("should handle numeric field names and values", async () => { const redis = ctx.redis; const key = "hash-numeric-test"; const result = await redis.hset(key, { 123: "value1", field2: 456 }); expect(result).toBe(2); const value1 = await redis.hget(key, "123"); expect(value1).toBe("value1"); const value2 = await redis.hget(key, "field2"); expect(value2).toBe("456"); }); test("should throw error for odd number of variadic arguments", async () => { const redis = ctx.redis; const key = "hash-error-test"; expect(async () => { await redis.hset(key, "field1", "value1", "field2"); }).toThrow("HSET requires field-value pairs (even number of arguments after key)"); }); test("should throw error for empty object", async () => { const redis = ctx.redis; const key = "hash-empty-test"; expect(async () => { await redis.hset(key, {}); }).toThrow("HSET requires at least one field-value pair"); }); test("should throw error for array with odd number of elements", async () => { const redis = ctx.redis; const key = "hmset-error-test"; expect(async () => { await redis.hmset(key, ["field1", "value1", "field2"]); }).toThrow("Array must have an even number of elements (field-value pairs)"); }); test("should handle large number of fields", async () => { const redis = ctx.redis; const key = "hash-large-test"; const fields: Record = {}; for (let i = 0; i < 100; i++) { fields[`field${i}`] = `value${i}`; } const result = await redis.hset(key, fields); expect(result).toBe(100); const value0 = await redis.hget(key, "field0"); expect(value0).toBe("value0"); const value99 = await redis.hget(key, "field99"); expect(value99).toBe("value99"); }); test("should set hash field only if it doesn't exist using hsetnx", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result1 = await redis.hsetnx(key, "name", "John"); expect(result1).toBe(true); const result2 = await redis.hsetnx(key, "name", "Jane"); expect(result2).toBe(false); const value = await redis.hget(key, "name"); expect(value).toBe("John"); const result3 = await redis.hsetnx(key, "age", "30"); expect(result3).toBe(true); }); test("should get and delete hash field using hgetdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const values = await redis.hgetdel(key, "FIELDS", 1, "name"); expect(values).toEqual(["John"]); const check = await redis.hget(key, "name"); expect(check).toBeNull(); const age = await redis.hget(key, "age"); expect(age).toBe("30"); }); test("should get and delete multiple hash fields using hgetdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const values = await redis.hgetdel(key, "FIELDS", 2, "name", "city"); expect(values).toEqual(["John", "NYC"]); expect(await redis.hget(key, "name")).toBeNull(); expect(await redis.hget(key, "city")).toBeNull(); expect(await redis.hget(key, "age")).toBe("30"); }); test("should get hash field with expiration using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const values = await redis.hgetex(key, "EX", 10, "FIELDS", 1, "name"); expect(values).toEqual(["John"]); const check = await redis.hget(key, "name"); expect(check).toBe("John"); const ttls = await redis.httl(key, "FIELDS", 1, "name"); expect(ttls).toHaveLength(1); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(10); }); test("should get hash fields without expiration using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const values = await redis.hgetex(key, "FIELDS", 2, "name", "age"); expect(values).toEqual(["John", "30"]); }); test("should get hash fields with PX expiration using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const values = await redis.hgetex(key, "PX", 5000, "FIELDS", 1, "name"); expect(values).toEqual(["John"]); const ttls = await redis.hpttl(key, "FIELDS", 1, "name"); expect(ttls).toHaveLength(1); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(5000); }); test("should get hash fields with EXAT using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const values = await redis.hgetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name"); expect(values).toEqual(["John"]); }); test("should get hash fields with PXAT using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const futureTimestamp = Date.now() + 60000; const values = await redis.hgetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name"); expect(values).toEqual(["John"]); }); test("should get hash fields with PERSIST using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); const values = await redis.hgetex(key, "PERSIST", "FIELDS", 1, "name"); expect(values).toEqual(["John"]); }); test("should get multiple hash fields and return null for missing fields using hgetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const values = await redis.hgetex(key, "FIELDS", 3, "name", "age", "city"); expect(values).toEqual(["John", null, null]); }); test("should set hash field with expiration using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result = await redis.hsetex(key, "EX", 10, "FIELDS", 1, "name", "John"); expect(result).toBe(1); const value = await redis.hget(key, "name"); expect(value).toBe("John"); const ttls = await redis.httl(key, "FIELDS", 1, "name"); expect(ttls).toHaveLength(1); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(10); }); test("should set multiple hash fields with expiration using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result = await redis.hsetex(key, "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBe("30"); const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); expect(ttls).toHaveLength(2); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(10); expect(ttls[1]).toBeGreaterThan(0); expect(ttls[1]).toBeLessThanOrEqual(10); }); test("should set hash fields without expiration using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result = await redis.hsetex(key, "FIELDS", 2, "name", "John", "age", "30"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBe("30"); const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); expect(ttls).toEqual([-1, -1]); }); test("should set hash fields with PX (milliseconds) using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result = await redis.hsetex(key, "PX", 5000, "FIELDS", 1, "name", "John"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); }); test("should set hash fields with EXAT (unix timestamp seconds) using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.hsetex(key, "EXAT", futureTimestamp, "FIELDS", 1, "name", "John"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); }); test("should set hash fields with PXAT (unix timestamp milliseconds) using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const futureTimestamp = Date.now() + 60000; const result = await redis.hsetex(key, "PXAT", futureTimestamp, "FIELDS", 1, "name", "John"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); }); test("should set hash fields with KEEPTTL using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); const result = await redis.hsetex(key, "KEEPTTL", "FIELDS", 1, "name", "Jane"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("Jane"); }); test("should set hash fields with FNX flag using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const result1 = await redis.hsetex(key, "FNX", "FIELDS", 2, "name", "Jane", "age", "30"); expect(result1).toBe(0); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBeNull(); const result2 = await redis.hsetex(key, "FNX", "FIELDS", 2, "city", "NYC", "country", "USA"); expect(result2).toBe(1); expect(await redis.hget(key, "city")).toBe("NYC"); expect(await redis.hget(key, "country")).toBe("USA"); }); test("should set hash fields with FXX flag using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const result1 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); expect(result1).toBe(0); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBeNull(); await redis.hset(key, { age: "25" }); const result2 = await redis.hsetex(key, "FXX", "FIELDS", 2, "name", "Jane", "age", "30"); expect(result2).toBe(1); expect(await redis.hget(key, "name")).toBe("Jane"); expect(await redis.hget(key, "age")).toBe("30"); }); test("should set hash fields with FNX and EX combined using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const result1 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "John", "age", "30"); expect(result1).toBe(1); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBe("30"); const result2 = await redis.hsetex(key, "FNX", "EX", 10, "FIELDS", 2, "name", "Jane", "age", "35"); expect(result2).toBe(0); expect(await redis.hget(key, "name")).toBe("John"); expect(await redis.hget(key, "age")).toBe("30"); }); test("should set hash fields with FXX and PX combined using hsetex", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const result = await redis.hsetex(key, "FXX", "PX", 5000, "FIELDS", 2, "name", "Jane", "age", "35"); expect(result).toBe(1); expect(await redis.hget(key, "name")).toBe("Jane"); expect(await redis.hget(key, "age")).toBe("35"); }); test("should check TTL of hash fields using httl", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); await redis.hset(key, { age: "30" }); const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "nonexistent"); expect(ttls).toHaveLength(3); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(100); expect(ttls[1]).toBe(-1); expect(ttls[2]).toBe(-2); }); test("should check TTL of hash fields using hpttl in milliseconds", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const expireResult = await redis.hpexpire(key, 5000, "FIELDS", 1, "name"); expect(expireResult).toEqual([1]); const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls).toHaveLength(2); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(5000); expect(ttls[1]).toBe(-1); }); test("should delete hash fields using hdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const deleted = await redis.hdel(key, "age"); expect(deleted).toBe(1); const age = await redis.hget(key, "age"); expect(age).toBeNull(); const name = await redis.hget(key, "name"); expect(name).toBe("John"); }); test("should delete multiple hash fields using hdel", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC", country: "USA" }); const deleted = await redis.hdel(key, "age", "city"); expect(deleted).toBe(2); const remaining = await redis.hgetall(key); expect(remaining).toEqual({ name: "John", country: "USA" }); }); test("should return empty object for hgetall on non-existent key", async () => { const redis = ctx.redis; const key = "nonexistent-hgetall-" + randomUUIDv7(); const result = await redis.hgetall(key); expect(result).toEqual({}); }); test("should check if hash field exists using hexists", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const nameExists = await redis.hexists(key, "name"); expect(nameExists).toBe(true); const emailExists = await redis.hexists(key, "email"); expect(emailExists).toBe(false); }); test("should get random field using hrandfield", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const field = await redis.hrandfield(key); expect(["name", "age", "city"]).toContain(field); }); test("should get multiple random fields using hrandfield with count", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const fields = await redis.hrandfield(key, 2); expect(fields).toBeInstanceOf(Array); expect(fields.length).toBe(2); fields.forEach(field => { expect(["name", "age", "city"]).toContain(field); }); }); test("should get random fields with values using hrandfield WITHVALUES", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const fullData = { name: "Andy", age: "30", city: "Cupertino" }; await redis.hset(key, fullData); const result = await redis.hrandfield(key, 2, "WITHVALUES"); expect(result).toBeInstanceOf(Array); expect(result.length).toBe(2); const obj = Object.fromEntries(result); expect(Object.keys(obj).length).toBe(2); for (const [field, value] of Object.entries(obj)) { expect(fullData).toHaveProperty(field, value); } }); test("should scan hash using hscan", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const [cursor, fields] = await redis.hscan(key, 0); expect(typeof cursor).toBe("string"); expect(fields).toBeInstanceOf(Array); expect(fields.length).toBe(6); const obj: Record = {}; for (let i = 0; i < fields.length; i += 2) { obj[fields[i]] = fields[i + 1]; } expect(obj).toEqual({ name: "John", age: "30", city: "NYC" }); }); test("should scan hash with pattern using hscan MATCH", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { field1: "val1", field2: "val2", other: "val3" }); const [cursor, fields] = await redis.hscan(key, 0, "MATCH", "field*"); expect(typeof cursor).toBe("string"); expect(fields).toBeInstanceOf(Array); const obj: Record = {}; for (let i = 0; i < fields.length; i += 2) { obj[fields[i]] = fields[i + 1]; } expect(obj.field1).toBe("val1"); expect(obj.field2).toBe("val2"); expect(obj.other).toBeUndefined(); }); test("should scan hash with count using hscan COUNT", async () => { const redis = ctx.redis; const key = "user:" + randomUUIDv7().substring(0, 8); const fields: Record = {}; for (let i = 0; i < 20; i++) { fields[`field${i}`] = `value${i}`; } await redis.hset(key, fields); const [cursor, result] = await redis.hscan(key, 0, "COUNT", 5); expect(typeof cursor).toBe("string"); expect(result).toBeInstanceOf(Array); expect(result.length).toBeGreaterThan(0); }); test("should increment hash field by integer using hincrby", async () => { const redis = ctx.redis; const key = "hincrby-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { counter: "10" }); const result1 = await redis.hincrby(key, "counter", 5); expect(result1).toBe(15); const result2 = await redis.hincrby(key, "counter", -3); expect(result2).toBe(12); const value = await redis.hget(key, "counter"); expect(value).toBe("12"); }); test("should increment hash field from zero using hincrby", async () => { const redis = ctx.redis; const key = "hincrby-zero-test:" + randomUUIDv7().substring(0, 8); const result = await redis.hincrby(key, "newfield", 42); expect(result).toBe(42); const value = await redis.hget(key, "newfield"); expect(value).toBe("42"); }); test("should increment hash field by float using hincrbyfloat", async () => { const redis = ctx.redis; const key = "hincrbyfloat-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { price: "10.5" }); const result1 = await redis.hincrbyfloat(key, "price", 2.3); expect(result1).toBe("12.8"); const result2 = await redis.hincrbyfloat(key, "price", -0.8); expect(result2).toBe("12"); const value = await redis.hget(key, "price"); expect(value).toBe("12"); }); test("should increment hash field from zero using hincrbyfloat", async () => { const redis = ctx.redis; const key = "hincrbyfloat-zero-test:" + randomUUIDv7().substring(0, 8); const result = await redis.hincrbyfloat(key, "newfield", 3.14); expect(result).toBe("3.14"); const value = await redis.hget(key, "newfield"); expect(value).toBe("3.14"); }); test("should get all hash keys using hkeys", async () => { const redis = ctx.redis; const key = "hkeys-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const keys = await redis.hkeys(key); expect(keys).toBeInstanceOf(Array); expect(keys.length).toBe(3); expect(keys).toContain("name"); expect(keys).toContain("age"); expect(keys).toContain("city"); }); test("should return empty array for non-existent key using hkeys", async () => { const redis = ctx.redis; const key = "hkeys-nonexistent:" + randomUUIDv7().substring(0, 8); const keys = await redis.hkeys(key); expect(keys).toEqual([]); }); test("should get hash length using hlen", async () => { const redis = ctx.redis; const key = "hlen-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const length = await redis.hlen(key); expect(length).toBe(3); await redis.hset(key, { country: "USA" }); const newLength = await redis.hlen(key); expect(newLength).toBe(4); }); test("should return 0 for non-existent key using hlen", async () => { const redis = ctx.redis; const key = "hlen-nonexistent:" + randomUUIDv7().substring(0, 8); const length = await redis.hlen(key); expect(length).toBe(0); }); test("should get multiple hash values using hmget", async () => { const redis = ctx.redis; const key = "hmget-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const values = await redis.hmget(key, "name", "age", "city"); expect(values).toEqual(["John", "30", "NYC"]); }); test("should return null for missing fields using hmget", async () => { const redis = ctx.redis; const key = "hmget-missing-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const values = await redis.hmget(key, "name", "age", "city"); expect(values).toEqual(["John", null, null]); }); test("should get all hash values using hvals", async () => { const redis = ctx.redis; const key = "hvals-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const values = await redis.hvals(key); expect(values).toBeInstanceOf(Array); expect(values.length).toBe(3); expect(values).toContain("John"); expect(values).toContain("30"); expect(values).toContain("NYC"); }); test("should return empty array for non-existent key using hvals", async () => { const redis = ctx.redis; const key = "hvals-nonexistent:" + randomUUIDv7().substring(0, 8); const values = await redis.hvals(key); expect(values).toEqual([]); }); test("should get hash field string length using hstrlen", async () => { const redis = ctx.redis; const key = "hstrlen-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", description: "Software Engineer" }); const nameLen = await redis.hstrlen(key, "name"); expect(nameLen).toBe(4); const descLen = await redis.hstrlen(key, "description"); expect(descLen).toBe(17); }); test("should return 0 for non-existent field using hstrlen", async () => { const redis = ctx.redis; const key = "hstrlen-nonexistent:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John" }); const length = await redis.hstrlen(key, "age"); expect(length).toBe(0); }); test("should expire hash fields using hexpire", async () => { const redis = ctx.redis; const key = "hexpire-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30", city: "NYC" }); const result = await redis.hexpire(key, 10, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); const ttls = await redis.httl(key, "FIELDS", 3, "name", "age", "city"); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(10); expect(ttls[1]).toBeGreaterThan(0); expect(ttls[1]).toBeLessThanOrEqual(10); expect(ttls[2]).toBe(-1); }); test("should expire hash fields with NX flag using hexpire", async () => { const redis = ctx.redis; const key = "hexpire-nx-test:" + randomUUIDv7().substring(0, 8); await redis.hsetex(key, "EX", 100, "FIELDS", 1, "name", "John"); await redis.hset(key, { age: "30" }); const result = await redis.hexpire(key, 10, "NX", "FIELDS", 2, "name", "age"); expect(result).toEqual([0, 1]); }); test("should expire hash fields at specific time using hexpireat", async () => { const redis = ctx.redis; const key = "hexpireat-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const futureTimestamp = Math.floor(Date.now() / 1000) + 60; const result = await redis.hexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(60); expect(ttls[1]).toBeGreaterThan(0); expect(ttls[1]).toBeLessThanOrEqual(60); }); test("should get hash field expiration time using hexpiretime", async () => { const redis = ctx.redis; const key = "hexpiretime-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const futureTimestamp = Math.floor(Date.now() / 1000) + 100; await redis.hexpireat(key, futureTimestamp, "FIELDS", 1, "name"); const expiretimes = await redis.hexpiretime(key, "FIELDS", 2, "name", "age"); expect(expiretimes).toHaveLength(2); expect(expiretimes[0]).toBeGreaterThan(0); expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); expect(expiretimes[1]).toBe(-1); }); test("should expire hash fields at specific time in milliseconds using hpexpireat", async () => { const redis = ctx.redis; const key = "hpexpireat-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const futureTimestamp = Date.now() + 60000; const result = await redis.hpexpireat(key, futureTimestamp, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); const ttls = await redis.hpttl(key, "FIELDS", 2, "name", "age"); expect(ttls[0]).toBeGreaterThan(0); expect(ttls[0]).toBeLessThanOrEqual(60100); expect(ttls[1]).toBeGreaterThan(0); expect(ttls[1]).toBeLessThanOrEqual(60100); }); test("should get hash field expiration time in milliseconds using hpexpiretime", async () => { const redis = ctx.redis; const key = "hpexpiretime-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const futureTimestamp = Date.now() + 100000; await redis.hpexpireat(key, futureTimestamp, "FIELDS", 1, "name"); const expiretimes = await redis.hpexpiretime(key, "FIELDS", 2, "name", "age"); expect(expiretimes).toHaveLength(2); expect(expiretimes[0]).toBeGreaterThan(0); expect(expiretimes[0]).toBeLessThanOrEqual(futureTimestamp); expect(expiretimes[1]).toBe(-1); }); test("should persist hash fields using hpersist", async () => { const redis = ctx.redis; const key = "hpersist-test:" + randomUUIDv7().substring(0, 8); await redis.hsetex(key, "EX", 100, "FIELDS", 2, "name", "John", "age", "30"); const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); expect(result).toEqual([1, 1]); const ttls = await redis.httl(key, "FIELDS", 2, "name", "age"); expect(ttls).toEqual([-1, -1]); }); test("should return 0 for fields without expiration using hpersist", async () => { const redis = ctx.redis; const key = "hpersist-noexpire-test:" + randomUUIDv7().substring(0, 8); await redis.hset(key, { name: "John", age: "30" }); const result = await redis.hpersist(key, "FIELDS", 2, "name", "age"); expect(result).toEqual([-1, -1]); }); }); describe("Connection State", () => { test("should have a connected property", () => { const redis = ctx.redis; expect(typeof redis.connected).toBe("boolean"); }); }); describe("RESP3 Data Types", () => { test("should handle hash maps (dictionaries) as command responses", async () => { const redis = ctx.redis; const userId = "user:" + randomUUIDv7().substring(0, 8); const setResult = await redis.send("HSET", [userId, "name", "John", "age", "30", "active", "true"]); expect(setResult).toBeDefined(); const hash = await redis.send("HGETALL", [userId]); expect(hash).toBeDefined(); if (typeof hash === "object" && hash !== null) { expect(hash).toHaveProperty("name"); expect(hash).toHaveProperty("age"); expect(hash).toHaveProperty("active"); expect(hash.name).toBe("John"); expect(hash.age).toBe("30"); expect(hash.active).toBe("true"); } }); test("should handle sets as command responses", async () => { const redis = ctx.redis; const setKey = "colors:" + randomUUIDv7().substring(0, 8); const addResult = await redis.send("SADD", [setKey, "red", "blue", "green"]); expect(addResult).toBeDefined(); const setMembers = await redis.send("SMEMBERS", [setKey]); expect(setMembers).toBeDefined(); expect(Array.isArray(setMembers)).toBe(true); expect(setMembers).toContain("red"); expect(setMembers).toContain("blue"); expect(setMembers).toContain("green"); }); }); describe("Connection Options", () => { test("connection errors", async () => { const url = new URL(connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL); url.username = "badusername"; url.password = "secretpassword"; const customRedis = new RedisClient(url.toString(), { tls: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tls : false, }); expect(async () => { await customRedis.get("test"); }).toThrowErrorMatchingInlineSnapshot(`"WRONGPASS invalid username-password pair or user is disabled."`); }); const testKeyUniquePerDb = crypto.randomUUID(); test.each([...Array(16).keys()])("Connecting to database with url $url succeeds", async (dbId: number) => { const redis = createClient(connectionType, {}, dbId); const testValue = await redis.get(testKeyUniquePerDb); expect(testValue).toBeNull(); redis.close(); }); }); describe("Reconnections", () => { test.skip("should automatically reconnect after connection drop", async () => { const TEST_KEY = "test-key"; const TEST_VALUE = "test-value"; if (!ctx.redis || !ctx.redis.connected) { ctx.redis = createClient(connectionType); } const valueBeforeStart = await ctx.redis.get(TEST_KEY); expect(valueBeforeStart).toBeNull(); await ctx.redis.set(TEST_KEY, TEST_VALUE); const valueAfterSet = await ctx.redis.get(TEST_KEY); expect(valueAfterSet).toBe(TEST_VALUE); await ctx.restartServer(); const valueAfterStop = await ctx.redis.get(TEST_KEY); expect(valueAfterStop).toBe(TEST_VALUE); }); }); describe("PUB/SUB", () => { var i = 0; const testChannel = () => { return `test-channel-${i++}`; }; const testKey = () => { return `test-key-${i++}`; }; const testValue = () => { return `test-value-${i++}`; }; const testMessage = () => { return `test-message-${i++}`; }; beforeEach(async () => { await ctx.cleanupSubscribers(); }); test("publishing to a channel does not fail", async () => { expect(await ctx.redis.publish(testChannel(), testMessage())).toBe(0); }); test("setting in subscriber mode gracefully fails", async () => { const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(testChannel(), () => {}); expect(() => subscriber.set(testKey(), testValue())).toThrow( "RedisClient.prototype.set cannot be called while in subscriber mode", ); await subscriber.unsubscribe(testChannel()); }); test("setting after unsubscribing works", async () => { const channel = testChannel(); const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); await subscriber.unsubscribe(channel); expect(ctx.redis.set(testKey(), testValue())).resolves.toEqual("OK"); }); test("subscribing to a channel receives messages", async () => { const TEST_MESSAGE_COUNT = 128; const subscriber = await ctx.newSubscriberClient(connectionType); const channel = testChannel(); const message = testMessage(); const counter = awaitableCounter(); await subscriber.subscribe(channel, (message, channel) => { counter.increment(); expect(channel).toBe(channel); expect(message).toBe(message); }); Array.from({ length: TEST_MESSAGE_COUNT }).forEach(async () => { expect(await ctx.redis.publish(channel, message)).toBe(1); }); await counter.untilValue(TEST_MESSAGE_COUNT); expect(counter.count()).toBe(TEST_MESSAGE_COUNT); }); test("messages are received in order", async () => { const channel = testChannel(); await ctx.redis.set("START-TEST", "1"); const TEST_MESSAGE_COUNT = 1024; const subscriber = await ctx.newSubscriberClient(connectionType); const counter = awaitableCounter(); var receivedMessages: string[] = []; await subscriber.subscribe(channel, message => { receivedMessages.push(message); counter.increment(); }); const sentMessages = Array.from({ length: TEST_MESSAGE_COUNT }).map(() => { return randomUUIDv7(); }); await Promise.all( sentMessages.map(async message => { expect(await ctx.redis.publish(channel, message)).toBe(1); }), ); await counter.untilValue(TEST_MESSAGE_COUNT); expect(receivedMessages.length).toBe(sentMessages.length); expect(receivedMessages).toEqual(sentMessages); await subscriber.unsubscribe(channel); await ctx.redis.set("STOP-TEST", "1"); }); test("subscribing to multiple channels receives messages", async () => { const TEST_MESSAGE_COUNT = 128; const subscriber = await ctx.newSubscriberClient(connectionType); const channels = [testChannel(), testChannel()]; const counter = awaitableCounter(); var receivedMessages: { [channel: string]: string[] } = {}; await subscriber.subscribe(channels, (message, channel) => { receivedMessages[channel] = receivedMessages[channel] || []; receivedMessages[channel].push(message); counter.increment(); }); var sentMessages: { [channel: string]: string[] } = {}; for (let i = 0; i < TEST_MESSAGE_COUNT; i++) { const channel = channels[randomCoinFlip() ? 0 : 1]; const message = randomUUIDv7(); expect(await ctx.redis.publish(channel, message)).toBe(1); sentMessages[channel] = sentMessages[channel] || []; sentMessages[channel].push(message); } await counter.untilValue(TEST_MESSAGE_COUNT); expect(Object.keys(receivedMessages).sort()).toEqual(Object.keys(sentMessages).sort()); for (const channel of channels) { if (sentMessages[channel]) { expect(receivedMessages[channel]).toEqual(sentMessages[channel]); } } await subscriber.unsubscribe(channels); }); test("unsubscribing from specific channels while remaining subscribed to others", async () => { const channel1 = "channel-1"; const channel2 = "channel-2"; const channel3 = "channel-3"; const subscriber = createClient(connectionType); await subscriber.connect(); let receivedMessages: { [channel: string]: string[] } = {}; const counter = awaitableCounter(); await subscriber.subscribe([channel1, channel2, channel3], (message, channel) => { receivedMessages[channel] = receivedMessages[channel] || []; receivedMessages[channel].push(message); counter.increment(); }); expect(await ctx.redis.publish(channel1, "msg1-before")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-before")).toBe(1); expect(await ctx.redis.publish(channel3, "msg3-before")).toBe(1); await counter.untilValue(3); await subscriber.unsubscribe(channel2); expect(await ctx.redis.publish(channel1, "msg1-after")).toBe(1); expect(await ctx.redis.publish(channel2, "msg2-after")).toBe(0); expect(await ctx.redis.publish(channel3, "msg3-after")).toBe(1); await counter.untilValue(5); expect(receivedMessages[channel1]).toEqual(["msg1-before", "msg1-after"]); expect(receivedMessages[channel2]).toEqual(["msg2-before"]); expect(receivedMessages[channel3]).toEqual(["msg3-before", "msg3-after"]); await subscriber.unsubscribe([channel1, channel3]); }); test("subscribing to the same channel multiple times", async () => { const subscriber = createClient(connectionType); await subscriber.connect(); const channel = testChannel(); const counter = awaitableCounter(); let callCount = 0; const listener = () => { callCount++; counter.increment(); }; let callCount2 = 0; const listener2 = () => { callCount2++; counter.increment(); }; await subscriber.subscribe(channel, listener); await subscriber.subscribe(channel, listener2); expect(await ctx.redis.publish(channel, "test-message")).toBe(1); await counter.untilValue(2); expect(callCount).toBe(1); expect(callCount2).toBe(1); await subscriber.unsubscribe(channel); }); test("empty string messages", async () => { const channel = "empty-message-channel"; const subscriber = createClient(connectionType); await subscriber.connect(); const counter = awaitableCounter(); let receivedMessage: string | undefined = undefined; await subscriber.subscribe(channel, message => { receivedMessage = message; counter.increment(); }); expect(await ctx.redis.publish(channel, "")).toBe(1); await counter.untilValue(1); expect(receivedMessage).not.toBeUndefined(); expect(receivedMessage!).toBe(""); await subscriber.unsubscribe(channel); }); test("special characters in channel names", async () => { const subscriber = createClient(connectionType); await subscriber.connect(); const specialChannels = [ "channel:with:colons", "channel with spaces", "channel-with-unicode-😀", "channel[with]brackets", "channel@with#special$chars", ]; for (const channel of specialChannels) { const counter = awaitableCounter(); let received = false; await subscriber.subscribe(channel, () => { received = true; counter.increment(); }); expect(await ctx.redis.publish(channel, "test")).toBe(1); await counter.untilValue(1); expect(received).toBe(true); await subscriber.unsubscribe(channel); } }); test("ping works in subscription mode", async () => { const channel = "ping-test-channel"; const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); const pong = await subscriber.ping(); expect(pong).toBe("PONG"); const customPing = await subscriber.ping("hello"); expect(customPing).toBe("hello"); }); test("publish does not work from a subscribed client", async () => { const channel = "self-publish-channel"; const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); expect(async () => subscriber.publish(channel, "self-published")).toThrow( "RedisClient.prototype.publish cannot be called while in subscriber mode.", ); }); test("complete unsubscribe restores normal command mode", async () => { const channel = "restore-test-channel"; const testKey = "restore-test-key"; const subscriber = await ctx.newSubscriberClient(connectionType); await subscriber.subscribe(channel, () => {}); expect(() => subscriber.set(testKey, testValue())).toThrow( "RedisClient.prototype.set cannot be called while in subscriber mode.", ); await subscriber.unsubscribe(); const result = await ctx.redis.set(testKey, "value"); expect(result).toBe("OK"); const value = await ctx.redis.get(testKey); expect(value).toBe("value"); }); test("publishing without subscribers succeeds", async () => { const channel = "no-subscribers-channel"; expect(await ctx.redis.publish(channel, "message")).toBe(0); }); test("unsubscribing from non-subscribed channels", async () => { const channel = "never-subscribed-channel"; expect(() => ctx.redis.unsubscribe(channel)).toThrow( "RedisClient.prototype.unsubscribe can only be called while in subscriber mode.", ); }); test("high volume pub/sub", async () => { const channel = testChannel(); const MESSAGE_COUNT = 1000; const MESSAGE_SIZE = 1024 * 1024; let byteCounter = awaitableCounter(5_000); // 5s timeout const subscriber = await ctx.redis.duplicate(); await subscriber.subscribe(channel, message => { byteCounter.incrementBy(message.length); }); for (let i = 0; i < MESSAGE_COUNT; i++) { await ctx.redis.publish(channel, "X".repeat(MESSAGE_SIZE)); } expect(await byteCounter.untilValue(MESSAGE_COUNT * MESSAGE_SIZE)).toBe(MESSAGE_COUNT * MESSAGE_SIZE); subscriber.close(); }); test("callback errors don't crash the client (without IPC)", async () => { const channel = "error-callback-channel"; const subscriberProc = spawn({ cmd: [bunExe(), `${__dirname}/valkey.failing-subscriber-no-ipc.ts`], stdout: "pipe", stderr: "inherit", stdin: "pipe", env: { ...process.env, NODE_ENV: "development" }, }); const reader = subscriberProc.stdout.getReader(); async function* readLines() { const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); buffer = lines.pop() || ""; for (const line of lines) { yield line; } } } async function waitForChildMessage(expectedEvent: MsgT["event"]): Promise { for await (const line of readLines()) { const parsed = JSON.parse(line); if (typeof parsed !== "object") { throw new Error("Expected object message"); } if (parsed.event === undefined || typeof parsed.event !== "string") { throw new Error("Expected event field as a string"); } if (parsed.event !== expectedEvent) { throw new Error(`Expected event ${expectedEvent} but got ${parsed.event}`); } return parsed as MsgT; } throw new Error("Input stream unexpectedly closed"); } async function messageChild(msg: MsgT): Promise { subscriberProc.stdin!.write(JSON.stringify(msg) + "\n"); } try { // Wait for the process to announce it is ready for messages. await waitForChildMessage("ready-for-url"); // Tell the child to start and connect to Redis. await messageChild({ event: "start", url: connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, tlsPaths: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tlsPaths : undefined, }); await waitForChildMessage("ready"); expect(await ctx.redis.publish(channel, "message1")).toBeGreaterThanOrEqual(1); expect(await waitForChildMessage("message")).toMatchObject({ index: 1 }); // This should throw inside the child process, so it should notify us. expect(await ctx.redis.publish(channel, "message2")).toBeGreaterThanOrEqual(1); await waitForChildMessage("exception"); expect(await ctx.redis.publish(channel, "message1")).toBeGreaterThanOrEqual(1); expect(await waitForChildMessage("message")).toMatchObject({ index: 3 }); } finally { subscriberProc.kill(); await subscriberProc.exited; } }); test("callback errors don't crash the client", async () => { const channel = "error-callback-channel"; const STEP_WAITING_FOR_URL = 1; const STEP_SUBSCRIBED = 2; const STEP_FIRST_MESSAGE = 3; const STEP_SECOND_MESSAGE = 4; const STEP_THIRD_MESSAGE = 5; const stepCounter = awaitableCounter(); let currentMessage: any = {}; const subscriberProc = spawn({ cmd: [bunExe(), `${__dirname}/valkey.failing-subscriber.ts`], stdout: "inherit", stderr: "inherit", ipc: msg => { currentMessage = msg; stepCounter.increment(); }, env: { ...process.env, NODE_ENV: "development", }, }); await stepCounter.untilValue(STEP_WAITING_FOR_URL); expect(currentMessage.event).toBe("waiting-for-url"); subscriberProc.send({ event: "start", url: connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL, tlsPaths: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tlsPaths : undefined, } as RedisTestStartMessage); try { await stepCounter.untilValue(STEP_SUBSCRIBED); expect(currentMessage.event).toBe("ready"); expect(await ctx.redis.publish(channel, "message1")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_FIRST_MESSAGE); expect(currentMessage.event).toBe("message"); expect(currentMessage.index).toBe(1); expect(await ctx.redis.publish(channel, "message2")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_SECOND_MESSAGE); expect(currentMessage.event).toBe("exception"); //expect(currentMessage.index).toBe(2); expect(await ctx.redis.publish(channel, "message3")).toBeGreaterThanOrEqual(1); await stepCounter.untilValue(STEP_THIRD_MESSAGE); expect(currentMessage.event).toBe("message"); expect(currentMessage.index).toBe(3); } finally { subscriberProc.kill(); await subscriberProc.exited; } }); test("subscriptions return correct counts", async () => { const subscriber = createClient(connectionType); await subscriber.connect(); expect(await subscriber.subscribe("chan1", () => {})).toBe(1); expect(await subscriber.subscribe("chan2", () => {})).toBe(2); }); test("unsubscribing from listeners", async () => { const channel = "error-callback-channel"; const subscriber = createClient(connectionType); await subscriber.connect(); const counter = awaitableCounter(); let messageCount1 = 0; const listener1 = () => { messageCount1++; counter.increment(); }; await subscriber.subscribe(channel, listener1); let messageCount2 = 0; const listener2 = () => { messageCount2++; counter.increment(); }; await subscriber.subscribe(channel, listener2); await ctx.redis.publish(channel, "message1"); await counter.untilValue(2); expect(messageCount1).toBe(1); expect(messageCount2).toBe(1); console.log("Unsubscribing listener2"); await subscriber.unsubscribe(channel, listener2); await ctx.redis.publish(channel, "message1"); await counter.untilValue(3); expect(messageCount1).toBe(2); expect(messageCount2).toBe(1); }); }); describe("duplicate()", () => { test("should create duplicate of connected client that gets connected", async () => { const duplicate = await ctx.redis.duplicate(); expect(duplicate.connected).toBe(true); expect(duplicate).not.toBe(ctx.redis); await ctx.redis.set("test-original", "original-value"); await duplicate.set("test-duplicate", "duplicate-value"); expect(await ctx.redis.get("test-duplicate")).toBe("duplicate-value"); expect(await duplicate.get("test-original")).toBe("original-value"); duplicate.close(); }); test("should preserve connection configuration in duplicate", async () => { await ctx.redis.connect(); const duplicate = await ctx.redis.duplicate(); const testKey = `duplicate-config-test-${randomUUIDv7().substring(0, 8)}`; const testValue = "test-value"; await ctx.redis.set(testKey, testValue); const retrievedValue = await duplicate.get(testKey); expect(retrievedValue).toBe(testValue); duplicate.close(); }); test("should allow duplicate to work independently from original", async () => { const duplicate = await ctx.redis.duplicate(); duplicate.close(); const testKey = `independent-test-${randomUUIDv7().substring(0, 8)}`; const testValue = "independent-value"; await ctx.redis.set(testKey, testValue); const retrievedValue = await ctx.redis.get(testKey); expect(retrievedValue).toBe(testValue); }); test("should handle duplicate of client in subscriber mode", async () => { const subscriber = await ctx.newSubscriberClient(connectionType); const testChannel = "test-subscriber-duplicate"; await subscriber.subscribe(testChannel, () => {}); const duplicate = await subscriber.duplicate(); expect(() => duplicate.set("test-key", "test-value")).not.toThrow(); await subscriber.unsubscribe(testChannel); }); test("should create multiple duplicates from same client", async () => { await ctx.redis.connect(); const duplicate1 = await ctx.redis.duplicate(); const duplicate2 = await ctx.redis.duplicate(); const duplicate3 = await ctx.redis.duplicate(); expect(duplicate1.connected).toBe(true); expect(duplicate2.connected).toBe(true); expect(duplicate3.connected).toBe(true); const testKey = `multi-duplicate-test-${randomUUIDv7().substring(0, 8)}`; await duplicate1.set(`${testKey}-1`, "value-1"); await duplicate2.set(`${testKey}-2`, "value-2"); await duplicate3.set(`${testKey}-3`, "value-3"); expect(await duplicate1.get(`${testKey}-1`)).toBe("value-1"); expect(await duplicate2.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate3.get(`${testKey}-3`)).toBe("value-3"); expect(await duplicate1.get(`${testKey}-2`)).toBe("value-2"); expect(await duplicate2.get(`${testKey}-3`)).toBe("value-3"); expect(await duplicate3.get(`${testKey}-1`)).toBe("value-1"); duplicate1.close(); duplicate2.close(); duplicate3.close(); }); test("should duplicate client that failed to connect", async () => { const url = new URL(connectionType === ConnectionType.TLS ? TLS_REDIS_URL : DEFAULT_REDIS_URL); url.username = "invaliduser"; url.password = "invalidpassword"; const failedRedis = new RedisClient(url.toString(), { tls: connectionType === ConnectionType.TLS ? TLS_REDIS_OPTIONS.tls : false, }); let connectionFailed = false; try { await failedRedis.connect(); } catch { connectionFailed = true; } expect(connectionFailed).toBe(true); expect(failedRedis.connected).toBe(false); const duplicate = await failedRedis.duplicate(); expect(duplicate.connected).toBe(false); }); test("should handle duplicate timing with concurrent operations", async () => { await ctx.redis.connect(); const testKey = `concurrent-test-${randomUUIDv7().substring(0, 8)}`; const originalOperation = ctx.redis.set(testKey, "original-value"); const duplicate = await ctx.redis.duplicate(); await originalOperation; expect(await duplicate.get(testKey)).toBe("original-value"); duplicate.close(); }); }); }); }