@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -52
- package/src/index.ts +10 -28
- package/src/utils/constants.ts +2 -56
- package/src/utils/cookieSanitizer.ts +59 -59
- package/src/utils/operationRouter.ts +75 -65
- package/src/utils/types.ts +709 -1557
- package/src/vtexClient.ts +354 -343
- package/src/README.md +0 -6
- package/src/__conformance__/upstream.test.ts +0 -158
- package/src/__tests__/client-segment-cookie.test.ts +0 -255
- package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
- package/src/__tests__/schemas.test.ts +0 -71
- package/src/actions/address.ts +0 -250
- package/src/actions/analytics/sendEvent.ts +0 -86
- package/src/actions/auth.ts +0 -333
- package/src/actions/checkout.ts +0 -659
- package/src/actions/index.ts +0 -11
- package/src/actions/masterData.ts +0 -168
- package/src/actions/misc.ts +0 -188
- package/src/actions/newsletter.ts +0 -105
- package/src/actions/orders.ts +0 -34
- package/src/actions/profile.ts +0 -201
- package/src/actions/session.ts +0 -85
- package/src/actions/trigger.ts +0 -43
- package/src/actions/wishlist.ts +0 -114
- package/src/client.ts +0 -724
- package/src/commerceLoaders.ts +0 -267
- package/src/hooks/__tests__/createCart.render.test.ts +0 -114
- package/src/hooks/__tests__/createCart.test.ts +0 -92
- package/src/hooks/__tests__/createUseCart.test.ts +0 -184
- package/src/hooks/__tests__/createUseUser.test.ts +0 -48
- package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
- package/src/hooks/cartQuery.ts +0 -204
- package/src/hooks/createCart.ts +0 -427
- package/src/hooks/createUseCart.ts +0 -360
- package/src/hooks/createUseUser.ts +0 -153
- package/src/hooks/createUseWishlist.ts +0 -242
- package/src/hooks/index.ts +0 -25
- package/src/hooks/useAutocomplete.ts +0 -83
- package/src/hooks/useCart.ts +0 -243
- package/src/hooks/useUser.ts +0 -78
- package/src/hooks/useWishlist.ts +0 -119
- package/src/invoke.ts +0 -235
- package/src/loaders/ProductDetailsPage.ts +0 -1
- package/src/loaders/ProductList.ts +0 -1
- package/src/loaders/ProductListingPage.ts +0 -1
- package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
- package/src/loaders/address.ts +0 -115
- package/src/loaders/autocomplete.ts +0 -56
- package/src/loaders/brands.ts +0 -44
- package/src/loaders/cart/attachments.ts +0 -53
- package/src/loaders/cart/full.ts +0 -67
- package/src/loaders/cart/gifts.ts +0 -43
- package/src/loaders/cart/orderFormId.ts +0 -31
- package/src/loaders/cart/shipping.test.ts +0 -125
- package/src/loaders/cart/shipping.ts +0 -136
- package/src/loaders/cart/summary.ts +0 -36
- package/src/loaders/cart.ts +0 -52
- package/src/loaders/catalog.ts +0 -163
- package/src/loaders/collections.ts +0 -55
- package/src/loaders/index.ts +0 -20
- package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
- package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
- package/src/loaders/intelligentSearch/productList.ts +0 -170
- package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
- package/src/loaders/intelligentSearch/suggestions.ts +0 -44
- package/src/loaders/legacy/productDetailsPage.ts +0 -1
- package/src/loaders/legacy/productList.ts +0 -1
- package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
- package/src/loaders/legacy.ts +0 -710
- package/src/loaders/logistics.ts +0 -111
- package/src/loaders/minicart.ts +0 -78
- package/src/loaders/navbar.ts +0 -26
- package/src/loaders/orders.ts +0 -92
- package/src/loaders/pageType.ts +0 -68
- package/src/loaders/paths/PDPDefaultPath.ts +0 -51
- package/src/loaders/payment.ts +0 -97
- package/src/loaders/productListFull.ts +0 -157
- package/src/loaders/profile.ts +0 -138
- package/src/loaders/promotion.ts +0 -30
- package/src/loaders/search.ts +0 -124
- package/src/loaders/session.ts +0 -83
- package/src/loaders/user.ts +0 -87
- package/src/loaders/wishlist.ts +0 -89
- package/src/loaders/wishlistProducts.ts +0 -69
- package/src/loaders/workflow/products.ts +0 -62
- package/src/loaders/workflow.ts +0 -316
- package/src/logo.png +0 -0
- package/src/manifest.gen.ts +0 -89
- package/src/middleware.cacheHeaders.test.ts +0 -44
- package/src/middleware.ts +0 -240
- package/src/mod.ts +0 -173
- package/src/registry.ts +0 -9
- package/src/schemas.gen.ts +0 -3191
- package/src/schemas.ts +0 -30
- package/src/types.ts +0 -248
- package/src/utils/__tests__/cartProjection.test.ts +0 -118
- package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
- package/src/utils/__tests__/fetch.test.ts +0 -80
- package/src/utils/__tests__/fetchCache.test.ts +0 -155
- package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
- package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
- package/src/utils/__tests__/minicart.test.ts +0 -184
- package/src/utils/__tests__/operationRouter.test.ts +0 -227
- package/src/utils/__tests__/resilience.test.ts +0 -205
- package/src/utils/__tests__/resourceRange.test.ts +0 -32
- package/src/utils/__tests__/simulationCache.test.ts +0 -40
- package/src/utils/__tests__/sitemap.test.ts +0 -185
- package/src/utils/__tests__/slugify.test.ts +0 -31
- package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
- package/src/utils/__tests__/transform.test.ts +0 -1111
- package/src/utils/accountLoaders.ts +0 -203
- package/src/utils/authHelpers.ts +0 -85
- package/src/utils/batch.ts +0 -18
- package/src/utils/buildOfferShelf.test.ts +0 -59
- package/src/utils/cartProjection.ts +0 -96
- package/src/utils/cookies.ts +0 -167
- package/src/utils/enrichment.ts +0 -556
- package/src/utils/fetch.ts +0 -111
- package/src/utils/fetchCache.ts +0 -55
- package/src/utils/index.ts +0 -19
- package/src/utils/instrumentedFetch.ts +0 -98
- package/src/utils/intelligentSearch.ts +0 -96
- package/src/utils/legacy.ts +0 -139
- package/src/utils/minicart.ts +0 -139
- package/src/utils/pickAndOmit.ts +0 -25
- package/src/utils/proxy.ts +0 -495
- package/src/utils/resilience.ts +0 -352
- package/src/utils/resourceRange.ts +0 -10
- package/src/utils/segment.ts +0 -152
- package/src/utils/similars.ts +0 -35
- package/src/utils/simulationCache.ts +0 -73
- package/src/utils/sitemap.ts +0 -283
- package/src/utils/slugCache.ts +0 -28
- package/src/utils/slugify.ts +0 -13
- package/src/utils/transform.ts +0 -1691
- package/src/utils/vtexId.ts +0 -151
- package/src/vtexClient.test.ts +0 -204
- package/tsconfig.json +0 -7
package/src/schemas.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registers the real props schemas for VTEX loaders/actions into the CMS
|
|
3
|
-
* schema registry, so `GET /deco/meta` publishes full props forms (enums,
|
|
4
|
-
* titles, required) instead of the `__resolveType`-only stubs that
|
|
5
|
-
* registerCommerceLoaders() auto-registers.
|
|
6
|
-
*
|
|
7
|
-
* The schemas come from `schemas.gen.ts`, generated at build time by
|
|
8
|
-
* @decocms/blocks-cli's generate-app-schemas.ts (`bun run generate:schemas`)
|
|
9
|
-
* — Props are TypeScript types, erased at runtime, so extraction cannot
|
|
10
|
-
* happen in the site.
|
|
11
|
-
*
|
|
12
|
-
* Called from every server entrypoint a site can wire VTEX through
|
|
13
|
-
* (createVtexCommerceLoaders, mod.configure). Idempotent; real schemas also
|
|
14
|
-
* take precedence over stubs inside the registry, so call order relative to
|
|
15
|
-
* registerCommerceLoaders() doesn't matter.
|
|
16
|
-
*/
|
|
17
|
-
import { registerAppSchemas } from "@decocms/blocks/cms/client";
|
|
18
|
-
import { actionSchemas, loaderSchemas } from "./schemas.gen";
|
|
19
|
-
|
|
20
|
-
let registered = false;
|
|
21
|
-
|
|
22
|
-
export function registerVtexSchemas(): void {
|
|
23
|
-
if (registered) return;
|
|
24
|
-
registered = true;
|
|
25
|
-
registerAppSchemas({
|
|
26
|
-
namespace: "vtex",
|
|
27
|
-
loaders: loaderSchemas,
|
|
28
|
-
actions: actionSchemas,
|
|
29
|
-
});
|
|
30
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* VTEX Checkout / OrderForm types.
|
|
3
|
-
* These mirror the VTEX Checkout API response shapes.
|
|
4
|
-
* Aligned with deco-cx/apps vtex/utils/types.ts
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface OrderFormItem {
|
|
8
|
-
id: string;
|
|
9
|
-
productId: string;
|
|
10
|
-
productRefId?: string;
|
|
11
|
-
refId?: string;
|
|
12
|
-
ean?: string | null;
|
|
13
|
-
name: string;
|
|
14
|
-
skuName: string;
|
|
15
|
-
imageUrl: string;
|
|
16
|
-
detailUrl: string;
|
|
17
|
-
price: number;
|
|
18
|
-
listPrice: number;
|
|
19
|
-
manualPrice?: number | null;
|
|
20
|
-
quantity: number;
|
|
21
|
-
sellingPrice: number;
|
|
22
|
-
rewardValue?: number;
|
|
23
|
-
isGift?: boolean;
|
|
24
|
-
tax?: number;
|
|
25
|
-
seller: string;
|
|
26
|
-
sellerChain?: string[];
|
|
27
|
-
uniqueId: string;
|
|
28
|
-
parentItemIndex?: number | null;
|
|
29
|
-
parentAssemblyBinding?: string | null;
|
|
30
|
-
availability?: string;
|
|
31
|
-
measurementUnit?: string;
|
|
32
|
-
unitMultiplier?: number;
|
|
33
|
-
productCategoryIds?: string;
|
|
34
|
-
productCategories?: Record<string, string>;
|
|
35
|
-
additionalInfo?: {
|
|
36
|
-
brandName?: string;
|
|
37
|
-
brandId?: string;
|
|
38
|
-
dimension?: Record<string, string> | null;
|
|
39
|
-
offeringInfo?: unknown | null;
|
|
40
|
-
offeringType?: unknown | null;
|
|
41
|
-
offeringTypeId?: unknown | null;
|
|
42
|
-
};
|
|
43
|
-
attachments?: unknown[];
|
|
44
|
-
attachmentOfferings?: Array<{
|
|
45
|
-
name: string;
|
|
46
|
-
required: boolean;
|
|
47
|
-
schema: Record<string, unknown>;
|
|
48
|
-
}>;
|
|
49
|
-
offerings?: Array<{
|
|
50
|
-
type: string;
|
|
51
|
-
id: string;
|
|
52
|
-
name: string;
|
|
53
|
-
price: number;
|
|
54
|
-
}>;
|
|
55
|
-
priceTags?: Array<{
|
|
56
|
-
name: string;
|
|
57
|
-
value: number;
|
|
58
|
-
rawValue: number;
|
|
59
|
-
isPercentual: boolean;
|
|
60
|
-
identifier: string | null;
|
|
61
|
-
}>;
|
|
62
|
-
components?: unknown[];
|
|
63
|
-
bundleItems?: unknown[];
|
|
64
|
-
priceDefinition?: {
|
|
65
|
-
calculatedSellingPrice: number;
|
|
66
|
-
total: number;
|
|
67
|
-
sellingPrices: Array<{
|
|
68
|
-
value: number;
|
|
69
|
-
quantity: number;
|
|
70
|
-
}>;
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface Totalizer {
|
|
75
|
-
id: string;
|
|
76
|
-
name: string;
|
|
77
|
-
value: number;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface Message {
|
|
81
|
-
code: string;
|
|
82
|
-
text: string;
|
|
83
|
-
status: string;
|
|
84
|
-
fields?: Record<string, string>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export interface MarketingData {
|
|
88
|
-
utmSource?: string;
|
|
89
|
-
utmMedium?: string;
|
|
90
|
-
utmCampaign?: string;
|
|
91
|
-
utmiPage?: string;
|
|
92
|
-
utmiPart?: string;
|
|
93
|
-
utmiCampaign?: string;
|
|
94
|
-
coupon?: string;
|
|
95
|
-
marketingTags?: string[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface ClientProfileData {
|
|
99
|
-
email: string;
|
|
100
|
-
firstName?: string | null;
|
|
101
|
-
lastName?: string | null;
|
|
102
|
-
document?: string | null;
|
|
103
|
-
phone?: string | null;
|
|
104
|
-
corporateName?: string | null;
|
|
105
|
-
isCorporate?: boolean;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export interface StorePreferencesData {
|
|
109
|
-
countryCode: string;
|
|
110
|
-
saveUserData?: boolean;
|
|
111
|
-
timeZone?: string;
|
|
112
|
-
currencyCode: string;
|
|
113
|
-
currencyLocale?: number;
|
|
114
|
-
currencySymbol: string;
|
|
115
|
-
currencyFormatInfo?: {
|
|
116
|
-
currencyDecimalDigits: number;
|
|
117
|
-
currencyDecimalSeparator: string;
|
|
118
|
-
currencyGroupSeparator: string;
|
|
119
|
-
currencyGroupSize: number;
|
|
120
|
-
startsWithCurrencySymbol: boolean;
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface ClientPreferencesData {
|
|
125
|
-
locale: string;
|
|
126
|
-
optinNewsLetter?: boolean;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export interface ShippingData {
|
|
130
|
-
address?: {
|
|
131
|
-
postalCode?: string;
|
|
132
|
-
city?: string;
|
|
133
|
-
state?: string;
|
|
134
|
-
country?: string;
|
|
135
|
-
street?: string;
|
|
136
|
-
number?: string;
|
|
137
|
-
neighborhood?: string;
|
|
138
|
-
complement?: string;
|
|
139
|
-
reference?: string;
|
|
140
|
-
} | null;
|
|
141
|
-
selectedAddresses?: Array<{
|
|
142
|
-
postalCode?: string;
|
|
143
|
-
city?: string;
|
|
144
|
-
state?: string;
|
|
145
|
-
country?: string;
|
|
146
|
-
}>;
|
|
147
|
-
logisticsInfo?: Array<{
|
|
148
|
-
itemIndex: number;
|
|
149
|
-
selectedSla?: string;
|
|
150
|
-
selectedDeliveryChannel?: string;
|
|
151
|
-
slas?: Sla[];
|
|
152
|
-
}>;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface PaymentData {
|
|
156
|
-
updateStatus?: string;
|
|
157
|
-
installmentOptions?: unknown[];
|
|
158
|
-
paymentSystems?: unknown[];
|
|
159
|
-
payments?: unknown[];
|
|
160
|
-
giftCards?: unknown[];
|
|
161
|
-
availableAccounts?: unknown[];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface OrderForm {
|
|
165
|
-
orderFormId: string;
|
|
166
|
-
salesChannel: string;
|
|
167
|
-
loggedIn: boolean;
|
|
168
|
-
isCheckedIn: boolean;
|
|
169
|
-
storeId?: unknown | null;
|
|
170
|
-
checkedInPickupPointId?: unknown | null;
|
|
171
|
-
allowManualPrice: boolean;
|
|
172
|
-
canEditData: boolean;
|
|
173
|
-
userProfileId?: unknown | null;
|
|
174
|
-
userType?: unknown | null;
|
|
175
|
-
ignoreProfileData: boolean;
|
|
176
|
-
value: number;
|
|
177
|
-
messages: Message[];
|
|
178
|
-
items: OrderFormItem[];
|
|
179
|
-
selectableGifts?: unknown[];
|
|
180
|
-
totalizers: Totalizer[];
|
|
181
|
-
shippingData: ShippingData | null;
|
|
182
|
-
clientProfileData: ClientProfileData | null;
|
|
183
|
-
paymentData: PaymentData | null;
|
|
184
|
-
marketingData: MarketingData | null;
|
|
185
|
-
sellers?: Array<{ id: string; name: string; logo?: string }>;
|
|
186
|
-
clientPreferencesData?: ClientPreferencesData | null;
|
|
187
|
-
commercialConditionData?: unknown | null;
|
|
188
|
-
storePreferencesData?: StorePreferencesData | null;
|
|
189
|
-
giftRegistryData?: unknown | null;
|
|
190
|
-
openTextField?: unknown | null;
|
|
191
|
-
invoiceData?: unknown | null;
|
|
192
|
-
customData?: unknown | null;
|
|
193
|
-
itemMetadata?: unknown | null;
|
|
194
|
-
hooksData?: unknown | null;
|
|
195
|
-
ratesAndBenefitsData?: {
|
|
196
|
-
rateAndBenefitsIdentifiers?: unknown[];
|
|
197
|
-
teaser?: unknown[];
|
|
198
|
-
} | null;
|
|
199
|
-
subscriptionData?: unknown | null;
|
|
200
|
-
merchantContextData?: unknown | null;
|
|
201
|
-
itemsOrdination?: unknown | null;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export interface SimulationOrderForm {
|
|
205
|
-
items: Array<{
|
|
206
|
-
id: string;
|
|
207
|
-
quantity: number;
|
|
208
|
-
seller: string;
|
|
209
|
-
price?: number;
|
|
210
|
-
listPrice?: number;
|
|
211
|
-
offerings?: any[];
|
|
212
|
-
priceTags?: any[];
|
|
213
|
-
availability?: string;
|
|
214
|
-
}>;
|
|
215
|
-
logisticsInfo?: Array<{
|
|
216
|
-
itemIndex: number;
|
|
217
|
-
slas: Sla[];
|
|
218
|
-
selectedSla?: string;
|
|
219
|
-
selectedDeliveryChannel?: string;
|
|
220
|
-
}>;
|
|
221
|
-
paymentData?: {
|
|
222
|
-
installmentOptions?: any[];
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface Sla {
|
|
227
|
-
id: string;
|
|
228
|
-
name: string;
|
|
229
|
-
price: number;
|
|
230
|
-
shippingEstimate: string;
|
|
231
|
-
deliveryChannel?: string;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface SKU {
|
|
235
|
-
id: string;
|
|
236
|
-
seller: string;
|
|
237
|
-
quantity: number;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export interface VtexProduct {
|
|
241
|
-
productId: string;
|
|
242
|
-
productName: string;
|
|
243
|
-
brand: string;
|
|
244
|
-
categoryId: string;
|
|
245
|
-
categories: string[];
|
|
246
|
-
items: any[];
|
|
247
|
-
[key: string]: any;
|
|
248
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for `projectOrderForm` — the server-side fragmentation of a VTEX
|
|
3
|
-
* OrderForm into the requested `CartProjection`. Pure function, no I/O.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { Minicart } from "@decocms/apps-commerce/types";
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
-
import type { OrderForm } from "../../types";
|
|
9
|
-
import { projectOrderForm } from "../cartProjection";
|
|
10
|
-
|
|
11
|
-
function makeOrderForm(overrides: Partial<OrderForm> = {}): OrderForm {
|
|
12
|
-
return {
|
|
13
|
-
orderFormId: "of-123",
|
|
14
|
-
salesChannel: "1",
|
|
15
|
-
loggedIn: false,
|
|
16
|
-
isCheckedIn: false,
|
|
17
|
-
allowManualPrice: false,
|
|
18
|
-
canEditData: true,
|
|
19
|
-
ignoreProfileData: false,
|
|
20
|
-
value: 15000, // R$150.00 in cents
|
|
21
|
-
messages: [],
|
|
22
|
-
items: [
|
|
23
|
-
{
|
|
24
|
-
id: "sku-1",
|
|
25
|
-
productId: "prod-1",
|
|
26
|
-
name: "Camiseta",
|
|
27
|
-
skuName: "Camiseta P Azul",
|
|
28
|
-
imageUrl: "http://img.example/x.jpg",
|
|
29
|
-
detailUrl: "/camiseta/p",
|
|
30
|
-
price: 10000,
|
|
31
|
-
listPrice: 12000,
|
|
32
|
-
sellingPrice: 10000,
|
|
33
|
-
quantity: 1,
|
|
34
|
-
seller: "1",
|
|
35
|
-
uniqueId: "u1",
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: "sku-2",
|
|
39
|
-
productId: "prod-2",
|
|
40
|
-
name: "Boné",
|
|
41
|
-
skuName: "Boné U Preto",
|
|
42
|
-
imageUrl: "https://img.example/y.jpg",
|
|
43
|
-
detailUrl: "/bone/u",
|
|
44
|
-
price: 5000,
|
|
45
|
-
listPrice: 5000,
|
|
46
|
-
sellingPrice: 5000,
|
|
47
|
-
quantity: 2,
|
|
48
|
-
seller: "1",
|
|
49
|
-
uniqueId: "u2",
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
totalizers: [
|
|
53
|
-
{ id: "Items", name: "Items", value: 20000 },
|
|
54
|
-
{ id: "Discounts", name: "Discounts", value: -5000 },
|
|
55
|
-
],
|
|
56
|
-
shippingData: null,
|
|
57
|
-
clientProfileData: null,
|
|
58
|
-
paymentData: null,
|
|
59
|
-
marketingData: null,
|
|
60
|
-
...overrides,
|
|
61
|
-
} as OrderForm;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
describe("projectOrderForm", () => {
|
|
65
|
-
it("none → { ok: true } and discards the payload", () => {
|
|
66
|
-
expect(projectOrderForm(makeOrderForm(), "none")).toEqual({ ok: true });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("summary → orderFormId, summed quantities, total in major units", () => {
|
|
70
|
-
const r = projectOrderForm(makeOrderForm(), "summary") as {
|
|
71
|
-
orderFormId: string;
|
|
72
|
-
totalItems: number;
|
|
73
|
-
total: number;
|
|
74
|
-
};
|
|
75
|
-
expect(r.orderFormId).toBe("of-123");
|
|
76
|
-
expect(r.totalItems).toBe(3); // 1 + 2
|
|
77
|
-
expect(r.total).toBe(150); // 15000 cents → 150
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it("summary+items → summary plus slim, https-normalized items", () => {
|
|
81
|
-
const r = projectOrderForm(makeOrderForm(), "summary+items") as {
|
|
82
|
-
totalItems: number;
|
|
83
|
-
items: Array<{ item_name: string; image: string; price: number; quantity: number }>;
|
|
84
|
-
};
|
|
85
|
-
expect(r.totalItems).toBe(3);
|
|
86
|
-
expect(r.items).toHaveLength(2);
|
|
87
|
-
expect(r.items[0]).toMatchObject({
|
|
88
|
-
item_name: "Camiseta",
|
|
89
|
-
item_variant: "Camiseta P Azul",
|
|
90
|
-
image: "https://img.example/x.jpg", // http → https
|
|
91
|
-
price: 100, // 10000 cents
|
|
92
|
-
quantity: 1,
|
|
93
|
-
});
|
|
94
|
-
// slim projection must NOT leak the full VTEX item fields
|
|
95
|
-
expect(r.items[0]).not.toHaveProperty("uniqueId");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("minicart → canonical Minicart with major-unit totals", () => {
|
|
99
|
-
const r = projectOrderForm(makeOrderForm(), "minicart") as Minicart<OrderForm>;
|
|
100
|
-
expect(r.storefront.items).toHaveLength(2);
|
|
101
|
-
expect(r.storefront.total).toBe(150);
|
|
102
|
-
expect(r.original.orderFormId).toBe("of-123");
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it("raw → the untouched OrderForm", () => {
|
|
106
|
-
const of = makeOrderForm();
|
|
107
|
-
expect(projectOrderForm(of, "raw")).toBe(of);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("summary handles an empty cart without NaN", () => {
|
|
111
|
-
const r = projectOrderForm(
|
|
112
|
-
makeOrderForm({ items: [], value: 0, totalizers: [] }),
|
|
113
|
-
"summary",
|
|
114
|
-
) as { totalItems: number; total: number };
|
|
115
|
-
expect(r.totalItems).toBe(0);
|
|
116
|
-
expect(r.total).toBe(0);
|
|
117
|
-
});
|
|
118
|
-
});
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
_resetCookieWarnDedupForTests,
|
|
4
|
-
extractVtexCookies,
|
|
5
|
-
sanitizeOutboundCookieHeader,
|
|
6
|
-
VTEX_COOKIE_PREFIXES,
|
|
7
|
-
warnDroppedCookies,
|
|
8
|
-
} from "../cookieSanitizer";
|
|
9
|
-
|
|
10
|
-
describe("sanitizeOutboundCookieHeader", () => {
|
|
11
|
-
it("returns empty result for empty input", () => {
|
|
12
|
-
expect(sanitizeOutboundCookieHeader("")).toEqual({ cookies: "", dropped: [] });
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("preserves a clean ASCII cookie header unchanged in content", () => {
|
|
16
|
-
const raw = "checkout.vtex.com=__ofid=abc; vtex_segment=eyJ0b2tlbiI6IjEyMyJ9";
|
|
17
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
|
|
18
|
-
expect(dropped).toEqual([]);
|
|
19
|
-
expect(cookies).toBe("checkout.vtex.com=__ofid=abc; vtex_segment=eyJ0b2tlbiI6IjEyMyJ9");
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it("drops a cookie whose value contains non-ASCII bytes — a production VTEX storefront repro", () => {
|
|
23
|
-
// `á` is the UTF-8 encoding of `á` interpreted as Latin-1 — bytes 0xC3 0xA1.
|
|
24
|
-
// VTEX's janus gateway returns 503 deterministically when this reaches it.
|
|
25
|
-
const raw = "checkout.vtex.com=__ofid=abc; category_click=Eletroportáteis; vtex_segment=ok";
|
|
26
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
|
|
27
|
-
expect(cookies).toBe("checkout.vtex.com=__ofid=abc; vtex_segment=ok");
|
|
28
|
-
expect(dropped).toEqual([{ name: "category_click", reason: "non_ascii" }]);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("drops cookies with raw multi-byte UTF-8 characters", () => {
|
|
32
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(
|
|
33
|
-
"checkout.vtex.com=ok; pref=日本語; analytics=tracked",
|
|
34
|
-
);
|
|
35
|
-
expect(cookies).toBe("checkout.vtex.com=ok; analytics=tracked");
|
|
36
|
-
expect(dropped).toEqual([{ name: "pref", reason: "non_ascii" }]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("treats malformed pairs (no `=`, blank name) as dropped", () => {
|
|
40
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader("justaname; =headless; ok=1; ;");
|
|
41
|
-
expect(cookies).toBe("ok=1");
|
|
42
|
-
expect(dropped.map((d) => d.reason)).toEqual(["malformed", "malformed"]);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("preserves URL-encoded values containing `=` signs (JWT-shaped tokens)", () => {
|
|
46
|
-
const raw =
|
|
47
|
-
"VtexIdclientAutCookie=eyJhbGciOiJSUzI1NiIs.eyJzdWIiOiJxIn0.signature; CheckoutOrderFormOwnership=Vk1B%3D%3D";
|
|
48
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(raw);
|
|
49
|
-
expect(dropped).toEqual([]);
|
|
50
|
-
expect(cookies).toBe(raw);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it("rejects cookie names with separators (e.g. spaces) as malformed", () => {
|
|
54
|
-
const { dropped } = sanitizeOutboundCookieHeader("bad name=v; ok=1");
|
|
55
|
-
expect(dropped[0]).toEqual({ name: "bad name", reason: "malformed" });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("trims surrounding whitespace between pairs", () => {
|
|
59
|
-
const { cookies } = sanitizeOutboundCookieHeader(" a=1 ; b=2 ;c=3 ");
|
|
60
|
-
expect(cookies).toBe("a=1; b=2; c=3");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("preserves cookie order across the input", () => {
|
|
64
|
-
const { cookies } = sanitizeOutboundCookieHeader("a=1; b=2; c=3; d=4");
|
|
65
|
-
expect(cookies).toBe("a=1; b=2; c=3; d=4");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("with allowlist=true keeps only VTEX-prefixed cookies and reports the rest", () => {
|
|
69
|
-
const raw =
|
|
70
|
-
"FPID=abc; checkout.vtex.com=__ofid=xyz; _ga=tracking; vtex_segment=eyJ9; CheckoutOrderFormOwnership=token; __cf_bm=cf";
|
|
71
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(raw, { allowlist: true });
|
|
72
|
-
expect(cookies).toBe(
|
|
73
|
-
"checkout.vtex.com=__ofid=xyz; vtex_segment=eyJ9; CheckoutOrderFormOwnership=token",
|
|
74
|
-
);
|
|
75
|
-
expect(dropped.map((d) => d.name).sort()).toEqual(["FPID", "__cf_bm", "_ga"]);
|
|
76
|
-
expect(dropped.every((d) => d.reason === "not_in_allowlist")).toBe(true);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("allowlist mode still drops a VTEX-prefixed cookie that has a non-ASCII value", () => {
|
|
80
|
-
const raw = "vtex_segment=okãvalue; checkout.vtex.com=ok";
|
|
81
|
-
const { cookies, dropped } = sanitizeOutboundCookieHeader(raw, { allowlist: true });
|
|
82
|
-
expect(cookies).toBe("checkout.vtex.com=ok");
|
|
83
|
-
expect(dropped).toEqual([{ name: "vtex_segment", reason: "non_ascii" }]);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("VTEX_COOKIE_PREFIXES covers the cookies VTEX actions actually depend on", () => {
|
|
87
|
-
// Sanity check — if anyone removes one of these, every action that
|
|
88
|
-
// depends on it will silently 401/redirect on production.
|
|
89
|
-
for (const required of [
|
|
90
|
-
"VtexIdclientAutCookie",
|
|
91
|
-
"checkout.vtex.com",
|
|
92
|
-
"CheckoutOrderFormOwnership",
|
|
93
|
-
"vtex_session",
|
|
94
|
-
"vtex_segment",
|
|
95
|
-
]) {
|
|
96
|
-
expect(VTEX_COOKIE_PREFIXES).toContain(required);
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe("extractVtexCookies (allowlist convenience wrapper)", () => {
|
|
102
|
-
it("matches sanitizeOutboundCookieHeader(_, { allowlist: true })", () => {
|
|
103
|
-
const raw =
|
|
104
|
-
"FPID=abc; checkout.vtex.com=__ofid=xyz; category_click=Eletroportáteis; vtex_segment=ok";
|
|
105
|
-
expect(extractVtexCookies(raw)).toBe(
|
|
106
|
-
sanitizeOutboundCookieHeader(raw, { allowlist: true }).cookies,
|
|
107
|
-
);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("returns empty string for empty input", () => {
|
|
111
|
-
expect(extractVtexCookies("")).toBe("");
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe("warnDroppedCookies", () => {
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
_resetCookieWarnDedupForTests();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
afterEach(() => {
|
|
121
|
-
vi.restoreAllMocks();
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("emits one console.warn per dropped cookie with structured fields", () => {
|
|
125
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
126
|
-
warnDroppedCookies(
|
|
127
|
-
[
|
|
128
|
-
{ name: "category_click", reason: "non_ascii" },
|
|
129
|
-
{ name: "_ga", reason: "not_in_allowlist" },
|
|
130
|
-
],
|
|
131
|
-
"acct.vtexcommercestable.com.br",
|
|
132
|
-
);
|
|
133
|
-
expect(warn).toHaveBeenCalledTimes(2);
|
|
134
|
-
expect(warn.mock.calls[0]?.[0]).toMatch(
|
|
135
|
-
/host=acct\.vtexcommercestable\.com\.br name=category_click reason=non_ascii/,
|
|
136
|
-
);
|
|
137
|
-
expect(warn.mock.calls[1]?.[0]).toMatch(/name=_ga reason=not_in_allowlist/);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it("dedupes repeated drops of the same name+reason+host within the isolate lifetime", () => {
|
|
141
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
142
|
-
const drop = [{ name: "category_click", reason: "non_ascii" as const }];
|
|
143
|
-
warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
|
|
144
|
-
warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
|
|
145
|
-
warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
|
|
146
|
-
expect(warn).toHaveBeenCalledTimes(1);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("does not dedupe across different hosts", () => {
|
|
150
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
151
|
-
const drop = [{ name: "category_click", reason: "non_ascii" as const }];
|
|
152
|
-
warnDroppedCookies(drop, "acct.vtexcommercestable.com.br");
|
|
153
|
-
warnDroppedCookies(drop, "secure.example.com.br");
|
|
154
|
-
expect(warn).toHaveBeenCalledTimes(2);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("is a no-op when nothing was dropped", () => {
|
|
158
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
159
|
-
warnDroppedCookies([], "any.host");
|
|
160
|
-
expect(warn).not.toHaveBeenCalled();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { fetchAPI, fetchSafe, HttpError } from "../fetch";
|
|
3
|
-
|
|
4
|
-
const realFetch = globalThis.fetch;
|
|
5
|
-
|
|
6
|
-
function mockResponse(body: unknown, status = 200): Response {
|
|
7
|
-
return {
|
|
8
|
-
ok: status >= 200 && status < 300,
|
|
9
|
-
status,
|
|
10
|
-
statusText: status === 200 ? "OK" : "Error",
|
|
11
|
-
url: "https://example.com",
|
|
12
|
-
json: () => Promise.resolve(body),
|
|
13
|
-
} as Response;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
describe("fetchSafe", () => {
|
|
17
|
-
beforeEach(() => {
|
|
18
|
-
globalThis.fetch = vi.fn() as unknown as typeof fetch;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterEach(() => {
|
|
22
|
-
globalThis.fetch = realFetch;
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it("returns the response on 2xx", async () => {
|
|
26
|
-
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse({ ok: true }));
|
|
27
|
-
const res = await fetchSafe("https://example.com/api");
|
|
28
|
-
expect(res.status).toBe(200);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("throws HttpError on non-2xx", async () => {
|
|
32
|
-
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(mockResponse({}, 500));
|
|
33
|
-
await expect(fetchSafe("https://example.com/api")).rejects.toBeInstanceOf(HttpError);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("sanitizes utm_* and map params (drops <, > and non-Latin1)", async () => {
|
|
37
|
-
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
38
|
-
fetchMock.mockResolvedValue(mockResponse({}));
|
|
39
|
-
|
|
40
|
-
await fetchSafe("https://example.com/api?utm_source=<script>café</script>&keep=ok");
|
|
41
|
-
|
|
42
|
-
const callArg = fetchMock.mock.calls[0]?.[0] as string;
|
|
43
|
-
expect(callArg).toContain("utm_source=script");
|
|
44
|
-
expect(callArg).not.toContain("<");
|
|
45
|
-
expect(callArg).not.toContain("café");
|
|
46
|
-
expect(callArg).toContain("keep=ok");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("forwards init options to fetch", async () => {
|
|
50
|
-
const fetchMock = globalThis.fetch as ReturnType<typeof vi.fn>;
|
|
51
|
-
fetchMock.mockResolvedValue(mockResponse({}));
|
|
52
|
-
|
|
53
|
-
await fetchSafe("https://example.com/api", {
|
|
54
|
-
method: "POST",
|
|
55
|
-
headers: { "x-test": "1" },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const init = fetchMock.mock.calls[0]?.[1] as RequestInit;
|
|
59
|
-
expect(init.method).toBe("POST");
|
|
60
|
-
expect((init.headers as Record<string, string>)["x-test"]).toBe("1");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
describe("fetchAPI", () => {
|
|
65
|
-
beforeEach(() => {
|
|
66
|
-
globalThis.fetch = vi.fn() as unknown as typeof fetch;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
afterEach(() => {
|
|
70
|
-
globalThis.fetch = realFetch;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it("parses JSON on success", async () => {
|
|
74
|
-
(globalThis.fetch as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
75
|
-
mockResponse({ hello: "world" }),
|
|
76
|
-
);
|
|
77
|
-
const data = await fetchAPI<{ hello: string }>("https://example.com/api");
|
|
78
|
-
expect(data).toEqual({ hello: "world" });
|
|
79
|
-
});
|
|
80
|
-
});
|