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