diff --git a/src/services/redisService.ts b/src/services/redisService.ts index 6bad02b..f0d56e0 100644 --- a/src/services/redisService.ts +++ b/src/services/redisService.ts @@ -1,12 +1,22 @@ import { createClient } from "redis"; +export interface IRedisClient { + lPush(key: string, value: string): Promise; + lRange(key: string, start: number, stop: number): Promise; + del(key: string | string[]): Promise; + ping(): Promise; + connect(): Promise; + quit(): Promise; + isOpen: boolean; +} + export class RedisService { - private static client: ReturnType | null = null; + private static client: IRedisClient | null = null; static async start() { const rc = createClient({ url: "redis://localhost:6379" }); await rc.connect(); - this.client = rc; + this.client = rc as unknown as IRedisClient; console.log("\x1b[32m 💾 Redis connected successfully \x1b[0m"); } @@ -25,10 +35,10 @@ export class RedisService { } static lpush(key: string, value: string) { - return this.client!.lPush(key, value); + return this.op().lPush(key, value); } static lrange(key: string, start: number, stop: number) { - return this.client!.lRange(key, start, stop); + return this.op().lRange(key, start, stop); } } diff --git a/test/unit/redisService.test.ts b/test/unit/redisService.test.ts index 9076c48..35e3945 100644 --- a/test/unit/redisService.test.ts +++ b/test/unit/redisService.test.ts @@ -1,63 +1,82 @@ -import { describe, test, expect, mock, afterEach, vi } from "bun:test"; -import { RedisService } from "../../src/services/redisService"; +import {describe, test, expect, mock, spyOn, afterEach, beforeEach, type Mock, vi} from "bun:test"; +import {type IRedisClient, RedisService} from "../../src/services/redisService"; describe("RedisService - Unit tests", () => { + const fakeClient: IRedisClient = { + lPush: async () => 1, + lRange: async () => ["msg-mock"], + del: async () => 1, + ping: async () => "PONG", + connect: async () => {}, + quit: async () => {}, + isOpen: true, + }; + + let opSpy: Mock<() => IRedisClient>; + + beforeEach(() => { + opSpy = spyOn(RedisService, "op").mockReturnValue(fakeClient); + }); + afterEach(() => { - (RedisService as any).client = null; + opSpy.mockRestore(); }); test("Should call lPush on the client", async () => { - const fakeClient = { - lPush: mock(async () => 1), - }; - - (RedisService as any).client = fakeClient; + const lPushSpy = spyOn(fakeClient, "lPush"); await RedisService.lpush("messages", "hello"); - expect(fakeClient.lPush).toHaveBeenCalled(); - expect(fakeClient.lPush).toHaveBeenCalledWith("messages", "hello"); + expect(lPushSpy).toHaveBeenCalledWith("messages", "hello"); }); - test("Should call lRange on the client", async () => { - const fakeClient = { - lRange: mock(async () => ["hi"]), - }; + test("Should return mocked items from lRange", async () => { + const range = await RedisService.lrange("messages", 0, 1); + + expect(range).toEqual(["msg-mock"]); + expect(range).toHaveLength(1); + }); - (RedisService as any).client = fakeClient; + test("Should propagate errors when lPush fails", async () => { + const errorSpy = spyOn(fakeClient, "lPush").mockRejectedValue(new Error("Redis Connection Lost")); - const result = await RedisService.lrange("messages", 0, 1); + expect(RedisService.lpush("key", "val")).rejects.toThrow("Redis Connection Lost"); - expect(fakeClient.lRange).toHaveBeenCalled(); - expect(fakeClient.lRange).toHaveBeenCalledWith("messages", 0, 1); - expect(result).toEqual(["hi"]); + errorSpy.mockRestore(); }); - test("Should return 15 items from 0 to 14", async () => { - const fakeClient = { - lRange: mock(async () => - Array.from({ length: 15 }, (_, i) => `msg-${i}`), - ), - }; + test("stop() should NOT call quit if the client is already closed", async () => { + const quitSpy = spyOn(fakeClient, "quit"); + fakeClient.isOpen = false; - (RedisService as any).client = fakeClient; + await RedisService.stop(); - const range = await RedisService.lrange("messages", 0, 14); + expect(quitSpy).not.toHaveBeenCalled(); + fakeClient.isOpen = true; + }); + + test("op() should throw an error if client is null", () => { + opSpy.mockRestore(); + + expect(() => RedisService.op()).toThrow("RedisService not initialized"); - expect(fakeClient.lRange).toHaveBeenCalled(); - expect(fakeClient.lRange).toHaveBeenCalledWith("messages", 0, 14); - expect(range).toHaveLength(15); + opSpy = spyOn(RedisService, "op").mockReturnValue(fakeClient); }); - test("op() throws when client is not initialized", () => { - (RedisService as any).client = null; + test("Should call del with correct parameters", async () => { + const delSpy = spyOn(fakeClient, "del"); - expect(() => RedisService.op()).toThrow(); + await RedisService.op().del("my-key"); + + expect(delSpy).toHaveBeenCalledWith("my-key"); }); - test("stop() should not fail when client is null", async () => { - (RedisService as any).client = null; + test("Should return PONG when calling ping", async () => { + const pingSpy = spyOn(fakeClient, "ping").mockResolvedValue("PONG"); + + const result = await RedisService.op().ping(); - expect(() => RedisService.stop()).not.toThrow(); + expect(result).toBe("PONG"); + expect(pingSpy).toHaveBeenCalled(); }); });