@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,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
- });