@mohasinac/appkit 3.0.6 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
  4. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
  5. package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
  6. package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
  7. package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
  8. package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
  9. package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
  10. package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
  11. package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
  12. package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
  13. package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
  14. package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
  15. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
  16. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
  17. package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
  18. package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
  19. package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
  20. package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
  21. package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
  22. package/dist/_internal/server/features/lottery/actions.js +243 -0
  23. package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
  24. package/dist/_internal/server/features/lottery/adapters.js +21 -0
  25. package/dist/_internal/server/features/lottery/data.d.ts +20 -0
  26. package/dist/_internal/server/features/lottery/data.js +41 -0
  27. package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
  28. package/dist/_internal/server/features/lottery/metadata.js +31 -0
  29. package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
  30. package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
  31. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
  32. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
  33. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
  34. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
  35. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
  36. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
  37. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
  38. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
  39. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
  40. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
  41. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
  42. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
  43. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
  44. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
  45. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
  46. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
  47. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
  48. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
  49. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
  50. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +46 -0
  53. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  54. package/dist/client.d.ts +9 -0
  55. package/dist/client.js +9 -0
  56. package/dist/constants/field-names.d.ts +5 -1
  57. package/dist/constants/field-names.js +5 -1
  58. package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
  59. package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
  60. package/dist/features/admin/api/bids/route.d.ts +2 -2
  61. package/dist/features/admin/api/coupons/route.d.ts +2 -2
  62. package/dist/features/admin/api/products/route.d.ts +2 -2
  63. package/dist/features/admin/api/reviews/route.d.ts +2 -2
  64. package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
  65. package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
  66. package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
  67. package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
  68. package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
  69. package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
  70. package/dist/features/auth/api/route.d.ts +2 -2
  71. package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
  72. package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
  73. package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
  74. package/dist/features/auth/repository/__tests__/session.test.js +146 -0
  75. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
  76. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
  77. package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
  78. package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
  79. package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
  80. package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
  81. package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
  82. package/dist/features/auth/repository/__tests__/token.test.js +140 -0
  83. package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
  84. package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
  85. package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
  86. package/dist/features/auth/repository/__tests__/user.test.js +226 -0
  87. package/dist/features/auth/repository/session.repository.js +10 -1
  88. package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
  89. package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
  90. package/dist/features/cart/repository/cart.repository.js +2 -2
  91. package/dist/features/categories/api/[id]/route.d.ts +4 -4
  92. package/dist/features/categories/api/route.d.ts +2 -2
  93. package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
  94. package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
  95. package/dist/features/events/components/EventCard.js +1 -0
  96. package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
  97. package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
  98. package/dist/features/events/schemas/firestore.d.ts +3 -0
  99. package/dist/features/events/schemas/index.d.ts +834 -14
  100. package/dist/features/events/schemas/index.js +80 -1
  101. package/dist/features/events/types/index.d.ts +4 -1
  102. package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
  103. package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
  104. package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
  105. package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
  106. package/dist/features/homepage/api/[id]/route.d.ts +4 -4
  107. package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
  108. package/dist/features/homepage/api/carousel/route.d.ts +2 -2
  109. package/dist/features/homepage/api/route.d.ts +2 -2
  110. package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
  111. package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
  112. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
  113. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
  114. package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
  115. package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
  116. package/dist/features/lottery/schemas/firestore.d.ts +49 -0
  117. package/dist/features/lottery/schemas/firestore.js +25 -0
  118. package/dist/features/lottery/schemas/zod.d.ts +113 -0
  119. package/dist/features/lottery/schemas/zod.js +39 -0
  120. package/dist/features/lottery/types/index.d.ts +67 -0
  121. package/dist/features/lottery/types/index.js +55 -0
  122. package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
  123. package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
  124. package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
  125. package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
  126. package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
  127. package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
  128. package/dist/features/products/api/[id]/route.d.ts +4 -4
  129. package/dist/features/products/api/route.d.ts +2 -2
  130. package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
  131. package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
  132. package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
  133. package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
  134. package/dist/features/products/schemas/firestore.d.ts +15 -1
  135. package/dist/features/products/schemas/index.d.ts +12 -12
  136. package/dist/features/products/schemas/index.js +4 -2
  137. package/dist/features/products/types/index.d.ts +1 -1
  138. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
  139. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
  140. package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
  141. package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
  142. package/dist/features/reviews/api/[id]/route.d.ts +4 -4
  143. package/dist/features/reviews/api/route.d.ts +2 -2
  144. package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
  145. package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
  146. package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
  147. package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
  148. package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
  149. package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
  150. package/dist/features/seller/api/coupons/route.d.ts +2 -2
  151. package/dist/features/seller/api/offers/route.d.ts +2 -2
  152. package/dist/features/seller/api/products/route.d.ts +2 -2
  153. package/dist/features/seller/api/store/route.d.ts +2 -2
  154. package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
  155. package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
  156. package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
  157. package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
  158. package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
  159. package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
  160. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
  161. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
  162. package/dist/index.d.ts +14 -0
  163. package/dist/index.js +17 -0
  164. package/dist/next/api/routeHandler.d.ts +1 -1
  165. package/dist/next/routing/route-map.d.ts +13 -0
  166. package/dist/next/routing/route-map.js +6 -0
  167. package/dist/repositories/index.d.ts +1 -0
  168. package/dist/repositories/index.js +2 -0
  169. package/dist/security/index.d.ts +1 -1
  170. package/dist/security/index.js +1 -1
  171. package/dist/security/pii-schemas.d.ts +2 -0
  172. package/dist/security/pii-schemas.js +6 -0
  173. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  174. package/dist/seed/events-seed-data.js +85 -2
  175. package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
  176. package/dist/seed/index.d.ts +1 -0
  177. package/dist/seed/index.js +1 -0
  178. package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
  179. package/dist/seed/lottery-entries-seed-data.js +108 -0
  180. package/dist/seed/manifest.js +1 -0
  181. package/dist/seed/products-preorders-seed-data.d.ts +3 -1
  182. package/dist/server.d.ts +6 -0
  183. package/dist/server.js +6 -0
  184. package/dist/styles.css +1 -1
  185. package/dist/tailwind-utilities.css +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase/admin", () => ({
5
+ getAdminDb: () => db,
6
+ }));
7
+ vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
8
+ const actual = await importOriginal();
9
+ return { ...actual, prepareForFirestore: (d) => d };
10
+ });
11
+ vi.mock("../../../../contracts/field-ops", () => ({
12
+ serverTimestamp: () => new Date(),
13
+ increment: (n) => n,
14
+ arrayUnion: (...args) => args,
15
+ arrayRemove: (...args) => args,
16
+ deleteField: () => null,
17
+ registerFieldOps: vi.fn(),
18
+ }));
19
+ vi.mock("../../../../security", () => ({
20
+ encryptPiiFields: (d) => d,
21
+ decryptPiiFields: (d) => d,
22
+ addPiiIndices: (d) => d,
23
+ piiBlindIndex: (v) => `blind:${v}`,
24
+ encryptPayoutDetails: (d) => d,
25
+ decryptPayoutDetails: (d) => d,
26
+ encryptShippingConfig: (d) => d,
27
+ decryptShippingConfig: (d) => d,
28
+ USER_PII_FIELDS: [],
29
+ TOKEN_PII_FIELDS: [],
30
+ TOKEN_PII_INDEX_MAP: {},
31
+ }));
32
+ vi.mock("../../../../utils", async (importOriginal) => {
33
+ const actual = await importOriginal();
34
+ return { ...actual, resolveDate: (d) => (d instanceof Date ? d : d ? new Date(d) : null) };
35
+ });
36
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
37
+ vi.mock("../../../../monitoring", () => ({
38
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
39
+ }));
40
+ import { EmailVerificationTokenRepository, PasswordResetTokenRepository, } from "../token.repository";
41
+ const emailRepo = new EmailVerificationTokenRepository();
42
+ const passwordRepo = new PasswordResetTokenRepository();
43
+ function makeEmailToken(overrides = {}) {
44
+ return {
45
+ id: "token-email-abc123",
46
+ userId: "user-ravi",
47
+ token: "secure-random-token",
48
+ email: "ravi@example.com",
49
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
50
+ createdAt: new Date(),
51
+ updatedAt: new Date(),
52
+ ...overrides,
53
+ };
54
+ }
55
+ function makePasswordResetToken(overrides = {}) {
56
+ return {
57
+ id: "pw-token-abc123",
58
+ userId: "user-ravi",
59
+ token: "pw-reset-token",
60
+ email: "ravi@example.com",
61
+ expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 min from now
62
+ used: false,
63
+ createdAt: new Date(),
64
+ updatedAt: new Date(),
65
+ ...overrides,
66
+ };
67
+ }
68
+ beforeEach(() => {
69
+ vi.clearAllMocks();
70
+ db.collection.mockReturnValue(mockCollection);
71
+ mockCollection.doc.mockReturnValue(mockDocRef);
72
+ mockCollection.where.mockReturnValue(mockQuery);
73
+ mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
74
+ mockQuery.where.mockReturnValue(mockQuery);
75
+ mockQuery.orderBy.mockReturnValue(mockQuery);
76
+ mockQuery.limit.mockReturnValue(mockQuery);
77
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
78
+ mockDocRef.get.mockResolvedValue(makeSnap(makeEmailToken(), "token-email-abc123"));
79
+ mockDocRef.set.mockResolvedValue(undefined);
80
+ mockDocRef.update.mockResolvedValue(undefined);
81
+ mockDocRef.delete.mockResolvedValue(undefined);
82
+ db.batch.mockReturnValue(mockBatch);
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // EmailVerificationTokenRepository.findByToken
86
+ // ---------------------------------------------------------------------------
87
+ describe("EmailVerificationTokenRepository.findByToken", () => {
88
+ it("returns token document when found", async () => {
89
+ const token = makeEmailToken();
90
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: token.id, data: token }]));
91
+ const result = await emailRepo.findByToken("secure-random-token");
92
+ expect(result).not.toBeNull();
93
+ expect(result?.token).toBe("secure-random-token");
94
+ });
95
+ it("returns null when token does not exist", async () => {
96
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
97
+ const result = await emailRepo.findByToken("nonexistent-token");
98
+ expect(result).toBeNull();
99
+ });
100
+ it("queries by token field", async () => {
101
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
102
+ await emailRepo.findByToken("abc");
103
+ expect(mockCollection.where).toHaveBeenCalledWith("token", "==", "abc");
104
+ });
105
+ });
106
+ // ---------------------------------------------------------------------------
107
+ // EmailVerificationTokenRepository.isExpired
108
+ // ---------------------------------------------------------------------------
109
+ describe("EmailVerificationTokenRepository.isExpired", () => {
110
+ it("returns false when expiresAt is in the future", () => {
111
+ const token = makeEmailToken({ expiresAt: new Date(Date.now() + 60000) });
112
+ expect(emailRepo.isExpired(token)).toBe(false);
113
+ });
114
+ it("returns true when expiresAt is in the past", () => {
115
+ const token = makeEmailToken({ expiresAt: new Date(Date.now() - 1000) });
116
+ expect(emailRepo.isExpired(token)).toBe(true);
117
+ });
118
+ it("returns true when expiresAt is null/undefined", () => {
119
+ const token = makeEmailToken({ expiresAt: null });
120
+ expect(emailRepo.isExpired(token)).toBe(true);
121
+ });
122
+ });
123
+ // ---------------------------------------------------------------------------
124
+ // EmailVerificationTokenRepository.deleteAllForUser
125
+ // ---------------------------------------------------------------------------
126
+ describe("EmailVerificationTokenRepository.deleteAllForUser", () => {
127
+ it("deletes all tokens for the user in batch", async () => {
128
+ const tokens = [
129
+ makeEmailToken({ id: "t-1" }),
130
+ makeEmailToken({ id: "t-2" }),
131
+ ];
132
+ mockQuery.get.mockResolvedValue(makeQuerySnap(tokens.map((t) => ({ id: t.id, data: t }))));
133
+ await emailRepo.deleteAllForUser("user-ravi");
134
+ expect(mockBatch.delete).toHaveBeenCalledTimes(2);
135
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
136
+ });
137
+ it("commits even when no tokens exist for the user", async () => {
138
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
139
+ await emailRepo.deleteAllForUser("user-no-tokens");
140
+ expect(mockBatch.delete).not.toHaveBeenCalled();
141
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
142
+ });
143
+ });
144
+ // ---------------------------------------------------------------------------
145
+ // EmailVerificationTokenRepository.findByEmail (blind-index lookup)
146
+ // ---------------------------------------------------------------------------
147
+ describe("EmailVerificationTokenRepository.findByEmail", () => {
148
+ it("first queries emailIndex (blind index), not raw email", async () => {
149
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
150
+ await emailRepo.findByEmail("ravi@example.com");
151
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
152
+ });
153
+ it("falls back to plaintext email lookup when index returns empty", async () => {
154
+ // First call (index) returns empty, second call (email) also empty
155
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
156
+ await emailRepo.findByEmail("ravi@example.com");
157
+ // Two where calls — first for emailIndex, second for email
158
+ expect(mockCollection.where).toHaveBeenCalledTimes(2);
159
+ });
160
+ it("does NOT fall back if index returns results", async () => {
161
+ const token = makeEmailToken();
162
+ mockQuery.get.mockResolvedValueOnce(makeQuerySnap([{ id: token.id, data: token }]));
163
+ await emailRepo.findByEmail("ravi@example.com");
164
+ expect(mockCollection.where).toHaveBeenCalledTimes(1);
165
+ });
166
+ });
167
+ // ---------------------------------------------------------------------------
168
+ // EmailVerificationTokenRepository.deleteExpired
169
+ // ---------------------------------------------------------------------------
170
+ describe("EmailVerificationTokenRepository.deleteExpired", () => {
171
+ it("batch-deletes all expired tokens", async () => {
172
+ const tokens = [makeEmailToken({ id: "t-expired-1" }), makeEmailToken({ id: "t-expired-2" })];
173
+ mockQuery.get.mockResolvedValue(makeQuerySnap(tokens.map((t) => ({ id: t.id, data: t }))));
174
+ const count = await emailRepo.deleteExpired();
175
+ expect(count).toBe(2);
176
+ expect(mockBatch.delete).toHaveBeenCalledTimes(2);
177
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
178
+ });
179
+ it("queries expiresAt < now", async () => {
180
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
181
+ await emailRepo.deleteExpired();
182
+ expect(mockCollection.where).toHaveBeenCalledWith("expiresAt", "<", expect.any(Date));
183
+ });
184
+ });
185
+ // ---------------------------------------------------------------------------
186
+ // PasswordResetTokenRepository.markAsUsed
187
+ // ---------------------------------------------------------------------------
188
+ describe("PasswordResetTokenRepository.markAsUsed", () => {
189
+ it("sets used: true on the token", async () => {
190
+ const token = makePasswordResetToken({ used: false });
191
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
192
+ await passwordRepo.markAsUsed("pw-token-abc123");
193
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ used: true }));
194
+ });
195
+ it("sets usedAt timestamp", async () => {
196
+ const token = makePasswordResetToken();
197
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
198
+ await passwordRepo.markAsUsed("pw-token-abc123");
199
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ usedAt: expect.any(Date) }));
200
+ });
201
+ it("calls doc(tokenId) to target the correct token", async () => {
202
+ const token = makePasswordResetToken();
203
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
204
+ await passwordRepo.markAsUsed("pw-token-abc123");
205
+ expect(mockCollection.doc).toHaveBeenCalledWith("pw-token-abc123");
206
+ });
207
+ });
208
+ // ---------------------------------------------------------------------------
209
+ // PasswordResetTokenRepository.findUnusedForUser
210
+ // ---------------------------------------------------------------------------
211
+ describe("PasswordResetTokenRepository.findUnusedForUser", () => {
212
+ it("returns only tokens where used == false for the user", async () => {
213
+ const unused = makePasswordResetToken({ used: false });
214
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: unused.id, data: unused }]));
215
+ const results = await passwordRepo.findUnusedForUser("user-ravi");
216
+ expect(results).toHaveLength(1);
217
+ });
218
+ it("queries userId and used==false filters", async () => {
219
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
220
+ await passwordRepo.findUnusedForUser("user-ravi");
221
+ expect(mockCollection.where).toHaveBeenCalledWith("userId", "==", "user-ravi");
222
+ expect(mockQuery.where).toHaveBeenCalledWith("used", "==", false);
223
+ });
224
+ it("returns empty array when all tokens for user are used", async () => {
225
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
226
+ const results = await passwordRepo.findUnusedForUser("user-ravi");
227
+ expect(results).toEqual([]);
228
+ });
229
+ });
230
+ // ---------------------------------------------------------------------------
231
+ // PasswordResetTokenRepository.isExpired
232
+ // ---------------------------------------------------------------------------
233
+ describe("PasswordResetTokenRepository.isExpired", () => {
234
+ it("returns false when expiresAt is in the future", () => {
235
+ const token = makePasswordResetToken({ expiresAt: new Date(Date.now() + 60000) });
236
+ expect(passwordRepo.isExpired(token)).toBe(false);
237
+ });
238
+ it("returns true when expiresAt is in the past", () => {
239
+ const token = makePasswordResetToken({ expiresAt: new Date(Date.now() - 1000) });
240
+ expect(passwordRepo.isExpired(token)).toBe(true);
241
+ });
242
+ });
243
+ // ---------------------------------------------------------------------------
244
+ // PasswordResetTokenRepository.findByEmail (blind-index lookup)
245
+ // ---------------------------------------------------------------------------
246
+ describe("PasswordResetTokenRepository.findByEmail", () => {
247
+ it("queries by HMAC blind index (not plaintext email)", async () => {
248
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
249
+ await passwordRepo.findByEmail("ravi@example.com");
250
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
251
+ });
252
+ it("does not expose raw email in the Firestore query field", async () => {
253
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
254
+ await passwordRepo.findByEmail("ravi@example.com");
255
+ // First call is index lookup — must NOT use raw email
256
+ const firstCallArg = mockCollection.where.mock.calls[0][2];
257
+ expect(firstCallArg).not.toBe("ravi@example.com");
258
+ expect(firstCallArg).toMatch(/^blind:/);
259
+ });
260
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase", () => ({
5
+ getAdminDb: () => db,
6
+ BaseRepository: class {
7
+ get db() { return db; }
8
+ constructor(col) { this.collection = col; }
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ getCollection() { return db.collection(this.collection); }
11
+ mapDoc(snap) {
12
+ return { id: snap.id, ...snap.data() };
13
+ }
14
+ async findBy(field, value) {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const snap = await db.collection(this.collection).where(field, "==", value).get();
17
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
18
+ }
19
+ async findOneBy(field, value) {
20
+ const results = await this.findBy(field, value);
21
+ return results[0] ?? null;
22
+ }
23
+ async findById(id) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const snap = await db.collection(this.collection).doc(id).get();
26
+ return snap.exists ? { id: snap.id, ...snap.data() } : null;
27
+ }
28
+ async findByIdOrFail(id) {
29
+ const doc = await this.findById(id);
30
+ if (!doc)
31
+ throw new Error("Not found");
32
+ return doc;
33
+ }
34
+ async create(data) {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const ref = await db.collection(this.collection).add(data);
37
+ return { id: ref.id, ...data };
38
+ }
39
+ },
40
+ }));
41
+ vi.mock("../../../../monitoring", () => ({
42
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
43
+ }));
44
+ vi.mock("../../../../errors/normalize", () => ({
45
+ normalizeError: vi.fn(),
46
+ }));
47
+ vi.mock("../../../../security", () => ({
48
+ TOKEN_PII_FIELDS: ["email"],
49
+ TOKEN_PII_INDEX_MAP: { email: "emailIndex" },
50
+ encryptPiiFields: (data) => data,
51
+ decryptPiiFields: (data) => data,
52
+ addPiiIndices: (data) => data,
53
+ piiBlindIndex: (v) => `idx:${v}`,
54
+ }));
55
+ vi.mock("../../../../utils", () => ({
56
+ resolveDate: (d) => (d instanceof Date ? d : d ? new Date(d) : null),
57
+ }));
58
+ import { EmailVerificationTokenRepository, PasswordResetTokenRepository, } from "../token.repository";
59
+ const emailRepo = new EmailVerificationTokenRepository();
60
+ const passwordRepo = new PasswordResetTokenRepository();
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
64
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
65
+ mockDocRef.set.mockResolvedValue(undefined);
66
+ mockDocRef.update.mockResolvedValue(undefined);
67
+ mockDocRef.delete.mockResolvedValue(undefined);
68
+ mockCollection.doc.mockReturnValue(mockDocRef);
69
+ mockCollection.add.mockResolvedValue({ id: "token-id-123" });
70
+ db.collection.mockReturnValue(mockCollection);
71
+ db.batch.mockReturnValue(mockBatch);
72
+ mockBatch.commit.mockResolvedValue(undefined);
73
+ });
74
+ describe("EmailVerificationTokenRepository.findByToken", () => {
75
+ it("token exists → returns document", async () => {
76
+ const tokenData = { userId: "user-1", email: "a@b.com", expiresAt: new Date(Date.now() + 60000) };
77
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
78
+ const result = await emailRepo.findByToken("some-token");
79
+ expect(result).not.toBeNull();
80
+ expect(result?.userId).toBe("user-1");
81
+ });
82
+ it("token does not exist → returns null", async () => {
83
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
84
+ const result = await emailRepo.findByToken("nonexistent-token");
85
+ expect(result).toBeNull();
86
+ });
87
+ });
88
+ describe("EmailVerificationTokenRepository.isExpired", () => {
89
+ it("expiresAt in past → returns true", () => {
90
+ const pastToken = { id: "t1", userId: "u", email: "a@b.com", expiresAt: new Date(Date.now() - 1000) };
91
+ expect(emailRepo.isExpired(pastToken)).toBe(true);
92
+ });
93
+ it("expiresAt in future → returns false", () => {
94
+ const futureToken = { id: "t1", userId: "u", email: "a@b.com", expiresAt: new Date(Date.now() + 60000) };
95
+ expect(emailRepo.isExpired(futureToken)).toBe(false);
96
+ });
97
+ });
98
+ describe("EmailVerificationTokenRepository.deleteAllForUser", () => {
99
+ it("batch-deletes all tokens for the user", async () => {
100
+ mockQuery.get.mockResolvedValue(makeQuerySnap([
101
+ { id: "tok-1", data: { userId: "user-1" } },
102
+ { id: "tok-2", data: { userId: "user-1" } },
103
+ ]));
104
+ await emailRepo.deleteAllForUser("user-1");
105
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
106
+ });
107
+ it("no tokens for user → still calls commit (empty batch)", async () => {
108
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
109
+ await emailRepo.deleteAllForUser("user-no-tokens");
110
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
111
+ });
112
+ });
113
+ describe("EmailVerificationTokenRepository.findByEmail", () => {
114
+ it("uses HMAC blind index for lookup (emailIndex field)", async () => {
115
+ const tokenData = { userId: "u", emailIndex: "idx:a@b.com", expiresAt: new Date() };
116
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
117
+ await emailRepo.findByEmail("a@b.com");
118
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "idx:a@b.com");
119
+ });
120
+ });
121
+ describe("PasswordResetTokenRepository.markAsUsed", () => {
122
+ it("sets usedAt timestamp and used: true on the document", async () => {
123
+ mockDocRef.get.mockResolvedValue(makeSnap({ userId: "user-1", token: "tok", used: false }));
124
+ await passwordRepo.markAsUsed("tok-1");
125
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ used: true, usedAt: expect.any(Date) }));
126
+ });
127
+ });
128
+ describe("PasswordResetTokenRepository.findUnusedForUser", () => {
129
+ it("returns null when all tokens have used: true", async () => {
130
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
131
+ const result = await passwordRepo.findUnusedForUser("user-1");
132
+ expect(result).toEqual([]);
133
+ });
134
+ it("returns unused tokens (used: false)", async () => {
135
+ const tokenData = { userId: "user-1", token: "abc", used: false, expiresAt: new Date() };
136
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
137
+ const result = await passwordRepo.findUnusedForUser("user-1");
138
+ expect(result).toHaveLength(1);
139
+ });
140
+ });