@mohasinac/appkit 3.0.4 → 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/auctions/data.d.ts +2 -4
- package/dist/_internal/server/features/auctions/data.js +3 -9
- 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/classified/adapters.d.ts +2 -15
- package/dist/_internal/server/features/classified/adapters.js +2 -17
- package/dist/_internal/server/features/classified/data.d.ts +1 -6
- package/dist/_internal/server/features/classified/data.js +2 -10
- package/dist/_internal/server/features/digital-code/adapters.d.ts +2 -15
- package/dist/_internal/server/features/digital-code/adapters.js +2 -17
- package/dist/_internal/server/features/digital-code/data.d.ts +1 -6
- package/dist/_internal/server/features/digital-code/data.js +2 -10
- package/dist/_internal/server/features/live/adapters.d.ts +2 -15
- package/dist/_internal/server/features/live/adapters.js +2 -17
- package/dist/_internal/server/features/live/data.d.ts +1 -6
- package/dist/_internal/server/features/live/data.js +2 -10
- 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/pre-orders/data.d.ts +2 -4
- package/dist/_internal/server/features/pre-orders/data.js +3 -9
- package/dist/_internal/server/features/prize-draws/actions.d.ts +2 -0
- package/dist/_internal/server/features/prize-draws/actions.js +22 -0
- package/dist/_internal/server/features/prize-draws/data.d.ts +1 -17
- package/dist/_internal/server/features/prize-draws/data.js +2 -20
- package/dist/_internal/server/features/prize-draws/index.d.ts +3 -1
- package/dist/_internal/server/features/prize-draws/index.js +2 -0
- package/dist/_internal/server/features/prize-draws/service.d.ts +3 -0
- package/dist/_internal/server/features/prize-draws/service.js +19 -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/features/shared/listing-adapters.d.ts +19 -0
- package/dist/_internal/server/features/shared/listing-adapters.js +20 -0
- package/dist/_internal/server/features/shared/listing-data-factory.d.ts +9 -0
- package/dist/_internal/server/features/shared/listing-data-factory.js +33 -0
- package/dist/_internal/server/features/stores/data.d.ts +3 -14
- package/dist/_internal/server/features/stores/data.js +6 -44
- 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/cart/schema.d.ts +8 -8
- package/dist/_internal/shared/features/cart/schema.js +3 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/listing-types/_registry.js +2 -0
- package/dist/_internal/shared/listing-types/auction/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/auction/schema.js +7 -1
- package/dist/_internal/shared/listing-types/classified/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/classified/schema.js +3 -1
- package/dist/_internal/shared/listing-types/digital-code/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/digital-code/schema.js +7 -1
- package/dist/_internal/shared/listing-types/live/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/live/schema.js +3 -1
- package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/pre-order/schema.js +7 -1
- package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/prize-draw/schema.js +13 -1
- package/dist/_internal/shared/listing-types/standard/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/standard/schema.js +4 -1
- package/dist/_internal/shared/types/base-document.d.ts +15 -0
- package/dist/_internal/shared/types/base-document.js +1 -0
- package/dist/_internal/shared/types/index.d.ts +2 -0
- package/dist/_internal/shared/types/index.js +1 -0
- package/dist/client.d.ts +13 -4
- package/dist/client.js +11 -2
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +4 -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/addresses/schemas/firestore.d.ts +2 -4
- 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/components/AdminFulfillmentView.d.ts +6 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +107 -0
- package/dist/features/admin/components/AdminProductEditorView.js +21 -9
- package/dist/features/admin/components/AdminProductsView.js +3 -0
- package/dist/features/admin/components/DataListingView.d.ts +1 -0
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- 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/admin/schemas/firestore.d.ts +3 -8
- 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/auctions/schemas/firestore.d.ts +2 -4
- package/dist/features/auctions/schemas/index.d.ts +2 -2
- 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/before-after/schemas/index.d.ts +2 -2
- package/dist/features/blog/schemas/firestore.d.ts +2 -4
- package/dist/features/blog/schemas/index.d.ts +4 -4
- 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/cart/schemas/firestore.d.ts +2 -4
- 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/categories/schemas/firestore.d.ts +2 -4
- package/dist/features/categories/schemas/index.d.ts +4 -4
- 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 +5 -4
- package/dist/features/events/schemas/index.d.ts +836 -16
- 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/faq/schemas/firestore.d.ts +2 -4
- package/dist/features/faq/schemas/index.d.ts +2 -2
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- 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/homepage/schemas/firestore.d.ts +4 -12
- package/dist/features/homepage/schemas/index.d.ts +2 -2
- 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/messages/schemas/firestore.d.ts +2 -4
- package/dist/features/messages/schemas/index.d.ts +2 -2
- 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/orders/repository/orders.repository.d.ts +5 -0
- package/dist/features/orders/repository/orders.repository.js +26 -0
- package/dist/features/orders/schemas/firestore.d.ts +9 -5
- package/dist/features/orders/schemas/firestore.js +3 -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/payments/schemas/firestore.d.ts +2 -4
- package/dist/features/pre-orders/schemas/index.d.ts +2 -2
- 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/constants/action-defs.d.ts +16 -0
- package/dist/features/products/constants/action-defs.js +16 -0
- package/dist/features/products/constants/listing-tabs.d.ts +48 -0
- package/dist/features/products/constants/listing-tabs.js +12 -0
- 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/repository/products.repository.d.ts +37 -0
- package/dist/features/products/repository/products.repository.js +28 -1
- package/dist/features/products/schemas/firestore.d.ts +23 -7
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +18 -18
- package/dist/features/products/schemas/index.js +4 -2
- package/dist/features/products/types/index.d.ts +3 -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/promotions/schemas/firestore.d.ts +2 -4
- package/dist/features/promotions/schemas/index.d.ts +4 -4
- 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/reviews/schemas/firestore.d.ts +2 -4
- package/dist/features/reviews/schemas/index.d.ts +2 -2
- 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/scams/schemas/firestore.d.ts +2 -4
- package/dist/features/scams/schemas/index.d.ts +4 -4
- 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/search/schemas/index.d.ts +2 -2
- 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/components/BarcodeField.d.ts +17 -0
- package/dist/features/seller/components/BarcodeField.js +30 -0
- package/dist/features/seller/components/FulfillmentView.d.ts +5 -0
- package/dist/features/seller/components/FulfillmentView.js +120 -0
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -1
- package/dist/features/seller/components/PrintCenterView.js +52 -4
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +2 -1
- package/dist/features/seller/components/index.d.ts +4 -0
- package/dist/features/seller/components/index.js +2 -0
- 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/seller/schemas/index.d.ts +2 -2
- package/dist/features/store-extensions/schemas/firestore.d.ts +12 -44
- package/dist/features/stores/components/StoreClassifiedsListing.d.ts +5 -0
- package/dist/features/stores/components/StoreClassifiedsListing.js +48 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.js +20 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +23 -2
- package/dist/features/stores/components/StoreDigitalCodesListing.d.ts +5 -0
- package/dist/features/stores/components/StoreDigitalCodesListing.js +48 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +20 -0
- package/dist/features/stores/components/StoreLiveItemsListing.d.ts +5 -0
- package/dist/features/stores/components/StoreLiveItemsListing.js +48 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.js +20 -0
- package/dist/features/stores/components/index.d.ts +9 -0
- package/dist/features/stores/components/index.js +3 -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/stores/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/schemas/index.d.ts +4 -4
- 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/support/schemas/firestore.d.ts +2 -4
- package/dist/features/support/schemas/index.d.ts +2 -2
- 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 +24 -0
- package/dist/next/routing/route-map.js +11 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/schemas/registry.d.ts +24 -24
- 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 +5 -2
- 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 +5 -2
- package/dist/seed/products-standard-seed-data.js +8 -0
- package/dist/server-entry.d.ts +3 -0
- package/dist/server-entry.js +3 -0
- package/dist/server.d.ts +10 -4
- package/dist/server.js +6 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/id-generators.d.ts +8 -0
- package/dist/utils/id-generators.js +14 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,352 @@
|
|
|
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, mockTxn } = 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 {
|
|
10
|
+
...actual,
|
|
11
|
+
prepareForFirestore: (d) => d,
|
|
12
|
+
getFirestoreCount: vi.fn().mockResolvedValue(0),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
vi.mock("../../../../contracts/field-ops", () => ({
|
|
16
|
+
serverTimestamp: () => new Date(),
|
|
17
|
+
increment: (n) => n,
|
|
18
|
+
arrayUnion: (...args) => args,
|
|
19
|
+
arrayRemove: (...args) => args,
|
|
20
|
+
deleteField: () => null,
|
|
21
|
+
registerFieldOps: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../../../../security", () => ({
|
|
24
|
+
encryptPiiFields: (d) => d,
|
|
25
|
+
decryptPiiFields: (d) => d,
|
|
26
|
+
addPiiIndices: (d) => d,
|
|
27
|
+
piiBlindIndex: (v) => `blind:${v}`,
|
|
28
|
+
encryptPayoutDetails: (d) => d,
|
|
29
|
+
decryptPayoutDetails: (d) => d,
|
|
30
|
+
encryptShippingConfig: (d) => d,
|
|
31
|
+
decryptShippingConfig: (d) => d,
|
|
32
|
+
USER_PII_FIELDS: [],
|
|
33
|
+
TOKEN_PII_FIELDS: [],
|
|
34
|
+
TOKEN_PII_INDEX_MAP: {},
|
|
35
|
+
}));
|
|
36
|
+
vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
|
|
37
|
+
vi.mock("../../../../monitoring", () => ({
|
|
38
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
39
|
+
}));
|
|
40
|
+
import { UserRepository } from "../user.repository";
|
|
41
|
+
import { getFirestoreCount } from "../../../../providers/db-firebase";
|
|
42
|
+
const repo = new UserRepository();
|
|
43
|
+
function makeUserDoc(overrides = {}) {
|
|
44
|
+
return {
|
|
45
|
+
id: "user-ravi-kumar",
|
|
46
|
+
displayName: "Ravi Kumar",
|
|
47
|
+
email: "ravi@example.com",
|
|
48
|
+
emailVerified: false,
|
|
49
|
+
role: "user",
|
|
50
|
+
disabled: false,
|
|
51
|
+
createdAt: new Date(),
|
|
52
|
+
updatedAt: new Date(),
|
|
53
|
+
...overrides,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
db.collection.mockReturnValue(mockCollection);
|
|
59
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
60
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
61
|
+
mockCollection.orderBy = vi.fn().mockReturnValue(mockQuery);
|
|
62
|
+
mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
|
|
63
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
64
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
65
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
66
|
+
mockQuery.offset = vi.fn().mockReturnValue(mockQuery);
|
|
67
|
+
mockQuery.count = vi.fn().mockReturnValue({
|
|
68
|
+
get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }),
|
|
69
|
+
});
|
|
70
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
71
|
+
mockDocRef.get.mockResolvedValue(makeSnap(makeUserDoc(), "user-ravi-kumar"));
|
|
72
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
73
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
74
|
+
mockDocRef.delete.mockResolvedValue(undefined);
|
|
75
|
+
db.runTransaction.mockImplementation(async (cb) => cb(mockTxn));
|
|
76
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc(), "user-ravi-kumar"));
|
|
77
|
+
mockTxn.update.mockReturnValue(undefined);
|
|
78
|
+
getFirestoreCount.mockResolvedValue(0);
|
|
79
|
+
});
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// create
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
describe("UserRepository.create", () => {
|
|
84
|
+
it("generates a human-readable ID from displayName + email", async () => {
|
|
85
|
+
await repo.create({
|
|
86
|
+
displayName: "Ravi Kumar",
|
|
87
|
+
email: "ravi@example.com",
|
|
88
|
+
role: "user",
|
|
89
|
+
});
|
|
90
|
+
// ID should be based on first/last name — check it was passed to .doc()
|
|
91
|
+
const docCall = mockCollection.doc.mock.calls[0][0];
|
|
92
|
+
expect(docCall).toMatch(/ravi/i);
|
|
93
|
+
});
|
|
94
|
+
it("ID does not contain raw email address (no PII in document ID)", async () => {
|
|
95
|
+
await repo.create({
|
|
96
|
+
displayName: "Ravi Kumar",
|
|
97
|
+
email: "ravi@example.com",
|
|
98
|
+
role: "user",
|
|
99
|
+
});
|
|
100
|
+
const docCall = mockCollection.doc.mock.calls[0][0];
|
|
101
|
+
expect(docCall).not.toContain("ravi@example.com");
|
|
102
|
+
expect(docCall).not.toContain("@");
|
|
103
|
+
});
|
|
104
|
+
it("sets createdAt and updatedAt timestamps on the document", async () => {
|
|
105
|
+
await repo.create({ displayName: "Ravi Kumar", email: "ravi@example.com", role: "user" });
|
|
106
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
107
|
+
expect(setArg.createdAt).toBeInstanceOf(Date);
|
|
108
|
+
expect(setArg.updatedAt).toBeInstanceOf(Date);
|
|
109
|
+
});
|
|
110
|
+
it("stores the role from input", async () => {
|
|
111
|
+
await repo.create({ displayName: "Admin", email: "admin@example.com", role: "admin" });
|
|
112
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
113
|
+
expect(setArg.role).toBe("admin");
|
|
114
|
+
});
|
|
115
|
+
it("returns the document with the generated ID", async () => {
|
|
116
|
+
const result = await repo.create({
|
|
117
|
+
displayName: "Ravi Kumar",
|
|
118
|
+
email: "ravi@example.com",
|
|
119
|
+
role: "user",
|
|
120
|
+
});
|
|
121
|
+
expect(result.id).toBeTruthy();
|
|
122
|
+
expect(result.displayName).toBe("Ravi Kumar");
|
|
123
|
+
});
|
|
124
|
+
it("uses 'user' as firstName fallback when displayName is missing", async () => {
|
|
125
|
+
await repo.create({ email: "ravi@example.com", role: "user" });
|
|
126
|
+
const docCall = mockCollection.doc.mock.calls[0][0];
|
|
127
|
+
expect(docCall).toMatch(/user/i);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// findByEmail — blind index lookup
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
describe("UserRepository.findByEmail", () => {
|
|
134
|
+
it("queries emailIndex field using HMAC blind index, not plaintext email", async () => {
|
|
135
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
136
|
+
await repo.findByEmail("ravi@example.com");
|
|
137
|
+
expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
|
|
138
|
+
});
|
|
139
|
+
it("does NOT use raw email in the Firestore query", async () => {
|
|
140
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
141
|
+
await repo.findByEmail("ravi@example.com");
|
|
142
|
+
const calls = mockCollection.where.mock.calls;
|
|
143
|
+
for (const [, , value] of calls) {
|
|
144
|
+
expect(value).not.toBe("ravi@example.com");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
it("returns the user document when found", async () => {
|
|
148
|
+
const user = makeUserDoc();
|
|
149
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: user.id, data: user }]));
|
|
150
|
+
const result = await repo.findByEmail("ravi@example.com");
|
|
151
|
+
expect(result).not.toBeNull();
|
|
152
|
+
expect(result?.displayName).toBe("Ravi Kumar");
|
|
153
|
+
});
|
|
154
|
+
it("returns null when email is not registered", async () => {
|
|
155
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
156
|
+
const result = await repo.findByEmail("unknown@example.com");
|
|
157
|
+
expect(result).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
// ---------------------------------------------------------------------------
|
|
161
|
+
// isEmailRegistered
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
describe("UserRepository.isEmailRegistered", () => {
|
|
164
|
+
it("returns true when a user with that email exists", async () => {
|
|
165
|
+
const user = makeUserDoc();
|
|
166
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: user.id, data: user }]));
|
|
167
|
+
const result = await repo.isEmailRegistered("ravi@example.com");
|
|
168
|
+
expect(result).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
it("returns false when no user with that email exists", async () => {
|
|
171
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
172
|
+
const result = await repo.isEmailRegistered("unknown@example.com");
|
|
173
|
+
expect(result).toBe(false);
|
|
174
|
+
});
|
|
175
|
+
it("delegates to findByEmail (uses blind index, not plaintext)", async () => {
|
|
176
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
177
|
+
await repo.isEmailRegistered("ravi@example.com");
|
|
178
|
+
expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// updateLoginMetadata
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
describe("UserRepository.updateLoginMetadata", () => {
|
|
185
|
+
it("uses a Firestore transaction (runTransaction called)", async () => {
|
|
186
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc(), "user-ravi-kumar"));
|
|
187
|
+
await repo.updateLoginMetadata("user-ravi-kumar");
|
|
188
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
189
|
+
});
|
|
190
|
+
it("increments loginCount by 1 from current value of 0", async () => {
|
|
191
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc({ metadata: { loginCount: 0 } }), "user-ravi-kumar"));
|
|
192
|
+
await repo.updateLoginMetadata("user-ravi-kumar");
|
|
193
|
+
expect(mockTxn.update).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ "metadata.loginCount": 1 }));
|
|
194
|
+
});
|
|
195
|
+
it("increments loginCount by 1 from existing count of 5", async () => {
|
|
196
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc({ metadata: { loginCount: 5 } }), "user-ravi-kumar"));
|
|
197
|
+
await repo.updateLoginMetadata("user-ravi-kumar");
|
|
198
|
+
expect(mockTxn.update).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ "metadata.loginCount": 6 }));
|
|
199
|
+
});
|
|
200
|
+
it("sets lastSignInTime to current timestamp", async () => {
|
|
201
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc(), "user-ravi-kumar"));
|
|
202
|
+
await repo.updateLoginMetadata("user-ravi-kumar");
|
|
203
|
+
expect(mockTxn.update).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ "metadata.lastSignInTime": expect.any(Date) }));
|
|
204
|
+
});
|
|
205
|
+
it("sets updatedAt to current timestamp", async () => {
|
|
206
|
+
mockTxn.get.mockResolvedValue(makeSnap(makeUserDoc(), "user-ravi-kumar"));
|
|
207
|
+
await repo.updateLoginMetadata("user-ravi-kumar");
|
|
208
|
+
expect(mockTxn.update).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// updateProfileWithVerificationReset
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
describe("UserRepository.updateProfileWithVerificationReset", () => {
|
|
215
|
+
it("sets emailVerified: false when email changes", async () => {
|
|
216
|
+
const user = makeUserDoc({ email: "old@example.com", emailVerified: true });
|
|
217
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
218
|
+
await repo.updateProfileWithVerificationReset("user-ravi-kumar", {
|
|
219
|
+
email: "new@example.com",
|
|
220
|
+
});
|
|
221
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ emailVerified: false }));
|
|
222
|
+
});
|
|
223
|
+
it("does NOT reset emailVerified when email is unchanged", async () => {
|
|
224
|
+
const user = makeUserDoc({ email: "ravi@example.com", emailVerified: true });
|
|
225
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
226
|
+
await repo.updateProfileWithVerificationReset("user-ravi-kumar", {
|
|
227
|
+
email: "ravi@example.com",
|
|
228
|
+
});
|
|
229
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
230
|
+
expect(updateArg.emailVerified).toBeUndefined();
|
|
231
|
+
});
|
|
232
|
+
it("sets phoneVerified: false when phone number changes", async () => {
|
|
233
|
+
const user = makeUserDoc({ phoneNumber: "+911234567890", phoneVerified: true });
|
|
234
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
235
|
+
await repo.updateProfileWithVerificationReset("user-ravi-kumar", {
|
|
236
|
+
phoneNumber: "+910987654321",
|
|
237
|
+
});
|
|
238
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ phoneVerified: false }));
|
|
239
|
+
});
|
|
240
|
+
it("does NOT reset phoneVerified when phone is unchanged", async () => {
|
|
241
|
+
const user = makeUserDoc({ phoneNumber: "+911234567890", phoneVerified: true });
|
|
242
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
243
|
+
await repo.updateProfileWithVerificationReset("user-ravi-kumar", {
|
|
244
|
+
phoneNumber: "+911234567890",
|
|
245
|
+
});
|
|
246
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
247
|
+
expect(updateArg.phoneVerified).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
it("changing only displayName does NOT reset email or phone verification", async () => {
|
|
250
|
+
const user = makeUserDoc({ emailVerified: true, phoneVerified: true });
|
|
251
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
252
|
+
await repo.updateProfileWithVerificationReset("user-ravi-kumar", {
|
|
253
|
+
displayName: "Ravi K",
|
|
254
|
+
});
|
|
255
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
256
|
+
expect(updateArg.emailVerified).toBeUndefined();
|
|
257
|
+
expect(updateArg.phoneVerified).toBeUndefined();
|
|
258
|
+
});
|
|
259
|
+
it("throws DatabaseError when user does not exist", async () => {
|
|
260
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
261
|
+
await expect(repo.updateProfileWithVerificationReset("nonexistent-user", { displayName: "X" })).rejects.toThrow(/User not found/);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
// countByRole
|
|
266
|
+
// ---------------------------------------------------------------------------
|
|
267
|
+
describe("UserRepository.countByRole", () => {
|
|
268
|
+
it("queries role field with the specified role value", async () => {
|
|
269
|
+
getFirestoreCount.mockResolvedValue(3);
|
|
270
|
+
await repo.countByRole("seller");
|
|
271
|
+
expect(mockCollection.where).toHaveBeenCalledWith("role", "==", "seller");
|
|
272
|
+
});
|
|
273
|
+
it("returns the count from getFirestoreCount", async () => {
|
|
274
|
+
getFirestoreCount.mockResolvedValue(7);
|
|
275
|
+
const count = await repo.countByRole("admin");
|
|
276
|
+
expect(count).toBe(7);
|
|
277
|
+
});
|
|
278
|
+
it("returns 0 when no users have that role", async () => {
|
|
279
|
+
getFirestoreCount.mockResolvedValue(0);
|
|
280
|
+
const count = await repo.countByRole("moderator");
|
|
281
|
+
expect(count).toBe(0);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// findByRole
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
describe("UserRepository.findByRole", () => {
|
|
288
|
+
it("queries users with the specified role", async () => {
|
|
289
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
290
|
+
await repo.findByRole("seller");
|
|
291
|
+
expect(mockCollection.where).toHaveBeenCalledWith("role", "==", "seller");
|
|
292
|
+
});
|
|
293
|
+
it("returns only users matching the requested role", async () => {
|
|
294
|
+
const seller = makeUserDoc({ id: "user-seller", role: "seller" });
|
|
295
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "user-seller", data: seller }]));
|
|
296
|
+
const results = await repo.findByRole("seller");
|
|
297
|
+
expect(results).toHaveLength(1);
|
|
298
|
+
expect(results[0].role).toBe("seller");
|
|
299
|
+
});
|
|
300
|
+
it("returns empty array when no users have the role", async () => {
|
|
301
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
302
|
+
const results = await repo.findByRole("moderator");
|
|
303
|
+
expect(results).toEqual([]);
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
// update — encrypts PII before writing
|
|
308
|
+
// ---------------------------------------------------------------------------
|
|
309
|
+
describe("UserRepository.update", () => {
|
|
310
|
+
it("calls encryptPiiFields before super.update (passthrough in test = fields unchanged)", async () => {
|
|
311
|
+
const user = makeUserDoc();
|
|
312
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
313
|
+
await repo.update("user-ravi-kumar", { displayName: "New Name" });
|
|
314
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ displayName: "New Name" }));
|
|
315
|
+
});
|
|
316
|
+
it("sets updatedAt on every update", async () => {
|
|
317
|
+
const user = makeUserDoc();
|
|
318
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
319
|
+
await repo.update("user-ravi-kumar", { displayName: "New Name" });
|
|
320
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// markEmailAsVerified
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
describe("UserRepository.markEmailAsVerified", () => {
|
|
327
|
+
it("sets emailVerified: true on the user document", async () => {
|
|
328
|
+
const user = makeUserDoc({ emailVerified: false });
|
|
329
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
330
|
+
await repo.markEmailAsVerified("user-ravi-kumar");
|
|
331
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ emailVerified: true }));
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// disable / enable
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
describe("UserRepository.disable", () => {
|
|
338
|
+
it("sets disabled: true on the user document", async () => {
|
|
339
|
+
const user = makeUserDoc({ disabled: false });
|
|
340
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
341
|
+
await repo.disable("user-ravi-kumar");
|
|
342
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ disabled: true }));
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
describe("UserRepository.enable", () => {
|
|
346
|
+
it("sets disabled: false on the user document", async () => {
|
|
347
|
+
const user = makeUserDoc({ disabled: true });
|
|
348
|
+
mockDocRef.get.mockResolvedValue(makeSnap(user, "user-ravi-kumar"));
|
|
349
|
+
await repo.enable("user-ravi-kumar");
|
|
350
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ disabled: false }));
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// Mock security before importing the repo
|
|
3
|
+
vi.mock("../../../../security", () => ({
|
|
4
|
+
USER_PII_FIELDS: ["email", "phoneNumber"],
|
|
5
|
+
encryptPiiFields: (data) => data,
|
|
6
|
+
decryptPiiFields: (data) => data,
|
|
7
|
+
encryptPayoutDetails: (d) => d,
|
|
8
|
+
decryptPayoutDetails: (d) => d,
|
|
9
|
+
encryptShippingConfig: (d) => d,
|
|
10
|
+
decryptShippingConfig: (d) => d,
|
|
11
|
+
piiBlindIndex: (v) => `idx:${v}`,
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../../../contracts/field-ops", async (importOriginal) => {
|
|
14
|
+
const actual = await importOriginal();
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
increment: (n) => n,
|
|
18
|
+
serverTimestamp: () => new Date(),
|
|
19
|
+
arrayUnion: (...a) => a,
|
|
20
|
+
arrayRemove: (...a) => a,
|
|
21
|
+
deleteField: () => null,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
const mockDocGet = vi.fn();
|
|
25
|
+
const mockDocSet = vi.fn();
|
|
26
|
+
const mockDocUpdate = vi.fn();
|
|
27
|
+
const mockDocRef = { get: mockDocGet, set: mockDocSet, update: mockDocUpdate };
|
|
28
|
+
const mockWhere = vi.fn();
|
|
29
|
+
const mockOrderBy = vi.fn();
|
|
30
|
+
const mockLimit = vi.fn();
|
|
31
|
+
const mockQueryGet = vi.fn();
|
|
32
|
+
const mockTxGet = vi.fn();
|
|
33
|
+
const mockTxUpdate = vi.fn();
|
|
34
|
+
const mockTxSet = vi.fn();
|
|
35
|
+
const mockRunTransaction = vi.fn();
|
|
36
|
+
const mockDoc = vi.fn(() => mockDocRef);
|
|
37
|
+
const mockCollectionChain = {};
|
|
38
|
+
const txn = { get: mockTxGet, update: mockTxUpdate, set: mockTxSet };
|
|
39
|
+
function makeQueryChain(docs = []) {
|
|
40
|
+
const snap = {
|
|
41
|
+
docs: docs.map((d) => ({ data: () => d, id: d.id ?? "doc-id", exists: true })),
|
|
42
|
+
size: docs.length,
|
|
43
|
+
empty: docs.length === 0,
|
|
44
|
+
};
|
|
45
|
+
const chain = {
|
|
46
|
+
where: vi.fn(() => chain),
|
|
47
|
+
orderBy: vi.fn(() => chain),
|
|
48
|
+
limit: vi.fn(() => chain),
|
|
49
|
+
get: vi.fn().mockResolvedValue(snap),
|
|
50
|
+
};
|
|
51
|
+
return chain;
|
|
52
|
+
}
|
|
53
|
+
const db = {
|
|
54
|
+
collection: vi.fn((name) => {
|
|
55
|
+
if (!mockCollectionChain[name]) {
|
|
56
|
+
const chain = makeQueryChain();
|
|
57
|
+
chain.doc = mockDoc;
|
|
58
|
+
mockCollectionChain[name] = chain;
|
|
59
|
+
}
|
|
60
|
+
return mockCollectionChain[name];
|
|
61
|
+
}),
|
|
62
|
+
runTransaction: mockRunTransaction,
|
|
63
|
+
};
|
|
64
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
65
|
+
getAdminDb: () => db,
|
|
66
|
+
}));
|
|
67
|
+
vi.mock("../../../../providers/db-firebase", async () => {
|
|
68
|
+
const actual = await vi.importActual("../../../../providers/db-firebase");
|
|
69
|
+
return {
|
|
70
|
+
...actual,
|
|
71
|
+
getFirestoreCount: vi.fn().mockResolvedValue(0),
|
|
72
|
+
prepareForFirestore: (d) => d,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
import { UserRepository } from "../user.repository";
|
|
76
|
+
let repo;
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
// Reset collection chains
|
|
80
|
+
for (const k of Object.keys(mockCollectionChain)) {
|
|
81
|
+
delete mockCollectionChain[k];
|
|
82
|
+
}
|
|
83
|
+
mockRunTransaction.mockImplementation(async (fn) => {
|
|
84
|
+
await fn(txn);
|
|
85
|
+
});
|
|
86
|
+
repo = new UserRepository();
|
|
87
|
+
});
|
|
88
|
+
describe("UserRepository.isEmailRegistered", () => {
|
|
89
|
+
it("returns true when blind-index matches an existing document", async () => {
|
|
90
|
+
const chain = makeQueryChain([{ id: "user-1", email: "enc:test@test.com", emailIndex: "idx:test@test.com", role: "user" }]);
|
|
91
|
+
chain.doc = mockDoc;
|
|
92
|
+
db.collection.mockReturnValue(chain);
|
|
93
|
+
const result = await repo.isEmailRegistered("test@test.com");
|
|
94
|
+
expect(result).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
it("returns false when no match", async () => {
|
|
97
|
+
const chain = makeQueryChain([]);
|
|
98
|
+
chain.doc = mockDoc;
|
|
99
|
+
db.collection.mockReturnValue(chain);
|
|
100
|
+
const result = await repo.isEmailRegistered("noone@test.com");
|
|
101
|
+
expect(result).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
it("uses HMAC blind index not plaintext for the query", async () => {
|
|
104
|
+
const chain = makeQueryChain([]);
|
|
105
|
+
chain.doc = mockDoc;
|
|
106
|
+
db.collection.mockReturnValue(chain);
|
|
107
|
+
await repo.isEmailRegistered("user@example.com");
|
|
108
|
+
// The where clause must use 'idx:user@example.com' (blind index), not the raw email
|
|
109
|
+
expect(chain.where).toHaveBeenCalledWith("emailIndex", "==", "idx:user@example.com");
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe("UserRepository.updateLoginMetadata", () => {
|
|
113
|
+
it("uses a Firestore transaction", async () => {
|
|
114
|
+
const chain = makeQueryChain();
|
|
115
|
+
chain.doc = mockDoc;
|
|
116
|
+
db.collection.mockReturnValue(chain);
|
|
117
|
+
mockDocGet.mockResolvedValue({ data: () => ({ metadata: { loginCount: 3 } }), exists: true });
|
|
118
|
+
mockTxGet.mockResolvedValue({ data: () => ({ metadata: { loginCount: 3 } }), exists: true });
|
|
119
|
+
await repo.updateLoginMetadata("user-1");
|
|
120
|
+
expect(mockRunTransaction).toHaveBeenCalled();
|
|
121
|
+
});
|
|
122
|
+
it("increments loginCount by 1 inside the transaction", async () => {
|
|
123
|
+
const chain = makeQueryChain();
|
|
124
|
+
chain.doc = mockDoc;
|
|
125
|
+
db.collection.mockReturnValue(chain);
|
|
126
|
+
mockTxGet.mockResolvedValue({ data: () => ({ metadata: { loginCount: 5 } }), exists: true });
|
|
127
|
+
await repo.updateLoginMetadata("user-1");
|
|
128
|
+
const updateArgs = mockTxUpdate.mock.calls[0];
|
|
129
|
+
expect(updateArgs[1]["metadata.loginCount"]).toBe(6);
|
|
130
|
+
});
|
|
131
|
+
it("sets lastSignInTime in the transaction update", async () => {
|
|
132
|
+
const chain = makeQueryChain();
|
|
133
|
+
chain.doc = mockDoc;
|
|
134
|
+
db.collection.mockReturnValue(chain);
|
|
135
|
+
mockTxGet.mockResolvedValue({ data: () => ({ metadata: { loginCount: 0 } }), exists: true });
|
|
136
|
+
const before = new Date();
|
|
137
|
+
await repo.updateLoginMetadata("user-1");
|
|
138
|
+
const after = new Date();
|
|
139
|
+
const updateArgs = mockTxUpdate.mock.calls[0];
|
|
140
|
+
const signInTime = updateArgs[1]["metadata.lastSignInTime"];
|
|
141
|
+
expect(signInTime.getTime()).toBeGreaterThanOrEqual(before.getTime() - 10);
|
|
142
|
+
expect(signInTime.getTime()).toBeLessThanOrEqual(after.getTime() + 10);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
describe("UserRepository.updateProfileWithVerificationReset", () => {
|
|
146
|
+
function setupFindById(userData) {
|
|
147
|
+
const chain = makeQueryChain();
|
|
148
|
+
chain.doc = mockDoc;
|
|
149
|
+
db.collection.mockReturnValue(chain);
|
|
150
|
+
mockDocGet.mockResolvedValue({
|
|
151
|
+
exists: true,
|
|
152
|
+
id: "user-1",
|
|
153
|
+
data: () => userData,
|
|
154
|
+
});
|
|
155
|
+
mockDocSet.mockResolvedValue(undefined);
|
|
156
|
+
mockDocUpdate.mockResolvedValue(undefined);
|
|
157
|
+
}
|
|
158
|
+
it("changing email sets emailVerified: false", async () => {
|
|
159
|
+
setupFindById({ email: "old@test.com", phoneNumber: "1111111111", emailVerified: true });
|
|
160
|
+
await repo.updateProfileWithVerificationReset("user-1", { email: "new@test.com" });
|
|
161
|
+
expect(mockDocUpdate).toHaveBeenCalledWith(expect.objectContaining({ emailVerified: false }));
|
|
162
|
+
});
|
|
163
|
+
it("changing phone sets phoneVerified: false", async () => {
|
|
164
|
+
setupFindById({ email: "same@test.com", phoneNumber: "1111111111", phoneVerified: true });
|
|
165
|
+
await repo.updateProfileWithVerificationReset("user-1", { phoneNumber: "2222222222" });
|
|
166
|
+
expect(mockDocUpdate).toHaveBeenCalledWith(expect.objectContaining({ phoneVerified: false }));
|
|
167
|
+
});
|
|
168
|
+
it("changing displayName does NOT reset email or phone verification", async () => {
|
|
169
|
+
setupFindById({ displayName: "Old Name", email: "same@test.com", phoneNumber: "1111111111", emailVerified: true });
|
|
170
|
+
await repo.updateProfileWithVerificationReset("user-1", { displayName: "New Name" });
|
|
171
|
+
const updateArgs = mockDocUpdate.mock.calls[0][0];
|
|
172
|
+
expect(updateArgs.emailVerified).toBeUndefined();
|
|
173
|
+
expect(updateArgs.phoneVerified).toBeUndefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("UserRepository.findByRole", () => {
|
|
177
|
+
it("queries where role == requested role", async () => {
|
|
178
|
+
const chain = makeQueryChain([]);
|
|
179
|
+
chain.doc = mockDoc;
|
|
180
|
+
db.collection.mockReturnValue(chain);
|
|
181
|
+
await repo.findByRole("seller");
|
|
182
|
+
expect(chain.where).toHaveBeenCalledWith("role", "==", "seller");
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
describe("UserRepository.countByRole", () => {
|
|
186
|
+
it("returns the Firestore count for the given role", async () => {
|
|
187
|
+
const { getFirestoreCount } = await import("../../../../providers/db-firebase");
|
|
188
|
+
getFirestoreCount.mockResolvedValue(7);
|
|
189
|
+
const chain = makeQueryChain([]);
|
|
190
|
+
chain.doc = mockDoc;
|
|
191
|
+
db.collection.mockReturnValue(chain);
|
|
192
|
+
const count = await repo.countByRole("seller");
|
|
193
|
+
expect(count).toBe(7);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
describe("UserRepository.create", () => {
|
|
197
|
+
it("generates a human-readable slug ID from name + email", async () => {
|
|
198
|
+
const chain = makeQueryChain();
|
|
199
|
+
chain.doc = mockDoc;
|
|
200
|
+
db.collection.mockReturnValue(chain);
|
|
201
|
+
mockDocSet.mockResolvedValue(undefined);
|
|
202
|
+
const user = await repo.create({
|
|
203
|
+
uid: "uid-ravi-kumar",
|
|
204
|
+
displayName: "Ravi Kumar",
|
|
205
|
+
email: "ravi@test.com",
|
|
206
|
+
phoneNumber: null,
|
|
207
|
+
photoURL: null,
|
|
208
|
+
role: "user",
|
|
209
|
+
emailVerified: false,
|
|
210
|
+
disabled: false,
|
|
211
|
+
});
|
|
212
|
+
// ID must contain a slug fragment from the name, not a raw UUID
|
|
213
|
+
expect(user.id).toMatch(/ravi/i);
|
|
214
|
+
// ID must NOT be the raw email address
|
|
215
|
+
expect(user.id).not.toContain("ravi@test.com");
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
describe("UserRepository.findByEmail", () => {
|
|
219
|
+
it("uses blind index (idx:email) not plaintext for lookup", async () => {
|
|
220
|
+
const chain = makeQueryChain([]);
|
|
221
|
+
chain.doc = mockDoc;
|
|
222
|
+
db.collection.mockReturnValue(chain);
|
|
223
|
+
await repo.findByEmail("user@example.com");
|
|
224
|
+
expect(chain.where).toHaveBeenCalledWith("emailIndex", "==", "idx:user@example.com");
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -45,11 +45,20 @@ export class SessionRepository extends BaseRepository {
|
|
|
45
45
|
// still holds the cookie. Firestore Admin throws `5 NOT_FOUND` in that
|
|
46
46
|
// case — swallow it so the activity ping is best-effort, not a 500.
|
|
47
47
|
// Any other error is rethrown so genuine failures still surface.
|
|
48
|
+
//
|
|
49
|
+
// BaseRepository.update() wraps Firestore errors in DatabaseError, so we
|
|
50
|
+
// must also inspect .data (the original cause) for the NOT_FOUND signal.
|
|
48
51
|
const code = err?.code;
|
|
49
52
|
const msg = err?.message ?? "";
|
|
53
|
+
const cause = err?.data;
|
|
54
|
+
const causeCode = cause?.code;
|
|
55
|
+
const causeMsg = cause?.message ?? "";
|
|
50
56
|
const isNotFound = code === 5 ||
|
|
51
57
|
code === "not-found" ||
|
|
52
|
-
|
|
58
|
+
causeCode === 5 ||
|
|
59
|
+
causeCode === "not-found" ||
|
|
60
|
+
/No document to update|NOT_FOUND/i.test(msg) ||
|
|
61
|
+
/No document to update|NOT_FOUND/i.test(causeMsg);
|
|
53
62
|
if (!isNotFound)
|
|
54
63
|
throw err;
|
|
55
64
|
}
|
|
@@ -22,8 +22,8 @@ export declare const beforeAfterItemSchema: z.ZodObject<{
|
|
|
22
22
|
sortOrder: z.ZodDefault<z.ZodNumber>;
|
|
23
23
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
24
24
|
}, "strip", z.ZodTypeAny, {
|
|
25
|
-
id: string;
|
|
26
25
|
title: string;
|
|
26
|
+
id: string;
|
|
27
27
|
sortOrder: number;
|
|
28
28
|
isActive: boolean;
|
|
29
29
|
createdAt?: string | undefined;
|
|
@@ -34,8 +34,8 @@ export declare const beforeAfterItemSchema: z.ZodObject<{
|
|
|
34
34
|
afterImageUrl?: string | undefined;
|
|
35
35
|
durationWeeks?: number | undefined;
|
|
36
36
|
}, {
|
|
37
|
-
id: string;
|
|
38
37
|
title: string;
|
|
38
|
+
id: string;
|
|
39
39
|
createdAt?: string | undefined;
|
|
40
40
|
description?: string | undefined;
|
|
41
41
|
sortOrder?: number | undefined;
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { MediaFieldInput, MediaField } from "../../media/types";
|
|
5
5
|
import type { BlogPostStatus, BlogPostCategory } from "../types";
|
|
6
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
6
7
|
/** Runtime-accessible blog post status values — use instead of bare string literals. */
|
|
7
8
|
export declare const BlogPostStatusValues: {
|
|
8
9
|
readonly DRAFT: BlogPostStatus;
|
|
9
10
|
readonly PUBLISHED: BlogPostStatus;
|
|
10
11
|
readonly ARCHIVED: BlogPostStatus;
|
|
11
12
|
};
|
|
12
|
-
export interface BlogPostDocument {
|
|
13
|
-
id: string;
|
|
13
|
+
export interface BlogPostDocument extends BaseDocument {
|
|
14
14
|
title: string;
|
|
15
15
|
slug: string;
|
|
16
16
|
excerpt: string;
|
|
@@ -30,8 +30,6 @@ export interface BlogPostDocument {
|
|
|
30
30
|
views: number;
|
|
31
31
|
metaTitle?: string;
|
|
32
32
|
metaDescription?: string;
|
|
33
|
-
createdAt: Date;
|
|
34
|
-
updatedAt: Date;
|
|
35
33
|
}
|
|
36
34
|
export declare const BLOG_POSTS_COLLECTION: "blogPosts";
|
|
37
35
|
export declare const BLOG_POSTS_INDEXED_FIELDS: readonly ["status", "publishedAt", "isFeatured", "category", "slug"];
|