@decocms/apps-vtex 8.1.0-next.0 → 8.1.0-next.2

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 (163) hide show
  1. package/dist/index.d.ts +6 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +6 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/utils/constants.d.ts +28 -0
  6. package/dist/utils/constants.d.ts.map +1 -0
  7. package/dist/utils/constants.js +17 -0
  8. package/dist/utils/constants.js.map +1 -0
  9. package/dist/utils/cookieSanitizer.d.ts +87 -0
  10. package/dist/utils/cookieSanitizer.d.ts.map +1 -0
  11. package/dist/utils/cookieSanitizer.js +136 -0
  12. package/dist/utils/cookieSanitizer.js.map +1 -0
  13. package/dist/utils/operationRouter.d.ts +25 -0
  14. package/dist/utils/operationRouter.d.ts.map +1 -0
  15. package/dist/utils/operationRouter.js +120 -0
  16. package/dist/utils/operationRouter.js.map +1 -0
  17. package/dist/utils/types.d.ts +944 -0
  18. package/dist/utils/types.d.ts.map +1 -0
  19. package/dist/utils/types.js +25 -0
  20. package/dist/utils/types.js.map +1 -0
  21. package/dist/vtexClient.d.ts +173 -0
  22. package/dist/vtexClient.d.ts.map +1 -0
  23. package/dist/vtexClient.js +242 -0
  24. package/dist/vtexClient.js.map +1 -0
  25. package/package.json +16 -57
  26. package/src/index.ts +11 -29
  27. package/src/utils/constants.ts +2 -56
  28. package/src/utils/cookieSanitizer.ts +59 -59
  29. package/src/utils/operationRouter.ts +75 -65
  30. package/src/utils/types.ts +709 -1557
  31. package/src/vtexClient.ts +358 -347
  32. package/src/README.md +0 -6
  33. package/src/__conformance__/upstream.test.ts +0 -158
  34. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  35. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  36. package/src/__tests__/schemas.test.ts +0 -71
  37. package/src/actions/address.ts +0 -250
  38. package/src/actions/analytics/sendEvent.ts +0 -86
  39. package/src/actions/auth.ts +0 -333
  40. package/src/actions/checkout.ts +0 -659
  41. package/src/actions/index.ts +0 -11
  42. package/src/actions/masterData.ts +0 -168
  43. package/src/actions/misc.ts +0 -188
  44. package/src/actions/newsletter.ts +0 -105
  45. package/src/actions/orders.ts +0 -34
  46. package/src/actions/profile.ts +0 -201
  47. package/src/actions/session.ts +0 -85
  48. package/src/actions/trigger.ts +0 -43
  49. package/src/actions/wishlist.ts +0 -114
  50. package/src/client.ts +0 -724
  51. package/src/commerceLoaders.ts +0 -267
  52. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  53. package/src/hooks/__tests__/createCart.test.ts +0 -92
  54. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  55. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  56. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  57. package/src/hooks/cartQuery.ts +0 -204
  58. package/src/hooks/createCart.ts +0 -427
  59. package/src/hooks/createUseCart.ts +0 -360
  60. package/src/hooks/createUseUser.ts +0 -153
  61. package/src/hooks/createUseWishlist.ts +0 -242
  62. package/src/hooks/index.ts +0 -25
  63. package/src/hooks/useAutocomplete.ts +0 -83
  64. package/src/hooks/useCart.ts +0 -243
  65. package/src/hooks/useUser.ts +0 -78
  66. package/src/hooks/useWishlist.ts +0 -119
  67. package/src/invoke.ts +0 -235
  68. package/src/loaders/ProductDetailsPage.ts +0 -1
  69. package/src/loaders/ProductList.ts +0 -1
  70. package/src/loaders/ProductListingPage.ts +0 -1
  71. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  72. package/src/loaders/address.ts +0 -115
  73. package/src/loaders/autocomplete.ts +0 -56
  74. package/src/loaders/brands.ts +0 -44
  75. package/src/loaders/cart/attachments.ts +0 -53
  76. package/src/loaders/cart/full.ts +0 -67
  77. package/src/loaders/cart/gifts.ts +0 -43
  78. package/src/loaders/cart/orderFormId.ts +0 -31
  79. package/src/loaders/cart/shipping.test.ts +0 -125
  80. package/src/loaders/cart/shipping.ts +0 -136
  81. package/src/loaders/cart/summary.ts +0 -36
  82. package/src/loaders/cart.ts +0 -52
  83. package/src/loaders/catalog.ts +0 -163
  84. package/src/loaders/collections.ts +0 -55
  85. package/src/loaders/index.ts +0 -20
  86. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  87. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  88. package/src/loaders/intelligentSearch/productList.ts +0 -170
  89. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  90. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  91. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  92. package/src/loaders/legacy/productList.ts +0 -1
  93. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  94. package/src/loaders/legacy.ts +0 -710
  95. package/src/loaders/logistics.ts +0 -111
  96. package/src/loaders/minicart.ts +0 -78
  97. package/src/loaders/navbar.ts +0 -26
  98. package/src/loaders/orders.ts +0 -92
  99. package/src/loaders/pageType.ts +0 -68
  100. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  101. package/src/loaders/payment.ts +0 -97
  102. package/src/loaders/productListFull.ts +0 -157
  103. package/src/loaders/profile.ts +0 -138
  104. package/src/loaders/promotion.ts +0 -30
  105. package/src/loaders/search.ts +0 -124
  106. package/src/loaders/session.ts +0 -83
  107. package/src/loaders/user.ts +0 -87
  108. package/src/loaders/wishlist.ts +0 -89
  109. package/src/loaders/wishlistProducts.ts +0 -69
  110. package/src/loaders/workflow/products.ts +0 -62
  111. package/src/loaders/workflow.ts +0 -316
  112. package/src/logo.png +0 -0
  113. package/src/manifest.gen.ts +0 -89
  114. package/src/middleware.cacheHeaders.test.ts +0 -44
  115. package/src/middleware.ts +0 -240
  116. package/src/mod.ts +0 -173
  117. package/src/registry.ts +0 -9
  118. package/src/schemas.gen.ts +0 -3191
  119. package/src/schemas.ts +0 -30
  120. package/src/types.ts +0 -248
  121. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  122. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  123. package/src/utils/__tests__/fetch.test.ts +0 -80
  124. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  125. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  126. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  127. package/src/utils/__tests__/minicart.test.ts +0 -184
  128. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  129. package/src/utils/__tests__/resilience.test.ts +0 -205
  130. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  131. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  132. package/src/utils/__tests__/sitemap.test.ts +0 -185
  133. package/src/utils/__tests__/slugify.test.ts +0 -31
  134. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  135. package/src/utils/__tests__/transform.test.ts +0 -1111
  136. package/src/utils/accountLoaders.ts +0 -203
  137. package/src/utils/authHelpers.ts +0 -85
  138. package/src/utils/batch.ts +0 -18
  139. package/src/utils/buildOfferShelf.test.ts +0 -59
  140. package/src/utils/cartProjection.ts +0 -96
  141. package/src/utils/cookies.ts +0 -167
  142. package/src/utils/enrichment.ts +0 -556
  143. package/src/utils/fetch.ts +0 -111
  144. package/src/utils/fetchCache.ts +0 -55
  145. package/src/utils/index.ts +0 -19
  146. package/src/utils/instrumentedFetch.ts +0 -98
  147. package/src/utils/intelligentSearch.ts +0 -96
  148. package/src/utils/legacy.ts +0 -139
  149. package/src/utils/minicart.ts +0 -139
  150. package/src/utils/pickAndOmit.ts +0 -25
  151. package/src/utils/proxy.ts +0 -495
  152. package/src/utils/resilience.ts +0 -352
  153. package/src/utils/resourceRange.ts +0 -10
  154. package/src/utils/segment.ts +0 -152
  155. package/src/utils/similars.ts +0 -35
  156. package/src/utils/simulationCache.ts +0 -73
  157. package/src/utils/sitemap.ts +0 -283
  158. package/src/utils/slugCache.ts +0 -28
  159. package/src/utils/slugify.ts +0 -13
  160. package/src/utils/transform.ts +0 -1691
  161. package/src/utils/vtexId.ts +0 -151
  162. package/src/vtexClient.test.ts +0 -204
  163. package/tsconfig.json +0 -7
package/src/README.md DELETED
@@ -1,6 +0,0 @@
1
- VTEX is a cloud-based e-commerce platform and digital commerce company that provides a comprehensive set of tools and services for businesses looking to establish and manage their online retail operations.
2
-
3
- This app wrapps VTEX API into a comprehensive set of loaders/actions/workflows
4
- empowering non technical users to interact and act upon their headless commerce.
5
-
6
- If you want to use a custom search engine (Algolia, Typesense etc), you will need to fill the App Key & App Token properties. For these, follow this guide
@@ -1,158 +0,0 @@
1
- // @vitest-environment node
2
- /**
3
- * Conformance: upstream-clients.mdx (Calling APIs) against the next-major
4
- * client packages. Only the v8 client modules are checked; the v7 app
5
- * surfaces that still ship beside them are out of scope until v7 is removed.
6
- */
7
- import fs from "node:fs";
8
- import path from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- import { createAlgoliaClient } from "@decocms/apps-algolia";
11
- import { createVtexClient } from "@decocms/apps-vtex";
12
- import { describe, expect, it, vi } from "vitest";
13
-
14
- const PACKAGES = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
15
-
16
- /** The next-major client module of each platform package. */
17
- const V8_CLIENTS: Record<string, string[]> = {
18
- "apps-vtex": ["src/vtexClient.ts"],
19
- "apps-shopify": ["src/v8/client.ts", "src/v8/graphqlOperationName.ts"],
20
- "apps-wake": ["src/wakeClient.ts"],
21
- "apps-magento": ["src/magentoClient.ts"],
22
- "apps-algolia": ["src/index.ts"],
23
- "apps-resend": ["src/emails.ts"],
24
- "apps-sfmc-personalization": ["src/index.ts"],
25
- };
26
-
27
- /** The source without comments, so doc examples (`process.env.X!` in a JSDoc) don't count. */
28
- function code(pkg: string, file: string): string {
29
- return fs
30
- .readFileSync(path.join(PACKAGES, pkg, file), "utf8")
31
- .replace(/\/\*[\s\S]*?\*\//g, "")
32
- .replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
33
- }
34
-
35
- describe("what a client is (upstream-clients.mdx)", () => {
36
- it("up-01: VTEX, Shopify, Wake, Magento, Algolia, Resend (and others) ship a client built on createInstrumentedFetch", () => {
37
- for (const [pkg, files] of Object.entries(V8_CLIENTS)) {
38
- expect(fs.existsSync(path.join(PACKAGES, pkg, "package.json"))).toBe(true);
39
- expect([pkg, code(pkg, files[0]!)]).toEqual([
40
- pkg,
41
- expect.stringMatching(/import \{[^}]*createInstrumentedFetch[^}]*\} from "@decocms\/blocks\/fetch"/),
42
- ]);
43
- }
44
- });
45
-
46
- it("up-02: clients take settings as arguments: no environment reads inside a client", () => {
47
- for (const [pkg, files] of Object.entries(V8_CLIENTS)) {
48
- for (const file of files) {
49
- expect([pkg, file, /process\.env|import\.meta\.env|Deno\.env/.test(code(pkg, file))]).toEqual(
50
- [pkg, file, false],
51
- );
52
- }
53
- }
54
- });
55
-
56
- it("up-03: no React hooks, client components or shared-commerce converters in a client", () => {
57
- for (const [pkg, files] of Object.entries(V8_CLIENTS)) {
58
- for (const file of files) {
59
- const text = code(pkg, file);
60
- const found = [
61
- /["']use client["']/,
62
- /from ["']react["']/,
63
- /\buse(Cart|User|Wishlist)\b/,
64
- /@decocms\/apps-commerce/,
65
- /from ["']\.\/(loaders|actions|hooks)\//,
66
- ].filter((pattern) => pattern.test(text));
67
- expect([pkg, file, found]).toEqual([pkg, file, []]);
68
- }
69
- }
70
- });
71
-
72
- it("up-05: the Salesforce client is @decocms/apps-sfmc-personalization; @decocms/apps-salesforce is gone", () => {
73
- const pkg = JSON.parse(
74
- fs.readFileSync(path.join(PACKAGES, "apps-sfmc-personalization/package.json"), "utf8"),
75
- );
76
- expect(pkg.name).toBe("@decocms/apps-sfmc-personalization");
77
- const names = fs
78
- .readdirSync(PACKAGES)
79
- .filter((dir) => fs.existsSync(path.join(PACKAGES, dir, "package.json")))
80
- .map((dir) => JSON.parse(fs.readFileSync(path.join(PACKAGES, dir, "package.json"), "utf8")).name);
81
- expect(names).not.toContain("@decocms/apps-salesforce");
82
- });
83
-
84
- it("up-07: clients don't cache upstream responses", () => {
85
- for (const [pkg, files] of Object.entries(V8_CLIENTS)) {
86
- for (const file of files) {
87
- expect([
88
- pkg,
89
- file,
90
- /createFetchCache|fetchWithCache|fetchCache|caches\.default|caches\.open|cachedLoader/.test(
91
- code(pkg, file),
92
- ),
93
- ]).toEqual([pkg, file, false]);
94
- }
95
- }
96
- });
97
- });
98
-
99
- describe("call a client (upstream-clients.mdx)", () => {
100
- it("up-06: createVtexClient({ account, appKey, appToken }) and vtex.search.products({ query, count })", async () => {
101
- const fetch = vi.fn(
102
- async (_input: string | URL | Request, _init?: RequestInit) =>
103
- new Response(JSON.stringify({ products: [] }), { status: 200 }),
104
- );
105
- const vtex = createVtexClient({
106
- account: "mystore",
107
- appKey: "key",
108
- appToken: "token",
109
- fetch: fetch as unknown as typeof globalThis.fetch,
110
- });
111
- const products = await vtex.search.products({ query: "linen shirt", count: 12 });
112
- expect(products).toEqual({ products: [] });
113
- const url = new URL(String(fetch.mock.calls[0]![0]));
114
- expect(url.host).toBe("mystore.vtexcommercestable.com.br");
115
- expect(url.pathname).toContain("product_search");
116
- expect(url.searchParams.get("query")).toBe("linen shirt");
117
- expect(url.searchParams.get("count")).toBe("12");
118
- });
119
- });
120
-
121
- describe("retries and failures (upstream-clients.mdx)", () => {
122
- it("up-08: the VTEX client retries by default", async () => {
123
- const statuses = [503, 200];
124
- const fetch = vi.fn(
125
- async () => new Response("{}", { status: statuses.shift() ?? 200 }),
126
- );
127
- const vtex = createVtexClient({
128
- account: "mystore",
129
- fetch: fetch as unknown as typeof globalThis.fetch,
130
- });
131
- await vtex.search.products({ query: "shirt" });
132
- expect(fetch).toHaveBeenCalledTimes(2);
133
- });
134
-
135
- it("up-08: the VTEX client's circuit breaker fails fast after repeated failures", async () => {
136
- const fetch = vi.fn(async () => new Response("{}", { status: 503 }));
137
- const vtex = createVtexClient({
138
- account: "mystore",
139
- fetch: fetch as unknown as typeof globalThis.fetch,
140
- });
141
- for (let i = 0; i < 5; i++) await expect(vtex.search.products({ query: "shirt" })).rejects.toThrow();
142
- const before = fetch.mock.calls.length;
143
- await expect(vtex.search.products({ query: "shirt" })).rejects.toThrow();
144
- expect(fetch.mock.calls.length).toBe(before);
145
- }, 20_000);
146
-
147
- it("up-08: other clients leave retries off (one attempt on a 503)", async () => {
148
- const fetch = vi.fn(async () => new Response("{}", { status: 503 }));
149
- const algolia = createAlgoliaClient(
150
- { applicationId: "APP123", apiKey: "k" },
151
- { fetch: fetch as unknown as typeof globalThis.fetch },
152
- );
153
- await expect(algolia.search([{ indexName: "products", query: "shirt" }] as never)).rejects.toThrow(
154
- "algolia search failed with HTTP 503",
155
- );
156
- expect(fetch).toHaveBeenCalledTimes(1);
157
- });
158
- });
@@ -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.acme.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\.acme\.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.acme.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\.acme\.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
- });