Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/services/redisService.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
import { createClient } from "redis";

export interface IRedisClient {
lPush(key: string, value: string): Promise<number>;
lRange(key: string, start: number, stop: number): Promise<string[]>;
del(key: string | string[]): Promise<number>;
ping(): Promise<string>;
connect(): Promise<any>;
quit(): Promise<any>;
isOpen: boolean;
}

export class RedisService {
private static client: ReturnType<typeof createClient> | 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");
}

Expand All @@ -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);
}
}
91 changes: 55 additions & 36 deletions test/unit/redisService.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});