redis_hf / tests /server.test.js
tudragon154203
Add broader Redis REST test coverage
bd4d582
Raw
History Blame Contribute Delete
10.6 kB
import { beforeAll, afterAll, describe, expect, it } from "vitest";
import Fastify from "fastify";
import Redis from "ioredis";
process.env.REDIS_REST_TOKEN = "test-token";
process.env.INTERNAL_REDIS_URL = process.env.INTERNAL_REDIS_URL || "redis://127.0.0.1:6379";
const redis = new Redis(process.env.INTERNAL_REDIS_URL, {
lazyConnect: true,
maxRetriesPerRequest: 1,
commandTimeout: 2000,
});
const BLOCKED_COMMANDS = new Set([
"flushall", "config", "shutdown", "save", "bgsave", "client", "monitor",
"debug", "cluster", "replicaof", "slaveof", "replconf", "pfdebug",
]);
const ALLOWED_COMMANDS = new Set([
"get", "set", "del", "exists", "expire", "ttl", "incr", "decr",
"mget", "mset", "hget", "hset", "hdel", "hgetall",
"lpush", "rpush", "lpop", "rpop", "lrange",
"sadd", "srem", "smembers",
"zadd", "zrem", "zrange",
"ping",
"scan", "keys", "flushdb", "setex", "psetex", "getdel", "getex",
"zcard", "zscore", "zrevrange",
"xadd", "xread",
"setnx", "getset", "append", "strlen", "incrby", "decrby",
"expireat", "pexpire", "pttl", "persist",
"rename", "type", "lindex", "llen", "lrem", "ltrim",
"hexists", "hincrby", "hkeys", "hvals", "hlen", "hmget", "hmset",
"sismember", "scard", "sdiff", "sinter", "sunion",
"zrank", "zcount", "zremrangebyrank", "zremrangebyscore",
"srandmember", "spop",
]);
function formatResult(raw) {
if (raw === null || raw === undefined) return null;
if (typeof raw === "bigint") return Number(raw);
if (Buffer.isBuffer(raw)) return raw.toString("utf-8");
if (Array.isArray(raw)) return raw.map(formatResult);
return raw;
}
async function executeCommand(raw) {
if (!Array.isArray(raw) || raw.length === 0) return { error: "Invalid command format" };
const cmd = String(raw[0]).toLowerCase();
const args = raw.slice(1).map(String);
if (BLOCKED_COMMANDS.has(cmd)) return { error: `Command '${cmd.toUpperCase()}' is blocked` };
if (!ALLOWED_COMMANDS.has(cmd)) return { error: `Unsupported command: ${cmd.toUpperCase()}` };
try {
const result = await redis.call(cmd, ...args);
return { result: formatResult(result) };
} catch (err) {
return { error: err.message };
}
}
function isAuthed(request) {
const header = request.headers.authorization;
if (!header) return false;
const m = header.match(/^Bearer\s+(.+)$/i);
return m ? m[1] === process.env.REDIS_REST_TOKEN : false;
}
function buildApp() {
const app = Fastify({ bodyLimit: 1024 * 1024 });
app.get("/health", async () => {
try {
await redis.ping();
return { status: "ok", redis: "connected" };
} catch {
return { status: "error", redis: "disconnected" };
}
});
app.get("/", async () => ({ service: "hf-redis-rest", status: "running" }));
app.get("/:command/*", async (request, reply) => {
if (!isAuthed(request)) return reply.code(401).send({ error: "Unauthorized" });
const parts = request.url.slice(1).split("/").filter(Boolean);
return executeCommand(parts);
});
app.post("/pipeline", async (request, reply) => {
if (!isAuthed(request)) return reply.code(401).send({ error: "Unauthorized" });
if (!Array.isArray(request.body)) return reply.code(400).send({ error: "Request body must be an array of commands" });
const results = [];
for (const cmd of request.body) results.push(await executeCommand(cmd));
return results;
});
app.post("/", async (request, reply) => {
if (!isAuthed(request)) return reply.code(401).send({ error: "Unauthorized" });
if (!request.body) return { service: "hf-redis-rest", status: "running" };
if (!Array.isArray(request.body)) return reply.code(400).send({ error: "Request body must be a JSON array" });
return executeCommand(request.body);
});
return app;
}
describe("hf-redis-rest", () => {
let app;
beforeAll(async () => {
await redis.connect();
await redis.flushdb();
app = buildApp();
await app.ready();
});
afterAll(async () => {
await app.close();
await redis.quit();
});
it("serves service info at root", async () => {
const res = await app.inject({ method: "GET", url: "/" });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ service: "hf-redis-rest", status: "running" });
});
it("serves health without auth", async () => {
const res = await app.inject({ method: "GET", url: "/health" });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ status: "ok", redis: "connected" });
});
it("rejects missing token", async () => {
const res = await app.inject({ method: "POST", url: "/", headers: { "content-type": "application/json" }, payload: ["PING"] });
expect(res.statusCode).toBe(401);
expect(res.json()).toEqual({ error: "Unauthorized" });
});
it("rejects wrong token", async () => {
const res = await app.inject({
method: "POST",
url: "/",
headers: { authorization: "Bearer nope", "content-type": "application/json" },
payload: ["PING"],
});
expect(res.statusCode).toBe(401);
});
it("supports PING, SET, GET, DEL", async () => {
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["PING"] })).json()).toEqual({ result: "PONG" });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["SET", "hello", "world"] })).json()).toEqual({ result: "OK" });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["GET", "hello"] })).json()).toEqual({ result: "world" });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["DEL", "hello"] })).json()).toEqual({ result: 1 });
});
it("supports expiration and ttl", async () => {
await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["SET", "tmp", "v"] });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["EXPIRE", "tmp", "60"] })).json()).toEqual({ result: 1 });
const ttl = (await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["TTL", "tmp"] })).json();
expect(ttl.result).toBeGreaterThanOrEqual(0);
await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["DEL", "tmp", "counter"] });
});
it("supports hashes, lists, sets, and sorted sets", async () => {
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["HSET", "h", "field", "value"] })).json()).toEqual({ result: 1 });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["HGET", "h", "field"] })).json()).toEqual({ result: "value" });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["LPUSH", "l", "one", "two"] })).json()).toEqual({ result: 2 });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["LRANGE", "l", "0", "-1"] })).json()).toEqual({ result: ["two", "one"] });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["SADD", "s", "a", "b"] })).json()).toEqual({ result: 2 });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["SMEMBERS", "s"] })).json()).toEqual({ result: ["a", "b"] });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["ZADD", "z", "1", "x", "2", "y"] })).json()).toEqual({ result: 2 });
expect((await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["ZRANGE", "z", "0", "-1"] })).json()).toEqual({ result: ["x", "y"] });
});
it("supports pipeline", async () => {
const res = await app.inject({
method: "POST",
url: "/pipeline",
headers: { authorization: "Bearer test-token", "content-type": "application/json" },
payload: [["SET", "p", "1"], ["GET", "p"]],
});
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual([{ result: "OK" }, { result: "1" }]);
});
it("supports path-style commands", async () => {
await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["SET", "pathkey", "value"] });
const res = await app.inject({ method: "GET", url: "/get/pathkey", headers: { authorization: "Bearer test-token" } });
expect(res.statusCode).toBe(200);
expect(res.json()).toEqual({ result: "value" });
});
it("blocks dangerous commands", async () => {
const res = await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["FLUSHALL"] });
expect(res.json()).toEqual({ error: "Command 'FLUSHALL' is blocked" });
});
it("rejects unsupported commands", async () => {
const res = await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: ["NOPE"] });
expect(res.json()).toEqual({ error: "Unsupported command: NOPE" });
});
it("rejects invalid body shapes", async () => {
const res = await app.inject({ method: "POST", url: "/", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: { a: 1 } });
expect(res.statusCode).toBe(400);
});
it("returns error for invalid pipeline body", async () => {
const res = await app.inject({ method: "POST", url: "/pipeline", headers: { authorization: "Bearer test-token", "content-type": "application/json" }, payload: { a: 1 } });
expect(res.statusCode).toBe(400);
});
});