From 8cb31b81226d15d6dd378d7a6391b36302a368be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9C=A4=EC=A0=95=EB=AF=BC?= Date: Mon, 20 Jul 2026 16:31:42 +0900 Subject: [PATCH] Add end-to-end wire format test for point ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Firestore에 저장된 Timestamp가 DTO를 거쳐 JSON으로 나갈 때까지 전 구간 검증 - 앱 포인트 내역 크래시(_seconds Map을 as String으로 캐스트)의 회귀 방지용 - relatedDate 같은 날짜 전용 필드의 YYYY-MM-DD 형식 유지도 함께 확인 --- tests/repositories/ledgerWireFormat.test.ts | 75 +++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/repositories/ledgerWireFormat.test.ts diff --git a/tests/repositories/ledgerWireFormat.test.ts b/tests/repositories/ledgerWireFormat.test.ts new file mode 100644 index 0000000..2ea3791 --- /dev/null +++ b/tests/repositories/ledgerWireFormat.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { Timestamp } from "firebase-admin/firestore"; +import { firestore } from "../../src/firebase"; +import { listLedger } from "../../src/repositories/pointLedgerRepository"; +import { getWallet } from "../../src/repositories/walletRepository"; +import { toLedgerEntryDto, toWalletDto } from "../../src/types/dto/rewardDto"; +import { PointLedgerType } from "../../src/types/points"; + +/** + * 앱의 "포인트 내역" 화면 크래시 재현 방지. + * + * Firestore 에 실제로 저장된 Timestamp 를 읽어 응답 DTO 까지 태웠을 때 + * 와이어에 `{_seconds,_nanoseconds}` 가 나오지 않는지 끝까지 확인한다. + * (클라이언트는 이 필드를 `DateTime.parse(... as String)` 로 받으므로 + * Map 이 오면 캐스트 예외로 화면 전체가 죽는다.) + */ + +const uid = "ledger-wire-user"; +const UTC_ISO = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/; + +describe("포인트 내역 와이어 형식 (Firestore -> DTO)", () => { + beforeEach(async () => { + await firestore.recursiveDelete(firestore.doc(`users/${uid}`)); + }); + + it("저장된 Timestamp 가 UTC ISO 문자열로 나가고 _seconds 가 남지 않는다", async () => { + const createdAt = Timestamp.fromDate(new Date("2026-07-20T05:30:00.000Z")); + await firestore.doc(`users/${uid}/pointLedger/tx-1`).set({ + txId: "tx-1", + uid, + type: PointLedgerType.AttendanceDaily, + op: "credit", + amount: 20, + availableBefore: 0, + availableAfter: 20, + reservedBefore: 0, + reservedAfter: 0, + relatedDate: "2026-07-20", + createdAt, + }); + + const page = await listLedger(uid, 20); + expect(page.items).toHaveLength(1); + + const dto = page.items.map((entry) => toLedgerEntryDto(entry.id, entry)); + const json = JSON.stringify({ items: dto, cursor: page.cursor }); + + expect(json).not.toContain("_seconds"); + expect(json).not.toContain("_nanoseconds"); + expect(JSON.parse(json).items[0].createdAt).toBe("2026-07-20T05:30:00.000Z"); + // 날짜 전용 필드는 형식이 유지되어야 한다 + expect(JSON.parse(json).items[0].relatedDate).toBe("2026-07-20"); + }); + + it("지갑 문서의 createdAt/updatedAt 도 UTC ISO 문자열로 나간다", async () => { + const now = Timestamp.fromDate(new Date("2026-07-20T05:30:00.000Z")); + await firestore.doc(`users/${uid}/wallet/current`).set({ + availableBalance: 20, + reservedBalance: 0, + totalEarned: 20, + totalSpent: 0, + version: 1, + createdAt: now, + updatedAt: now, + }); + + const wallet = await getWallet(uid); + expect(wallet).not.toBeNull(); + + const json = JSON.stringify(toWalletDto(wallet!)); + expect(json).not.toContain("_seconds"); + expect(JSON.parse(json).createdAt).toMatch(UTC_ISO); + expect(JSON.parse(json).updatedAt).toMatch(UTC_ISO); + }); +});