import { beforeEach, describe, expect, it } from "vitest"; import { rtdb } from "../../src/firebase"; import { changeVote, deleteGameVotes, getAllUserVotes, getCounts, getUserDateVotes, getUserVote, getVotesByDate, isVoteDateIndexBackfilled, deleteUserVoteGame, deleteUserVoteIndex, submitVote, } from "../../src/repositories/voteRepository"; import type { DateString } from "../../src/types/dateString"; const gameId = "20260412HTLG0"; const gameId2 = "20260412WOSK0"; const uid = "test-uid-1"; const uid2 = "test-uid-2"; const date = "2026-04-12" as DateString; describe("voteRepository (RTDB)", () => { beforeEach(async () => { console.log("[setup] /votes, /userVotes 초기화"); await rtdb.ref("/votes").remove(); await rtdb.ref("/userVotes").remove(); }); describe("submitVote", () => { it("home 투표 시 homeCount가 1 증가하고 인덱스가 저장된다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); const counts = await getCounts(gameId); const userVote = await getUserVote(gameId, uid); const dateVotes = await getUserDateVotes(uid, date); console.log("[home vote] counts:", counts); console.log("[home vote] userVote:", userVote); console.log("[home vote] dateVotes:", dateVotes); expect(counts).toEqual({ homeCount: 1, awayCount: 0, drawCount: 0 }); expect(userVote).toEqual({ team: "LG" }); expect(dateVotes[gameId]).toEqual({ team: "LG" }); }); it("away 투표 시 awayCount만 증가한다", async () => { await submitVote({ gameId, uid, date, side: "away", team: "KIA" }); const counts = await getCounts(gameId); console.log("[away vote] counts:", counts); expect(counts).toEqual({ homeCount: 0, awayCount: 1, drawCount: 0 }); }); it("무승부 투표 시 drawCount만 증가하고 team은 DRAW로 저장된다", async () => { await submitVote({ gameId, uid, date, side: "draw", team: "DRAW" }); const counts = await getCounts(gameId); const userVote = await getUserVote(gameId, uid); console.log("[draw vote] counts:", counts); expect(counts).toEqual({ homeCount: 0, awayCount: 0, drawCount: 1 }); expect(userVote).toEqual({ team: "DRAW" }); }); }); describe("changeVote", () => { it("home→away 변경 시 카운트가 재분배되고 인덱스가 갱신된다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); console.log("[change] 초기:", await getCounts(gameId)); await changeVote({ gameId, uid, date, oldSide: "home", newSide: "away", newTeam: "KIA", }); const counts = await getCounts(gameId); const userVote = await getUserVote(gameId, uid); const dateVotes = await getUserDateVotes(uid, date); console.log("[change] 변경 후 counts:", counts); console.log("[change] userVote:", userVote); expect(counts).toEqual({ homeCount: 0, awayCount: 1, drawCount: 0 }); expect(userVote).toEqual({ team: "KIA" }); expect(dateVotes[gameId]).toEqual({ team: "KIA" }); }); it("home→draw 변경 시 drawCount로 재분배된다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await changeVote({ gameId, uid, date, oldSide: "home", newSide: "draw", newTeam: "DRAW", }); const counts = await getCounts(gameId); const userVote = await getUserVote(gameId, uid); console.log("[change→draw] counts:", counts); expect(counts).toEqual({ homeCount: 0, awayCount: 0, drawCount: 1 }); expect(userVote).toEqual({ team: "DRAW" }); }); }); describe("getUserVote", () => { it("존재하지 않으면 null을 반환한다", async () => { const result = await getUserVote(gameId, "nonexistent-uid"); expect(result).toBeNull(); }); }); describe("getCounts", () => { it("counts 노드가 없으면 0으로 채워 반환한다", async () => { const result = await getCounts("nonexistent-game"); console.log("[counts-empty]", result); expect(result).toEqual({ homeCount: 0, awayCount: 0, drawCount: 0 }); }); }); describe("getAllUserVotes", () => { it("여러 유저의 투표를 맵으로 반환한다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await submitVote({ gameId, uid: uid2, date, side: "away", team: "KIA" }); const all = await getAllUserVotes(gameId); console.log("[all votes]", all); expect(Object.keys(all).sort()).toEqual([uid, uid2].sort()); expect(all[uid]).toEqual({ team: "LG" }); expect(all[uid2]).toEqual({ team: "KIA" }); }); it("투표가 없으면 빈 객체를 반환한다", async () => { const result = await getAllUserVotes("nonexistent-game"); expect(result).toEqual({}); }); }); describe("getUserDateVotes", () => { it("특정 날짜의 여러 경기 투표를 맵으로 반환한다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await submitVote({ gameId: gameId2, uid, date, side: "away", team: "키움", }); const result = await getUserDateVotes(uid, date); console.log("[date votes]", result); expect(Object.keys(result).sort()).toEqual([gameId, gameId2].sort()); expect(result[gameId]).toEqual({ team: "LG" }); expect(result[gameId2]).toEqual({ team: "키움" }); }); }); describe("deleteGameVotes", () => { it("/votes/{gameId}는 삭제되지만 /userVotes 인덱스는 남는다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await deleteGameVotes(gameId); const counts = await getCounts(gameId); const users = await getAllUserVotes(gameId); const dateVotes = await getUserDateVotes(uid, date); console.log("[delete] counts:", counts); console.log("[delete] users:", users); console.log("[delete] dateVotes (남아있음):", dateVotes); expect(counts).toEqual({ homeCount: 0, awayCount: 0, drawCount: 0 }); expect(users).toEqual({}); expect(dateVotes[gameId]).toEqual({ team: "LG" }); }); }); }); /** * 날짜별 인덱스는 `/userVotes`의 (date, uid) 전치 미러다. dailyArchive가 이쪽만 * 읽으므로, 원본을 바꾸는 모든 경로가 미러도 같이 갱신해야 투표가 유실되지 않는다. */ describe("voteRepository — /userVotesByDate 미러", () => { beforeEach(async () => { await rtdb.ref("/votes").remove(); await rtdb.ref("/userVotes").remove(); await rtdb.ref("/userVotesByDate").remove(); }); it("submitVote가 날짜별 인덱스에도 기록한다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); const byDate = await getVotesByDate(date); expect(byDate[uid]?.[gameId]).toEqual({ team: "LG" }); }); it("여러 유저의 같은 날짜 투표가 한 노드에 모인다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await submitVote({ gameId, uid: uid2, date, side: "away", team: "KIA" }); await submitVote({ gameId: gameId2, uid, date, side: "draw", team: "DRAW" }); const byDate = await getVotesByDate(date); expect(Object.keys(byDate).sort()).toEqual([uid, uid2].sort()); expect(Object.keys(byDate[uid])).toHaveLength(2); }); it("changeVote가 미러의 팀도 함께 바꾼다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await changeVote({ gameId, uid, date, oldSide: "home", newSide: "away", newTeam: "KIA", }); const byDate = await getVotesByDate(date); expect(byDate[uid][gameId]).toEqual({ team: "KIA" }); }); it("deleteUserVoteGame이 양쪽에서 제거한다", async () => { await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await submitVote({ gameId: gameId2, uid, date, side: "away", team: "KIA" }); await deleteUserVoteGame(uid, date, gameId); expect(await getUserDateVotes(uid, date)).toEqual({ [gameId2]: { team: "KIA" } }); const byDate = await getVotesByDate(date); expect(byDate[uid]).toEqual({ [gameId2]: { team: "KIA" } }); }); it("deleteUserVoteIndex가 그 유저의 모든 날짜 미러를 지운다", async () => { const other = "2026-04-13" as DateString; await submitVote({ gameId, uid, date, side: "home", team: "LG" }); await submitVote({ gameId: gameId2, uid, date: other, side: "away", team: "KIA" }); await submitVote({ gameId, uid: uid2, date, side: "home", team: "LG" }); await deleteUserVoteIndex(uid); expect(await getUserDateVotes(uid, date)).toEqual({}); expect((await getVotesByDate(date))[uid]).toBeUndefined(); expect((await getVotesByDate(other))[uid]).toBeUndefined(); // 다른 유저 기록은 남아 있어야 한다 expect((await getVotesByDate(date))[uid2]).toEqual({ [gameId]: { team: "LG" } }); }); it("투표가 없는 날짜는 빈 객체를 돌려준다", async () => { expect(await getVotesByDate("2026-01-01" as DateString)).toEqual({}); }); }); describe("voteRepository — 백필 완료 표시", () => { beforeEach(async () => { await rtdb.ref("/userVotesByDateMeta").remove(); }); it("표시가 없으면 false", async () => { expect(await isVoteDateIndexBackfilled()).toBe(false); }); it("표시가 있으면 true — dailyArchive가 레거시 폴백을 건너뛰는 근거", async () => { await rtdb.ref("/userVotesByDateMeta/backfilledAt").set(new Date().toISOString()); expect(await isVoteDateIndexBackfilled()).toBe(true); }); });