@@ -57,14 +57,9 @@ describe('Storefront API', () => {
5757 } )
5858
5959 test ( 'retrieves _shopify_essential and storefront_digest cookies when a password is provided' , async ( ) => {
60- // Given
60+ // Given: POST /password runs first (cold, returns the digest), then the
61+ // preview HEAD runs last and owns the final _shopify_essential.
6162 vi . mocked ( shopifyFetch )
62- . mockResolvedValueOnce (
63- response ( {
64- status : 200 ,
65- headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
66- } ) ,
67- )
6863 . mockResolvedValueOnce (
6964 response ( {
7065 status : 302 ,
@@ -74,6 +69,12 @@ describe('Storefront API', () => {
7469 } ,
7570 } ) ,
7671 )
72+ . mockResolvedValueOnce (
73+ response ( {
74+ status : 200 ,
75+ headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
76+ } ) ,
77+ )
7778
7879 // When
7980 const cookies = await getStorefrontSessionCookies (
@@ -89,12 +90,6 @@ describe('Storefront API', () => {
8990
9091 test ( 'passes caller-provided headers when retrieving session cookies' , async ( ) => {
9192 vi . mocked ( shopifyFetch )
92- . mockResolvedValueOnce (
93- response ( {
94- status : 200 ,
95- headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
96- } ) ,
97- )
9893 . mockResolvedValueOnce (
9994 response ( {
10095 status : 302 ,
@@ -104,6 +99,12 @@ describe('Storefront API', () => {
10499 } ,
105100 } ) ,
106101 )
102+ . mockResolvedValueOnce (
103+ response ( {
104+ status : 200 ,
105+ headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
106+ } ) ,
107+ )
107108
108109 await getStorefrontSessionCookies (
109110 'https://example-store.myshopify.com' ,
@@ -117,9 +118,10 @@ describe('Storefront API', () => {
117118 } ,
118119 )
119120
121+ // Password POST runs first, then the preview HEAD.
120122 expect ( shopifyFetch ) . toHaveBeenNthCalledWith (
121123 1 ,
122- 'https://example-store.myshopify.com?preview_theme_id=123456&_fd=0&pb=0 ' ,
124+ 'https://example-store.myshopify.com/password ' ,
123125 expect . objectContaining ( {
124126 headers : expect . objectContaining ( {
125127 Signature : 'signature-value' ,
@@ -130,7 +132,7 @@ describe('Storefront API', () => {
130132 )
131133 expect ( shopifyFetch ) . toHaveBeenNthCalledWith (
132134 2 ,
133- 'https://example-store.myshopify.com/password ' ,
135+ 'https://example-store.myshopify.com?preview_theme_id=123456&_fd=0&pb=0 ' ,
134136 expect . objectContaining ( {
135137 headers : expect . objectContaining ( {
136138 Signature : 'signature-value' ,
@@ -141,6 +143,59 @@ describe('Storefront API', () => {
141143 )
142144 } )
143145
146+ test ( 'runs POST /password first, then forwards the digest essential to the trailing preview HEAD' , async ( ) => {
147+ const digestEssential = ':DIGEST==123:'
148+ const finalEssential = ':DIGEST+PREVIEW==456:'
149+
150+ // Given: password POST returns the digest-bearing essential; the preview
151+ // HEAD then returns the final essential carrying both fields.
152+ vi . mocked ( shopifyFetch )
153+ . mockResolvedValueOnce (
154+ response ( {
155+ status : 302 ,
156+ headers : {
157+ 'set-cookie' : `_shopify_essential=${ digestEssential } ; path=/; HttpOnly` ,
158+ location : 'https://example-store.myshopify.com/' ,
159+ } ,
160+ } ) ,
161+ )
162+ . mockResolvedValueOnce (
163+ response ( {
164+ status : 200 ,
165+ headers : { 'set-cookie' : `_shopify_essential=${ finalEssential } ; path=/; HttpOnly` } ,
166+ } ) ,
167+ )
168+
169+ // When
170+ const cookies = await getStorefrontSessionCookies (
171+ 'https://example-store.myshopify.com' ,
172+ 'example-store.myshopify.com' ,
173+ '123456' ,
174+ 'password' ,
175+ )
176+
177+ // Then: exactly two fetches, in order — POST /password, then the preview HEAD.
178+ expect ( shopifyFetch ) . toHaveBeenCalledTimes ( 2 )
179+ expect ( shopifyFetch ) . toHaveBeenNthCalledWith (
180+ 1 ,
181+ 'https://example-store.myshopify.com/password' ,
182+ expect . objectContaining ( { method : 'POST' } ) ,
183+ )
184+ // The preview HEAD carries the preview_theme_id AND forwards the digest essential.
185+ expect ( shopifyFetch ) . toHaveBeenNthCalledWith (
186+ 2 ,
187+ 'https://example-store.myshopify.com?preview_theme_id=123456&_fd=0&pb=0' ,
188+ expect . objectContaining ( {
189+ method : 'HEAD' ,
190+ headers : expect . objectContaining ( {
191+ Cookie : `_shopify_essential=${ digestEssential } ` ,
192+ } ) ,
193+ } ) ,
194+ )
195+ // The final cookie is the trailing preview HEAD's essential (both fields).
196+ expect ( cookies ) . toEqual ( { _shopify_essential : finalEssential } )
197+ } )
198+
144199 test ( `throws an ShopifyEssentialError when _shopify_essential can't be defined` , async ( ) => {
145200 // Given
146201 vi . mocked ( shopifyFetch )
@@ -185,21 +240,13 @@ describe('Storefront API', () => {
185240 } )
186241
187242 test ( 'throws an error when the password is wrong' , async ( ) => {
188- // Given
189- vi . mocked ( shopifyFetch )
190- . mockResolvedValueOnce (
191- response ( {
192- status : 200 ,
193- headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
194- text : ( ) => Promise . resolve ( '' ) ,
195- } ) ,
196- )
197- . mockResolvedValueOnce (
198- response ( {
199- status : 401 ,
200- text : ( ) => Promise . resolve ( '' ) ,
201- } ) ,
202- )
243+ // Given: the password POST runs first and rejects the credentials.
244+ vi . mocked ( shopifyFetch ) . mockResolvedValueOnce (
245+ response ( {
246+ status : 401 ,
247+ text : ( ) => Promise . resolve ( '' ) ,
248+ } ) ,
249+ )
203250
204251 // When
205252 const cookies = getStorefrontSessionCookies (
@@ -255,25 +302,27 @@ describe('Storefront API', () => {
255302 } )
256303
257304 test ( 'handles storefront_digest migration to _shopify_essential cookie' , async ( ) => {
258- const originalEssential = ':AABBCCDDEEFFGGHH==123:'
259- const authenticatedEssential = ':NEWESSENTIAL==456:'
305+ const digestEssential = ':AABBCCDDEEFFGGHH==123:'
306+ const finalEssential = ':NEWESSENTIAL==456:'
260307
308+ // Given: password POST mints the digest-bearing essential first; the
309+ // trailing preview HEAD returns the final essential carrying both fields.
261310 vi . mocked ( shopifyFetch )
262- . mockResolvedValueOnce (
263- response ( {
264- status : 200 ,
265- headers : { 'set-cookie' : `_shopify_essential=${ originalEssential } ; path=/; HttpOnly` } ,
266- } ) ,
267- )
268311 . mockResolvedValueOnce (
269312 response ( {
270313 status : 302 ,
271314 headers : {
272- 'set-cookie' : `_shopify_essential=${ authenticatedEssential } ; path=/; HttpOnly` ,
315+ 'set-cookie' : `_shopify_essential=${ digestEssential } ; path=/; HttpOnly` ,
273316 location : 'https://example-store.myshopify.com/' ,
274317 } ,
275318 } ) ,
276319 )
320+ . mockResolvedValueOnce (
321+ response ( {
322+ status : 200 ,
323+ headers : { 'set-cookie' : `_shopify_essential=${ finalEssential } ; path=/; HttpOnly` } ,
324+ } ) ,
325+ )
277326
278327 // When
279328 const cookies = await getStorefrontSessionCookies (
@@ -283,32 +332,34 @@ describe('Storefront API', () => {
283332 'password' ,
284333 )
285334
286- // Then
335+ // Then: the trailing preview HEAD's essential wins.
287336 expect ( cookies ) . toEqual ( {
288- _shopify_essential : authenticatedEssential ,
337+ _shopify_essential : finalEssential ,
289338 } )
290339 } )
291340
292341 test ( 'handles theme kit access with _shopify_essential cookies' , async ( ) => {
293- const originalEssential = ':AABBCCDDEEFFGGHH==123:'
294- const authenticatedEssential = ':NEWESSENTIAL==456:'
342+ const digestEssential = ':AABBCCDDEEFFGGHH==123:'
343+ const finalEssential = ':NEWESSENTIAL==456:'
295344
345+ // Given: password POST first, preview HEAD last (same order as the
346+ // standard flow) even when routed through the theme kit access domain.
296347 vi . mocked ( shopifyFetch )
297- . mockResolvedValueOnce (
298- response ( {
299- status : 200 ,
300- headers : { 'set-cookie' : `_shopify_essential=${ originalEssential } ; path=/; HttpOnly` } ,
301- } ) ,
302- )
303348 . mockResolvedValueOnce (
304349 response ( {
305350 status : 302 ,
306351 headers : {
307- 'set-cookie' : `_shopify_essential=${ authenticatedEssential } ; path=/; HttpOnly` ,
352+ 'set-cookie' : `_shopify_essential=${ digestEssential } ; path=/; HttpOnly` ,
308353 location : 'https://example-store.myshopify.com/' ,
309354 } ,
310355 } ) ,
311356 )
357+ . mockResolvedValueOnce (
358+ response ( {
359+ status : 200 ,
360+ headers : { 'set-cookie' : `_shopify_essential=${ finalEssential } ; path=/; HttpOnly` } ,
361+ } ) ,
362+ )
312363
313364 // When
314365 const cookies = await getStorefrontSessionCookies (
@@ -320,19 +371,15 @@ describe('Storefront API', () => {
320371
321372 // Then
322373 expect ( cookies ) . toEqual ( {
323- _shopify_essential : authenticatedEssential ,
374+ _shopify_essential : finalEssential ,
324375 } )
325376 } )
326377
327378 test ( 'handles case when storefront_digest is present (non-migrated case)' , async ( ) => {
328- // Given: storefront_digest is still being used
379+ // Given: storefront_digest is still a standalone cookie. Password POST
380+ // runs first and returns only the digest; the trailing preview HEAD
381+ // mints the _shopify_essential.
329382 vi . mocked ( shopifyFetch )
330- . mockResolvedValueOnce (
331- response ( {
332- status : 200 ,
333- headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
334- } ) ,
335- )
336383 . mockResolvedValueOnce (
337384 response ( {
338385 status : 302 ,
@@ -342,6 +389,12 @@ describe('Storefront API', () => {
342389 } ,
343390 } ) ,
344391 )
392+ . mockResolvedValueOnce (
393+ response ( {
394+ status : 200 ,
395+ headers : { 'set-cookie' : '_shopify_essential=:AABBCCDDEEFFGGHH==123:; path=/; HttpOnly' } ,
396+ } ) ,
397+ )
345398
346399 // When
347400 const cookies = await getStorefrontSessionCookies (
@@ -359,21 +412,13 @@ describe('Storefront API', () => {
359412 } )
360413
361414 test ( 'throws error when pasword page does not return a 302' , async ( ) => {
362- // Given: password redirects correctly but _shopify_essential doesn't change (shouldn't happen)
363- const sameEssential = ':AABBCCDDEEFFGGHH==123:'
364-
365- vi . mocked ( shopifyFetch )
366- . mockResolvedValueOnce (
367- response ( {
368- status : 200 ,
369- headers : { 'set-cookie' : `_shopify_essential=${ sameEssential } ; path=/; HttpOnly` } ,
370- } ) ,
371- )
372- . mockResolvedValueOnce (
373- response ( {
374- status : 200 ,
375- } ) ,
376- )
415+ // Given: the password POST runs first and does not redirect to the
416+ // storefront (no 302), so the session cannot be created.
417+ vi . mocked ( shopifyFetch ) . mockResolvedValueOnce (
418+ response ( {
419+ status : 200 ,
420+ } ) ,
421+ )
377422
378423 // When
379424 const cookies = getStorefrontSessionCookies (
0 commit comments