@faststore/core 4.10.1-dev.6 → 4.10.1-dev.7
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.
- package/.turbo/turbo-generate.log +3 -3
- package/.turbo/turbo-test.log +176 -167
- package/@generated/graphql.ts +1 -0
- package/@generated/schema.graphql +1 -1
- package/CHANGELOG.md +29 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +3 -3
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerBody.tsx +5 -7
- package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +4 -1
- package/src/components/account/Layout/Layout.tsx +5 -7
- package/src/components/account/Menu/Menu.tsx +1 -1
- package/src/components/navigation/Navbar/Navbar.tsx +3 -3
- package/src/components/navigation/Navbar/NavbarAccountArea.tsx +5 -5
- package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +3 -3
- package/src/experimental/myAccountCmsServerSideProps.ts +3 -3
- package/src/experimental/myAccountServerSideProps.ts +4 -4
- package/src/pages/[slug]/p.tsx +11 -2
- package/src/pages/api/fs/logout.ts +18 -10
- package/src/pages/api/fs/switch-contract.ts +170 -21
- package/src/pages/api/graphql.ts +4 -3
- package/src/pages/pvt/account/cards/index.tsx +5 -5
- package/src/pages/pvt/account/orders/[id].tsx +5 -5
- package/src/pages/pvt/account/orders/index.tsx +5 -5
- package/src/pages/pvt/account/profile.tsx +5 -5
- package/src/pages/pvt/account/quotes/index.tsx +5 -5
- package/src/pages/pvt/account/security.tsx +5 -5
- package/src/pages/pvt/account/user-details.tsx +6 -6
- package/src/sdk/account/getB2BSessionClaims.ts +1 -1
- package/src/sdk/account/getIsB2BBuyer.ts +28 -0
- package/src/sdk/account/refreshToken.ts +89 -11
- package/src/sdk/account/refreshTokenPaths.ts +25 -0
- package/src/sdk/account/useRefreshToken.ts +10 -5
- package/src/sdk/product/isProductNotFound.ts +13 -0
- package/src/sdk/product/useProductNotFoundRedirect.ts +43 -0
- package/src/sdk/product/useProductQuery.ts +29 -0
- package/src/sdk/session/index.ts +4 -4
- package/src/utils/getCheckoutParentDomain.ts +53 -0
- package/src/utils/trustedForwardedHost.ts +18 -0
- package/test/components/account/Layout.test.tsx +1 -1
- package/test/components/account/Menu.browser.test.tsx +7 -5
- package/test/components/account/OrganizationDrawer.browser.test.tsx +13 -13
- package/test/components/account/OrganizationDrawerBody.test.tsx +3 -3
- package/test/components/account/OrganizationSignInButton.browser.test.tsx +19 -4
- package/test/components/navigation/Navbar.browser.test.tsx +3 -3
- package/test/components/navigation/NavbarAccountArea.browser.test.tsx +5 -5
- package/test/components/navigation/NavbarSlider.browser.test.tsx +3 -3
- package/test/experimental/myAccountCmsServerSideProps.test.ts +5 -5
- package/test/pages/api/logout.test.ts +106 -0
- package/test/pages/api/switch-contract.test.ts +443 -15
- package/test/pages/pvt/account/cards/index.test.ts +3 -3
- package/test/sdk/account/getIsB2BBuyer.test.ts +52 -0
- package/test/sdk/account/refreshToken.browser.test.ts +10 -1
- package/test/sdk/account/refreshToken.test.ts +150 -18
- package/test/sdk/account/useRefreshToken.browser.test.ts +4 -1
- package/test/sdk/product/useProductNotFoundRedirect.browser.test.ts +83 -0
- package/test/sdk/product/useProductQuery.browser.test.ts +154 -0
- package/test/sdk/session/refreshTokenRoute.browser.test.ts +106 -0
- package/test/sdk/session/validateSession.browser.test.ts +4 -1
- package/test/utils/trustedForwardedHost.test.ts +21 -0
- package/src/sdk/account/getIsRepresentative.ts +0 -20
|
@@ -3,11 +3,19 @@ import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
|
|
3
3
|
|
|
4
4
|
import type { SwitchContractResponse } from '../../../src/pages/api/fs/switch-contract'
|
|
5
5
|
|
|
6
|
-
vi.
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
const mockConfig = vi.hoisted(() => ({
|
|
7
|
+
default: {
|
|
8
|
+
api: { storeId: 'test-store' },
|
|
9
|
+
experimental: { refreshToken: true } as Record<string, unknown>,
|
|
10
|
+
} as {
|
|
11
|
+
api: { storeId: string }
|
|
12
|
+
experimental: Record<string, unknown>
|
|
13
|
+
secureSubdomain?: string
|
|
14
|
+
},
|
|
9
15
|
}))
|
|
10
16
|
|
|
17
|
+
vi.mock('discovery.config', () => ({ __esModule: true, ...mockConfig }))
|
|
18
|
+
|
|
11
19
|
const CONTRACT_ID = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
|
|
12
20
|
const ORDER_FORM_COOKIES =
|
|
13
21
|
'checkout.vtex.com=__ofid=abc123; CheckoutOrderFormOwnership=owner-1'
|
|
@@ -74,6 +82,7 @@ const setCookiesFrom = (res: { setHeader: Mock }): string[] => {
|
|
|
74
82
|
describe('/api/fs/switch-contract', () => {
|
|
75
83
|
beforeEach(() => {
|
|
76
84
|
vi.resetModules()
|
|
85
|
+
mockConfig.default.secureSubdomain = undefined
|
|
77
86
|
global.fetch = vi.fn().mockResolvedValue({
|
|
78
87
|
ok: true,
|
|
79
88
|
status: 200,
|
|
@@ -185,20 +194,299 @@ describe('/api/fs/switch-contract', () => {
|
|
|
185
194
|
expect(res.json).toHaveBeenCalledWith({ success: true })
|
|
186
195
|
})
|
|
187
196
|
|
|
188
|
-
it('
|
|
197
|
+
it('writes the auth cookie with the same scope VTEX ID uses', async () => {
|
|
189
198
|
const handler = await getHandler()
|
|
190
199
|
const { req, res } = createReqRes()
|
|
191
200
|
|
|
192
201
|
await handler(req, res)
|
|
193
202
|
|
|
194
203
|
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
204
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=new-contract-token')
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
expect(authCookie).toBe(
|
|
208
|
+
'VtexIdclientAutCookie_test-store=new-contract-token; Domain=store.example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=None'
|
|
209
|
+
)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('stays Secure even when the proxy forwards plain http', async () => {
|
|
213
|
+
const handler = await getHandler()
|
|
214
|
+
const { req, res } = createReqRes({
|
|
215
|
+
headers: { 'x-forwarded-proto': 'http' },
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
await handler(req, res)
|
|
219
|
+
|
|
220
|
+
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
221
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=new-contract-token')
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
expect(authCookie).toContain('; Secure')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('expires a leftover host-only auth cookie before setting the scoped one', async () => {
|
|
228
|
+
const handler = await getHandler()
|
|
229
|
+
const { req, res } = createReqRes()
|
|
230
|
+
|
|
231
|
+
await handler(req, res)
|
|
232
|
+
|
|
233
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
234
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
expect(authCookies).toHaveLength(3)
|
|
238
|
+
expect(authCookies[0]).toMatch(/^VtexIdclientAutCookie_test-store=;/)
|
|
239
|
+
expect(authCookies[0]).toContain('max-age=0')
|
|
240
|
+
expect(authCookies[0]).toContain('path=/')
|
|
241
|
+
// Host-only: no Domain, so it cannot touch VTEX ID's scoped cookie.
|
|
242
|
+
expect(authCookies[0].toLowerCase()).not.toContain('domain=')
|
|
243
|
+
expect(authCookies[2]).toContain('Domain=store.example.com')
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('expires the copy VTEX ID refresh leaves on the parent domain', async () => {
|
|
247
|
+
const handler = await getHandler()
|
|
248
|
+
const { req, res } = createReqRes()
|
|
249
|
+
|
|
250
|
+
await handler(req, res)
|
|
251
|
+
|
|
252
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
253
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
expect(authCookies[1]).toMatch(/^VtexIdclientAutCookie_test-store=;/)
|
|
257
|
+
expect(authCookies[1]).toContain('max-age=0')
|
|
258
|
+
expect(authCookies[1]).toContain('path=/')
|
|
259
|
+
expect(authCookies[1]).toContain('domain=example.com')
|
|
260
|
+
// The new token is never written to the parent domain.
|
|
261
|
+
expect(
|
|
262
|
+
authCookies.filter((cookie) => cookie.includes('new-contract-token'))
|
|
263
|
+
).toEqual([expect.stringContaining('Domain=store.example.com;')])
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it.each([
|
|
267
|
+
['a single-label parent', 'store.com', []],
|
|
268
|
+
['a deep host', 'a.b.store.com', ['b.store.com', 'store.com']],
|
|
269
|
+
['a two-level suffix', 'www.store.com.br', ['store.com.br', 'com.br']],
|
|
270
|
+
['a .co.uk store', 'www.store.co.uk', ['store.co.uk', 'co.uk']],
|
|
271
|
+
['a .com.au store', 'shop.store.com.au', ['store.com.au', 'com.au']],
|
|
272
|
+
])('expires every parent domain for %s', async (_label, host, parents) => {
|
|
273
|
+
const handler = await getHandler()
|
|
274
|
+
const { req, res } = createReqRes({ headers: { host } })
|
|
275
|
+
|
|
276
|
+
await handler(req, res)
|
|
277
|
+
|
|
278
|
+
const expiredDomains = setCookiesFrom(res)
|
|
279
|
+
.filter((cookie) =>
|
|
280
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=;')
|
|
281
|
+
)
|
|
282
|
+
.map((cookie) => cookie.match(/domain=([^;]+)/)?.[1])
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
|
|
285
|
+
expect(expiredDomains).toEqual(parents)
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it.each(['www.store.com.br', 'www.store.co.uk', 'shop.store.com.au'])(
|
|
289
|
+
'writes the new token only on the request host for %s, never on a parent or public suffix',
|
|
290
|
+
async (host) => {
|
|
291
|
+
const handler = await getHandler()
|
|
292
|
+
const { req, res } = createReqRes({ headers: { host } })
|
|
293
|
+
|
|
294
|
+
await handler(req, res)
|
|
295
|
+
|
|
296
|
+
// Parents (including public suffixes such as co.uk, which the browser
|
|
297
|
+
// rejects) only ever get expiries; the token is scoped to the host.
|
|
298
|
+
expect(
|
|
299
|
+
setCookiesFrom(res).filter((cookie) =>
|
|
300
|
+
cookie.includes('new-contract-token')
|
|
301
|
+
)
|
|
302
|
+
).toEqual([expect.stringContaining(`; Domain=${host}; Path=/;`)])
|
|
303
|
+
}
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
describe('with checkout on a sibling host', () => {
|
|
307
|
+
const tokenCookies = (cookies: string[]) =>
|
|
308
|
+
cookies.filter((cookie) => cookie.includes('new-contract-token'))
|
|
309
|
+
|
|
310
|
+
it('writes the same token on the host and on the shared parent', async () => {
|
|
311
|
+
mockConfig.default.secureSubdomain = 'https://secure.example.com'
|
|
312
|
+
const handler = await getHandler()
|
|
313
|
+
const { req, res } = createReqRes()
|
|
314
|
+
|
|
315
|
+
await handler(req, res)
|
|
316
|
+
|
|
317
|
+
const cookies = setCookiesFrom(res)
|
|
318
|
+
|
|
319
|
+
expect(tokenCookies(cookies)).toEqual([
|
|
320
|
+
'VtexIdclientAutCookie_test-store=new-contract-token; Domain=store.example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=None',
|
|
321
|
+
'VtexIdclientAutCookie_test-store=new-contract-token; Domain=example.com; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=None',
|
|
322
|
+
])
|
|
323
|
+
expect(
|
|
324
|
+
cookies.some(
|
|
325
|
+
(cookie) =>
|
|
326
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=;') &&
|
|
327
|
+
cookie.includes('domain=example.com;')
|
|
328
|
+
)
|
|
329
|
+
).toBe(false)
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it('still expires the parents above the shared one', async () => {
|
|
333
|
+
mockConfig.default.secureSubdomain = 'https://secure.b.store.com'
|
|
334
|
+
const handler = await getHandler()
|
|
335
|
+
const { req, res } = createReqRes({ headers: { host: 'a.b.store.com' } })
|
|
336
|
+
|
|
337
|
+
await handler(req, res)
|
|
338
|
+
|
|
339
|
+
const cookies = setCookiesFrom(res)
|
|
340
|
+
const expiredDomains = cookies
|
|
341
|
+
.filter((cookie) =>
|
|
342
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=;')
|
|
343
|
+
)
|
|
344
|
+
.map((cookie) => cookie.match(/domain=([^;]+)/)?.[1])
|
|
345
|
+
.filter(Boolean)
|
|
346
|
+
|
|
347
|
+
expect(expiredDomains).toEqual(['store.com'])
|
|
348
|
+
expect(tokenCookies(cookies)).toEqual([
|
|
349
|
+
expect.stringContaining('; Domain=a.b.store.com; Path=/;'),
|
|
350
|
+
expect.stringContaining('; Domain=b.store.com; Path=/;'),
|
|
351
|
+
])
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it.each([
|
|
355
|
+
['on the store host', 'https://store.example.com'],
|
|
356
|
+
['on a subdomain of the store host', 'https://secure.store.example.com'],
|
|
357
|
+
['on an unrelated domain', 'https://secure.other.org'],
|
|
358
|
+
['with an invalid URL', 'not a url'],
|
|
359
|
+
])('writes only the host copy with checkout %s', async (_label, url) => {
|
|
360
|
+
mockConfig.default.secureSubdomain = url
|
|
361
|
+
const handler = await getHandler()
|
|
362
|
+
const { req, res } = createReqRes()
|
|
363
|
+
|
|
364
|
+
await handler(req, res)
|
|
365
|
+
|
|
366
|
+
expect(tokenCookies(setCookiesFrom(res))).toEqual([
|
|
367
|
+
expect.stringContaining('; Domain=store.example.com; Path=/;'),
|
|
368
|
+
])
|
|
369
|
+
})
|
|
370
|
+
|
|
371
|
+
it('keeps preview hosts host-only', async () => {
|
|
372
|
+
mockConfig.default.secureSubdomain = 'https://secure.vtex.app'
|
|
373
|
+
const handler = await getHandler()
|
|
374
|
+
const { req, res } = createReqRes({
|
|
375
|
+
headers: { host: 'sfj-abc--test-store.preview.vtex.app' },
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
await handler(req, res)
|
|
379
|
+
|
|
380
|
+
const cookies = tokenCookies(setCookiesFrom(res))
|
|
381
|
+
|
|
382
|
+
expect(cookies).toHaveLength(1)
|
|
383
|
+
expect(cookies[0].toLowerCase()).not.toContain('domain=')
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
it('keeps vid_rt on the store host', async () => {
|
|
387
|
+
mockConfig.default.secureSubdomain = 'https://secure.example.com'
|
|
388
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
389
|
+
ok: true,
|
|
390
|
+
status: 200,
|
|
391
|
+
json: async () => successPayload,
|
|
392
|
+
headers: {
|
|
393
|
+
getSetCookie: () => [
|
|
394
|
+
'vid_rt=rt-1; path=/api/authenticator/v1/refreshtoken/webstore; secure; httponly; Domain=test-store.myvtex.com',
|
|
395
|
+
],
|
|
396
|
+
},
|
|
397
|
+
})
|
|
398
|
+
const handler = await getHandler()
|
|
399
|
+
const { req, res } = createReqRes()
|
|
400
|
+
|
|
401
|
+
await handler(req, res)
|
|
402
|
+
|
|
403
|
+
expect(
|
|
404
|
+
setCookiesFrom(res).filter((cookie) => cookie.startsWith('vid_rt='))
|
|
405
|
+
).toEqual([expect.stringMatching(/; Domain=store\.example\.com$/)])
|
|
406
|
+
})
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('scopes the cookie to an {account}.vtex.app production host', async () => {
|
|
410
|
+
// Unlike previews, this domain receives the browser host in `Host`.
|
|
411
|
+
const handler = await getHandler()
|
|
412
|
+
const { req, res } = createReqRes({
|
|
413
|
+
headers: { host: 'test-store.vtex.app' },
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
await handler(req, res)
|
|
417
|
+
|
|
418
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
419
|
+
cookie.includes('new-contract-token')
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
expect(authCookies).toEqual([
|
|
423
|
+
expect.stringContaining('; Domain=test-store.vtex.app; Path=/;'),
|
|
424
|
+
])
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
it.each([
|
|
428
|
+
['a preview Host', { host: 'sfj-abc--test-store.preview.vtex.app' }],
|
|
429
|
+
[
|
|
430
|
+
'a preview x-forwarded-host behind an internal Host',
|
|
431
|
+
{
|
|
432
|
+
host: 'discovery.internal',
|
|
433
|
+
'x-forwarded-host': 'sfj-abc--test-store.preview.vtex.app',
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
['a .localhost Host', { host: 'store.localhost:3000' }],
|
|
437
|
+
['a trailing-dot FQDN', { host: 'store.example.com.' }],
|
|
438
|
+
])('writes a single host-only cookie for %s', async (_label, headers) => {
|
|
439
|
+
const handler = await getHandler()
|
|
440
|
+
const { req, res } = createReqRes({ headers })
|
|
441
|
+
|
|
442
|
+
await handler(req, res)
|
|
443
|
+
|
|
444
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
195
445
|
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
196
446
|
)
|
|
197
447
|
|
|
198
|
-
expect(
|
|
199
|
-
expect(
|
|
200
|
-
expect(
|
|
201
|
-
|
|
448
|
+
expect(authCookies).toHaveLength(1)
|
|
449
|
+
expect(authCookies[0]).toContain('new-contract-token; Path=/;')
|
|
450
|
+
expect(authCookies[0].toLowerCase()).not.toContain('domain=')
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('does not let x-forwarded-host choose the Domain', async () => {
|
|
454
|
+
const handler = await getHandler()
|
|
455
|
+
const { req, res } = createReqRes({
|
|
456
|
+
headers: { 'x-forwarded-host': 'attacker.example.org' },
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
await handler(req, res)
|
|
460
|
+
|
|
461
|
+
const cookies = setCookiesFrom(res)
|
|
462
|
+
|
|
463
|
+
expect(cookies).toEqual(
|
|
464
|
+
expect.arrayContaining([
|
|
465
|
+
expect.stringContaining(
|
|
466
|
+
'new-contract-token; Domain=store.example.com;'
|
|
467
|
+
),
|
|
468
|
+
])
|
|
469
|
+
)
|
|
470
|
+
expect(
|
|
471
|
+
cookies.some((cookie) => cookie.includes('attacker.example.org'))
|
|
472
|
+
).toBe(false)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('strips the port from the Host header', async () => {
|
|
476
|
+
const handler = await getHandler()
|
|
477
|
+
const { req, res } = createReqRes({
|
|
478
|
+
headers: { host: 'store.example.com:443' },
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
await handler(req, res)
|
|
482
|
+
|
|
483
|
+
expect(setCookiesFrom(res)).toEqual(
|
|
484
|
+
expect.arrayContaining([
|
|
485
|
+
expect.stringContaining(
|
|
486
|
+
'new-contract-token; Domain=store.example.com; Path=/'
|
|
487
|
+
),
|
|
488
|
+
])
|
|
489
|
+
)
|
|
202
490
|
})
|
|
203
491
|
|
|
204
492
|
it.each([
|
|
@@ -222,7 +510,7 @@ describe('/api/fs/switch-contract', () => {
|
|
|
222
510
|
await handler(req, res)
|
|
223
511
|
|
|
224
512
|
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
225
|
-
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
513
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=new-contract-token')
|
|
226
514
|
)
|
|
227
515
|
|
|
228
516
|
// Max-Age=0 would delete the very credential this response installs.
|
|
@@ -231,19 +519,154 @@ describe('/api/fs/switch-contract', () => {
|
|
|
231
519
|
}
|
|
232
520
|
)
|
|
233
521
|
|
|
234
|
-
it(
|
|
522
|
+
it.each([
|
|
523
|
+
['localhost', 'localhost:3000'],
|
|
524
|
+
['127.0.0.1', '127.0.0.1:3000'],
|
|
525
|
+
['a LAN IP', '192.168.1.10'],
|
|
526
|
+
['an unsafe host', 'store.example.com;evil=1'],
|
|
527
|
+
])('writes a single host-only cookie on %s', async (_label, host) => {
|
|
235
528
|
const handler = await getHandler()
|
|
236
529
|
const { req, res } = createReqRes({
|
|
237
|
-
headers: { 'x-forwarded-proto': 'http' },
|
|
530
|
+
headers: { host, 'x-forwarded-proto': 'http' },
|
|
238
531
|
})
|
|
239
532
|
|
|
240
533
|
await handler(req, res)
|
|
241
534
|
|
|
242
|
-
const
|
|
535
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
243
536
|
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
244
537
|
)
|
|
245
538
|
|
|
246
|
-
|
|
539
|
+
// No Domain means the cookie already replaces the host-only one, so
|
|
540
|
+
// expiring it in the same batch would delete the new credential.
|
|
541
|
+
expect(authCookies).toEqual([
|
|
542
|
+
'VtexIdclientAutCookie_test-store=new-contract-token; Path=/; Max-Age=3600; HttpOnly; SameSite=Lax',
|
|
543
|
+
])
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
it.each([
|
|
547
|
+
['a LAN IP', '192.168.1.10'],
|
|
548
|
+
['an IPv6 literal', '[2001:db8::1]'],
|
|
549
|
+
])(
|
|
550
|
+
'keeps the host-only cookie Secure on %s over https',
|
|
551
|
+
async (_label, host) => {
|
|
552
|
+
const handler = await getHandler()
|
|
553
|
+
const { req, res } = createReqRes({
|
|
554
|
+
headers: { host, 'x-forwarded-proto': 'https' },
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
await handler(req, res)
|
|
558
|
+
|
|
559
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
560
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
expect(authCookies).toEqual([
|
|
564
|
+
'VtexIdclientAutCookie_test-store=new-contract-token; Path=/; Max-Age=3600; HttpOnly; Secure; SameSite=Lax',
|
|
565
|
+
])
|
|
566
|
+
}
|
|
567
|
+
)
|
|
568
|
+
|
|
569
|
+
it.each([
|
|
570
|
+
['plain http', false, ''],
|
|
571
|
+
['a TLS connection', true, '; Secure'],
|
|
572
|
+
])(
|
|
573
|
+
'follows the connection without x-forwarded-proto on %s',
|
|
574
|
+
async (_label, encrypted, secureAttr) => {
|
|
575
|
+
const handler = await getHandler()
|
|
576
|
+
const { req, res } = createReqRes({
|
|
577
|
+
headers: { host: '192.168.1.10', 'x-forwarded-proto': undefined },
|
|
578
|
+
socket: { encrypted } as unknown as NextApiRequest['socket'],
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
await handler(req, res)
|
|
582
|
+
|
|
583
|
+
const authCookies = setCookiesFrom(res).filter((cookie) =>
|
|
584
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
expect(authCookies).toEqual([
|
|
588
|
+
`VtexIdclientAutCookie_test-store=new-contract-token; Path=/; Max-Age=3600; HttpOnly${secureAttr}; SameSite=Lax`,
|
|
589
|
+
])
|
|
590
|
+
}
|
|
591
|
+
)
|
|
592
|
+
|
|
593
|
+
it('relays the upstream vid_rt re-scoped to the storefront host', async () => {
|
|
594
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
595
|
+
ok: true,
|
|
596
|
+
status: 200,
|
|
597
|
+
json: async () => successPayload,
|
|
598
|
+
headers: {
|
|
599
|
+
getSetCookie: () => [
|
|
600
|
+
'vid_rt=rt-1; expires=Thu, 22 Oct 2027 00:00:00 GMT; domain=test-store.myvtex.com; path=/api/authenticator/v1/refreshtoken/webstore; secure; samesite=strict; httponly',
|
|
601
|
+
'VtexIdclientAutCookie_test-store=upstream-token; domain=test-store.myvtex.com; path=/',
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
})
|
|
605
|
+
const handler = await getHandler()
|
|
606
|
+
const { req, res } = createReqRes()
|
|
607
|
+
|
|
608
|
+
await handler(req, res)
|
|
609
|
+
|
|
610
|
+
const cookies = setCookiesFrom(res)
|
|
611
|
+
const vidRt = cookies.filter((cookie) => cookie.startsWith('vid_rt='))
|
|
612
|
+
|
|
613
|
+
expect(vidRt).toEqual([
|
|
614
|
+
'vid_rt=rt-1; expires=Thu, 22 Oct 2027 00:00:00 GMT; path=/api/authenticator/v1/refreshtoken/webstore; secure; samesite=strict; httponly; Domain=store.example.com',
|
|
615
|
+
])
|
|
616
|
+
// Only vid_rt is relayed; the auth cookie still comes from the payload.
|
|
617
|
+
expect(cookies.some((cookie) => cookie.includes('upstream-token'))).toBe(
|
|
618
|
+
false
|
|
619
|
+
)
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
it('does not relay vid_rt when refresh token is disabled', async () => {
|
|
623
|
+
mockConfig.default.experimental = {}
|
|
624
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
625
|
+
ok: true,
|
|
626
|
+
status: 200,
|
|
627
|
+
json: async () => successPayload,
|
|
628
|
+
headers: {
|
|
629
|
+
getSetCookie: () => [
|
|
630
|
+
'vid_rt=rt-1; domain=test-store.myvtex.com; path=/api/authenticator/v1/refreshtoken/webstore; secure; httponly',
|
|
631
|
+
],
|
|
632
|
+
},
|
|
633
|
+
})
|
|
634
|
+
const handler = await getHandler()
|
|
635
|
+
const { req, res } = createReqRes()
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
await handler(req, res)
|
|
639
|
+
} finally {
|
|
640
|
+
mockConfig.default.experimental = { refreshToken: true }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const cookies = setCookiesFrom(res)
|
|
644
|
+
expect(cookies.some((cookie) => cookie.startsWith('vid_rt='))).toBe(false)
|
|
645
|
+
// The contract switch itself still goes through.
|
|
646
|
+
expect(
|
|
647
|
+
cookies.some((cookie) => cookie.includes('new-contract-token'))
|
|
648
|
+
).toBe(true)
|
|
649
|
+
})
|
|
650
|
+
|
|
651
|
+
it('relays vid_rt host-only when the auth cookie is host-only', async () => {
|
|
652
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
653
|
+
ok: true,
|
|
654
|
+
status: 200,
|
|
655
|
+
json: async () => successPayload,
|
|
656
|
+
headers: {
|
|
657
|
+
getSetCookie: () => [
|
|
658
|
+
'vid_rt=rt-1; domain=test-store.myvtex.com; path=/api/vtexid/refreshtoken/webstore; httponly',
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
})
|
|
662
|
+
const handler = await getHandler()
|
|
663
|
+
const { req, res } = createReqRes({ headers: { host: 'localhost:3000' } })
|
|
664
|
+
|
|
665
|
+
await handler(req, res)
|
|
666
|
+
|
|
667
|
+
expect(
|
|
668
|
+
setCookiesFrom(res).filter((cookie) => cookie.startsWith('vid_rt='))
|
|
669
|
+
).toEqual(['vid_rt=rt-1; path=/api/vtexid/refreshtoken/webstore; httponly'])
|
|
247
670
|
})
|
|
248
671
|
|
|
249
672
|
it('also emits the accountAuthCookie when present', async () => {
|
|
@@ -263,9 +686,14 @@ describe('/api/fs/switch-contract', () => {
|
|
|
263
686
|
|
|
264
687
|
await handler(req, res)
|
|
265
688
|
|
|
266
|
-
|
|
689
|
+
const cookies = setCookiesFrom(res)
|
|
690
|
+
|
|
691
|
+
expect(cookies).toEqual(
|
|
267
692
|
expect.arrayContaining([
|
|
268
|
-
expect.
|
|
693
|
+
expect.stringMatching(/^VtexIdclientAutCookie_account=;.*max-age=0/),
|
|
694
|
+
expect.stringContaining(
|
|
695
|
+
'VtexIdclientAutCookie_account=account-token; Domain=store.example.com'
|
|
696
|
+
),
|
|
269
697
|
])
|
|
270
698
|
)
|
|
271
699
|
})
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
|
|
|
2
2
|
|
|
3
3
|
const mockExecute = vi.hoisted(() => vi.fn())
|
|
4
4
|
const mockGetB2BSessionClaims = vi.hoisted(() => vi.fn())
|
|
5
|
-
const
|
|
5
|
+
const mockGetIsB2BBuyer = vi.hoisted(() => vi.fn(() => true))
|
|
6
6
|
const mockGetMyAccountRedirect = vi.hoisted(() =>
|
|
7
7
|
vi.fn(() => ({ isFaststoreMyAccountEnabled: true, redirect: undefined }))
|
|
8
8
|
)
|
|
@@ -30,8 +30,8 @@ vi.mock('src/components/cms/GlobalSections', () => ({
|
|
|
30
30
|
vi.mock('src/sdk/account/getB2BSessionClaims', () => ({
|
|
31
31
|
getB2BSessionClaims: mockGetB2BSessionClaims,
|
|
32
32
|
}))
|
|
33
|
-
vi.mock('src/sdk/account/
|
|
34
|
-
|
|
33
|
+
vi.mock('src/sdk/account/getIsB2BBuyer', () => ({
|
|
34
|
+
getIsB2BBuyer: mockGetIsB2BBuyer,
|
|
35
35
|
}))
|
|
36
36
|
vi.mock('src/utils/myAccountRedirect', () => ({
|
|
37
37
|
getMyAccountRedirect: mockGetMyAccountRedirect,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getIsB2BBuyer } from '../../../src/sdk/account/getIsB2BBuyer'
|
|
3
|
+
|
|
4
|
+
const ACCOUNT = 'storeaccount'
|
|
5
|
+
|
|
6
|
+
const makeJwt = (payload: Record<string, unknown>) =>
|
|
7
|
+
`header.${Buffer.from(JSON.stringify(payload)).toString('base64')}.signature`
|
|
8
|
+
|
|
9
|
+
const withToken = (payload: Record<string, unknown>) => ({
|
|
10
|
+
headers: { cookie: `VtexIdclientAutCookie_${ACCOUNT}=${makeJwt(payload)}` },
|
|
11
|
+
account: ACCOUNT,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('getIsB2BBuyer', () => {
|
|
15
|
+
it('is true when the token has a unit and a contract', () => {
|
|
16
|
+
expect(
|
|
17
|
+
getIsB2BBuyer(withToken({ unitId: 'unit-1', customerId: 'contract-1' }))
|
|
18
|
+
).toBe(true)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('does not depend on the isRepresentative claim', () => {
|
|
22
|
+
expect(
|
|
23
|
+
getIsB2BBuyer(
|
|
24
|
+
withToken({
|
|
25
|
+
unitId: 'unit-1',
|
|
26
|
+
customerId: 'contract-1',
|
|
27
|
+
isRepresentative: false,
|
|
28
|
+
})
|
|
29
|
+
)
|
|
30
|
+
).toBe(true)
|
|
31
|
+
expect(getIsB2BBuyer(withToken({ isRepresentative: true }))).toBe(false)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it.each([
|
|
35
|
+
['without a unit', { customerId: 'contract-1' }],
|
|
36
|
+
['without a contract', { unitId: 'unit-1' }],
|
|
37
|
+
['with blank claims', { unitId: ' ', customerId: '' }],
|
|
38
|
+
['with non-string claims', { unitId: 1, customerId: true }],
|
|
39
|
+
])('is false %s', (_label, payload) => {
|
|
40
|
+
expect(getIsB2BBuyer(withToken(payload))).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('is false without an auth cookie or with a malformed one', () => {
|
|
44
|
+
expect(getIsB2BBuyer({ headers: {}, account: ACCOUNT })).toBe(false)
|
|
45
|
+
expect(
|
|
46
|
+
getIsB2BBuyer({
|
|
47
|
+
headers: { cookie: `VtexIdclientAutCookie_${ACCOUNT}=not-a-jwt` },
|
|
48
|
+
account: ACCOUNT,
|
|
49
|
+
})
|
|
50
|
+
).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -14,7 +14,7 @@ describe('refreshTokenRequest (browser)', () => {
|
|
|
14
14
|
beforeEach(() => {
|
|
15
15
|
mockFetch.mockResolvedValue({
|
|
16
16
|
status: 200,
|
|
17
|
-
json: async () => ({
|
|
17
|
+
json: async () => ({ authStatus: 'Success' }),
|
|
18
18
|
})
|
|
19
19
|
})
|
|
20
20
|
|
|
@@ -31,4 +31,13 @@ describe('refreshTokenRequest (browser)', () => {
|
|
|
31
31
|
expect(url).toBe('/api/vtexid/refreshtoken/webstore')
|
|
32
32
|
expect(init.headers).toEqual({ 'content-type': 'application/json' })
|
|
33
33
|
})
|
|
34
|
+
|
|
35
|
+
it('uses the relative Authenticator route for B2B sessions', async () => {
|
|
36
|
+
await refreshTokenRequest({ isB2B: true })
|
|
37
|
+
|
|
38
|
+
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
39
|
+
expect(mockFetch.mock.calls[0][0]).toBe(
|
|
40
|
+
'/api/authenticator/v1/refreshtoken/webstore'
|
|
41
|
+
)
|
|
42
|
+
})
|
|
34
43
|
})
|