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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/package.json +7 -52
  2. package/src/index.ts +10 -28
  3. package/src/utils/constants.ts +2 -56
  4. package/src/utils/cookieSanitizer.ts +59 -59
  5. package/src/utils/operationRouter.ts +75 -65
  6. package/src/utils/types.ts +709 -1557
  7. package/src/vtexClient.ts +354 -343
  8. package/src/README.md +0 -6
  9. package/src/__conformance__/upstream.test.ts +0 -158
  10. package/src/__tests__/client-segment-cookie.test.ts +0 -255
  11. package/src/__tests__/client-set-cookie-forward.test.ts +0 -257
  12. package/src/__tests__/schemas.test.ts +0 -71
  13. package/src/actions/address.ts +0 -250
  14. package/src/actions/analytics/sendEvent.ts +0 -86
  15. package/src/actions/auth.ts +0 -333
  16. package/src/actions/checkout.ts +0 -659
  17. package/src/actions/index.ts +0 -11
  18. package/src/actions/masterData.ts +0 -168
  19. package/src/actions/misc.ts +0 -188
  20. package/src/actions/newsletter.ts +0 -105
  21. package/src/actions/orders.ts +0 -34
  22. package/src/actions/profile.ts +0 -201
  23. package/src/actions/session.ts +0 -85
  24. package/src/actions/trigger.ts +0 -43
  25. package/src/actions/wishlist.ts +0 -114
  26. package/src/client.ts +0 -724
  27. package/src/commerceLoaders.ts +0 -267
  28. package/src/hooks/__tests__/createCart.render.test.ts +0 -114
  29. package/src/hooks/__tests__/createCart.test.ts +0 -92
  30. package/src/hooks/__tests__/createUseCart.test.ts +0 -184
  31. package/src/hooks/__tests__/createUseUser.test.ts +0 -48
  32. package/src/hooks/__tests__/createUseWishlist.test.ts +0 -87
  33. package/src/hooks/cartQuery.ts +0 -204
  34. package/src/hooks/createCart.ts +0 -427
  35. package/src/hooks/createUseCart.ts +0 -360
  36. package/src/hooks/createUseUser.ts +0 -153
  37. package/src/hooks/createUseWishlist.ts +0 -242
  38. package/src/hooks/index.ts +0 -25
  39. package/src/hooks/useAutocomplete.ts +0 -83
  40. package/src/hooks/useCart.ts +0 -243
  41. package/src/hooks/useUser.ts +0 -78
  42. package/src/hooks/useWishlist.ts +0 -119
  43. package/src/invoke.ts +0 -235
  44. package/src/loaders/ProductDetailsPage.ts +0 -1
  45. package/src/loaders/ProductList.ts +0 -1
  46. package/src/loaders/ProductListingPage.ts +0 -1
  47. package/src/loaders/__tests__/legacyProductList.test.ts +0 -157
  48. package/src/loaders/address.ts +0 -115
  49. package/src/loaders/autocomplete.ts +0 -56
  50. package/src/loaders/brands.ts +0 -44
  51. package/src/loaders/cart/attachments.ts +0 -53
  52. package/src/loaders/cart/full.ts +0 -67
  53. package/src/loaders/cart/gifts.ts +0 -43
  54. package/src/loaders/cart/orderFormId.ts +0 -31
  55. package/src/loaders/cart/shipping.test.ts +0 -125
  56. package/src/loaders/cart/shipping.ts +0 -136
  57. package/src/loaders/cart/summary.ts +0 -36
  58. package/src/loaders/cart.ts +0 -52
  59. package/src/loaders/catalog.ts +0 -163
  60. package/src/loaders/collections.ts +0 -55
  61. package/src/loaders/index.ts +0 -20
  62. package/src/loaders/intelligentSearch/__tests__/productListingPage.test.ts +0 -51
  63. package/src/loaders/intelligentSearch/productDetailsPage.ts +0 -111
  64. package/src/loaders/intelligentSearch/productList.ts +0 -170
  65. package/src/loaders/intelligentSearch/productListingPage.ts +0 -525
  66. package/src/loaders/intelligentSearch/suggestions.ts +0 -44
  67. package/src/loaders/legacy/productDetailsPage.ts +0 -1
  68. package/src/loaders/legacy/productList.ts +0 -1
  69. package/src/loaders/legacy/relatedProductsLoader.ts +0 -79
  70. package/src/loaders/legacy.ts +0 -710
  71. package/src/loaders/logistics.ts +0 -111
  72. package/src/loaders/minicart.ts +0 -78
  73. package/src/loaders/navbar.ts +0 -26
  74. package/src/loaders/orders.ts +0 -92
  75. package/src/loaders/pageType.ts +0 -68
  76. package/src/loaders/paths/PDPDefaultPath.ts +0 -51
  77. package/src/loaders/payment.ts +0 -97
  78. package/src/loaders/productListFull.ts +0 -157
  79. package/src/loaders/profile.ts +0 -138
  80. package/src/loaders/promotion.ts +0 -30
  81. package/src/loaders/search.ts +0 -124
  82. package/src/loaders/session.ts +0 -83
  83. package/src/loaders/user.ts +0 -87
  84. package/src/loaders/wishlist.ts +0 -89
  85. package/src/loaders/wishlistProducts.ts +0 -69
  86. package/src/loaders/workflow/products.ts +0 -62
  87. package/src/loaders/workflow.ts +0 -316
  88. package/src/logo.png +0 -0
  89. package/src/manifest.gen.ts +0 -89
  90. package/src/middleware.cacheHeaders.test.ts +0 -44
  91. package/src/middleware.ts +0 -240
  92. package/src/mod.ts +0 -173
  93. package/src/registry.ts +0 -9
  94. package/src/schemas.gen.ts +0 -3191
  95. package/src/schemas.ts +0 -30
  96. package/src/types.ts +0 -248
  97. package/src/utils/__tests__/cartProjection.test.ts +0 -118
  98. package/src/utils/__tests__/cookieSanitizer.test.ts +0 -162
  99. package/src/utils/__tests__/fetch.test.ts +0 -80
  100. package/src/utils/__tests__/fetchCache.test.ts +0 -155
  101. package/src/utils/__tests__/instrumentedFetch.test.ts +0 -158
  102. package/src/utils/__tests__/intelligentSearch.test.ts +0 -88
  103. package/src/utils/__tests__/minicart.test.ts +0 -184
  104. package/src/utils/__tests__/operationRouter.test.ts +0 -227
  105. package/src/utils/__tests__/resilience.test.ts +0 -205
  106. package/src/utils/__tests__/resourceRange.test.ts +0 -32
  107. package/src/utils/__tests__/simulationCache.test.ts +0 -40
  108. package/src/utils/__tests__/sitemap.test.ts +0 -185
  109. package/src/utils/__tests__/slugify.test.ts +0 -31
  110. package/src/utils/__tests__/storefrontBaseUrl.test.ts +0 -61
  111. package/src/utils/__tests__/transform.test.ts +0 -1111
  112. package/src/utils/accountLoaders.ts +0 -203
  113. package/src/utils/authHelpers.ts +0 -85
  114. package/src/utils/batch.ts +0 -18
  115. package/src/utils/buildOfferShelf.test.ts +0 -59
  116. package/src/utils/cartProjection.ts +0 -96
  117. package/src/utils/cookies.ts +0 -167
  118. package/src/utils/enrichment.ts +0 -556
  119. package/src/utils/fetch.ts +0 -111
  120. package/src/utils/fetchCache.ts +0 -55
  121. package/src/utils/index.ts +0 -19
  122. package/src/utils/instrumentedFetch.ts +0 -98
  123. package/src/utils/intelligentSearch.ts +0 -96
  124. package/src/utils/legacy.ts +0 -139
  125. package/src/utils/minicart.ts +0 -139
  126. package/src/utils/pickAndOmit.ts +0 -25
  127. package/src/utils/proxy.ts +0 -495
  128. package/src/utils/resilience.ts +0 -352
  129. package/src/utils/resourceRange.ts +0 -10
  130. package/src/utils/segment.ts +0 -152
  131. package/src/utils/similars.ts +0 -35
  132. package/src/utils/simulationCache.ts +0 -73
  133. package/src/utils/sitemap.ts +0 -283
  134. package/src/utils/slugCache.ts +0 -28
  135. package/src/utils/slugify.ts +0 -13
  136. package/src/utils/transform.ts +0 -1691
  137. package/src/utils/vtexId.ts +0 -151
  138. package/src/vtexClient.test.ts +0 -204
  139. package/tsconfig.json +0 -7
@@ -1,227 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { vtexOperationRouter } from "../operationRouter";
3
-
4
- describe("vtexOperationRouter", () => {
5
- const acct = "https://store.vtexcommercestable.com.br";
6
-
7
- describe("Intelligent Search", () => {
8
- it("captures the IS endpoint as the operation suffix", () => {
9
- expect(
10
- vtexOperationRouter(
11
- `${acct}/api/io/_v/api/intelligent-search/product_search/electronics`,
12
- "GET",
13
- ),
14
- ).toBe("intelligent-search.product_search");
15
- expect(
16
- vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/top_searches`, "GET"),
17
- ).toBe("intelligent-search.top_searches");
18
- expect(vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/facets`, "GET")).toBe(
19
- "intelligent-search.facets",
20
- );
21
- expect(
22
- vtexOperationRouter(`${acct}/api/io/_v/api/intelligent-search/search_suggestions`, "GET"),
23
- ).toBe("intelligent-search.search_suggestions");
24
- });
25
-
26
- it("strips query string before matching", () => {
27
- expect(
28
- vtexOperationRouter(
29
- `${acct}/api/io/_v/api/intelligent-search/product_search?query=foo&sort=price`,
30
- "GET",
31
- ),
32
- ).toBe("intelligent-search.product_search");
33
- });
34
- });
35
-
36
- describe("Checkout / orderForm", () => {
37
- it("differentiates create vs get on orderForm", () => {
38
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm`, "POST")).toBe(
39
- "checkout.orderform.create",
40
- );
41
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm`, "GET")).toBe(
42
- "checkout.orderform.get",
43
- );
44
- });
45
-
46
- it("differentiates items add/update/remove by HTTP method", () => {
47
- const url = `${acct}/api/checkout/pub/orderForm/abc123/items`;
48
- expect(vtexOperationRouter(url, "POST")).toBe("checkout.orderform.items.add");
49
- expect(vtexOperationRouter(url, "PATCH")).toBe("checkout.orderform.items.update");
50
- expect(vtexOperationRouter(url, "PUT")).toBe("checkout.orderform.items.update");
51
- expect(vtexOperationRouter(url, "DELETE")).toBe("checkout.orderform.items.remove");
52
- });
53
-
54
- it("recognizes the /items/update legacy mass-update endpoint", () => {
55
- expect(
56
- vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc123/items/update`, "POST"),
57
- ).toBe("checkout.orderform.items.update");
58
- });
59
-
60
- it("handles coupons, profile, shippingData, paymentData", () => {
61
- const id = "abc123";
62
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/coupons`, "POST")).toBe(
63
- "checkout.orderform.coupons",
64
- );
65
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/profile`, "POST")).toBe(
66
- "checkout.orderform.profile",
67
- );
68
- expect(
69
- vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/shippingData/...`, "POST"),
70
- ).toBe("checkout.orderform.shipping");
71
- expect(
72
- vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/${id}/paymentData/...`, "POST"),
73
- ).toBe("checkout.orderform.payment");
74
- });
75
-
76
- it("matches the singleton orderForm/{id} root with the right method", () => {
77
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc`, "GET")).toBe(
78
- "checkout.orderform.get",
79
- );
80
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForm/abc`, "PATCH")).toBe(
81
- "checkout.orderform.update",
82
- );
83
- });
84
-
85
- it("maps simulation, regions, postal-code", () => {
86
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/orderForms/simulation`, "POST")).toBe(
87
- "checkout.simulation",
88
- );
89
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/regions`, "GET")).toBe(
90
- "checkout.regions",
91
- );
92
- expect(vtexOperationRouter(`${acct}/api/checkout/pub/postal-code/BRA/01310`, "GET")).toBe(
93
- "checkout.postal-code",
94
- );
95
- });
96
- });
97
-
98
- describe("Sessions + segments", () => {
99
- it("differentiates sessions GET vs POST", () => {
100
- expect(vtexOperationRouter(`${acct}/api/sessions`, "GET")).toBe("sessions.get");
101
- expect(vtexOperationRouter(`${acct}/api/sessions`, "POST")).toBe("sessions.update");
102
- });
103
-
104
- it("matches segments", () => {
105
- expect(vtexOperationRouter(`${acct}/api/segments/abc-123`, "GET")).toBe("segments.get");
106
- });
107
- });
108
-
109
- describe("Catalog System", () => {
110
- it("captures the most-specific catalog endpoints first", () => {
111
- expect(
112
- vtexOperationRouter(`${acct}/api/catalog_system/pub/portal/pagetype/eletronicos`, "GET"),
113
- ).toBe("catalog.pagetype");
114
- expect(
115
- vtexOperationRouter(
116
- `${acct}/api/catalog_system/pub/products/crossselling/whoboughtalsobought/123`,
117
- "GET",
118
- ),
119
- ).toBe("catalog.crossselling.whoboughtalsobought");
120
- expect(
121
- vtexOperationRouter(`${acct}/api/catalog_system/pub/products/variations/123`, "GET"),
122
- ).toBe("catalog.products.variations");
123
- expect(
124
- vtexOperationRouter(`${acct}/api/catalog_system/pub/products/search/?fq=x`, "GET"),
125
- ).toBe("catalog.products.search");
126
- expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/facets/search/x`, "GET")).toBe(
127
- "catalog.facets.search",
128
- );
129
- expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/category/tree/3`, "GET")).toBe(
130
- "catalog.category.tree",
131
- );
132
- expect(
133
- vtexOperationRouter(`${acct}/api/catalog_system/pvt/sku/stockkeepingunitbyid/123`, "GET"),
134
- ).toBe("catalog.sku");
135
- });
136
-
137
- it("falls back to catalog.other for unrecognized catalog paths", () => {
138
- expect(
139
- vtexOperationRouter(`${acct}/api/catalog_system/pvt/specification/groupGet/123`, "GET"),
140
- ).toBe("catalog.specification");
141
- expect(vtexOperationRouter(`${acct}/api/catalog_system/pub/brand/list`, "GET")).toBe(
142
- "catalog.brand",
143
- );
144
- });
145
- });
146
-
147
- describe("Masterdata", () => {
148
- it("encodes the entity name as the operation suffix", () => {
149
- expect(vtexOperationRouter(`${acct}/api/dataentities/AD/search`, "GET")).toBe(
150
- "masterdata.AD",
151
- );
152
- expect(vtexOperationRouter(`${acct}/api/dataentities/wishlist_lists/documents`, "POST")).toBe(
153
- "masterdata.wishlist_lists",
154
- );
155
- });
156
- });
157
-
158
- describe("OMS", () => {
159
- it("differentiates orders list vs cancel", () => {
160
- expect(vtexOperationRouter(`${acct}/api/oms/user/orders`, "GET")).toBe("oms.orders");
161
- expect(vtexOperationRouter(`${acct}/api/oms/user/orders/v999-01/cancel`, "POST")).toBe(
162
- "oms.orders.cancel",
163
- );
164
- expect(vtexOperationRouter(`${acct}/api/oms/pvt/orders/v999-01`, "GET")).toBe(
165
- "oms.orders.pvt",
166
- );
167
- });
168
- });
169
-
170
- describe("VTEX ID", () => {
171
- it("maps the auth surface", () => {
172
- expect(vtexOperationRouter(`${acct}/api/vtexid/pub/logout?scope=x`, "GET")).toBe(
173
- "vtexid.logout",
174
- );
175
- expect(vtexOperationRouter(`${acct}/api/vtexid/pub/authentication/start`, "GET")).toBe(
176
- "vtexid.authentication.start",
177
- );
178
- expect(
179
- vtexOperationRouter(`${acct}/api/vtexid/pub/authentication/classic/validate`, "POST"),
180
- ).toBe("vtexid.authentication.validate");
181
- expect(vtexOperationRouter(`${acct}/api/vtexid/pub/authenticated/user`, "GET")).toBe(
182
- "vtexid.user",
183
- );
184
- });
185
-
186
- it("falls back to vtexid.other for unmapped vtexid paths", () => {
187
- expect(vtexOperationRouter(`${acct}/api/vtexid/pub/refreshtoken`, "POST")).toBe(
188
- "vtexid.other",
189
- );
190
- });
191
- });
192
-
193
- describe("VTEX IO + GraphQL", () => {
194
- it("matches the IO private graphql endpoint", () => {
195
- expect(vtexOperationRouter(`https://store.myvtex.com/_v/private/graphql/v1`, "POST")).toBe(
196
- "io.graphql",
197
- );
198
- });
199
-
200
- it("matches the IO segment endpoint", () => {
201
- expect(
202
- vtexOperationRouter(`https://store.myvtex.com/_v/segment/admin-pvt/whatever`, "GET"),
203
- ).toBe("io.segment");
204
- });
205
- });
206
-
207
- describe("Edge cases", () => {
208
- it("returns undefined for fully unrecognized URLs", () => {
209
- expect(vtexOperationRouter(`${acct}/somethingelse/random`, "GET")).toBeUndefined();
210
- });
211
-
212
- it("does not throw on unparseable URLs and still tries to match", () => {
213
- expect(vtexOperationRouter("not-a-real-url", "GET")).toBeUndefined();
214
- expect(vtexOperationRouter("/api/sessions?x=1", "GET")).toBe("sessions.get");
215
- });
216
-
217
- it("is case-insensitive on the method", () => {
218
- expect(vtexOperationRouter(`${acct}/api/sessions`, "post")).toBe("sessions.update");
219
- expect(vtexOperationRouter(`${acct}/api/sessions`, "Get")).toBe("sessions.get");
220
- });
221
-
222
- it("recognizes sitemap.xml + sitemap-products-0.xml", () => {
223
- expect(vtexOperationRouter(`${acct}/sitemap.xml`, "GET")).toBe("sitemap");
224
- expect(vtexOperationRouter(`${acct}/sitemap-products-0.xml`, "GET")).toBe("sitemap");
225
- });
226
- });
227
- });
@@ -1,205 +0,0 @@
1
- import { RequestContext } from "@decocms/blocks/sdk/requestContext";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import {
4
- createResilientFetch,
5
- isNonRetryableVtexError,
6
- resetResilienceState,
7
- VtexCircuitOpenError,
8
- VtexTimeoutError,
9
- } from "../resilience";
10
-
11
- function res(status: number, body = "ok"): Response {
12
- return {
13
- ok: status >= 200 && status < 300,
14
- status,
15
- statusText: "",
16
- json: async () => body,
17
- } as Response;
18
- }
19
-
20
- beforeEach(() => {
21
- resetResilienceState();
22
- });
23
-
24
- describe("createResilientFetch", () => {
25
- it("passes a healthy GET through untouched, no retry", async () => {
26
- const underlying = vi.fn(async () => res(200, "prod"));
27
- const rf = createResilientFetch(underlying as unknown as typeof fetch);
28
- const r = await rf("https://acct.vtexcommercestable.com.br/ok");
29
- expect(r.status).toBe(200);
30
- expect(underlying).toHaveBeenCalledOnce();
31
- });
32
-
33
- it("never retries a POST (mutation), even on 5xx", async () => {
34
- const underlying = vi.fn(async () => res(500));
35
- const rf = createResilientFetch(underlying as unknown as typeof fetch);
36
- const r = await rf("https://acct.vtexcommercestable.com.br/checkout", {
37
- method: "POST",
38
- body: "{}",
39
- });
40
- expect(r.status).toBe(500);
41
- expect(underlying).toHaveBeenCalledOnce();
42
- });
43
-
44
- it("retries an idempotent GET on a thrown network error, then succeeds", async () => {
45
- let n = 0;
46
- const underlying = vi.fn(async () => {
47
- n++;
48
- if (n < 3) throw new Error("ECONNRESET");
49
- return res(200);
50
- });
51
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
52
- backoffBaseMs: 1,
53
- backoffCapMs: 2,
54
- });
55
- const r = await rf("https://acct.vtexcommercestable.com.br/search");
56
- expect(r.status).toBe(200);
57
- expect(underlying).toHaveBeenCalledTimes(3);
58
- });
59
-
60
- it("opens the circuit after N consecutive failures and then fails fast", async () => {
61
- const underlying = vi.fn(async () => res(503));
62
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
63
- breakerConsecutiveFailures: 5,
64
- });
65
- const host = "https://acct.vtexcommercestable.com.br";
66
- // 5 POSTs (1 attempt each) → 5 consecutive breaker failures → open.
67
- for (let i = 0; i < 5; i++) {
68
- await rf(`${host}/p`, { method: "POST", body: "x" });
69
- }
70
- const before = underlying.mock.calls.length;
71
- await expect(rf(`${host}/p`, { method: "POST", body: "x" })).rejects.toBeInstanceOf(
72
- VtexCircuitOpenError,
73
- );
74
- // Fail-fast must not touch the origin.
75
- expect(underlying.mock.calls.length).toBe(before);
76
- });
77
-
78
- it("half-opens after cooldown and closes on a successful probe", async () => {
79
- vi.useFakeTimers();
80
- try {
81
- let healthy = false;
82
- const underlying = vi.fn(async () => (healthy ? res(200) : res(503)));
83
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
84
- breakerConsecutiveFailures: 3,
85
- breakerOpenCooldownMs: 5_000,
86
- });
87
- const host = "https://acct.vtexcommercestable.com.br";
88
- for (let i = 0; i < 3; i++) await rf(`${host}/p`, { method: "POST", body: "x" });
89
- // Open now → fail fast.
90
- await expect(rf(`${host}/p`, { method: "POST", body: "x" })).rejects.toBeInstanceOf(
91
- VtexCircuitOpenError,
92
- );
93
- // After cooldown, a probe is allowed; make it succeed.
94
- healthy = true;
95
- await vi.advanceTimersByTimeAsync(5_001);
96
- const r = await rf(`${host}/p`, { method: "POST", body: "x" });
97
- expect(r.status).toBe(200);
98
- } finally {
99
- vi.useRealTimers();
100
- }
101
- });
102
-
103
- it("aborts a hung request with the per-attempt timeout", async () => {
104
- vi.useFakeTimers();
105
- try {
106
- const underlying = vi.fn(
107
- (_input: RequestInfo | URL, init?: RequestInit) =>
108
- new Promise<Response>((_resolve, reject) => {
109
- init?.signal?.addEventListener("abort", () =>
110
- reject(Object.assign(new Error("aborted"), { name: "AbortError" })),
111
- );
112
- }),
113
- );
114
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
115
- maxRetries: 0,
116
- perAttemptTimeoutMs: 8_000,
117
- });
118
- const p = rf("https://acct.vtexcommercestable.com.br/hung");
119
- p.catch(() => {});
120
- await vi.advanceTimersByTimeAsync(8_001);
121
- await expect(p).rejects.toBeInstanceOf(VtexTimeoutError);
122
- } finally {
123
- vi.useRealTimers();
124
- }
125
- });
126
-
127
- it("aborts an in-flight VTEX call when the ambient request signal aborts", async () => {
128
- const ac = new AbortController();
129
- const request = new Request("https://store.example/page", { signal: ac.signal });
130
- const underlying = vi.fn(
131
- (_input: RequestInfo | URL, init?: RequestInit) =>
132
- new Promise<Response>((_resolve, reject) => {
133
- init?.signal?.addEventListener("abort", () =>
134
- reject(Object.assign(new Error("aborted"), { name: "AbortError" })),
135
- );
136
- }),
137
- );
138
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
139
- maxRetries: 0,
140
- perAttemptTimeoutMs: 60_000,
141
- });
142
-
143
- await RequestContext.run(request, async () => {
144
- const p = rf("https://acct.vtexcommercestable.com.br/slow");
145
- p.catch(() => {});
146
- ac.abort();
147
- await expect(p).rejects.toThrow();
148
- });
149
- expect(underlying).toHaveBeenCalled();
150
- });
151
-
152
- it("does NOT count an external (caller/ambient) abort as a breaker failure", async () => {
153
- const underlying = vi.fn(
154
- (_input: RequestInfo | URL, init?: RequestInit) =>
155
- new Promise<Response>((_resolve, reject) => {
156
- init?.signal?.addEventListener("abort", () =>
157
- reject(Object.assign(new Error("aborted"), { name: "AbortError" })),
158
- );
159
- }),
160
- );
161
- const host = "https://acct.vtexcommercestable.com.br";
162
- const rf = createResilientFetch(underlying as unknown as typeof fetch, {
163
- maxRetries: 0,
164
- breakerConsecutiveFailures: 3,
165
- });
166
-
167
- // Abort 5 caller requests (> the 3-failure threshold). If these counted as
168
- // breaker failures, the circuit would open against a healthy upstream.
169
- for (let i = 0; i < 5; i++) {
170
- const ac = new AbortController();
171
- const p = rf(`${host}/x`, { signal: ac.signal });
172
- p.catch(() => {});
173
- ac.abort();
174
- await expect(p).rejects.toThrow();
175
- }
176
-
177
- // Breaker must still be CLOSED: a healthy call succeeds (not VtexCircuitOpenError).
178
- const healthy = createResilientFetch((async () => res(200)) as unknown as typeof fetch);
179
- const r = await healthy(`${host}/x`);
180
- expect(r.status).toBe(200);
181
- });
182
-
183
- it("honors the kill-switch env var", async () => {
184
- const prev = process.env.VTEX_RESILIENCE_DISABLED;
185
- process.env.VTEX_RESILIENCE_DISABLED = "true";
186
- try {
187
- const underlying = vi.fn(async () => res(200));
188
- const rf = createResilientFetch(underlying as unknown as typeof fetch);
189
- await rf("https://acct.vtexcommercestable.com.br/x", { method: "POST", body: "y" });
190
- expect(underlying).toHaveBeenCalledOnce();
191
- } finally {
192
- if (prev === undefined) delete process.env.VTEX_RESILIENCE_DISABLED;
193
- else process.env.VTEX_RESILIENCE_DISABLED = prev;
194
- }
195
- });
196
- });
197
-
198
- describe("isNonRetryableVtexError", () => {
199
- it("flags circuit-open and timeout errors, not generic ones", () => {
200
- expect(isNonRetryableVtexError(new VtexCircuitOpenError("h"))).toBe(true);
201
- expect(isNonRetryableVtexError(new VtexTimeoutError("h", 1))).toBe(true);
202
- expect(isNonRetryableVtexError(new Error("boom"))).toBe(false);
203
- expect(isNonRetryableVtexError(null)).toBe(false);
204
- });
205
- });
@@ -1,32 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resourceRange } from "../resourceRange";
3
-
4
- describe("resourceRange", () => {
5
- it("returns from=0 and to=take when skip is 0", () => {
6
- expect(resourceRange(0, 50)).toEqual({ from: 0, to: 50 });
7
- });
8
-
9
- it("caps take at 100", () => {
10
- expect(resourceRange(0, 200)).toEqual({ from: 0, to: 100 });
11
- });
12
-
13
- it("applies skip offset", () => {
14
- expect(resourceRange(10, 50)).toEqual({ from: 10, to: 60 });
15
- });
16
-
17
- it("handles skip + take > 100 by capping take", () => {
18
- expect(resourceRange(50, 200)).toEqual({ from: 50, to: 150 });
19
- });
20
-
21
- it("treats negative skip as 0", () => {
22
- expect(resourceRange(-5, 10)).toEqual({ from: 0, to: 10 });
23
- });
24
-
25
- it("handles zero take", () => {
26
- expect(resourceRange(0, 0)).toEqual({ from: 0, to: 0 });
27
- });
28
-
29
- it("handles take of exactly 100", () => {
30
- expect(resourceRange(0, 100)).toEqual({ from: 0, to: 100 });
31
- });
32
- });
@@ -1,40 +0,0 @@
1
- import { afterEach, describe, expect, it } from "vitest";
2
- import {
3
- __resetSimulationCacheForTests,
4
- getSimulationCache,
5
- setSimulationCache,
6
- } from "../simulationCache";
7
-
8
- describe("simulationCache (#373)", () => {
9
- afterEach(() => {
10
- __resetSimulationCacheForTests();
11
- });
12
-
13
- it("default in-process cache: returns null on miss, then the value after put", async () => {
14
- const cache = getSimulationCache();
15
- expect(await cache.get("k")).toBeNull();
16
- await cache.put("k", "v", 60);
17
- expect(await cache.get("k")).toBe("v");
18
- });
19
-
20
- it("expires an entry past its TTL", async () => {
21
- const cache = getSimulationCache();
22
- await cache.put("k", "v", 0); // already expired
23
- await new Promise((r) => setTimeout(r, 5));
24
- expect(await cache.get("k")).toBeNull();
25
- });
26
-
27
- it("setSimulationCache injects a custom implementation", async () => {
28
- const store = new Map<string, string>();
29
- setSimulationCache({
30
- get: (k) => store.get(k) ?? null,
31
- put: (k, v) => {
32
- store.set(k, v);
33
- },
34
- });
35
- const cache = getSimulationCache();
36
- await cache.put("k", "custom-value", 60);
37
- expect(await cache.get("k")).toBe("custom-value");
38
- expect(store.get("k")).toBe("custom-value");
39
- });
40
- });
@@ -1,185 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { configureVtex } from "../../client";
3
- import { createVtexSitemapProxy, isVtexSitemapPath } from "../sitemap";
4
-
5
- const ACCOUNT = "myaccount";
6
- const VTEX_HOST = `${ACCOUNT}.vtexcommercestable.com.br`;
7
-
8
- beforeEach(() => {
9
- configureVtex({ account: ACCOUNT });
10
- });
11
-
12
- afterEach(() => {
13
- vi.restoreAllMocks();
14
- });
15
-
16
- describe("isVtexSitemapPath", () => {
17
- it.each([
18
- ["/sitemap.xml", true],
19
- ["/sitemap/products-1.xml", true],
20
- ["/sitemap/category-3.xml", true],
21
- ["/sitemap/", true],
22
- ["/sitemap", false],
23
- ["/", false],
24
- ["/checkout", false],
25
- ["/sitemap-busca.xml", false],
26
- ])("%s → %s", (pathname, expected) => {
27
- expect(isVtexSitemapPath(pathname)).toBe(expected);
28
- });
29
- });
30
-
31
- describe("createVtexSitemapProxy", () => {
32
- function makeFetch(
33
- responseBody: string,
34
- init: { status?: number; ok?: boolean } = {},
35
- ): typeof fetch {
36
- const status = init.status ?? 200;
37
- return vi.fn(
38
- async () =>
39
- new Response(responseBody, {
40
- status,
41
- headers: { "content-type": "application/xml" },
42
- }),
43
- ) as unknown as typeof fetch;
44
- }
45
-
46
- const SITEMAP_INDEX = `<?xml version="1.0" encoding="UTF-8"?>
47
- <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
48
- <sitemap><loc>https://${VTEX_HOST}/sitemap/products-1.xml</loc></sitemap>
49
- <sitemap><loc>https://${VTEX_HOST}/sitemap/category-1.xml</loc></sitemap>
50
- </sitemapindex>`;
51
-
52
- const PRODUCT_SUB_SITEMAP = `<?xml version="1.0" encoding="UTF-8"?>
53
- <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
54
- <url><loc>https://${VTEX_HOST}/p/some-product/p</loc></url>
55
- </urlset>`;
56
-
57
- it("returns null for non-sitemap paths", async () => {
58
- const proxy = createVtexSitemapProxy({ fetchImpl: makeFetch("") });
59
- const url = new URL("https://www.mystore.com/checkout");
60
- await expect(proxy(new Request(url), url)).resolves.toBeNull();
61
- });
62
-
63
- it("proxies /sitemap.xml and rewrites VTEX hostname to storefront origin", async () => {
64
- const fetchImpl = makeFetch(SITEMAP_INDEX);
65
- const proxy = createVtexSitemapProxy({ fetchImpl });
66
- const url = new URL("https://www.mystore.com/sitemap.xml");
67
-
68
- const res = await proxy(new Request(url), url);
69
-
70
- expect(res).not.toBeNull();
71
- expect(res!.status).toBe(200);
72
- expect(res!.headers.get("content-type")).toBe("application/xml; charset=utf-8");
73
- expect(fetchImpl).toHaveBeenCalledWith(`https://${VTEX_HOST}/sitemap.xml`);
74
-
75
- const xml = await res!.text();
76
- expect(xml).not.toContain(VTEX_HOST);
77
- expect(xml).toContain("https://www.mystore.com/sitemap/products-1.xml");
78
- expect(xml).toContain("https://www.mystore.com/sitemap/category-1.xml");
79
- });
80
-
81
- it("proxies /sitemap/* sub-sitemaps with hostname rewrite", async () => {
82
- const fetchImpl = makeFetch(PRODUCT_SUB_SITEMAP);
83
- const proxy = createVtexSitemapProxy({ fetchImpl });
84
- const url = new URL("https://www.mystore.com/sitemap/products-1.xml");
85
-
86
- const res = await proxy(new Request(url), url);
87
-
88
- expect(res).not.toBeNull();
89
- expect(fetchImpl).toHaveBeenCalledWith(`https://${VTEX_HOST}/sitemap/products-1.xml`);
90
- const xml = await res!.text();
91
- expect(xml).toContain("https://www.mystore.com/p/some-product/p");
92
- expect(xml).not.toContain(VTEX_HOST);
93
- });
94
-
95
- it("injects extraSitemaps entries into /sitemap.xml only", async () => {
96
- const fetchImpl = makeFetch(SITEMAP_INDEX);
97
- const proxy = createVtexSitemapProxy({
98
- fetchImpl,
99
- extraSitemaps: ["/sitemap-busca.xml", "extra-bare", "https://cdn.example.com/static.xml"],
100
- });
101
- const url = new URL("https://www.mystore.com/sitemap.xml");
102
- const xml = await (await proxy(new Request(url), url))!.text();
103
-
104
- expect(xml).toContain("<loc>https://www.mystore.com/sitemap-busca.xml</loc>");
105
- expect(xml).toContain("<loc>https://www.mystore.com/extra-bare</loc>");
106
- expect(xml).toContain("<loc>https://cdn.example.com/static.xml</loc>");
107
- // Extra entries are inserted before the closing tag (i.e. inside the index).
108
- expect(xml.indexOf("sitemap-busca.xml")).toBeLessThan(xml.indexOf("</sitemapindex>"));
109
- });
110
-
111
- it("does not inject extraSitemaps into sub-sitemaps", async () => {
112
- const fetchImpl = makeFetch(PRODUCT_SUB_SITEMAP);
113
- const proxy = createVtexSitemapProxy({
114
- fetchImpl,
115
- extraSitemaps: ["/sitemap-busca.xml"],
116
- });
117
- const url = new URL("https://www.mystore.com/sitemap/products-1.xml");
118
- const xml = await (await proxy(new Request(url), url))!.text();
119
-
120
- expect(xml).not.toContain("sitemap-busca.xml");
121
- });
122
-
123
- it("returns 502 when VTEX origin returns non-OK", async () => {
124
- const fetchImpl = makeFetch("upstream is down", { status: 503 });
125
- const proxy = createVtexSitemapProxy({ fetchImpl });
126
- const url = new URL("https://www.mystore.com/sitemap.xml");
127
- const res = await proxy(new Request(url), url);
128
- expect(res!.status).toBe(502);
129
- });
130
-
131
- it("returns 502 when fetch throws", async () => {
132
- const fetchImpl = vi.fn(async () => {
133
- throw new Error("network down");
134
- }) as unknown as typeof fetch;
135
- const proxy = createVtexSitemapProxy({ fetchImpl });
136
- const url = new URL("https://www.mystore.com/sitemap.xml");
137
- const res = await proxy(new Request(url), url);
138
- expect(res!.status).toBe(502);
139
- });
140
-
141
- it("uses the configured environment", async () => {
142
- const fetchImpl = makeFetch(SITEMAP_INDEX);
143
- const proxy = createVtexSitemapProxy({
144
- fetchImpl,
145
- environment: "vtexcommercebeta",
146
- });
147
- const url = new URL("https://www.mystore.com/sitemap.xml");
148
- await proxy(new Request(url), url);
149
-
150
- expect(fetchImpl).toHaveBeenCalledWith(
151
- `https://${ACCOUNT}.vtexcommercebeta.com.br/sitemap.xml`,
152
- );
153
- });
154
-
155
- it("honors a custom Cache-Control header", async () => {
156
- const fetchImpl = makeFetch(SITEMAP_INDEX);
157
- const proxy = createVtexSitemapProxy({
158
- fetchImpl,
159
- cacheControl: "public, max-age=60",
160
- });
161
- const url = new URL("https://www.mystore.com/sitemap.xml");
162
- const res = await proxy(new Request(url), url);
163
- expect(res!.headers.get("cache-control")).toBe("public, max-age=60");
164
- });
165
-
166
- it("emits the default Cache-Control by default", async () => {
167
- const fetchImpl = makeFetch(SITEMAP_INDEX);
168
- const proxy = createVtexSitemapProxy({ fetchImpl });
169
- const url = new URL("https://www.mystore.com/sitemap.xml");
170
- const res = await proxy(new Request(url), url);
171
- expect(res!.headers.get("cache-control")).toBe(
172
- "public, s-maxage=3600, stale-while-revalidate=86400",
173
- );
174
- });
175
-
176
- it("respects non-default VTEX domain (e.g. .com)", async () => {
177
- configureVtex({ account: ACCOUNT, domain: "com" });
178
- const fetchImpl = makeFetch(SITEMAP_INDEX);
179
- const proxy = createVtexSitemapProxy({ fetchImpl });
180
- const url = new URL("https://www.mystore.com/sitemap.xml");
181
- await proxy(new Request(url), url);
182
-
183
- expect(fetchImpl).toHaveBeenCalledWith(`https://${ACCOUNT}.vtexcommercestable.com/sitemap.xml`);
184
- });
185
- });