mmday-firebase/tests/services/gameSyncService.test.ts
윤정민 c7e4ed8c22 Standardize game status values and make favorite team optional.
- `GameStatus` 타입을 `scheduled`, `live`, `completed`, `cancelled`로 변경하여 상태 관리의 일관성을 높였습니다.
- 사용자 프로필의 `favoriteTeamCode`를 선택 사항으로 변경하고, null 또는 undefined 입력에 대한 처리를 추가했습니다.
- KBO 데이터 병합 과정에 상세 로그를 도입하여 실시간 데이터 매칭 및 외부 API 호출 실패 상황에 대한 가시성을 확보했습니다.
- 상태 값 변경에 따라 영향받는 서비스 로직과 테스트 코드를 일괄 업데이트했습니다.
2026-04-14 23:08:45 +09:00

118 lines
3.9 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { firestore } from "../../src/firebase.js";
import {
toGameDoc,
toTime,
syncGamesForMonth,
} from "../../src/services/gameSyncService.js";
import type { ScheduleGame } from "../../src/kbo/schedule.js";
function baseGame(overrides: Partial<ScheduleGame> = {}): ScheduleGame {
return {
date: "04.12",
dayOfWeek: "일",
time: "18:30",
awayTeamCode: "HT",
homeTeamCode: "LG",
awayScore: null,
homeScore: null,
status: "scheduled",
stadium: "잠실",
broadcast: "KN-T",
note: "-",
gameId: "20260412HTLG0",
...overrides,
};
}
describe("gameSyncService.toTime", () => {
it("KST 18:30은 UTC 09:30으로 변환된다", () => {
const ts = toTime(2026, "04.12", "18:30");
const d = ts.toDate();
expect(d.toISOString()).toBe("2026-04-12T09:30:00.000Z");
});
it("시간이 비어있으면 00:00으로 처리한다", () => {
const ts = toTime(2026, "04.12", "");
expect(ts.toDate().toISOString()).toBe("2026-04-11T15:00:00.000Z");
});
});
describe("gameSyncService.toGameDoc", () => {
it("scheduled 경기는 status=scheduled, 승리팀 없음", () => {
const doc = toGameDoc(2026, baseGame());
expect(doc).not.toBeNull();
expect(doc!.status).toBe("scheduled");
expect(doc!.homeTeamCode).toBe("LG");
expect(doc!.winningTeamCode).toBeUndefined();
});
it("completed 경기는 status=completed, 점수 기반 winningTeamCode 설정", () => {
const doc = toGameDoc(
2026,
baseGame({ status: "completed", homeScore: 7, awayScore: 2 })
);
expect(doc!.status).toBe("completed");
expect(doc!.winningTeamCode).toBe("LG");
});
it("cancelled 경기는 status=cancelled", () => {
const doc = toGameDoc(2026, baseGame({ status: "cancelled" }));
expect(doc!.status).toBe("cancelled");
});
it("무승부는 winningTeamCode가 없다", () => {
const doc = toGameDoc(
2026,
baseGame({ status: "completed", homeScore: 3, awayScore: 3 })
);
expect(doc!.winningTeamCode).toBeUndefined();
});
it("gameId가 null이면 null 반환", () => {
const doc = toGameDoc(2026, baseGame({ gameId: null }));
expect(doc).toBeNull();
});
});
describe("gameSyncService.syncGamesForMonth (실제 KBO 호출)", () => {
beforeEach(async () => {
await firestore.recursiveDelete(firestore.collection("games"));
await firestore.recursiveDelete(firestore.collection("kboCache"));
await firestore.recursiveDelete(firestore.collection("kboLocks"));
});
it("실제 KBO 일정을 fetch하여 games 컬렉션에 upsert한다", async () => {
console.log("[sync] 2025-04 경기 sync 시작");
const t0 = Date.now();
const count = await syncGamesForMonth(2025, 4);
console.log(`[sync] ${count}경기 저장 완료 (${Date.now() - t0}ms)`);
expect(count).toBeGreaterThan(0);
const snap = await firestore.collection("games").get();
// 더블헤더 등으로 같은 gameId가 반복되면 문서 수가 count보다 작을 수 있다
expect(snap.size).toBeGreaterThan(0);
expect(snap.size).toBeLessThanOrEqual(count);
const first = snap.docs[0].data();
console.log("[sync] 예시 문서:", { id: snap.docs[0].id, ...first });
expect(first).toHaveProperty("homeTeamCode");
expect(first).toHaveProperty("awayTeamCode");
expect(first).toHaveProperty("status");
expect(first).toHaveProperty("time");
}, 30000);
it("동일 월을 두 번 sync해도 중복 없이 upsert된다", async () => {
const first = await syncGamesForMonth(2025, 4);
const snapAfterFirst = await firestore.collection("games").get();
console.log(`[idempotent] 1차: ${first}`);
const second = await syncGamesForMonth(2025, 4);
const snapAfterSecond = await firestore.collection("games").get();
console.log(`[idempotent] 2차: ${second}`);
expect(snapAfterSecond.size).toBe(snapAfterFirst.size);
}, 30000);
});