Ctrl K
ComunidadGuías…

server/storage/redis.test.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { randomUUID } from "node:crypto";
import env from "@server/env";
import Logger from "@server/logging/Logger";
import Redis from "./redis";

describe("RedisAdapter#zaddWithSequence", () => {
  let key: string;

  beforeEach(() => {
    key = `test:sequence:${randomUUID()}`;
  });

  afterEach(async () => {
    vi.restoreAllMocks();
    await Redis.defaultClient.del(key);
  });

  it("orders concurrent additions and moves repeated members to the end", async () => {
    const sequence = 2 ** 52;
    await Redis.defaultClient.zadd(key, sequence, "previous");

    const scores = await Promise.all([
      Redis.defaultClient.zaddWithSequence(key, "a", 60),
      Redis.defaultClient.zaddWithSequence(key, "b", 60),
      Redis.defaultClient.zaddWithSequence(key, "a", 60),
    ]);

    expect(scores).toEqual([sequence + 1, sequence + 2, sequence + 3]);
    expect(await Redis.defaultClient.zrange(key, 0, -1)).toEqual([
      "previous",
      "b",
      "a",
    ]);
    expect(await Redis.defaultClient.zscore(key, "a")).toBe(
      String(sequence + 3)
    );
  });

  it("creates the set and refreshes its expiry on subsequent edits", async () => {
    const first = await Redis.defaultClient.zaddWithSequence(key, "a", 60);
    await Redis.defaultClient.expire(key, 1);
    const second = await Redis.defaultClient.zaddWithSequence(key, "b", 60);

    expect(second).toBeGreaterThan(first);
    expect(await Redis.defaultClient.ttl(key)).toBeGreaterThan(1);
    expect(await Redis.defaultClient.ttl(key)).toBeLessThanOrEqual(60);
  });

  it("reads the latest member and its sequence", async () => {
    expect(await Redis.defaultClient.zlatestWithSequence(key)).toBeUndefined();

    await Redis.defaultClient.zaddWithSequence(key, "a", 60);
    const sequence = await Redis.defaultClient.zaddWithSequence(key, "b", 60);

    expect(await Redis.defaultClient.zlatestWithSequence(key)).toEqual({
      member: "b",
      sequence,
    });
  });

  it("propagates Redis failures to the caller", async () => {
    vi.spyOn(Redis.defaultClient, "eval").mockRejectedValueOnce(
      new Error("Redis unavailable")
    );

    await expect(
      Redis.defaultClient.zaddWithSequence(key, "a", 60)
    ).rejects.toThrow("Redis unavailable");
  });
});

describe("RedisAdapter unavailable timeout", () => {
  let client: Redis;

  beforeEach(() => {
    vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
    vi.spyOn(Logger, "fatal").mockImplementation(() => undefined);
    client = new Redis(env.REDIS_URL);
  });

  afterEach(() => {
    client.disconnect();
    vi.useRealTimers();
    vi.restoreAllMocks();
  });

  it("exits when the connection does not recover", () => {
    client.status = "reconnecting";
    client.emit("reconnecting");
    vi.advanceTimersByTime(env.REDIS_UNAVAILABLE_TIMEOUT);

    expect(Logger.fatal).toHaveBeenCalledWith(
      "Redis connection unavailable",
      expect.any(Error),
      expect.objectContaining({ status: "reconnecting" })
    );
  });

  it("does not exit when the connection recovers", () => {
    client.status = "reconnecting";
    client.emit("reconnecting");
    vi.advanceTimersByTime(env.REDIS_UNAVAILABLE_TIMEOUT / 2);

    client.status = "ready";
    client.emit("ready");
    vi.advanceTimersByTime(env.REDIS_UNAVAILABLE_TIMEOUT);

    expect(Logger.fatal).not.toHaveBeenCalled();
  });

  it("does not exit after the connection is closed", () => {
    client.status = "reconnecting";
    client.emit("reconnecting");

    client.status = "end";
    client.emit("end");
    vi.advanceTimersByTime(env.REDIS_UNAVAILABLE_TIMEOUT);

    expect(Logger.fatal).not.toHaveBeenCalled();
  });
});