@faststore/core 4.10.1-dev.4 → 4.10.1-dev.6
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 +267 -229
- package/CHANGELOG.md +17 -0
- package/package.json +6 -6
- package/src/pages/api/graphql.ts +97 -57
- package/src/pages/pvt/account/403.tsx +5 -4
- package/src/sdk/account/useRefreshToken.ts +5 -6
- package/src/sdk/product/usePageProductsQuery.ts +80 -21
- package/src/sdk/session/index.ts +42 -30
- package/src/utils/isLocalHost.ts +2 -16
- package/src/utils/shouldBypassRefreshToken.ts +63 -0
- package/test/pages/api/graphql.test.ts +175 -0
- package/test/pages/pvt/account/403.test.ts +67 -0
- package/test/sdk/account/useRefreshToken.browser.test.ts +69 -0
- package/test/sdk/product/usePageProductsQuery.browser.test.ts +260 -0
- package/test/sdk/product/usePageProductsQuery.deliveryPromise.browser.test.ts +130 -0
- package/test/sdk/session/validateSession.browser.test.ts +108 -5
- package/test/utils/shouldBypassRefreshToken.browser.test.ts +38 -0
- package/test/utils/shouldBypassRefreshToken.test.ts +98 -0
- package/test/utils/isLocalHost.browser.test.ts +0 -37
|
@@ -9,14 +9,23 @@ import { GraphQLError } from 'graphql'
|
|
|
9
9
|
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
11
|
|
|
12
|
+
import discoveryConfig from '../../../discovery.config'
|
|
12
13
|
import handler from '../../../src/pages/api/graphql'
|
|
13
14
|
import { execute } from '../../../src/server'
|
|
15
|
+
import { shouldForceRefreshTokenForValidateSession } from '../../../src/utils/validateSessionRefreshToken'
|
|
14
16
|
|
|
15
17
|
vi.mock('../../../src/server', () => ({
|
|
16
18
|
execute: vi.fn(),
|
|
17
19
|
}))
|
|
18
20
|
|
|
21
|
+
vi.mock('../../../src/utils/validateSessionRefreshToken', () => ({
|
|
22
|
+
shouldForceRefreshTokenForValidateSession: vi.fn(),
|
|
23
|
+
}))
|
|
24
|
+
|
|
19
25
|
const mockedExecute = vi.mocked(execute)
|
|
26
|
+
const mockedShouldForceRefreshToken = vi.mocked(
|
|
27
|
+
shouldForceRefreshTokenForValidateSession
|
|
28
|
+
)
|
|
20
29
|
|
|
21
30
|
const createResponse = ({ headersSent = false } = {}) => {
|
|
22
31
|
const res = {
|
|
@@ -229,3 +238,169 @@ describe('/api/graphql request handling', () => {
|
|
|
229
238
|
expect(res.end).toHaveBeenCalled()
|
|
230
239
|
})
|
|
231
240
|
})
|
|
241
|
+
|
|
242
|
+
describe('/api/graphql ValidateSession refresh-token bypass', () => {
|
|
243
|
+
const originalRefreshToken = discoveryConfig.experimental.refreshToken
|
|
244
|
+
|
|
245
|
+
const createValidateSessionRequest = (
|
|
246
|
+
host: string,
|
|
247
|
+
extraHeaders: Record<string, string> = {}
|
|
248
|
+
) =>
|
|
249
|
+
({
|
|
250
|
+
method: 'GET',
|
|
251
|
+
url: '/api/graphql',
|
|
252
|
+
headers: { host, ...extraHeaders },
|
|
253
|
+
query: {
|
|
254
|
+
operationName: 'ValidateSession',
|
|
255
|
+
operationHash: 'hash',
|
|
256
|
+
variables: JSON.stringify({ session: {} }),
|
|
257
|
+
},
|
|
258
|
+
}) as unknown as NextApiRequest
|
|
259
|
+
|
|
260
|
+
beforeEach(() => {
|
|
261
|
+
vi.clearAllMocks()
|
|
262
|
+
discoveryConfig.experimental.refreshToken = true
|
|
263
|
+
mockedShouldForceRefreshToken.mockReturnValue(true)
|
|
264
|
+
mockedExecute.mockResolvedValue({
|
|
265
|
+
data: { validateSession: null },
|
|
266
|
+
errors: undefined,
|
|
267
|
+
extensions: { cookies: new Map(), cacheControl: undefined },
|
|
268
|
+
} as never)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
afterEach(() => {
|
|
272
|
+
discoveryConfig.experimental.refreshToken = originalRefreshToken
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
it.each([
|
|
276
|
+
'localhost:3000',
|
|
277
|
+
'127.0.0.1:3000',
|
|
278
|
+
'brandless-abc123.preview.vtex.app',
|
|
279
|
+
])('does not force a 401 on %s', async (host) => {
|
|
280
|
+
const res = createResponse()
|
|
281
|
+
await handler(createValidateSessionRequest(host), res)
|
|
282
|
+
|
|
283
|
+
expect(mockedShouldForceRefreshToken).not.toHaveBeenCalled()
|
|
284
|
+
expect(res.status).not.toHaveBeenCalledWith(401)
|
|
285
|
+
expect(mockedExecute).toHaveBeenCalled()
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
it('does not force a 401 on a preview behind the ingress', async () => {
|
|
289
|
+
const res = createResponse()
|
|
290
|
+
await handler(
|
|
291
|
+
createValidateSessionRequest('faststore-internal:3000', {
|
|
292
|
+
'x-forwarded-host': 'brandless-abc123.preview.vtex.app',
|
|
293
|
+
}),
|
|
294
|
+
res
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
expect(mockedShouldForceRefreshToken).not.toHaveBeenCalled()
|
|
298
|
+
expect(res.status).not.toHaveBeenCalledWith(401)
|
|
299
|
+
expect(mockedExecute).toHaveBeenCalled()
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
it.each(['brandless.vtex.app', 'brandless.fast.store'])(
|
|
303
|
+
'forces a 401 on the production host %s',
|
|
304
|
+
async (host) => {
|
|
305
|
+
const res = createResponse()
|
|
306
|
+
await handler(createValidateSessionRequest(host), res)
|
|
307
|
+
|
|
308
|
+
expect(res.status).toHaveBeenCalledWith(401)
|
|
309
|
+
expect(mockedExecute).not.toHaveBeenCalled()
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
describe('/api/graphql success cache-control', () => {
|
|
315
|
+
const originalGraphqlCacheControl =
|
|
316
|
+
discoveryConfig.experimental.graphqlCacheControl
|
|
317
|
+
|
|
318
|
+
const mockExecuteWithCacheControl = (cacheControl?: unknown) => {
|
|
319
|
+
mockedExecute.mockResolvedValue({
|
|
320
|
+
data: {},
|
|
321
|
+
errors: undefined,
|
|
322
|
+
extensions: { cookies: new Map(), cacheControl },
|
|
323
|
+
} as never)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
beforeEach(() => {
|
|
327
|
+
vi.clearAllMocks()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
afterEach(() => {
|
|
331
|
+
discoveryConfig.experimental.graphqlCacheControl =
|
|
332
|
+
originalGraphqlCacheControl
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('uses the cache-control collected while executing the operation', async () => {
|
|
336
|
+
mockExecuteWithCacheControl({ scope: 'public', sMaxAge: 120 })
|
|
337
|
+
|
|
338
|
+
const res = createResponse()
|
|
339
|
+
await handler(createRequest(), res)
|
|
340
|
+
|
|
341
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
342
|
+
'cache-control',
|
|
343
|
+
'public, s-maxage=120, stale-while-revalidate=0'
|
|
344
|
+
)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it('makes collected cache-control private when there is an auth cookie', async () => {
|
|
348
|
+
mockExecuteWithCacheControl({ scope: 'public', sMaxAge: 120 })
|
|
349
|
+
|
|
350
|
+
const req = createRequest()
|
|
351
|
+
req.headers.cookie = 'VtexIdclientAutCookie_store=jwt'
|
|
352
|
+
const res = createResponse()
|
|
353
|
+
await handler(req, res)
|
|
354
|
+
|
|
355
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
356
|
+
'cache-control',
|
|
357
|
+
'private, s-maxage=120, stale-while-revalidate=0'
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it('falls back to the default max-age for GET queries without collected cache-control', async () => {
|
|
362
|
+
discoveryConfig.experimental.graphqlCacheControl = {
|
|
363
|
+
maxAge: 0,
|
|
364
|
+
staleWhileRevalidate: 0,
|
|
365
|
+
}
|
|
366
|
+
mockExecuteWithCacheControl()
|
|
367
|
+
|
|
368
|
+
const res = createResponse()
|
|
369
|
+
await handler(createRequest(), res)
|
|
370
|
+
|
|
371
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
372
|
+
'cache-control',
|
|
373
|
+
'public, s-maxage=300, stale-while-revalidate=3600'
|
|
374
|
+
)
|
|
375
|
+
})
|
|
376
|
+
|
|
377
|
+
it('uses the configured max-age and stale-while-revalidate for GET queries', async () => {
|
|
378
|
+
discoveryConfig.experimental.graphqlCacheControl = {
|
|
379
|
+
maxAge: 60,
|
|
380
|
+
staleWhileRevalidate: 30,
|
|
381
|
+
}
|
|
382
|
+
mockExecuteWithCacheControl()
|
|
383
|
+
|
|
384
|
+
const req = createRequest()
|
|
385
|
+
req.headers.cookie = 'VtexIdclientAutCookie_store=jwt'
|
|
386
|
+
const res = createResponse()
|
|
387
|
+
await handler(req, res)
|
|
388
|
+
|
|
389
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
390
|
+
'cache-control',
|
|
391
|
+
'private, s-maxage=60, stale-while-revalidate=30'
|
|
392
|
+
)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('does not cache operations that are not GET queries', async () => {
|
|
396
|
+
mockExecuteWithCacheControl()
|
|
397
|
+
|
|
398
|
+
const res = createResponse()
|
|
399
|
+
await handler(createRequest('SubscribeToNewsletter'), res)
|
|
400
|
+
|
|
401
|
+
expect(res.setHeader).toHaveBeenCalledWith(
|
|
402
|
+
'cache-control',
|
|
403
|
+
'no-cache, no-store'
|
|
404
|
+
)
|
|
405
|
+
})
|
|
406
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
const mockExecute = vi.hoisted(() => vi.fn())
|
|
4
|
+
|
|
5
|
+
vi.mock('src/server', () => ({ execute: mockExecute }))
|
|
6
|
+
vi.mock('src/server/cms/global', () => ({
|
|
7
|
+
injectGlobalSections: vi.fn(() => ({ sections: [], settings: {} })),
|
|
8
|
+
}))
|
|
9
|
+
vi.mock('src/server/cms/fetchMyAccountPageContent', () => ({
|
|
10
|
+
fetchMyAccountPageContent: vi.fn(async () => ({ sections: [] })),
|
|
11
|
+
}))
|
|
12
|
+
vi.mock('src/components/cms/GlobalSections', () => ({
|
|
13
|
+
getGlobalSectionsData: vi.fn(() => [
|
|
14
|
+
Promise.resolve(null),
|
|
15
|
+
Promise.resolve(null),
|
|
16
|
+
Promise.resolve(null),
|
|
17
|
+
]),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
import storeConfig from 'discovery.config'
|
|
21
|
+
import { getServerSideProps } from 'src/pages/pvt/account/403'
|
|
22
|
+
|
|
23
|
+
function makeContext(host: string) {
|
|
24
|
+
return {
|
|
25
|
+
req: { headers: { host } },
|
|
26
|
+
query: {},
|
|
27
|
+
previewData: undefined,
|
|
28
|
+
locale: undefined,
|
|
29
|
+
} as any
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('/pvt/account/403 getServerSideProps refresh-token bypass', () => {
|
|
33
|
+
const originalRefreshToken = storeConfig.experimental.refreshToken
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
storeConfig.experimental.refreshToken = true
|
|
37
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
38
|
+
mockExecute.mockResolvedValue({
|
|
39
|
+
data: null,
|
|
40
|
+
errors: [{ extensions: { status: 401 } }],
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
storeConfig.experimental.refreshToken = originalRefreshToken
|
|
46
|
+
vi.restoreAllMocks()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it.each([
|
|
50
|
+
'localhost:3000',
|
|
51
|
+
'127.0.0.1:3000',
|
|
52
|
+
'brandless-abc123.preview.vtex.app',
|
|
53
|
+
])('does not ask for a token refresh on %s', async (host) => {
|
|
54
|
+
const result: any = await getServerSideProps(makeContext(host))
|
|
55
|
+
|
|
56
|
+
expect(result.props.needsRefreshToken).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it.each(['brandless.vtex.app', 'brandless.fast.store'])(
|
|
60
|
+
'asks for a token refresh on the production host %s',
|
|
61
|
+
async (host) => {
|
|
62
|
+
const result: any = await getServerSideProps(makeContext(host))
|
|
63
|
+
|
|
64
|
+
expect(result.props.needsRefreshToken).toBe(true)
|
|
65
|
+
}
|
|
66
|
+
)
|
|
67
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { renderHook, waitFor } from '@testing-library/react'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
const mockRefreshTokenRequest = vi.hoisted(() => vi.fn())
|
|
5
|
+
const mockLogoutAndClearSession = vi.hoisted(() => vi.fn())
|
|
6
|
+
|
|
7
|
+
vi.mock('../../../src/sdk/account/refreshToken', () => ({
|
|
8
|
+
refreshTokenRequest: mockRefreshTokenRequest,
|
|
9
|
+
isRefreshTokenSuccessful: () => false,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
vi.mock('../../../src/sdk/session', () => ({
|
|
13
|
+
logoutAndClearSession: mockLogoutAndClearSession,
|
|
14
|
+
sessionStore: { read: () => null, readInitial: () => ({}) },
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
import { useRefreshToken } from '../../../src/sdk/account/useRefreshToken'
|
|
18
|
+
|
|
19
|
+
const originalLocation = window.location
|
|
20
|
+
|
|
21
|
+
function stubHostname(hostname: string) {
|
|
22
|
+
// jsdom forbids assigning to `window.location` directly, so we redefine the
|
|
23
|
+
// descriptor with a minimal stub that exposes the hostname we want to test.
|
|
24
|
+
Object.defineProperty(window, 'location', {
|
|
25
|
+
configurable: true,
|
|
26
|
+
value: { ...originalLocation, hostname },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('useRefreshToken', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
mockRefreshTokenRequest.mockResolvedValue({ status: 'error' })
|
|
33
|
+
mockLogoutAndClearSession.mockResolvedValue(undefined)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.clearAllMocks()
|
|
38
|
+
Object.defineProperty(window, 'location', {
|
|
39
|
+
configurable: true,
|
|
40
|
+
value: originalLocation,
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it.each(['localhost', 'brandless-abc123.preview.vtex.app'])(
|
|
45
|
+
'shows the 403 view without refreshing or logging out on %s',
|
|
46
|
+
async (hostname) => {
|
|
47
|
+
stubHostname(hostname)
|
|
48
|
+
|
|
49
|
+
const { result } = renderHook(() => useRefreshToken(true))
|
|
50
|
+
|
|
51
|
+
await waitFor(() => expect(result.current.shouldShow403).toBe(true))
|
|
52
|
+
expect(mockRefreshTokenRequest).not.toHaveBeenCalled()
|
|
53
|
+
expect(mockLogoutAndClearSession).not.toHaveBeenCalled()
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
it.each(['brandless.vtex.app', 'brandless.fast.store'])(
|
|
58
|
+
'refreshes the token and logs out when it fails on %s',
|
|
59
|
+
async (hostname) => {
|
|
60
|
+
stubHostname(hostname)
|
|
61
|
+
|
|
62
|
+
const { result } = renderHook(() => useRefreshToken(true))
|
|
63
|
+
|
|
64
|
+
await waitFor(() => expect(result.current.shouldShow403).toBe(true))
|
|
65
|
+
expect(mockRefreshTokenRequest).toHaveBeenCalledTimes(1)
|
|
66
|
+
expect(mockLogoutAndClearSession).toHaveBeenCalledTimes(1)
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
})
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react'
|
|
6
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
|
|
8
|
+
const mockUseSession = vi.hoisted(() => vi.fn())
|
|
9
|
+
const mockUseQuery = vi.hoisted(() => vi.fn())
|
|
10
|
+
const mockUseLocalizedVariables = vi.hoisted(() => vi.fn())
|
|
11
|
+
|
|
12
|
+
vi.mock('@generated', () => ({ gql: (query: unknown) => query }))
|
|
13
|
+
vi.mock('discovery.config', () => ({
|
|
14
|
+
default: { deliveryPromise: { enabled: false } },
|
|
15
|
+
}))
|
|
16
|
+
vi.mock('@faststore/sdk', () => ({
|
|
17
|
+
useSearch: () => ({
|
|
18
|
+
state: { sort: 'score_desc', term: '', selectedFacets: [] },
|
|
19
|
+
itemsPerPage: 12,
|
|
20
|
+
}),
|
|
21
|
+
}))
|
|
22
|
+
vi.mock('src/sdk/session', () => ({ useSession: mockUseSession }))
|
|
23
|
+
vi.mock('src/sdk/graphql/useQuery', () => ({ useQuery: mockUseQuery }))
|
|
24
|
+
vi.mock('src/sdk/product/useLocalizedVariables', () => ({
|
|
25
|
+
useLocalizedVariables: mockUseLocalizedVariables,
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
hashContractScope,
|
|
30
|
+
useCreateUseGalleryPage,
|
|
31
|
+
} from '../../../src/sdk/product/usePageProductsQuery'
|
|
32
|
+
|
|
33
|
+
const serverManyProductsVariables = {
|
|
34
|
+
first: 12,
|
|
35
|
+
after: '0',
|
|
36
|
+
sort: 'score_desc' as const,
|
|
37
|
+
term: '',
|
|
38
|
+
selectedFacets: [
|
|
39
|
+
{ key: 'category-1', value: 'computers---tablets' },
|
|
40
|
+
{ key: 'channel', value: '{"salesChannel":"1"}' },
|
|
41
|
+
{ key: 'locale', value: 'en-CA' },
|
|
42
|
+
],
|
|
43
|
+
sponsoredCount: 3,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type GalleryProps = NonNullable<Parameters<typeof useCreateUseGalleryPage>[0]>
|
|
47
|
+
|
|
48
|
+
const initialPages = {
|
|
49
|
+
search: { products: { pageInfo: { totalCount: 2 }, edges: [] } },
|
|
50
|
+
} as unknown as GalleryProps['initialPages']
|
|
51
|
+
|
|
52
|
+
const renderFirstGalleryPage = () => {
|
|
53
|
+
const { result: created } = renderHook(() =>
|
|
54
|
+
useCreateUseGalleryPage({ initialPages, serverManyProductsVariables })
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return renderHook(() => created.current.useGalleryPage(0))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('useCreateUseGalleryPage', () => {
|
|
61
|
+
afterEach(() => {
|
|
62
|
+
vi.clearAllMocks()
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('reuses the SSG first page for anonymous shoppers', () => {
|
|
66
|
+
mockUseSession.mockReturnValue({
|
|
67
|
+
postalCode: null,
|
|
68
|
+
b2b: null,
|
|
69
|
+
isValidating: false,
|
|
70
|
+
})
|
|
71
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
72
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
73
|
+
|
|
74
|
+
const { result } = renderFirstGalleryPage()
|
|
75
|
+
|
|
76
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
77
|
+
expect.anything(),
|
|
78
|
+
serverManyProductsVariables,
|
|
79
|
+
expect.objectContaining({ doNotRun: true })
|
|
80
|
+
)
|
|
81
|
+
expect(result.current.data).toBe(initialPages)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('re-fetches the first page for B2B buyers so the contract assortment applies', () => {
|
|
85
|
+
mockUseSession.mockReturnValue({
|
|
86
|
+
postalCode: null,
|
|
87
|
+
b2b: { customerId: 'contract-137' },
|
|
88
|
+
isValidating: false,
|
|
89
|
+
})
|
|
90
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
91
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
92
|
+
|
|
93
|
+
renderFirstGalleryPage()
|
|
94
|
+
|
|
95
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
96
|
+
expect.anything(),
|
|
97
|
+
{
|
|
98
|
+
...serverManyProductsVariables,
|
|
99
|
+
_contract: hashContractScope('contract-137'),
|
|
100
|
+
},
|
|
101
|
+
expect.objectContaining({ doNotRun: false })
|
|
102
|
+
)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it('still re-fetches for B2B buyers whose customerId resolves to an empty string', () => {
|
|
106
|
+
// `buildB2bSession` in @faststore/api can resolve `customerId` to `''`
|
|
107
|
+
// (see validateSessionHelpers.ts) even for a representative with an
|
|
108
|
+
// active contract. The fix must not depend on the value being truthy —
|
|
109
|
+
// only on the `b2b` session existing — or it silently falls back to the
|
|
110
|
+
// anonymous behavior (reusing the unfiltered SSG page).
|
|
111
|
+
mockUseSession.mockReturnValue({
|
|
112
|
+
postalCode: null,
|
|
113
|
+
b2b: { customerId: '' },
|
|
114
|
+
isValidating: false,
|
|
115
|
+
})
|
|
116
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
117
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
118
|
+
|
|
119
|
+
renderFirstGalleryPage()
|
|
120
|
+
|
|
121
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
122
|
+
expect.anything(),
|
|
123
|
+
{ ...serverManyProductsVariables, _contract: hashContractScope('b2b') },
|
|
124
|
+
expect.objectContaining({ doNotRun: false })
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('hashes the contract scope to a stable value', () => {
|
|
129
|
+
// Golden value: catches a change to the hash itself, which the other
|
|
130
|
+
// tests miss because they derive their expectations from it.
|
|
131
|
+
expect(hashContractScope('contract-137')).toBe('185k3l4sd4t')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('keeps the raw contract id out of the query variables', () => {
|
|
135
|
+
mockUseSession.mockReturnValue({
|
|
136
|
+
postalCode: null,
|
|
137
|
+
b2b: { customerId: 'contract-137', unitId: 'unit-42' },
|
|
138
|
+
isValidating: false,
|
|
139
|
+
})
|
|
140
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
141
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
142
|
+
|
|
143
|
+
renderFirstGalleryPage()
|
|
144
|
+
|
|
145
|
+
// The variables go in the GET URL, so they must not carry the raw ids.
|
|
146
|
+
const variables = JSON.stringify(mockUseQuery.mock.calls.at(-1)?.[1])
|
|
147
|
+
expect(variables).not.toContain('contract-137')
|
|
148
|
+
expect(variables).not.toContain('unit-42')
|
|
149
|
+
expect(hashContractScope('contract-a')).not.toBe(
|
|
150
|
+
hashContractScope('contract-b')
|
|
151
|
+
)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('tells B2B buyers apart by organizational unit when customerId is empty', () => {
|
|
155
|
+
mockUseSession.mockReturnValue({
|
|
156
|
+
postalCode: null,
|
|
157
|
+
b2b: { customerId: '', unitId: 'unit-42' },
|
|
158
|
+
isValidating: false,
|
|
159
|
+
})
|
|
160
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
161
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
162
|
+
|
|
163
|
+
renderFirstGalleryPage()
|
|
164
|
+
|
|
165
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
166
|
+
expect.anything(),
|
|
167
|
+
{
|
|
168
|
+
...serverManyProductsVariables,
|
|
169
|
+
_contract: hashContractScope('unit-42'),
|
|
170
|
+
},
|
|
171
|
+
expect.objectContaining({ doNotRun: false })
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('re-fetches the first page when the buyer switches contract', () => {
|
|
176
|
+
const contractPage = {
|
|
177
|
+
search: { products: { pageInfo: { totalCount: 1 }, edges: [] } },
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
mockUseSession.mockReturnValue({
|
|
181
|
+
postalCode: null,
|
|
182
|
+
b2b: { customerId: 'contract-a' },
|
|
183
|
+
isValidating: false,
|
|
184
|
+
})
|
|
185
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
186
|
+
mockUseQuery.mockReturnValue({ data: contractPage })
|
|
187
|
+
|
|
188
|
+
const { result: created } = renderHook(() =>
|
|
189
|
+
useCreateUseGalleryPage({ initialPages, serverManyProductsVariables })
|
|
190
|
+
)
|
|
191
|
+
const { rerender } = renderHook(() => created.current.useGalleryPage(0))
|
|
192
|
+
|
|
193
|
+
mockUseSession.mockReturnValue({
|
|
194
|
+
postalCode: null,
|
|
195
|
+
b2b: { customerId: 'contract-b' },
|
|
196
|
+
isValidating: false,
|
|
197
|
+
})
|
|
198
|
+
rerender()
|
|
199
|
+
|
|
200
|
+
expect(mockUseQuery).toHaveBeenLastCalledWith(
|
|
201
|
+
expect.anything(),
|
|
202
|
+
{
|
|
203
|
+
...serverManyProductsVariables,
|
|
204
|
+
_contract: hashContractScope('contract-b'),
|
|
205
|
+
},
|
|
206
|
+
expect.objectContaining({ doNotRun: false })
|
|
207
|
+
)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('stops scoping the first page once the B2B session ends', () => {
|
|
211
|
+
const contractPage = {
|
|
212
|
+
search: { products: { pageInfo: { totalCount: 1 }, edges: [] } },
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
mockUseSession.mockReturnValue({
|
|
216
|
+
postalCode: null,
|
|
217
|
+
b2b: { customerId: 'contract-a' },
|
|
218
|
+
isValidating: false,
|
|
219
|
+
})
|
|
220
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
221
|
+
mockUseQuery.mockReturnValue({ data: contractPage })
|
|
222
|
+
|
|
223
|
+
const { result: created } = renderHook(() =>
|
|
224
|
+
useCreateUseGalleryPage({ initialPages, serverManyProductsVariables })
|
|
225
|
+
)
|
|
226
|
+
const { rerender } = renderHook(() => created.current.useGalleryPage(0))
|
|
227
|
+
|
|
228
|
+
mockUseSession.mockReturnValue({
|
|
229
|
+
postalCode: null,
|
|
230
|
+
b2b: null,
|
|
231
|
+
isValidating: false,
|
|
232
|
+
})
|
|
233
|
+
rerender()
|
|
234
|
+
|
|
235
|
+
expect(mockUseQuery).toHaveBeenLastCalledWith(
|
|
236
|
+
expect.anything(),
|
|
237
|
+
serverManyProductsVariables,
|
|
238
|
+
expect.objectContaining({ doNotRun: false })
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('ignores the postal code while deliveryPromise is disabled', () => {
|
|
243
|
+
mockUseSession.mockReturnValue({
|
|
244
|
+
postalCode: '01310-100',
|
|
245
|
+
b2b: null,
|
|
246
|
+
isValidating: false,
|
|
247
|
+
})
|
|
248
|
+
mockUseLocalizedVariables.mockReturnValue(serverManyProductsVariables)
|
|
249
|
+
mockUseQuery.mockReturnValue({ data: null })
|
|
250
|
+
|
|
251
|
+
const { result } = renderFirstGalleryPage()
|
|
252
|
+
|
|
253
|
+
expect(mockUseQuery).toHaveBeenCalledWith(
|
|
254
|
+
expect.anything(),
|
|
255
|
+
serverManyProductsVariables,
|
|
256
|
+
expect.objectContaining({ doNotRun: true })
|
|
257
|
+
)
|
|
258
|
+
expect(result.current.data).toBe(initialPages)
|
|
259
|
+
})
|
|
260
|
+
})
|