@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.
Files changed (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. 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
- });