mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 18:38:55 +00:00
286 lines
9.6 KiB
TypeScript
286 lines
9.6 KiB
TypeScript
import { randomUUIDv7 } from "bun";
|
|
import { beforeEach, describe, expect, test } from "bun:test";
|
|
import { ConnectionType, createClient, ctx, isEnabled } from "../test-utils";
|
|
|
|
/**
|
|
* Test suite for error handling, protocol failures, and edge cases
|
|
* - Command errors (wrong arguments, invalid syntax)
|
|
* - Protocol parsing failures
|
|
* - Null/undefined/invalid input handling
|
|
* - Type errors
|
|
* - Edge cases
|
|
*/
|
|
describe.skipIf(!isEnabled)("Valkey: Error Handling", () => {
|
|
beforeEach(() => {
|
|
if (ctx.redis?.connected) {
|
|
ctx.redis.close?.();
|
|
}
|
|
ctx.redis = createClient(ConnectionType.TCP);
|
|
});
|
|
describe("Command Errors", () => {
|
|
test("should handle invalid command arguments", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Wrong number of arguments
|
|
|
|
expect(async () => await client.send("SET", ["key"])).toThrowErrorMatchingInlineSnapshot(
|
|
`"ERR wrong number of arguments for 'set' command"`,
|
|
); // Missing value argument
|
|
expect(async () => await client.send("INVALID_COMMAND", ["a"])).toThrowErrorMatchingInlineSnapshot(
|
|
`"ERR unknown command 'INVALID_COMMAND', with args beginning with: 'a' "`,
|
|
); // Invalid command
|
|
});
|
|
|
|
describe("should handle special character keys and values", async () => {
|
|
// Keys with special characters
|
|
const specialKeys = [
|
|
"key with spaces",
|
|
"key\nwith\nnewlines",
|
|
"key\twith\ttabs",
|
|
"key:with:colons",
|
|
"key-with-unicode-♥-❤-★",
|
|
];
|
|
|
|
// Values with special characters
|
|
const specialValues = [
|
|
"value with spaces",
|
|
"value\nwith\nnewlines",
|
|
"value\twith\ttabs",
|
|
"value:with:colons",
|
|
"value-with-unicode-♥-❤-★",
|
|
// RESP protocol special characters
|
|
"+OK\r\n",
|
|
"-ERR\r\n",
|
|
"$5\r\nhello\r\n",
|
|
"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n",
|
|
];
|
|
|
|
for (const key of specialKeys) {
|
|
for (const value of specialValues) {
|
|
const testKey = `special-key-${randomUUIDv7()}`;
|
|
test(`should handle special characters in key "${key}" and value "${value}"`, async () => {
|
|
const client = ctx.redis;
|
|
// Set and get should work with special characters
|
|
await client.set(testKey, value);
|
|
const result = await client.get(testKey);
|
|
expect(result).toBe(value);
|
|
});
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("Null/Undefined/Invalid Input Handling", () => {
|
|
test("should handle undefined/null command arguments", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// undefined key
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.get(undefined)).toThrowErrorMatchingInlineSnapshot(
|
|
`"Expected key to be a string or buffer for 'get'."`,
|
|
);
|
|
|
|
// null key
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.get(null)).toThrowErrorMatchingInlineSnapshot(
|
|
`"Expected key to be a string or buffer for 'get'."`,
|
|
);
|
|
|
|
// undefined value
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.set("valid-key", undefined)).toThrowErrorMatchingInlineSnapshot(
|
|
`"Expected value to be a string or buffer or number for 'set'."`,
|
|
);
|
|
|
|
expect(async () => await client.set("valid-key", null)).toThrowErrorMatchingInlineSnapshot(
|
|
`"Expected value to be a string or buffer or number for 'set'."`,
|
|
);
|
|
});
|
|
|
|
test("should handle invalid sendCommand inputs", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Undefined command
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.send(undefined, [])).toThrowErrorMatchingInlineSnapshot(
|
|
`"ERR unknown command 'undefined', with args beginning with: "`,
|
|
);
|
|
|
|
// Invalid args type
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.send("GET", "not-an-array")).toThrowErrorMatchingInlineSnapshot(
|
|
`"Arguments must be an array"`,
|
|
);
|
|
|
|
// Non-string command
|
|
// @ts-expect-error: Testing runtime behavior with invalid types
|
|
expect(async () => await client.send(123, [])).toThrowErrorMatchingInlineSnapshot(
|
|
`"ERR unknown command '123', with args beginning with: "`,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Protocol and Parser Edge Cases", () => {
|
|
test("should handle various data types correctly", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Integer/string conversions
|
|
await client.set("int-key", "42");
|
|
|
|
// INCR should return as number
|
|
const incrResult = await client.incr("int-key");
|
|
expect(typeof incrResult).toBe("number");
|
|
expect(incrResult).toBe(43);
|
|
|
|
// GET should return as string
|
|
const getResult = await client.get("int-key");
|
|
expect(typeof getResult).toBe("string");
|
|
expect(getResult).toBe("43");
|
|
|
|
// Boolean handling for EXISTS command
|
|
await client.set("exists-key", "value");
|
|
const existsResult = await client.exists("exists-key");
|
|
expect(typeof existsResult).toBe("boolean");
|
|
expect(existsResult).toBe(true);
|
|
|
|
const notExistsResult = await client.exists("not-exists-key");
|
|
expect(typeof notExistsResult).toBe("boolean");
|
|
expect(notExistsResult).toBe(false);
|
|
|
|
// Null handling for non-existent keys
|
|
const nullResult = await client.get("not-exists-key");
|
|
expect(nullResult).toBeNull();
|
|
});
|
|
|
|
test("should handle complex RESP3 types", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// HGETALL returns object in RESP3
|
|
const hashKey = `hash-${randomUUIDv7()}`;
|
|
await client.send("HSET", [hashKey, "field1", "value1", "field2", "value2"]);
|
|
|
|
const hashResult = await client.send("HGETALL", [hashKey]);
|
|
|
|
// Hash results should be objects in RESP3
|
|
expect(typeof hashResult).toBe("object");
|
|
expect(hashResult).not.toBeNull();
|
|
|
|
if (hashResult !== null) {
|
|
expect(hashResult.field1).toBe("value1");
|
|
expect(hashResult.field2).toBe("value2");
|
|
}
|
|
|
|
// Error type handling
|
|
expect(async () => await client.send("HGET", [])).toThrowErrorMatchingInlineSnapshot(
|
|
`"ERR wrong number of arguments for 'hget' command"`,
|
|
); // Missing key and field
|
|
|
|
// NULL handling from various commands
|
|
const nullResult = await client.send("HGET", [hashKey, "nonexistent"]);
|
|
expect(nullResult).toBeNull();
|
|
});
|
|
|
|
test("should handle RESP protocol boundaries", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Mix of command types to stress protocol parser
|
|
const commands = [
|
|
client.set("key1", "value1"),
|
|
client.get("key1"),
|
|
client.send("PING", []),
|
|
client.incr("counter"),
|
|
client.exists("key1"),
|
|
client.send("HSET", ["hash", "field", "value"]),
|
|
client.send("HGETALL", ["hash"]),
|
|
client.set("key2", "x".repeat(1000)), // Larger value
|
|
client.get("key2"),
|
|
];
|
|
|
|
// Run all commands in parallel to stress protocol handling
|
|
await Promise.all(commands);
|
|
|
|
// Verify data integrity
|
|
|
|
expect(await client.get("key1")).toBe("value1");
|
|
expect(await client.exists("key1")).toBe(true);
|
|
expect(await client.get("key2")).toBe("x".repeat(1000));
|
|
expect(await client.send("HGET", ["hash", "field"])).toBe("value");
|
|
});
|
|
});
|
|
|
|
describe("Resource Management and Edge Cases", () => {
|
|
test("should handle very large number of parallel commands", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Create a large number of parallel commands
|
|
const parallelCount = 1000;
|
|
const commands = [];
|
|
|
|
for (let i = 0; i < parallelCount; i++) {
|
|
const key = `parallel-key-${i}`;
|
|
commands.push(client.set(key, `value-${i}`));
|
|
}
|
|
|
|
// Execute all in parallel
|
|
await Promise.all(commands);
|
|
|
|
// Verify some random results
|
|
for (let i = 0; i < 10; i++) {
|
|
const index = Math.floor(Math.random() * parallelCount);
|
|
const key = `parallel-key-${index}`;
|
|
const value = await client.get(key);
|
|
expect(value).toBe(`value-${index}`);
|
|
}
|
|
});
|
|
|
|
test("should handle many rapid sequential commands", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Create many sequential commands
|
|
const sequentialCount = 500;
|
|
|
|
for (let i = 0; i < sequentialCount; i++) {
|
|
const key = `sequential-key-${i}`;
|
|
await client.set(key, `value-${i}`);
|
|
|
|
// Periodically verify to ensure integrity
|
|
if (i % 50 === 0) {
|
|
const value = await client.get(key);
|
|
expect(value).toBe(`value-${i}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test("should handle command after disconnect and reconnect", async () => {
|
|
const client = ctx.redis;
|
|
|
|
// Set initial value
|
|
const key = `reconnect-key-${randomUUIDv7()}`;
|
|
await client.set(key, "initial-value");
|
|
|
|
// Disconnect explicitly
|
|
client.close();
|
|
|
|
// This command should fail
|
|
expect(async () => await client.get(key)).toThrowErrorMatchingInlineSnapshot(`"Connection has failed"`);
|
|
});
|
|
|
|
test("should handle binary data", async () => {
|
|
// Binary data in both keys and values
|
|
const client = ctx.redis;
|
|
|
|
// Create Uint8Array with binary data
|
|
const binaryData = new Uint8Array([0, 1, 2, 3, 255, 254, 253, 252]);
|
|
const binaryString = String.fromCharCode(...binaryData);
|
|
|
|
await client.set("binary-key", binaryString);
|
|
|
|
// Get it back
|
|
const result = await client.get("binary-key");
|
|
|
|
// Compare binary data
|
|
expect(result).toBe(binaryString);
|
|
});
|
|
});
|
|
});
|