import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("@lila/db", () => ({ createLobby: vi.fn(), getLobbyByCodeWithPlayers: vi.fn(), addPlayer: vi.fn(), })); import { createLobby as createLobbyModel, getLobbyByCodeWithPlayers, addPlayer, } from "@lila/db"; import { createLobby, joinLobby } from "./lobbyService.js"; const mockCreateLobby = vi.mocked(createLobbyModel); const mockGetLobbyByCodeWithPlayers = vi.mocked(getLobbyByCodeWithPlayers); const mockAddPlayer = vi.mocked(addPlayer); const fakeLobby = { id: "00000000-0000-4000-8000-000000000001", code: "ABC123", hostUserId: "user-1", status: "waiting" as const, createdAt: new Date(), }; const fakeLobbyWithPlayers = { ...fakeLobby, players: [ { lobbyId: fakeLobby.id, userId: "user-1", score: 0, joinedAt: new Date(), user: { id: "user-1", name: "Alice" }, }, ], }; beforeEach(() => { vi.clearAllMocks(); mockCreateLobby.mockResolvedValue(fakeLobby); mockAddPlayer.mockResolvedValue({ lobbyId: fakeLobby.id, userId: "user-1", score: 0, joinedAt: new Date(), }); mockGetLobbyByCodeWithPlayers.mockResolvedValue(fakeLobbyWithPlayers); }); describe("createLobby", () => { it("creates a lobby and adds the host as the first player", async () => { const result = await createLobby("user-1"); expect(mockCreateLobby).toHaveBeenCalledOnce(); expect(mockAddPlayer).toHaveBeenCalledWith( fakeLobby.id, "user-1", expect.any(Number), ); expect(result.id).toBe(fakeLobby.id); }); it("retries on unique code collision", async () => { const uniqueViolation = Object.assign(new Error("unique"), { code: "23505", }); mockCreateLobby .mockRejectedValueOnce(uniqueViolation) .mockResolvedValueOnce(fakeLobby); const result = await createLobby("user-1"); expect(mockCreateLobby).toHaveBeenCalledTimes(2); expect(result.id).toBe(fakeLobby.id); }); it("throws after max retry attempts", async () => { const uniqueViolation = Object.assign(new Error("unique"), { code: "23505", }); mockCreateLobby.mockRejectedValue(uniqueViolation); await expect(createLobby("user-1")).rejects.toThrow( "Could not generate a unique lobby code", ); }); }); describe("joinLobby", () => { it("returns lobby with players when join succeeds", async () => { const fullLobby = { ...fakeLobbyWithPlayers, players: [ ...fakeLobbyWithPlayers.players, { lobbyId: fakeLobby.id, userId: "user-2", score: 0, joinedAt: new Date(), user: { id: "user-2", name: "Bob" }, }, ], }; mockGetLobbyByCodeWithPlayers .mockResolvedValueOnce(fakeLobbyWithPlayers) .mockResolvedValueOnce(fullLobby); const result = await joinLobby("ABC123", "user-2"); expect(mockAddPlayer).toHaveBeenCalledWith( fakeLobby.id, "user-2", expect.any(Number), ); expect(result.players).toHaveLength(2); }); it("throws NotFoundError when lobby does not exist", async () => { mockGetLobbyByCodeWithPlayers.mockResolvedValue(undefined); await expect(joinLobby("XXXXXX", "user-2")).rejects.toThrow( "Lobby not found", ); }); it("throws ConflictError when lobby is not waiting", async () => { mockGetLobbyByCodeWithPlayers.mockResolvedValue({ ...fakeLobbyWithPlayers, status: "in_progress" as const, }); await expect(joinLobby("ABC123", "user-2")).rejects.toThrow( "Game has already started", ); }); it("returns lobby idempotently when user already joined", async () => { mockGetLobbyByCodeWithPlayers.mockResolvedValue({ ...fakeLobbyWithPlayers, players: [ { lobbyId: fakeLobby.id, userId: "user-1", score: 0, joinedAt: new Date(), user: { id: "user-1", name: "Alice" }, }, ], }); const result = await joinLobby("ABC123", "user-1"); expect(mockAddPlayer).not.toHaveBeenCalled(); expect(result.players).toHaveLength(1); }); it("throws ConflictError when lobby is full", async () => { mockGetLobbyByCodeWithPlayers.mockResolvedValue({ ...fakeLobbyWithPlayers, players: Array.from({ length: 4 }, (_, i) => ({ lobbyId: fakeLobby.id, userId: `user-${i}`, score: 0, joinedAt: new Date(), user: { id: `user-${i}`, name: `Player ${i}` }, })), }); await expect(joinLobby("ABC123", "user-5")).rejects.toThrow( "Lobby is full", ); }); });