77
88import { RequestContext } from "@decocms/start/sdk/requestContext" ;
99import { afterEach , beforeEach , describe , expect , it , vi } from "vitest" ;
10- import { configureVtex , setVtexFetch , vtexFetchResponse } from "../client" ;
10+ import {
11+ configureVtex ,
12+ intelligentSearch ,
13+ setVtexFetch ,
14+ vtexCachedFetch ,
15+ vtexFetchResponse ,
16+ } from "../client" ;
17+ import { clearFetchCache } from "../utils/fetchCache" ;
1118
1219function mockResponse ( body : unknown = { } , status = 200 ) : Response {
1320 return {
@@ -43,6 +50,25 @@ function mockResponse(body: unknown = {}, status = 200): Response {
4350 * `vi.spyOn`. The spy is restored after `fn` resolves to keep tests
4451 * isolated. Nothing here depends on undici or ALS internals.
4552 */
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+
4672function withRequest < T > ( cookieHeader : string | null , fn : ( ) => Promise < T > ) : Promise < T > {
4773 const headers = new Headers ( ) ;
4874 if ( cookieHeader ) headers . set ( "cookie" , cookieHeader ) ;
@@ -79,8 +105,7 @@ describe("vtexFetchResponse — vtex_segment cookie forwarding", () => {
79105 await withRequest ( "vtex_segment=abc123; other=foo" , async ( ) => {
80106 await vtexFetchResponse ( "/api/catalog_system/pub/products/x" ) ;
81107 } ) ;
82- const headers = lastInit ?. headers as Record < string , string > ;
83- expect ( headers . cookie ) . toBe ( "vtex_segment=abc123" ) ;
108+ expect ( headerValue ( lastInit , "cookie" ) ) . toBe ( "vtex_segment=abc123" ) ;
84109 } ) ;
85110
86111 it ( "does not overwrite a caller-supplied cookie header" , async ( ) => {
@@ -89,8 +114,7 @@ describe("vtexFetchResponse — vtex_segment cookie forwarding", () => {
89114 headers : { cookie : "custom=zzz" } ,
90115 } ) ;
91116 } ) ;
92- const headers = lastInit ?. headers as Record < string , string > ;
93- expect ( headers . cookie ) . toBe ( "custom=zzz" ) ;
117+ expect ( headerValue ( lastInit , "cookie" ) ) . toBe ( "custom=zzz" ) ;
94118 } ) ;
95119
96120 it ( "does not overwrite a caller-supplied Cookie header (case-insensitive)" , async ( ) => {
@@ -99,44 +123,133 @@ describe("vtexFetchResponse — vtex_segment cookie forwarding", () => {
99123 headers : { Cookie : "custom=zzz" } ,
100124 } ) ;
101125 } ) ;
102- const headers = lastInit ?. headers as Record < string , string > ;
103- // The merged headers may contain both keys; the caller's value should win.
104- const lowered = Object . fromEntries (
105- Object . entries ( headers ) . map ( ( [ k , v ] ) => [ k . toLowerCase ( ) , v ] ) ,
106- ) ;
107- expect ( lowered . cookie ) . toBe ( "custom=zzz" ) ;
126+ expect ( headerValue ( lastInit , "cookie" ) ) . toBe ( "custom=zzz" ) ;
108127 } ) ;
109128
110129 it ( "is a no-op when there is no incoming cookie header" , async ( ) => {
111130 await withRequest ( null , async ( ) => {
112131 await vtexFetchResponse ( "/api/x" ) ;
113132 } ) ;
114- const headers = lastInit ?. headers as Record < string , string > ;
115- expect ( headers . cookie ) . toBeUndefined ( ) ;
133+ expect ( headerValue ( lastInit , "cookie" ) ) . toBeUndefined ( ) ;
116134 } ) ;
117135
118136 it ( "is a no-op when there is a cookie header but no vtex_segment" , async ( ) => {
119137 await withRequest ( "other=foo; another=bar" , async ( ) => {
120138 await vtexFetchResponse ( "/api/x" ) ;
121139 } ) ;
122- const headers = lastInit ?. headers as Record < string , string > ;
123- expect ( headers . cookie ) . toBeUndefined ( ) ;
140+ expect ( headerValue ( lastInit , "cookie" ) ) . toBeUndefined ( ) ;
124141 } ) ;
125142
126143 it ( "does not crash when called outside a RequestContext" , async ( ) => {
127144 await vtexFetchResponse ( "/api/x" ) ;
128- const headers = lastInit ?. headers as Record < string , string > ;
129- expect ( headers . cookie ) . toBeUndefined ( ) ;
145+ expect ( headerValue ( lastInit , "cookie" ) ) . toBeUndefined ( ) ;
130146 } ) ;
131147
132148 it ( "preserves auth headers alongside the forwarded cookie" , async ( ) => {
133149 configureVtex ( { account : "testaccount" , appKey : "k" , appToken : "t" } ) ;
134150 await withRequest ( "vtex_segment=abc123" , async ( ) => {
135151 await vtexFetchResponse ( "/api/x" ) ;
136152 } ) ;
137- const headers = lastInit ?. headers as Record < string , string > ;
138- expect ( headers [ "X-VTEX-API-AppKey" ] ) . toBe ( "k" ) ;
139- expect ( headers [ "X-VTEX-API-AppToken" ] ) . toBe ( "t" ) ;
140- expect ( headers . cookie ) . toBe ( "vtex_segment=abc123" ) ;
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" ) ;
141254 } ) ;
142255} ) ;
0 commit comments