@mohasinac/appkit 3.0.6 → 3.1.0
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.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
- package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
- package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
- package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
- package/dist/_internal/server/features/lottery/actions.js +243 -0
- package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
- package/dist/_internal/server/features/lottery/adapters.js +21 -0
- package/dist/_internal/server/features/lottery/data.d.ts +20 -0
- package/dist/_internal/server/features/lottery/data.js +41 -0
- package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
- package/dist/_internal/server/features/lottery/metadata.js +31 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +46 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/client.d.ts +9 -0
- package/dist/client.js +9 -0
- package/dist/constants/field-names.d.ts +5 -1
- package/dist/constants/field-names.js +5 -1
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
- package/dist/features/admin/api/bids/route.d.ts +2 -2
- package/dist/features/admin/api/coupons/route.d.ts +2 -2
- package/dist/features/admin/api/products/route.d.ts +2 -2
- package/dist/features/admin/api/reviews/route.d.ts +2 -2
- package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
- package/dist/features/auth/api/route.d.ts +2 -2
- package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
- package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.test.js +146 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
- package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.test.js +140 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
- package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.test.js +226 -0
- package/dist/features/auth/repository/session.repository.js +10 -1
- package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
- package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
- package/dist/features/cart/repository/cart.repository.js +2 -2
- package/dist/features/categories/api/[id]/route.d.ts +4 -4
- package/dist/features/categories/api/route.d.ts +2 -2
- package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
- package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
- package/dist/features/events/components/EventCard.js +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -0
- package/dist/features/events/schemas/index.d.ts +834 -14
- package/dist/features/events/schemas/index.js +80 -1
- package/dist/features/events/types/index.d.ts +4 -1
- package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
- package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
- package/dist/features/homepage/api/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/route.d.ts +2 -2
- package/dist/features/homepage/api/route.d.ts +2 -2
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
- package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
- package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
- package/dist/features/lottery/schemas/firestore.d.ts +49 -0
- package/dist/features/lottery/schemas/firestore.js +25 -0
- package/dist/features/lottery/schemas/zod.d.ts +113 -0
- package/dist/features/lottery/schemas/zod.js +39 -0
- package/dist/features/lottery/types/index.d.ts +67 -0
- package/dist/features/lottery/types/index.js +55 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
- package/dist/features/products/api/[id]/route.d.ts +4 -4
- package/dist/features/products/api/route.d.ts +2 -2
- package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
- package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
- package/dist/features/products/schemas/firestore.d.ts +15 -1
- package/dist/features/products/schemas/index.d.ts +12 -12
- package/dist/features/products/schemas/index.js +4 -2
- package/dist/features/products/types/index.d.ts +1 -1
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
- package/dist/features/reviews/api/[id]/route.d.ts +4 -4
- package/dist/features/reviews/api/route.d.ts +2 -2
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
- package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
- package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
- package/dist/features/seller/api/coupons/route.d.ts +2 -2
- package/dist/features/seller/api/offers/route.d.ts +2 -2
- package/dist/features/seller/api/products/route.d.ts +2 -2
- package/dist/features/seller/api/store/route.d.ts +2 -2
- package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
- package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
- package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
- package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/next/api/routeHandler.d.ts +1 -1
- package/dist/next/routing/route-map.d.ts +13 -0
- package/dist/next/routing/route-map.js +6 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +6 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/events-seed-data.js +85 -2
- package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
- package/dist/seed/lottery-entries-seed-data.js +108 -0
- package/dist/seed/manifest.js +1 -0
- package/dist/seed/products-preorders-seed-data.d.ts +3 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +6 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,356 @@
|
|
|
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
|
+
const { mockGetFirestoreCount } = vi.hoisted(() => ({
|
|
5
|
+
mockGetFirestoreCount: vi.fn().mockResolvedValue(0),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
8
|
+
getAdminDb: () => db,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
prepareForFirestore: (d) => d,
|
|
15
|
+
getFirestoreCount: mockGetFirestoreCount,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
vi.mock("../../../../contracts/field-ops", () => ({
|
|
19
|
+
serverTimestamp: () => new Date(),
|
|
20
|
+
increment: (n) => n,
|
|
21
|
+
arrayUnion: (...args) => args,
|
|
22
|
+
arrayRemove: (...args) => args,
|
|
23
|
+
deleteField: () => null,
|
|
24
|
+
registerFieldOps: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
27
|
+
normalizeError: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("../../../../monitoring", () => ({
|
|
30
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
31
|
+
}));
|
|
32
|
+
import { NotificationRepository } from "../notification.repository";
|
|
33
|
+
const repo = new NotificationRepository();
|
|
34
|
+
function makeNotifDoc(overrides = {}) {
|
|
35
|
+
return {
|
|
36
|
+
id: "notif-order-shipped-001",
|
|
37
|
+
userId: "user-ravi",
|
|
38
|
+
type: "order_shipped",
|
|
39
|
+
title: "Your order has shipped!",
|
|
40
|
+
body: "Order #123 is on its way.",
|
|
41
|
+
isRead: false,
|
|
42
|
+
entityId: "order-3-20260627-abc123",
|
|
43
|
+
entityType: "order",
|
|
44
|
+
createdAt: new Date(),
|
|
45
|
+
updatedAt: new Date(),
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
mockGetFirestoreCount.mockResolvedValue(0);
|
|
52
|
+
db.collection.mockReturnValue(mockCollection);
|
|
53
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
54
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
55
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
56
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
57
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
58
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
59
|
+
mockDocRef.get.mockResolvedValue(makeSnap(makeNotifDoc(), "notif-1"));
|
|
60
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
61
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
62
|
+
mockDocRef.delete.mockResolvedValue(undefined);
|
|
63
|
+
db.batch.mockReturnValue(mockBatch);
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// create
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
describe("NotificationRepository.create", () => {
|
|
69
|
+
it("sets isRead: false on new notifications", async () => {
|
|
70
|
+
await repo.create({
|
|
71
|
+
userId: "user-ravi",
|
|
72
|
+
type: "order_shipped",
|
|
73
|
+
priority: "normal",
|
|
74
|
+
title: "Shipped!",
|
|
75
|
+
message: "Your order shipped.",
|
|
76
|
+
});
|
|
77
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
78
|
+
expect(setArg.isRead).toBe(false);
|
|
79
|
+
});
|
|
80
|
+
it("stores all required fields", async () => {
|
|
81
|
+
await repo.create({
|
|
82
|
+
userId: "user-ravi",
|
|
83
|
+
type: "order_placed",
|
|
84
|
+
priority: "normal",
|
|
85
|
+
title: "Order placed",
|
|
86
|
+
message: "Thanks for your order.",
|
|
87
|
+
});
|
|
88
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
89
|
+
expect(setArg.userId).toBe("user-ravi");
|
|
90
|
+
expect(setArg.type).toBe("order_placed");
|
|
91
|
+
expect(setArg.title).toBe("Order placed");
|
|
92
|
+
});
|
|
93
|
+
it("sets createdAt and updatedAt timestamps", async () => {
|
|
94
|
+
await repo.create({ userId: "user-1", type: "general", priority: "normal", title: "Hi", message: "Hello" });
|
|
95
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
96
|
+
expect(setArg.createdAt).toBeInstanceOf(Date);
|
|
97
|
+
expect(setArg.updatedAt).toBeInstanceOf(Date);
|
|
98
|
+
});
|
|
99
|
+
it("uses collection.doc() with no args (auto-ID)", async () => {
|
|
100
|
+
await repo.create({ userId: "user-1", type: "general", priority: "normal", title: "T", message: "B" });
|
|
101
|
+
// doc() is called with no argument for auto-generated IDs
|
|
102
|
+
expect(mockCollection.doc).toHaveBeenCalledWith();
|
|
103
|
+
});
|
|
104
|
+
it("returns the created notification with id", async () => {
|
|
105
|
+
mockDocRef.id = "notif-auto-123";
|
|
106
|
+
const result = await repo.create({
|
|
107
|
+
userId: "user-ravi",
|
|
108
|
+
type: "bid_won",
|
|
109
|
+
priority: "normal",
|
|
110
|
+
title: "You won!",
|
|
111
|
+
message: "Congratulations.",
|
|
112
|
+
});
|
|
113
|
+
expect(result.id).toBe("notif-auto-123");
|
|
114
|
+
expect(result.isRead).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// markAsRead
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
describe("NotificationRepository.markAsRead", () => {
|
|
121
|
+
it("sets isRead: true on the notification", async () => {
|
|
122
|
+
await repo.markAsRead("notif-1");
|
|
123
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ isRead: true }));
|
|
124
|
+
});
|
|
125
|
+
it("sets readAt timestamp", async () => {
|
|
126
|
+
await repo.markAsRead("notif-1");
|
|
127
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ readAt: expect.any(Date) }));
|
|
128
|
+
});
|
|
129
|
+
it("sets updatedAt timestamp", async () => {
|
|
130
|
+
await repo.markAsRead("notif-1");
|
|
131
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
132
|
+
});
|
|
133
|
+
it("calls doc(id) to target the correct notification", async () => {
|
|
134
|
+
await repo.markAsRead("notif-abc");
|
|
135
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("notif-abc");
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// markAllAsRead
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
describe("NotificationRepository.markAllAsRead", () => {
|
|
142
|
+
it("queries unread notifications for the user", async () => {
|
|
143
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
144
|
+
await repo.markAllAsRead("user-ravi");
|
|
145
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("userId"), "==", "user-ravi");
|
|
146
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("isRead"), "==", false);
|
|
147
|
+
});
|
|
148
|
+
it("batch-updates all unread notifications to isRead: true", async () => {
|
|
149
|
+
const notifs = [
|
|
150
|
+
makeNotifDoc({ id: "n-1", isRead: false }),
|
|
151
|
+
makeNotifDoc({ id: "n-2", isRead: false }),
|
|
152
|
+
];
|
|
153
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(notifs.map((n) => ({ id: n.id, data: n }))));
|
|
154
|
+
await repo.markAllAsRead("user-ravi");
|
|
155
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(2);
|
|
156
|
+
const calls = mockBatch.update.mock.calls;
|
|
157
|
+
expect(calls.every((c) => c[1].isRead === true)).toBe(true);
|
|
158
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
159
|
+
});
|
|
160
|
+
it("returns 0 and does not batch-update when no unread notifications", async () => {
|
|
161
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
162
|
+
const count = await repo.markAllAsRead("user-ravi");
|
|
163
|
+
expect(count).toBe(0);
|
|
164
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
it("returns the count of notifications marked", async () => {
|
|
167
|
+
const notifs = [
|
|
168
|
+
makeNotifDoc({ id: "n-1", isRead: false }),
|
|
169
|
+
makeNotifDoc({ id: "n-2", isRead: false }),
|
|
170
|
+
makeNotifDoc({ id: "n-3", isRead: false }),
|
|
171
|
+
];
|
|
172
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(notifs.map((n) => ({ id: n.id, data: n }))));
|
|
173
|
+
const count = await repo.markAllAsRead("user-ravi");
|
|
174
|
+
expect(count).toBe(3);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// getUnreadCount
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
describe("NotificationRepository.getUnreadCount", () => {
|
|
181
|
+
it("returns the count of unread notifications for userId", async () => {
|
|
182
|
+
mockGetFirestoreCount.mockResolvedValue(7);
|
|
183
|
+
const count = await repo.getUnreadCount("user-ravi");
|
|
184
|
+
expect(count).toBe(7);
|
|
185
|
+
});
|
|
186
|
+
it("returns 0 when no unread notifications", async () => {
|
|
187
|
+
mockGetFirestoreCount.mockResolvedValue(0);
|
|
188
|
+
const count = await repo.getUnreadCount("user-ravi");
|
|
189
|
+
expect(count).toBe(0);
|
|
190
|
+
});
|
|
191
|
+
it("returns 0 on error (graceful fallback)", async () => {
|
|
192
|
+
mockGetFirestoreCount.mockRejectedValue(new Error("Firestore error"));
|
|
193
|
+
const count = await repo.getUnreadCount("user-ravi");
|
|
194
|
+
expect(count).toBe(0);
|
|
195
|
+
});
|
|
196
|
+
it("queries by userId and isRead == false", async () => {
|
|
197
|
+
mockGetFirestoreCount.mockResolvedValue(3);
|
|
198
|
+
await repo.getUnreadCount("user-xyz");
|
|
199
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("userId"), "==", "user-xyz");
|
|
200
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("isRead"), "==", false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// deleteAllForUser
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
describe("NotificationRepository.deleteAllForUser", () => {
|
|
207
|
+
it("batch-deletes all notifications for the user", async () => {
|
|
208
|
+
const notifs = [
|
|
209
|
+
makeNotifDoc({ id: "n-1" }),
|
|
210
|
+
makeNotifDoc({ id: "n-2" }),
|
|
211
|
+
];
|
|
212
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(notifs.map((n) => ({ id: n.id, data: n }))));
|
|
213
|
+
await repo.deleteAllForUser("user-ravi");
|
|
214
|
+
expect(mockBatch.delete).toHaveBeenCalledTimes(2);
|
|
215
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
216
|
+
});
|
|
217
|
+
it("returns 0 and skips batch when no notifications exist", async () => {
|
|
218
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
219
|
+
const count = await repo.deleteAllForUser("user-nobody");
|
|
220
|
+
expect(count).toBe(0);
|
|
221
|
+
expect(mockBatch.delete).not.toHaveBeenCalled();
|
|
222
|
+
});
|
|
223
|
+
it("returns count of deleted notifications", async () => {
|
|
224
|
+
const notifs = [
|
|
225
|
+
makeNotifDoc({ id: "n-1" }),
|
|
226
|
+
makeNotifDoc({ id: "n-2" }),
|
|
227
|
+
makeNotifDoc({ id: "n-3" }),
|
|
228
|
+
];
|
|
229
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(notifs.map((n) => ({ id: n.id, data: n }))));
|
|
230
|
+
const count = await repo.deleteAllForUser("user-ravi");
|
|
231
|
+
expect(count).toBe(3);
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// delete (single)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
describe("NotificationRepository.delete", () => {
|
|
238
|
+
it("calls doc(id).delete()", async () => {
|
|
239
|
+
await repo.delete("notif-abc");
|
|
240
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("notif-abc");
|
|
241
|
+
expect(mockDocRef.delete).toHaveBeenCalledOnce();
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
// ---------------------------------------------------------------------------
|
|
245
|
+
// getOldReadRefs
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
describe("NotificationRepository.getOldReadRefs", () => {
|
|
248
|
+
it("queries isRead == true and createdAt < cutoff date", async () => {
|
|
249
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
250
|
+
await repo.getOldReadRefs(30);
|
|
251
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("isRead"), "==", true);
|
|
252
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("createdAt"), "<", expect.any(Date));
|
|
253
|
+
});
|
|
254
|
+
it("cutoff date is approximately ttlDays ago", async () => {
|
|
255
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
256
|
+
const before = Date.now();
|
|
257
|
+
await repo.getOldReadRefs(30);
|
|
258
|
+
const cutoffArg = mockQuery.where.mock.calls.find((c) => c[0].includes("createdAt"))?.[2];
|
|
259
|
+
expect(cutoffArg).toBeInstanceOf(Date);
|
|
260
|
+
const expectedCutoff = before - 30 * 24 * 60 * 60 * 1000;
|
|
261
|
+
expect(cutoffArg.getTime()).toBeGreaterThanOrEqual(expectedCutoff - 1000);
|
|
262
|
+
expect(cutoffArg.getTime()).toBeLessThanOrEqual(expectedCutoff + 1000);
|
|
263
|
+
});
|
|
264
|
+
it("limits to 500 documents", async () => {
|
|
265
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
266
|
+
await repo.getOldReadRefs();
|
|
267
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(500);
|
|
268
|
+
});
|
|
269
|
+
it("does NOT include unread notifications (isRead==false docs excluded)", async () => {
|
|
270
|
+
// The query explicitly filters isRead==true — unread docs are never fetched
|
|
271
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
272
|
+
await repo.getOldReadRefs(30);
|
|
273
|
+
const whereCalls = mockCollection.where.mock.calls;
|
|
274
|
+
const isReadFilter = whereCalls.find((c) => c[0].includes("isRead"));
|
|
275
|
+
expect(isReadFilter?.[1]).toBe("==");
|
|
276
|
+
expect(isReadFilter?.[2]).toBe(true);
|
|
277
|
+
});
|
|
278
|
+
it("returns document refs", async () => {
|
|
279
|
+
const notif = makeNotifDoc({ id: "notif-old", isRead: true });
|
|
280
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: notif.id, data: notif }]));
|
|
281
|
+
const refs = await repo.getOldReadRefs(30);
|
|
282
|
+
expect(refs).toHaveLength(1);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// createInBatch
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
describe("NotificationRepository.createInBatch", () => {
|
|
289
|
+
it("stages notification via batch.create (does not commit)", () => {
|
|
290
|
+
const mockExternalBatch = {
|
|
291
|
+
create: vi.fn(),
|
|
292
|
+
update: vi.fn(),
|
|
293
|
+
delete: vi.fn(),
|
|
294
|
+
commit: vi.fn().mockResolvedValue(undefined),
|
|
295
|
+
};
|
|
296
|
+
repo.createInBatch(mockExternalBatch, {
|
|
297
|
+
userId: "user-ravi",
|
|
298
|
+
type: "order_shipped",
|
|
299
|
+
priority: "normal",
|
|
300
|
+
title: "Shipped!",
|
|
301
|
+
message: "On its way.",
|
|
302
|
+
});
|
|
303
|
+
expect(mockExternalBatch.create).toHaveBeenCalledOnce();
|
|
304
|
+
expect(mockExternalBatch.commit).not.toHaveBeenCalled();
|
|
305
|
+
});
|
|
306
|
+
it("sets isRead: false in the batched notification", () => {
|
|
307
|
+
const mockExternalBatch = { create: vi.fn(), update: vi.fn(), delete: vi.fn(), commit: vi.fn() };
|
|
308
|
+
repo.createInBatch(mockExternalBatch, {
|
|
309
|
+
userId: "user-ravi",
|
|
310
|
+
type: "general",
|
|
311
|
+
priority: "normal",
|
|
312
|
+
title: "Hi",
|
|
313
|
+
message: "Hello",
|
|
314
|
+
});
|
|
315
|
+
const createArg = mockExternalBatch.create.mock.calls[0][1];
|
|
316
|
+
expect(createArg.isRead).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
it("returns a document reference synchronously", () => {
|
|
319
|
+
const mockExternalBatch = { create: vi.fn(), update: vi.fn(), delete: vi.fn(), commit: vi.fn() };
|
|
320
|
+
const ref = repo.createInBatch(mockExternalBatch, {
|
|
321
|
+
userId: "user-1",
|
|
322
|
+
type: "general",
|
|
323
|
+
priority: "normal",
|
|
324
|
+
title: "T",
|
|
325
|
+
message: "B",
|
|
326
|
+
});
|
|
327
|
+
expect(ref).toBeDefined();
|
|
328
|
+
expect(ref.id).toBeDefined();
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
// ---------------------------------------------------------------------------
|
|
332
|
+
// findByUser
|
|
333
|
+
// ---------------------------------------------------------------------------
|
|
334
|
+
describe("NotificationRepository.findByUser", () => {
|
|
335
|
+
it("returns notifications for the given userId", async () => {
|
|
336
|
+
const notif = makeNotifDoc({ userId: "user-ravi" });
|
|
337
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: notif.id, data: notif }]));
|
|
338
|
+
const results = await repo.findByUser("user-ravi");
|
|
339
|
+
expect(results).toHaveLength(1);
|
|
340
|
+
});
|
|
341
|
+
it("orders by createdAt descending (newest first)", async () => {
|
|
342
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
343
|
+
await repo.findByUser("user-ravi");
|
|
344
|
+
expect(mockQuery.orderBy).toHaveBeenCalledWith(expect.stringContaining("createdAt"), "desc");
|
|
345
|
+
});
|
|
346
|
+
it("applies default limit of 20", async () => {
|
|
347
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
348
|
+
await repo.findByUser("user-ravi");
|
|
349
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(20);
|
|
350
|
+
});
|
|
351
|
+
it("applies custom limit when provided", async () => {
|
|
352
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
353
|
+
await repo.findByUser("user-ravi", 5);
|
|
354
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(5);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
const { db, mockDocRef, mockCollection } = makeMockDb();
|
|
4
|
+
const { mockCacheManager, mockDecryptSecret } = vi.hoisted(() => ({
|
|
5
|
+
mockCacheManager: {
|
|
6
|
+
get: vi.fn().mockReturnValue(null),
|
|
7
|
+
set: vi.fn(),
|
|
8
|
+
delete: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
mockDecryptSecret: vi.fn((v) => v.replace("enc:", "")),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
13
|
+
getAdminDb: () => db,
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
16
|
+
const actual = await importOriginal();
|
|
17
|
+
return {
|
|
18
|
+
...actual,
|
|
19
|
+
prepareForFirestore: (d) => d,
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
vi.mock("../../../../core", () => ({
|
|
23
|
+
cacheManager: mockCacheManager,
|
|
24
|
+
}));
|
|
25
|
+
vi.mock("../../../../security", () => ({
|
|
26
|
+
encryptSecret: (v) => `enc:${v}`,
|
|
27
|
+
decryptSecret: mockDecryptSecret,
|
|
28
|
+
maskSecret: (v) => `${v.slice(0, 4)}***`,
|
|
29
|
+
}));
|
|
30
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
31
|
+
normalizeError: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
vi.mock("../../../../monitoring", () => ({
|
|
34
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
35
|
+
}));
|
|
36
|
+
import { SiteSettingsRepository } from "../site-settings.repository";
|
|
37
|
+
// Create a fresh repo per test to avoid shared state from `siteSettingsRepository` singleton
|
|
38
|
+
function makeRepo() {
|
|
39
|
+
return new SiteSettingsRepository();
|
|
40
|
+
}
|
|
41
|
+
function makeSettingsDoc(overrides = {}) {
|
|
42
|
+
return {
|
|
43
|
+
id: "global",
|
|
44
|
+
siteName: "LetItRip",
|
|
45
|
+
credentials: {},
|
|
46
|
+
featureFlags: { useMockPayment: false, useMockShipping: false },
|
|
47
|
+
features: [],
|
|
48
|
+
createdAt: new Date(),
|
|
49
|
+
updatedAt: new Date(),
|
|
50
|
+
...overrides,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
56
|
+
mockDecryptSecret.mockImplementation((v) => v.replace("enc:", ""));
|
|
57
|
+
db.collection.mockReturnValue(mockCollection);
|
|
58
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
59
|
+
const settingsDoc = makeSettingsDoc();
|
|
60
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
61
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
62
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
63
|
+
});
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// getSingleton
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
describe("SiteSettingsRepository.getSingleton", () => {
|
|
68
|
+
it("returns cached value without hitting Firestore when cache is warm", async () => {
|
|
69
|
+
const cached = makeSettingsDoc({ siteName: "Cached LetItRip" });
|
|
70
|
+
mockCacheManager.get.mockReturnValue(cached);
|
|
71
|
+
const repo = makeRepo();
|
|
72
|
+
const result = await repo.getSingleton();
|
|
73
|
+
expect(result.siteName).toBe("Cached LetItRip");
|
|
74
|
+
expect(mockDocRef.get).not.toHaveBeenCalled();
|
|
75
|
+
});
|
|
76
|
+
it("fetches from Firestore when cache is cold (null)", async () => {
|
|
77
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
78
|
+
const repo = makeRepo();
|
|
79
|
+
await repo.getSingleton();
|
|
80
|
+
expect(mockDocRef.get).toHaveBeenCalledOnce();
|
|
81
|
+
});
|
|
82
|
+
it("populates cache after Firestore fetch", async () => {
|
|
83
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
84
|
+
const settingsDoc = makeSettingsDoc();
|
|
85
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
86
|
+
const repo = makeRepo();
|
|
87
|
+
await repo.getSingleton();
|
|
88
|
+
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining("site-settings"), expect.anything(), expect.objectContaining({ ttl: expect.any(Number) }));
|
|
89
|
+
});
|
|
90
|
+
it("creates default settings when document does not exist", async () => {
|
|
91
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
92
|
+
const repo = makeRepo();
|
|
93
|
+
await repo.getSingleton();
|
|
94
|
+
expect(mockDocRef.set).toHaveBeenCalledOnce();
|
|
95
|
+
});
|
|
96
|
+
it("returns default settings when document does not exist", async () => {
|
|
97
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
98
|
+
const repo = makeRepo();
|
|
99
|
+
const result = await repo.getSingleton();
|
|
100
|
+
expect(result.id).toBe("global");
|
|
101
|
+
expect(result.createdAt).toBeInstanceOf(Date);
|
|
102
|
+
});
|
|
103
|
+
it("caches the newly-created default settings", async () => {
|
|
104
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
105
|
+
const repo = makeRepo();
|
|
106
|
+
await repo.getSingleton();
|
|
107
|
+
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining("site-settings"), expect.anything(), expect.objectContaining({ ttl: expect.any(Number) }));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// updateSingleton
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
describe("SiteSettingsRepository.updateSingleton", () => {
|
|
114
|
+
it("clears cache before writing", async () => {
|
|
115
|
+
const repo = makeRepo();
|
|
116
|
+
await repo.updateSingleton({ branding: { siteName: "New Name" } });
|
|
117
|
+
expect(mockCacheManager.delete).toHaveBeenCalledWith(expect.stringContaining("site-settings"));
|
|
118
|
+
// delete happens before the update
|
|
119
|
+
const deleteCallOrder = mockCacheManager.delete.mock.invocationCallOrder[0];
|
|
120
|
+
const updateCallOrder = mockDocRef.update.mock.invocationCallOrder[0];
|
|
121
|
+
expect(deleteCallOrder).toBeLessThan(updateCallOrder);
|
|
122
|
+
});
|
|
123
|
+
it("writes updatedAt timestamp", async () => {
|
|
124
|
+
const repo = makeRepo();
|
|
125
|
+
await repo.updateSingleton({ branding: { siteName: "New" } });
|
|
126
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
127
|
+
expect(updateArg.updatedAt).toBeInstanceOf(Date);
|
|
128
|
+
});
|
|
129
|
+
it("encrypts credential values before writing", async () => {
|
|
130
|
+
const settingsDoc = makeSettingsDoc({ credentials: {} });
|
|
131
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
132
|
+
const repo = makeRepo();
|
|
133
|
+
await repo.updateSingleton({
|
|
134
|
+
credentials: { razorpayKeyId: "rzp_live_abc123" },
|
|
135
|
+
});
|
|
136
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
137
|
+
const creds = updateArg.credentials;
|
|
138
|
+
expect(creds.razorpayKeyId).toBe("enc:rzp_live_abc123");
|
|
139
|
+
});
|
|
140
|
+
it("does not overwrite existing credential with empty string", async () => {
|
|
141
|
+
const settingsDoc = makeSettingsDoc({
|
|
142
|
+
credentials: { razorpayKeyId: "enc:existing-key" },
|
|
143
|
+
});
|
|
144
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
145
|
+
const repo = makeRepo();
|
|
146
|
+
await repo.updateSingleton({
|
|
147
|
+
credentials: { razorpayKeyId: "" },
|
|
148
|
+
});
|
|
149
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
150
|
+
const creds = updateArg.credentials;
|
|
151
|
+
// Empty string → keep existing
|
|
152
|
+
expect(creds.razorpayKeyId).toBe("enc:existing-key");
|
|
153
|
+
});
|
|
154
|
+
it("re-caches the latest settings after update", async () => {
|
|
155
|
+
const repo = makeRepo();
|
|
156
|
+
await repo.updateSingleton({ branding: { siteName: "Updated" } });
|
|
157
|
+
// getSingleton is called after update to get latest — cacheManager.set re-called
|
|
158
|
+
expect(mockCacheManager.set).toHaveBeenCalled();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// getDecryptedCredentials
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
describe("SiteSettingsRepository.getDecryptedCredentials", () => {
|
|
165
|
+
it("returns decrypted API key values", async () => {
|
|
166
|
+
const settingsDoc = makeSettingsDoc({
|
|
167
|
+
credentials: { razorpayKeyId: "enc:rzp_live_abc123" },
|
|
168
|
+
});
|
|
169
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
170
|
+
const repo = makeRepo();
|
|
171
|
+
const result = await repo.getDecryptedCredentials();
|
|
172
|
+
expect(result.razorpayKeyId).toBe("rzp_live_abc123");
|
|
173
|
+
});
|
|
174
|
+
it("falls back to empty string when decryption fails", async () => {
|
|
175
|
+
mockDecryptSecret.mockImplementationOnce(() => {
|
|
176
|
+
throw new Error("auth-tag mismatch");
|
|
177
|
+
});
|
|
178
|
+
const settingsDoc = makeSettingsDoc({
|
|
179
|
+
credentials: { razorpayKeyId: "corrupt-blob" },
|
|
180
|
+
});
|
|
181
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
182
|
+
const repo = makeRepo();
|
|
183
|
+
const result = await repo.getDecryptedCredentials();
|
|
184
|
+
expect(result.razorpayKeyId).toBe("");
|
|
185
|
+
});
|
|
186
|
+
it("returns empty object when no credentials stored", async () => {
|
|
187
|
+
const settingsDoc = makeSettingsDoc({ credentials: {} });
|
|
188
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
189
|
+
const repo = makeRepo();
|
|
190
|
+
const result = await repo.getDecryptedCredentials();
|
|
191
|
+
expect(Object.keys(result)).toHaveLength(0);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// getCredentialsMasked
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
describe("SiteSettingsRepository.getCredentialsMasked", () => {
|
|
198
|
+
it("replaces credential values with masked versions", async () => {
|
|
199
|
+
const settingsDoc = makeSettingsDoc({
|
|
200
|
+
credentials: { razorpayKeyId: "enc:rzp_live_abc123" },
|
|
201
|
+
});
|
|
202
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
203
|
+
const repo = makeRepo();
|
|
204
|
+
const result = await repo.getCredentialsMasked();
|
|
205
|
+
// maskSecret("rzp_live_abc123") → "rzp_***" per our mock
|
|
206
|
+
expect(result.razorpayKeyId).toBe("rzp_***");
|
|
207
|
+
});
|
|
208
|
+
it("does not expose raw or encrypted credential values", async () => {
|
|
209
|
+
const settingsDoc = makeSettingsDoc({
|
|
210
|
+
credentials: { razorpayKeyId: "enc:secret-key" },
|
|
211
|
+
});
|
|
212
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
213
|
+
const repo = makeRepo();
|
|
214
|
+
const result = await repo.getCredentialsMasked();
|
|
215
|
+
expect(result.razorpayKeyId).not.toContain("secret-key");
|
|
216
|
+
expect(result.razorpayKeyId).not.toContain("enc:");
|
|
217
|
+
});
|
|
218
|
+
it("empty/falsy credential value → key omitted from masked output", async () => {
|
|
219
|
+
// getDecryptedCredentials skips falsy values; getCredentialsMasked
|
|
220
|
+
// only iterates decrypted keys → empty stored credential does not appear in output
|
|
221
|
+
const settingsDoc = makeSettingsDoc({ credentials: { razorpayKeyId: "" } });
|
|
222
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
223
|
+
const repo = makeRepo();
|
|
224
|
+
const result = await repo.getCredentialsMasked();
|
|
225
|
+
expect(result).not.toHaveProperty("razorpayKeyId");
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// getFeatures / updateFeatures
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
describe("SiteSettingsRepository.getFeatures", () => {
|
|
232
|
+
it("returns the features array from settings", async () => {
|
|
233
|
+
const features = [{ id: "f-1", name: "Fast Checkout", description: "Quick buy", icon: "zap", enabled: true }];
|
|
234
|
+
const settingsDoc = makeSettingsDoc({ features });
|
|
235
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
236
|
+
const repo = makeRepo();
|
|
237
|
+
const result = await repo.getFeatures();
|
|
238
|
+
expect(result).toHaveLength(1);
|
|
239
|
+
expect(result[0].name).toBe("Fast Checkout");
|
|
240
|
+
});
|
|
241
|
+
it("returns empty array when no features stored", async () => {
|
|
242
|
+
const settingsDoc = makeSettingsDoc({ features: undefined });
|
|
243
|
+
mockDocRef.get.mockResolvedValue(makeSnap(settingsDoc, "global"));
|
|
244
|
+
const repo = makeRepo();
|
|
245
|
+
const result = await repo.getFeatures();
|
|
246
|
+
expect(result).toEqual([]);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
describe("SiteSettingsRepository.updateFeatures", () => {
|
|
250
|
+
it("writes features array to Firestore", async () => {
|
|
251
|
+
const repo = makeRepo();
|
|
252
|
+
const features = [{ id: "f-1", name: "F1", description: "D1", icon: "star", enabled: true }];
|
|
253
|
+
await repo.updateFeatures(features);
|
|
254
|
+
expect(mockDocRef.update).toHaveBeenCalled();
|
|
255
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
256
|
+
expect(updateArg.features).toEqual(features);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// exists
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
describe("SiteSettingsRepository.exists", () => {
|
|
263
|
+
it("returns true when singleton document exists", async () => {
|
|
264
|
+
mockDocRef.get.mockResolvedValue(makeSnap(makeSettingsDoc(), "global"));
|
|
265
|
+
const repo = makeRepo();
|
|
266
|
+
const result = await repo.exists();
|
|
267
|
+
expect(result).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
it("returns false when document does not exist", async () => {
|
|
270
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
271
|
+
const repo = makeRepo();
|
|
272
|
+
const result = await repo.exists();
|
|
273
|
+
expect(result).toBe(false);
|
|
274
|
+
});
|
|
275
|
+
it("returns false when Firestore throws", async () => {
|
|
276
|
+
mockDocRef.get.mockRejectedValue(new Error("Firestore unavailable"));
|
|
277
|
+
const repo = makeRepo();
|
|
278
|
+
const result = await repo.exists();
|
|
279
|
+
expect(result).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// resetToDefaults
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
describe("SiteSettingsRepository.resetToDefaults", () => {
|
|
286
|
+
it("overwrites the singleton with default settings", async () => {
|
|
287
|
+
const repo = makeRepo();
|
|
288
|
+
await repo.resetToDefaults();
|
|
289
|
+
expect(mockDocRef.set).toHaveBeenCalledOnce();
|
|
290
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
291
|
+
expect(setArg.id).toBe("global");
|
|
292
|
+
});
|
|
293
|
+
it("caches the reset defaults", async () => {
|
|
294
|
+
const repo = makeRepo();
|
|
295
|
+
await repo.resetToDefaults();
|
|
296
|
+
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining("site-settings"), expect.anything(), expect.objectContaining({ ttl: expect.any(Number) }));
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|