@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
@@ -1,255 +0,0 @@
1
- /**
2
- * Regression tests for the auto-forwarding of `vtex_segment` on outgoing
3
- * VTEX API calls. Without this, Legacy Catalog endpoints don't see the
4
- * region cookie and return OutOfStock for products only available
5
- * through regional sellers — see vtex/client.ts:vtexFetchResponse.
6
- */
7
-
8
- import { RequestContext } from "@decocms/blocks/sdk/requestContext";
9
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
10
- import {
11
- configureVtex,
12
- intelligentSearch,
13
- setVtexFetch,
14
- vtexCachedFetch,
15
- vtexFetchResponse,
16
- } from "../client";
17
- import { clearFetchCache } from "../utils/fetchCache";
18
-
19
- function mockResponse(body: unknown = {}, status = 200): Response {
20
- return {
21
- ok: status >= 200 && status < 300,
22
- status,
23
- statusText: status === 200 ? "OK" : "Error",
24
- json: () => Promise.resolve(body),
25
- } as Response;
26
- }
27
-
28
- /**
29
- * Run `fn` inside a fake request context with the given cookie header.
30
- *
31
- * Two problems prevented a naive `RequestContext.run(new Request(...))`
32
- * approach from working:
33
- *
34
- * 1. Under the Fetch spec a Request's headers are in the "request"
35
- * guard mode, which silently drops forbidden request headers —
36
- * including `cookie` — at construction time. Node 22 / undici
37
- * enforces this strictly, so the cookie never reaches
38
- * `request.headers.get("cookie")`.
39
- * 2. `@decocms/start`'s `RequestContext` is backed by a
40
- * `RequestStore` that defaults to a NOOP implementation. The
41
- * ALS-backed store is installed by site code at worker boot, not
42
- * in unit tests. So `RequestContext.run(req, fn)` calls
43
- * `fn()` without any propagation, and `RequestContext.current`
44
- * inside `fn` still returns `null` — production code under test
45
- * never sees the test's cookie.
46
- *
47
- * Fix: build a fresh `Headers` object (which uses the "none" guard,
48
- * so `set("cookie", ...)` works), wrap it in a minimal `Ctx`-shaped
49
- * object, and override the `RequestContext.current` getter via
50
- * `vi.spyOn`. The spy is restored after `fn` resolves to keep tests
51
- * isolated. Nothing here depends on undici or ALS internals.
52
- */
53
- /**
54
- * Read a header from an init in a shape-agnostic way. After the
55
- * `mergeHeaders` refactor, `init.headers` is always a `Headers`
56
- * instance — but the helper handles legacy shapes too so the tests
57
- * stay robust if someone changes the merge implementation again.
58
- */
59
- function headerValue(init: RequestInit | undefined, name: string): string | undefined {
60
- const headers = init?.headers;
61
- if (!headers) return undefined;
62
- if (headers instanceof Headers) return headers.get(name) ?? undefined;
63
- if (Array.isArray(headers)) {
64
- const found = headers.find(([k]) => k.toLowerCase() === name.toLowerCase());
65
- return found?.[1];
66
- }
67
- const rec = headers as Record<string, string>;
68
- const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
69
- return key ? rec[key] : undefined;
70
- }
71
-
72
- function withRequest<T>(cookieHeader: string | null, fn: () => Promise<T>): Promise<T> {
73
- const headers = new Headers();
74
- if (cookieHeader) headers.set("cookie", cookieHeader);
75
- const fakeCtx = {
76
- request: { headers } as unknown as Request,
77
- signal: new AbortController().signal,
78
- responseHeaders: new Headers(),
79
- bag: new Map(),
80
- startedAt: Date.now(),
81
- };
82
- const spy = vi
83
- .spyOn(RequestContext, "current", "get")
84
- .mockReturnValue(fakeCtx as unknown as ReturnType<typeof Reflect.get>);
85
- return fn().finally(() => spy.mockRestore());
86
- }
87
-
88
- describe("vtexFetchResponse — vtex_segment cookie forwarding", () => {
89
- let lastInit: RequestInit | undefined;
90
-
91
- beforeEach(() => {
92
- configureVtex({ account: "testaccount" });
93
- lastInit = undefined;
94
- setVtexFetch(((_url: string, init?: RequestInit) => {
95
- lastInit = init;
96
- return Promise.resolve(mockResponse());
97
- }) as typeof fetch);
98
- });
99
-
100
- afterEach(() => {
101
- vi.restoreAllMocks();
102
- });
103
-
104
- it("forwards vtex_segment cookie when present and caller didn't set one", async () => {
105
- await withRequest("vtex_segment=abc123; other=foo", async () => {
106
- await vtexFetchResponse("/api/catalog_system/pub/products/x");
107
- });
108
- expect(headerValue(lastInit, "cookie")).toBe("vtex_segment=abc123");
109
- });
110
-
111
- it("does not overwrite a caller-supplied cookie header", async () => {
112
- await withRequest("vtex_segment=abc123", async () => {
113
- await vtexFetchResponse("/api/x", {
114
- headers: { cookie: "custom=zzz" },
115
- });
116
- });
117
- expect(headerValue(lastInit, "cookie")).toBe("custom=zzz");
118
- });
119
-
120
- it("does not overwrite a caller-supplied Cookie header (case-insensitive)", async () => {
121
- await withRequest("vtex_segment=abc123", async () => {
122
- await vtexFetchResponse("/api/x", {
123
- headers: { Cookie: "custom=zzz" },
124
- });
125
- });
126
- expect(headerValue(lastInit, "cookie")).toBe("custom=zzz");
127
- });
128
-
129
- it("is a no-op when there is no incoming cookie header", async () => {
130
- await withRequest(null, async () => {
131
- await vtexFetchResponse("/api/x");
132
- });
133
- expect(headerValue(lastInit, "cookie")).toBeUndefined();
134
- });
135
-
136
- it("is a no-op when there is a cookie header but no vtex_segment", async () => {
137
- await withRequest("other=foo; another=bar", async () => {
138
- await vtexFetchResponse("/api/x");
139
- });
140
- expect(headerValue(lastInit, "cookie")).toBeUndefined();
141
- });
142
-
143
- it("does not crash when called outside a RequestContext", async () => {
144
- await vtexFetchResponse("/api/x");
145
- expect(headerValue(lastInit, "cookie")).toBeUndefined();
146
- });
147
-
148
- it("preserves auth headers alongside the forwarded cookie", async () => {
149
- configureVtex({ account: "testaccount", appKey: "k", appToken: "t" });
150
- await withRequest("vtex_segment=abc123", async () => {
151
- await vtexFetchResponse("/api/x");
152
- });
153
- expect(headerValue(lastInit, "X-VTEX-API-AppKey")).toBe("k");
154
- expect(headerValue(lastInit, "X-VTEX-API-AppToken")).toBe("t");
155
- expect(headerValue(lastInit, "cookie")).toBe("vtex_segment=abc123");
156
- });
157
-
158
- // Regression: when init.headers is a Headers object (as
159
- // `createVtexCheckoutProxy` passes through `getVtexFetch()`), the
160
- // existing cookie must survive verbatim. The naive
161
- // `{ ...authHeaders, ...init?.headers }` spread collapses a Headers
162
- // instance to `{}` (Headers has no own enumerable entries), which
163
- // silently wipes the browser's full Cookie header — including the
164
- // orderForm cookie any checkout flow depends on.
165
- it("preserves an existing Cookie header when init.headers is a Headers instance", async () => {
166
- await withRequest("vtex_segment=abc123", async () => {
167
- const proxyInit: RequestInit = {
168
- headers: new Headers({
169
- cookie: "checkout.vtex.com=__ofid=xyz; vtex_segment=originalseg; foo=bar",
170
- }),
171
- };
172
- await vtexFetchResponse("/api/checkout/pub/orderForm", proxyInit);
173
- });
174
- expect(headerValue(lastInit, "cookie")).toContain("checkout.vtex.com=__ofid=xyz");
175
- });
176
- });
177
-
178
- // Module-level counter for cache-busting test URLs. `Date.now()` collides
179
- // when two tests run within the same millisecond and the SWR cache in
180
- // fetchWithCache short-circuits the second one — `_fetch` never runs and
181
- // `lastInit` stays `undefined`. Per-test ids are deterministic and
182
- // collision-free.
183
- let testUrlCounter = 0;
184
- const uniqPath = (prefix: string) => `${prefix}/${++testUrlCounter}`;
185
-
186
- describe("vtexCachedFetch — vtex_segment cookie forwarding", () => {
187
- let lastInit: RequestInit | undefined;
188
-
189
- beforeEach(() => {
190
- clearFetchCache();
191
- configureVtex({ account: "testaccount" });
192
- lastInit = undefined;
193
- setVtexFetch(((_url: string, init?: RequestInit) => {
194
- lastInit = init;
195
- return Promise.resolve(mockResponse({ ok: true }));
196
- }) as typeof fetch);
197
- });
198
-
199
- afterEach(() => {
200
- vi.restoreAllMocks();
201
- });
202
-
203
- it("forwards vtex_segment on cached GETs", async () => {
204
- await withRequest("vtex_segment=abc123", async () => {
205
- await vtexCachedFetch(uniqPath("/api/catalog_system/pub/products"));
206
- });
207
- expect(headerValue(lastInit, "cookie")).toBe("vtex_segment=abc123");
208
- });
209
-
210
- it("does not overwrite a caller-supplied cookie header", async () => {
211
- await withRequest("vtex_segment=abc123", async () => {
212
- await vtexCachedFetch(uniqPath("/api/x"), {
213
- headers: { cookie: "custom=zzz" },
214
- });
215
- });
216
- expect(headerValue(lastInit, "cookie")).toBe("custom=zzz");
217
- });
218
- });
219
-
220
- describe("intelligentSearch — vtex_segment cookie forwarding", () => {
221
- let lastInit: RequestInit | undefined;
222
-
223
- beforeEach(() => {
224
- // Reset the SWR cache: otherwise the second test in this block
225
- // can serve the first test's cached body without invoking the
226
- // stub _fetch, leaving lastInit undefined.
227
- clearFetchCache();
228
- configureVtex({ account: "testaccount" });
229
- lastInit = undefined;
230
- setVtexFetch(((_url: string, init?: RequestInit) => {
231
- lastInit = init;
232
- return Promise.resolve(mockResponse({ products: [] }));
233
- }) as typeof fetch);
234
- });
235
-
236
- afterEach(() => {
237
- vi.restoreAllMocks();
238
- });
239
-
240
- it("forwards vtex_segment when caller didn't pass cookieHeader", async () => {
241
- await withRequest("vtex_segment=abc123; other=foo", async () => {
242
- await intelligentSearch(uniqPath("/product_search"));
243
- });
244
- expect(headerValue(lastInit, "cookie")).toBe("vtex_segment=abc123");
245
- });
246
-
247
- it("respects an explicit cookieHeader override", async () => {
248
- await withRequest("vtex_segment=abc123", async () => {
249
- await intelligentSearch(uniqPath("/product_search"), undefined, {
250
- cookieHeader: "custom=zzz",
251
- });
252
- });
253
- expect(headerValue(lastInit, "cookie")).toBe("custom=zzz");
254
- });
255
- });
@@ -1,257 +0,0 @@
1
- /**
2
- * Regression tests for the Set-Cookie propagation chain through
3
- * `vtexFetchWithCookies`. Without this chain, VTEX's `checkout.vtex.com`
4
- * and `CheckoutOrderFormOwnership` cookies never reach the browser via
5
- * `createServerFn` actions, the storefront's local `__orderFormId`
6
- * drifts away from VTEX's server-side orderForm, and the user lands
7
- * on `/checkout` with an empty cart.
8
- *
9
- * Two failure modes covered here:
10
- *
11
- * (1) Inbound capture — VTEX `Set-Cookie` headers must be appended
12
- * to `RequestContext.responseHeaders`, skipping the two IS
13
- * cookies that the middleware owns (`vtex_is_session`,
14
- * `vtex_is_anonymous`), and the `domain=` attribute must be
15
- * stripped so the browser scopes the cookie to the storefront.
16
- *
17
- * (2) Outbound merge — when the caller passes `init.headers` as a
18
- * `Headers` instance (the `createVtexCheckoutProxy` factory does
19
- * this through `getVtexFetch()`), spreading it as a plain object
20
- * collapses to `{}` and silently wipes every other header the
21
- * caller set. The Headers-aware merge in `vtexFetchWithCookies`
22
- * keeps the bug from sneaking back in.
23
- */
24
-
25
- import { RequestContext } from "@decocms/blocks/sdk/requestContext";
26
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
27
- import { configureVtex, setVtexFetch, vtexFetchWithCookies } from "../client";
28
-
29
- function mockResponse(opts?: { body?: unknown; status?: number; setCookies?: string[] }): Response {
30
- const status = opts?.status ?? 200;
31
- const headers = new Headers();
32
- for (const c of opts?.setCookies ?? []) headers.append("set-cookie", c);
33
- return {
34
- ok: status >= 200 && status < 300,
35
- status,
36
- statusText: status === 200 ? "OK" : "Error",
37
- headers,
38
- json: () => Promise.resolve(opts?.body ?? {}),
39
- } as Response;
40
- }
41
-
42
- function headerValue(init: RequestInit | undefined, name: string): string | undefined {
43
- const headers = init?.headers;
44
- if (!headers) return undefined;
45
- if (headers instanceof Headers) return headers.get(name) ?? undefined;
46
- if (Array.isArray(headers)) {
47
- const found = headers.find(([k]) => k.toLowerCase() === name.toLowerCase());
48
- return found?.[1];
49
- }
50
- const rec = headers as Record<string, string>;
51
- const key = Object.keys(rec).find((k) => k.toLowerCase() === name.toLowerCase());
52
- return key ? rec[key] : undefined;
53
- }
54
-
55
- function withRequest<T>(
56
- cookieHeader: string | null,
57
- fn: (ctx: { responseHeaders: Headers }) => Promise<T>,
58
- requestUrl = "https://store.example.com/api/checkout/pub/orderForm",
59
- ): Promise<T> {
60
- const reqHeaders = new Headers();
61
- if (cookieHeader) reqHeaders.set("cookie", cookieHeader);
62
- const responseHeaders = new Headers();
63
- const fakeCtx = {
64
- request: { headers: reqHeaders, url: requestUrl } as unknown as Request,
65
- signal: new AbortController().signal,
66
- responseHeaders,
67
- bag: new Map(),
68
- startedAt: Date.now(),
69
- };
70
- const spy = vi
71
- .spyOn(RequestContext, "current", "get")
72
- .mockReturnValue(fakeCtx as unknown as ReturnType<typeof Reflect.get>);
73
- return fn({ responseHeaders }).finally(() => spy.mockRestore());
74
- }
75
-
76
- describe("vtexFetchWithCookies — inbound Set-Cookie capture", () => {
77
- let lastInit: RequestInit | undefined;
78
-
79
- beforeEach(() => {
80
- configureVtex({ account: "testaccount" });
81
- lastInit = undefined;
82
- });
83
-
84
- afterEach(() => {
85
- vi.restoreAllMocks();
86
- });
87
-
88
- it("captures upstream Set-Cookie into RequestContext.responseHeaders", async () => {
89
- setVtexFetch(((_url: string, init?: RequestInit) => {
90
- lastInit = init;
91
- return Promise.resolve(
92
- mockResponse({
93
- setCookies: [
94
- "checkout.vtex.com=__ofid=abc123; Path=/; HttpOnly; Secure",
95
- "CheckoutOrderFormOwnership=def456; Path=/; HttpOnly",
96
- ],
97
- }),
98
- );
99
- }) as typeof fetch);
100
-
101
- const captured = await withRequest("vtex_segment=seg1", async ({ responseHeaders }) => {
102
- await vtexFetchWithCookies("/api/checkout/pub/orderForm");
103
- return responseHeaders.getSetCookie();
104
- });
105
-
106
- expect(captured).toHaveLength(2);
107
- expect(captured.some((c) => c.startsWith("checkout.vtex.com="))).toBe(true);
108
- expect(captured.some((c) => c.startsWith("CheckoutOrderFormOwnership="))).toBe(true);
109
- });
110
-
111
- // The cart server-fn cookie MUST land at the same scope as the checkout
112
- // proxy (domain-scoped to the storefront host) and native VTEX
113
- // (`domain=<host>`). Stripping the Domain (host-only) creates a second,
114
- // distinct cookie that drifts from the proxy's and causes the
115
- // nondeterministic empty-cart bug. So we rewrite Domain, not strip it.
116
- it("rewrites the Domain= attribute to the storefront host (matches the proxy)", async () => {
117
- setVtexFetch((() =>
118
- Promise.resolve(
119
- mockResponse({
120
- setCookies: [
121
- "checkout.vtex.com=__ofid=abc; Domain=.vtexcommercestable.com.br; Path=/; HttpOnly",
122
- ],
123
- }),
124
- )) as typeof fetch);
125
-
126
- const captured = await withRequest(
127
- "foo=bar",
128
- async ({ responseHeaders }) => {
129
- await vtexFetchWithCookies("/api/checkout/pub/orderForm");
130
- return responseHeaders.getSetCookie();
131
- },
132
- "https://www.casaevideo.com.br/api/checkout/pub/orderForm",
133
- );
134
-
135
- // domain-scoped to the storefront host, NOT the original VTEX domain
136
- expect(captured[0]).toMatch(/Domain=www\.casaevideo\.com\.br/);
137
- expect(captured[0]).not.toMatch(/vtexcommercestable/i);
138
- expect(captured[0]).toContain("checkout.vtex.com=__ofid=abc");
139
- });
140
-
141
- it("rewrites only the Domain attribute, not a domain= substring in the cookie value", async () => {
142
- setVtexFetch((() =>
143
- Promise.resolve(
144
- mockResponse({
145
- // Pathological value embedding `domain=` before the first `;`.
146
- setCookies: [
147
- "checkout.vtex.com=__ofid=domain=keep; Domain=.vtexcommercestable.com.br; Path=/",
148
- ],
149
- }),
150
- )) as typeof fetch);
151
-
152
- const captured = await withRequest(
153
- "foo=bar",
154
- async ({ responseHeaders }) => {
155
- await vtexFetchWithCookies("/api/checkout/pub/orderForm");
156
- return responseHeaders.getSetCookie();
157
- },
158
- "https://www.casaevideo.com.br/api/checkout/pub/orderForm",
159
- );
160
-
161
- // the value's `domain=keep` is untouched; the attribute is rewritten
162
- expect(captured[0]).toContain("__ofid=domain=keep");
163
- expect(captured[0]).toMatch(/;\s*Domain=www\.casaevideo\.com\.br/);
164
- expect(captured[0]).not.toMatch(/vtexcommercestable/i);
165
- });
166
-
167
- it("falls back to stripping Domain when there is no request scope (module init)", async () => {
168
- setVtexFetch((() =>
169
- Promise.resolve(
170
- mockResponse({
171
- setCookies: ["checkout.vtex.com=__ofid=abc; Domain=.vtexcommercestable.com.br; Path=/"],
172
- }),
173
- )) as typeof fetch);
174
- // No withRequest wrapper → RequestContext.current is null.
175
- await expect(vtexFetchWithCookies("/api/checkout/pub/orderForm")).resolves.toBeDefined();
176
- });
177
-
178
- it("skips Intelligent Search cookies (managed by middleware, not actions)", async () => {
179
- setVtexFetch((() =>
180
- Promise.resolve(
181
- mockResponse({
182
- setCookies: [
183
- "checkout.vtex.com=__ofid=abc; Path=/",
184
- "vtex_is_session=ignore-me; Path=/",
185
- "vtex_is_anonymous=ignore-me-too; Path=/",
186
- "CheckoutOrderFormOwnership=def; Path=/",
187
- ],
188
- }),
189
- )) as typeof fetch);
190
-
191
- const captured = await withRequest("foo=bar", async ({ responseHeaders }) => {
192
- await vtexFetchWithCookies("/api/checkout/pub/orderForm");
193
- return responseHeaders.getSetCookie();
194
- });
195
-
196
- expect(captured.some((c) => c.startsWith("checkout.vtex.com="))).toBe(true);
197
- expect(captured.some((c) => c.startsWith("CheckoutOrderFormOwnership="))).toBe(true);
198
- expect(captured.some((c) => c.startsWith("vtex_is_session="))).toBe(false);
199
- expect(captured.some((c) => c.startsWith("vtex_is_anonymous="))).toBe(false);
200
- });
201
-
202
- it("does not crash when called outside a RequestContext", async () => {
203
- setVtexFetch((() =>
204
- Promise.resolve(
205
- mockResponse({
206
- setCookies: ["checkout.vtex.com=__ofid=abc; Path=/"],
207
- }),
208
- )) as typeof fetch);
209
- await expect(vtexFetchWithCookies("/api/checkout/pub/orderForm")).resolves.toBeDefined();
210
- });
211
-
212
- // Regression for Hole B: when init.headers is a Headers instance,
213
- // the previous Record-cast + spread collapsed it to `{}`, wiping
214
- // every other header (auth, content-type) the caller set. After
215
- // the fix, the Headers-aware merge preserves all of them.
216
- it("preserves other caller headers when init.headers is a Headers instance", async () => {
217
- setVtexFetch(((_url: string, init?: RequestInit) => {
218
- lastInit = init;
219
- return Promise.resolve(mockResponse());
220
- }) as typeof fetch);
221
-
222
- await withRequest("vtex_segment=abc; foo=bar", async () => {
223
- await vtexFetchWithCookies("/api/checkout/pub/orderForm", {
224
- headers: new Headers({
225
- "X-Custom-Trace": "trace-id",
226
- "X-VTEX-Operation": "test-op",
227
- }),
228
- });
229
- });
230
-
231
- expect(headerValue(lastInit, "x-custom-trace")).toBe("trace-id");
232
- expect(headerValue(lastInit, "x-vtex-operation")).toBe("test-op");
233
- // Caller didn't pass a Cookie — auto-injection picks up the
234
- // request's cookie and forwards it without dropping the
235
- // other headers.
236
- expect(headerValue(lastInit, "cookie")).toBeDefined();
237
- });
238
-
239
- it("preserves an existing Cookie header passed via Headers and sanitises it in place", async () => {
240
- setVtexFetch(((_url: string, init?: RequestInit) => {
241
- lastInit = init;
242
- return Promise.resolve(mockResponse());
243
- }) as typeof fetch);
244
-
245
- await withRequest("vtex_segment=abc", async () => {
246
- await vtexFetchWithCookies("/api/checkout/pub/orderForm", {
247
- headers: new Headers({
248
- "X-Custom": "keep-me",
249
- cookie: "checkout.vtex.com=__ofid=xyz; vtex_segment=mine",
250
- }),
251
- });
252
- });
253
-
254
- expect(headerValue(lastInit, "x-custom")).toBe("keep-me");
255
- expect(headerValue(lastInit, "cookie")).toContain("checkout.vtex.com=__ofid=xyz");
256
- });
257
- });
@@ -1,71 +0,0 @@
1
- /**
2
- * End-to-end guard for the acceptance criteria of "real props schemas in
3
- * /deco/meta": after a site wires VTEX the standard way
4
- * (createVtexCommerceLoaders → registerCommerceLoaders), the composed meta
5
- * must publish the real props of the app's loaders/actions — enums included —
6
- * not the `__resolveType`-only stubs.
7
- */
8
-
9
- import { registerCommerceLoaders } from "@decocms/blocks/cms";
10
- import { composeMeta, type MetaResponse } from "@decocms/blocks/cms/client";
11
- import { describe, expect, it } from "vitest";
12
- import { createVtexCommerceLoaders } from "../commerceLoaders";
13
- import { registerVtexSchemas } from "../schemas";
14
-
15
- const b64 = (s: string) => Buffer.from(s).toString("base64");
16
-
17
- const emptySiteMeta = (): MetaResponse => ({
18
- major: 1,
19
- version: "1.0.0",
20
- namespace: "site",
21
- site: "test",
22
- manifest: { blocks: {} },
23
- schema: { definitions: {}, root: {} },
24
- });
25
-
26
- describe("vtex schemas in the composed meta", () => {
27
- // Mirrors a real site setup: build the loader map (this also registers the
28
- // real schemas), then register it (which auto-registers the stubs).
29
- registerCommerceLoaders(createVtexCommerceLoaders());
30
- registerVtexSchemas();
31
- const meta = composeMeta(emptySiteMeta());
32
- const def = (key: string) => meta.schema.definitions[b64(key)];
33
-
34
- it("publishes real props for the intelligent-search PDP loader (both key forms)", () => {
35
- for (const key of [
36
- "vtex/loaders/intelligentSearch/productDetailsPage.ts",
37
- "vtex/loaders/intelligentSearch/productDetailsPage",
38
- ]) {
39
- const d = def(key);
40
- expect(d, key).toBeDefined();
41
- expect(d.properties.slug, key).toMatchObject({ type: "string" });
42
- expect(d.properties.__resolveType.enum, key).toEqual([key]);
43
- expect(d.additionalProperties, key).toBeUndefined();
44
- }
45
- });
46
-
47
- it("publishes the legacy PLP loader with its sort enum preserved", () => {
48
- const d = def("vtex/loaders/legacy/productListingPage.ts");
49
- expect(d).toBeDefined();
50
- expect(d.properties.sort.enum).toContain("OrderByPriceDESC");
51
- expect(d.properties.sort.enum).toContain("OrderByPriceASC");
52
- expect(d.properties.count).toMatchObject({ type: "number" });
53
- expect(d.properties.fq).toMatchObject({ type: "string" });
54
- });
55
-
56
- it("publishes real props for a checkout action", () => {
57
- const d = def("vtex/actions/checkout/addItemsToCart.ts");
58
- expect(d).toBeDefined();
59
- expect(d.properties.orderItems).toMatchObject({ type: "array" });
60
- expect(d.additionalProperties).toBeUndefined();
61
- });
62
-
63
- it("keeps the JSON-editor stub shape for keys without a generated schema", () => {
64
- // Registered by createVtexCommerceLoaders but owned by another namespace —
65
- // no schemas.gen entry, so the stub (additionalProperties: true) survives.
66
- const d = def("commerce/loaders/navbar.ts");
67
- expect(d).toBeDefined();
68
- expect(d.additionalProperties).toBe(true);
69
- expect(Object.keys(d.properties)).toEqual(["__resolveType"]);
70
- });
71
- });