Spaces:
Sleeping
Sleeping
| 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); | |
| }); | |
| }); | |