@faststore/core 4.8.1-dev.1 → 4.8.1-dev.2
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 +126 -123
- package/@generated/gql.ts +3 -3
- package/@generated/graphql.ts +4 -2
- package/@generated/persisted-documents.json +1 -1
- package/@generated/schema.graphql +4 -0
- package/CHANGELOG.md +6 -0
- package/package.json +6 -6
- package/src/components/account/Drawer/OrganizationDrawer/ContractSwitcher/ContractSwitcher.tsx +4 -6
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawer.tsx +18 -3
- package/src/components/account/Drawer/OrganizationDrawer/OrganizationDrawerHeader.tsx +1 -0
- package/src/components/account/Drawer/OrganizationDrawer/section.module.scss +7 -0
- package/src/components/account/Drawer/OrganizationSignInButton/OrganizationSignInButton.tsx +7 -9
- package/src/components/account/Menu/Menu.tsx +24 -7
- package/src/components/navigation/Navbar/Navbar.tsx +17 -19
- package/src/components/navigation/Navbar/NavbarAccountArea.tsx +57 -0
- package/src/components/navigation/NavbarSlider/NavbarSlider.tsx +16 -18
- package/src/pages/api/fs/switch-contract.ts +194 -0
- package/src/sdk/account/changeContractToken.ts +19 -63
- package/src/sdk/account/clearPersistedSessionState.ts +13 -7
- package/src/sdk/account/useAvailableContracts.ts +1 -0
- package/src/sdk/account/useSwitchContract.ts +11 -3
- package/src/sdk/session/index.ts +2 -1
- package/src/sdk/session/isSignInAreaResolved.ts +12 -0
- package/test/components/account/ContractSwitcher.browser.test.tsx +42 -0
- package/test/components/account/Menu.browser.test.tsx +123 -0
- package/test/components/account/OrganizationDrawer.browser.test.tsx +148 -0
- package/test/components/account/OrganizationDrawerHeader.browser.test.tsx +2 -0
- package/test/components/account/OrganizationSignInButton.browser.test.tsx +49 -0
- package/test/components/navigation/Navbar.browser.test.tsx +183 -0
- package/test/components/navigation/NavbarAccountArea.browser.test.tsx +115 -0
- package/test/components/navigation/NavbarSlider.browser.test.tsx +143 -0
- package/test/pages/api/switch-contract.test.ts +385 -0
- package/test/sdk/account/changeContractToken.browser.test.ts +23 -71
- package/test/sdk/account/clearPersistedSessionState.browser.test.ts +24 -0
- package/test/sdk/account/useSwitchContract.browser.test.ts +21 -0
- package/test/sdk/session/isSignInAreaResolved.test.ts +22 -0
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import type { NextApiRequest, NextApiResponse } from 'next'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi, type Mock } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import type { SwitchContractResponse } from '../../../src/pages/api/fs/switch-contract'
|
|
5
|
+
|
|
6
|
+
vi.mock('discovery.config', () => ({
|
|
7
|
+
__esModule: true,
|
|
8
|
+
default: { api: { storeId: 'test-store' } },
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
const CONTRACT_ID = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
|
|
12
|
+
const ORDER_FORM_COOKIES =
|
|
13
|
+
'checkout.vtex.com=__ofid=abc123; CheckoutOrderFormOwnership=owner-1'
|
|
14
|
+
const AUTH_COOKIE = 'VtexIdclientAutCookie_test-store=old-token'
|
|
15
|
+
|
|
16
|
+
const successPayload = {
|
|
17
|
+
authStatus: 'Success',
|
|
18
|
+
expiresIn: 3600,
|
|
19
|
+
authCookie: {
|
|
20
|
+
Name: 'VtexIdclientAutCookie_test-store',
|
|
21
|
+
Value: 'new-contract-token',
|
|
22
|
+
},
|
|
23
|
+
accountAuthCookie: null,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createReqRes(overrides: Partial<NextApiRequest> = {}): {
|
|
27
|
+
req: NextApiRequest
|
|
28
|
+
res: NextApiResponse<SwitchContractResponse> & {
|
|
29
|
+
status: Mock
|
|
30
|
+
json: Mock
|
|
31
|
+
end: Mock
|
|
32
|
+
setHeader: Mock
|
|
33
|
+
}
|
|
34
|
+
} {
|
|
35
|
+
const res: Record<string, unknown> = {
|
|
36
|
+
status: vi.fn().mockImplementation(function (this: unknown) {
|
|
37
|
+
return this
|
|
38
|
+
}),
|
|
39
|
+
json: vi.fn(),
|
|
40
|
+
end: vi.fn(),
|
|
41
|
+
setHeader: vi.fn(),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
req: {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
body: { contractId: CONTRACT_ID },
|
|
48
|
+
query: {},
|
|
49
|
+
...overrides,
|
|
50
|
+
headers: {
|
|
51
|
+
host: 'store.example.com',
|
|
52
|
+
cookie: `${AUTH_COOKIE}; ${ORDER_FORM_COOKIES}`,
|
|
53
|
+
'x-forwarded-proto': 'https',
|
|
54
|
+
...overrides.headers,
|
|
55
|
+
},
|
|
56
|
+
} as unknown as NextApiRequest,
|
|
57
|
+
res: res as unknown as ReturnType<typeof createReqRes>['res'],
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function getHandler() {
|
|
62
|
+
const mod = await import('../../../src/pages/api/fs/switch-contract')
|
|
63
|
+
return mod.default
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const setCookiesFrom = (res: { setHeader: Mock }): string[] => {
|
|
67
|
+
const call = res.setHeader.mock.calls.find(
|
|
68
|
+
([name]) => String(name).toLowerCase() === 'set-cookie'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return (call?.[1] as string[] | undefined) ?? []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe('/api/fs/switch-contract', () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
vi.resetModules()
|
|
77
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
78
|
+
ok: true,
|
|
79
|
+
status: 200,
|
|
80
|
+
json: async () => successPayload,
|
|
81
|
+
})
|
|
82
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('returns 405 for non-POST requests', async () => {
|
|
86
|
+
const handler = await getHandler()
|
|
87
|
+
const { req, res } = createReqRes({ method: 'GET' })
|
|
88
|
+
|
|
89
|
+
await handler(req, res)
|
|
90
|
+
|
|
91
|
+
expect(res.status).toHaveBeenCalledWith(405)
|
|
92
|
+
expect(res.end).toHaveBeenCalled()
|
|
93
|
+
expect(global.fetch).not.toHaveBeenCalled()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('returns 400 when contractId is missing', async () => {
|
|
97
|
+
const handler = await getHandler()
|
|
98
|
+
const { req, res } = createReqRes({ body: {} })
|
|
99
|
+
|
|
100
|
+
await handler(req, res)
|
|
101
|
+
|
|
102
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
103
|
+
expect(res.json).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({
|
|
105
|
+
success: false,
|
|
106
|
+
error: 'contractId is required',
|
|
107
|
+
})
|
|
108
|
+
)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns 400 when the body is missing', async () => {
|
|
112
|
+
const handler = await getHandler()
|
|
113
|
+
const { req, res } = createReqRes({ body: undefined })
|
|
114
|
+
|
|
115
|
+
await handler(req, res)
|
|
116
|
+
|
|
117
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('returns 400 when contractId is not a string', async () => {
|
|
121
|
+
const handler = await getHandler()
|
|
122
|
+
const { req, res } = createReqRes({ body: { contractId: 42 } })
|
|
123
|
+
|
|
124
|
+
await handler(req, res)
|
|
125
|
+
|
|
126
|
+
expect(res.status).toHaveBeenCalledWith(400)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('relays to switch-properties forwarding the buyer cookie', async () => {
|
|
130
|
+
const handler = await getHandler()
|
|
131
|
+
const { req, res } = createReqRes()
|
|
132
|
+
|
|
133
|
+
await handler(req, res)
|
|
134
|
+
|
|
135
|
+
expect(global.fetch).toHaveBeenCalledTimes(1)
|
|
136
|
+
const [url, init] = (global.fetch as Mock).mock.calls[0]
|
|
137
|
+
|
|
138
|
+
expect(url).toBe(
|
|
139
|
+
'https://test-store.myvtex.com/api/authenticator/storefront/credential/switch-properties?an=test-store'
|
|
140
|
+
)
|
|
141
|
+
expect(init.method).toBe('POST')
|
|
142
|
+
expect(init.headers.cookie).toBe(`${AUTH_COOKIE}; ${ORDER_FORM_COOKIES}`)
|
|
143
|
+
expect(init.signal).toBeDefined()
|
|
144
|
+
expect(JSON.parse(init.body as string)).toEqual({
|
|
145
|
+
properties: { customerId: CONTRACT_ID },
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('refuses to follow redirects so the cookie cannot leak cross-origin', async () => {
|
|
150
|
+
const handler = await getHandler()
|
|
151
|
+
const { req, res } = createReqRes()
|
|
152
|
+
|
|
153
|
+
await handler(req, res)
|
|
154
|
+
|
|
155
|
+
const [, init] = (global.fetch as Mock).mock.calls[0]
|
|
156
|
+
|
|
157
|
+
expect(init.redirect).toBe('error')
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
it('emits the new auth cookie and the orderForm expiry in one batch', async () => {
|
|
161
|
+
const handler = await getHandler()
|
|
162
|
+
const { req, res } = createReqRes()
|
|
163
|
+
|
|
164
|
+
await handler(req, res)
|
|
165
|
+
|
|
166
|
+
const cookies = setCookiesFrom(res)
|
|
167
|
+
|
|
168
|
+
expect(cookies).toEqual(
|
|
169
|
+
expect.arrayContaining([
|
|
170
|
+
expect.stringContaining(
|
|
171
|
+
'VtexIdclientAutCookie_test-store=new-contract-token'
|
|
172
|
+
),
|
|
173
|
+
])
|
|
174
|
+
)
|
|
175
|
+
expect(
|
|
176
|
+
cookies.filter((cookie) => cookie.startsWith('checkout.vtex.com=;'))
|
|
177
|
+
.length
|
|
178
|
+
).toBeGreaterThan(0)
|
|
179
|
+
expect(
|
|
180
|
+
cookies.filter((cookie) =>
|
|
181
|
+
cookie.startsWith('CheckoutOrderFormOwnership=;')
|
|
182
|
+
).length
|
|
183
|
+
).toBeGreaterThan(0)
|
|
184
|
+
expect(res.status).toHaveBeenCalledWith(200)
|
|
185
|
+
expect(res.json).toHaveBeenCalledWith({ success: true })
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('carries Max-Age from expiresIn and Secure behind https', async () => {
|
|
189
|
+
const handler = await getHandler()
|
|
190
|
+
const { req, res } = createReqRes()
|
|
191
|
+
|
|
192
|
+
await handler(req, res)
|
|
193
|
+
|
|
194
|
+
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
195
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
expect(authCookie).toContain('Max-Age=3600')
|
|
199
|
+
expect(authCookie).toContain('Path=/')
|
|
200
|
+
expect(authCookie).toContain('SameSite=Lax')
|
|
201
|
+
expect(authCookie).toContain('Secure')
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it.each([
|
|
205
|
+
['zero', 0],
|
|
206
|
+
['negative', -1],
|
|
207
|
+
['fractional', 1.5],
|
|
208
|
+
['a numeric string', '3600'],
|
|
209
|
+
['not a number', 'soon'],
|
|
210
|
+
['missing', undefined],
|
|
211
|
+
])(
|
|
212
|
+
'falls back to the default Max-Age when expiresIn is %s',
|
|
213
|
+
async (_label, expiresIn) => {
|
|
214
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
215
|
+
ok: true,
|
|
216
|
+
status: 200,
|
|
217
|
+
json: async () => ({ ...successPayload, expiresIn }),
|
|
218
|
+
})
|
|
219
|
+
const handler = await getHandler()
|
|
220
|
+
const { req, res } = createReqRes()
|
|
221
|
+
|
|
222
|
+
await handler(req, res)
|
|
223
|
+
|
|
224
|
+
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
225
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
// Max-Age=0 would delete the very credential this response installs.
|
|
229
|
+
expect(authCookie).toContain('Max-Age=86400')
|
|
230
|
+
expect(res.status).toHaveBeenCalledWith(200)
|
|
231
|
+
}
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
it('omits Secure on plain http', async () => {
|
|
235
|
+
const handler = await getHandler()
|
|
236
|
+
const { req, res } = createReqRes({
|
|
237
|
+
headers: { 'x-forwarded-proto': 'http' },
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
await handler(req, res)
|
|
241
|
+
|
|
242
|
+
const authCookie = setCookiesFrom(res).find((cookie) =>
|
|
243
|
+
cookie.startsWith('VtexIdclientAutCookie_test-store=')
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
expect(authCookie).not.toContain('Secure')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('also emits the accountAuthCookie when present', async () => {
|
|
250
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
251
|
+
ok: true,
|
|
252
|
+
status: 200,
|
|
253
|
+
json: async () => ({
|
|
254
|
+
...successPayload,
|
|
255
|
+
accountAuthCookie: {
|
|
256
|
+
Name: 'VtexIdclientAutCookie_account',
|
|
257
|
+
Value: 'account-token',
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
})
|
|
261
|
+
const handler = await getHandler()
|
|
262
|
+
const { req, res } = createReqRes()
|
|
263
|
+
|
|
264
|
+
await handler(req, res)
|
|
265
|
+
|
|
266
|
+
expect(setCookiesFrom(res)).toEqual(
|
|
267
|
+
expect.arrayContaining([
|
|
268
|
+
expect.stringContaining('VtexIdclientAutCookie_account=account-token'),
|
|
269
|
+
])
|
|
270
|
+
)
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
it('does not expire orderForm cookies that the request does not carry', async () => {
|
|
274
|
+
const handler = await getHandler()
|
|
275
|
+
const { req, res } = createReqRes({ headers: { cookie: AUTH_COOKIE } })
|
|
276
|
+
|
|
277
|
+
await handler(req, res)
|
|
278
|
+
|
|
279
|
+
const cookies = setCookiesFrom(res)
|
|
280
|
+
|
|
281
|
+
expect(
|
|
282
|
+
cookies.some((cookie) => cookie.startsWith('checkout.vtex.com=;'))
|
|
283
|
+
).toBe(false)
|
|
284
|
+
expect(res.status).toHaveBeenCalledWith(200)
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('returns 401 and sets no cookies when the upstream rejects the token', async () => {
|
|
288
|
+
;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 401 })
|
|
289
|
+
const handler = await getHandler()
|
|
290
|
+
const { req, res } = createReqRes()
|
|
291
|
+
|
|
292
|
+
await handler(req, res)
|
|
293
|
+
|
|
294
|
+
expect(res.status).toHaveBeenCalledWith(401)
|
|
295
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('maps upstream 403 to 401', async () => {
|
|
299
|
+
;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 403 })
|
|
300
|
+
const handler = await getHandler()
|
|
301
|
+
const { req, res } = createReqRes()
|
|
302
|
+
|
|
303
|
+
await handler(req, res)
|
|
304
|
+
|
|
305
|
+
expect(res.status).toHaveBeenCalledWith(401)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it('returns 502 and sets no cookies on other upstream failures', async () => {
|
|
309
|
+
;(global.fetch as Mock).mockResolvedValue({ ok: false, status: 500 })
|
|
310
|
+
const handler = await getHandler()
|
|
311
|
+
const { req, res } = createReqRes()
|
|
312
|
+
|
|
313
|
+
await handler(req, res)
|
|
314
|
+
|
|
315
|
+
expect(res.status).toHaveBeenCalledWith(502)
|
|
316
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
it('returns 502 and sets no cookies when authStatus is not success', async () => {
|
|
320
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
321
|
+
ok: true,
|
|
322
|
+
status: 200,
|
|
323
|
+
json: async () => ({ ...successPayload, authStatus: 'WrongCredentials' }),
|
|
324
|
+
})
|
|
325
|
+
const handler = await getHandler()
|
|
326
|
+
const { req, res } = createReqRes()
|
|
327
|
+
|
|
328
|
+
await handler(req, res)
|
|
329
|
+
|
|
330
|
+
expect(res.status).toHaveBeenCalledWith(502)
|
|
331
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
it('returns 502 and sets no cookies when no auth cookie comes back', async () => {
|
|
335
|
+
;(global.fetch as Mock).mockResolvedValue({
|
|
336
|
+
ok: true,
|
|
337
|
+
status: 200,
|
|
338
|
+
json: async () => ({ ...successPayload, authCookie: null }),
|
|
339
|
+
})
|
|
340
|
+
const handler = await getHandler()
|
|
341
|
+
const { req, res } = createReqRes()
|
|
342
|
+
|
|
343
|
+
await handler(req, res)
|
|
344
|
+
|
|
345
|
+
expect(res.status).toHaveBeenCalledWith(502)
|
|
346
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('returns 504 when the upstream call times out', async () => {
|
|
350
|
+
const timeout = new Error('aborted')
|
|
351
|
+
timeout.name = 'TimeoutError'
|
|
352
|
+
;(global.fetch as Mock).mockRejectedValue(timeout)
|
|
353
|
+
const handler = await getHandler()
|
|
354
|
+
const { req, res } = createReqRes()
|
|
355
|
+
|
|
356
|
+
await handler(req, res)
|
|
357
|
+
|
|
358
|
+
expect(res.status).toHaveBeenCalledWith(504)
|
|
359
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it('returns 502 when the upstream call throws', async () => {
|
|
363
|
+
;(global.fetch as Mock).mockRejectedValue(new Error('network failure'))
|
|
364
|
+
const handler = await getHandler()
|
|
365
|
+
const { req, res } = createReqRes()
|
|
366
|
+
|
|
367
|
+
await handler(req, res)
|
|
368
|
+
|
|
369
|
+
expect(res.status).toHaveBeenCalledWith(502)
|
|
370
|
+
expect(res.setHeader).not.toHaveBeenCalled()
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('does not log the error object, only its name', async () => {
|
|
374
|
+
;(global.fetch as Mock).mockRejectedValue(new Error('token=super-secret'))
|
|
375
|
+
const handler = await getHandler()
|
|
376
|
+
const { req, res } = createReqRes()
|
|
377
|
+
|
|
378
|
+
await handler(req, res)
|
|
379
|
+
|
|
380
|
+
const logged = (console.error as Mock).mock.calls.flat().join(' ')
|
|
381
|
+
|
|
382
|
+
expect(logged).not.toContain('super-secret')
|
|
383
|
+
expect(logged).toContain('Error')
|
|
384
|
+
})
|
|
385
|
+
})
|
|
@@ -4,44 +4,25 @@
|
|
|
4
4
|
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
6
|
|
|
7
|
-
vi.mock('discovery.config', () => ({
|
|
8
|
-
default: {
|
|
9
|
-
api: { storeId: 'b2bfaststoredev' },
|
|
10
|
-
},
|
|
11
|
-
}))
|
|
12
|
-
|
|
13
7
|
const mockFetch = vi.hoisted(() => vi.fn())
|
|
14
8
|
vi.mock('isomorphic-unfetch', () => ({ __esModule: true, default: mockFetch }))
|
|
15
9
|
|
|
16
10
|
import {
|
|
17
11
|
ContractSwitchError,
|
|
18
|
-
applyVtexAuthCookieFromSwitchResponse,
|
|
19
12
|
changeContractToken,
|
|
20
13
|
isContractSwitchEnabled,
|
|
21
14
|
} from '../../../src/sdk/account/changeContractToken'
|
|
22
15
|
|
|
23
|
-
const successPayload = {
|
|
24
|
-
authStatus: 'Success',
|
|
25
|
-
expiresIn: 86_399,
|
|
26
|
-
authCookie: {
|
|
27
|
-
Name: 'VtexIdclientAutCookie_b2bfaststoredev',
|
|
28
|
-
Value: 'new-contract-token',
|
|
29
|
-
},
|
|
30
|
-
accountAuthCookie: null,
|
|
31
|
-
}
|
|
32
|
-
|
|
33
16
|
describe('changeContractToken', () => {
|
|
34
17
|
beforeEach(() => {
|
|
35
|
-
document.cookie = ''
|
|
36
18
|
mockFetch.mockResolvedValue({
|
|
37
19
|
ok: true,
|
|
38
20
|
status: 200,
|
|
39
|
-
json: async () =>
|
|
21
|
+
json: async () => ({ success: true }),
|
|
40
22
|
})
|
|
41
23
|
})
|
|
42
24
|
|
|
43
25
|
afterEach(() => {
|
|
44
|
-
document.cookie = ''
|
|
45
26
|
vi.clearAllMocks()
|
|
46
27
|
})
|
|
47
28
|
|
|
@@ -51,35 +32,30 @@ describe('changeContractToken', () => {
|
|
|
51
32
|
|
|
52
33
|
it('rejects when contractId is missing', async () => {
|
|
53
34
|
await expect(changeContractToken('')).rejects.toThrow(ContractSwitchError)
|
|
35
|
+
expect(mockFetch).not.toHaveBeenCalled()
|
|
54
36
|
})
|
|
55
37
|
|
|
56
|
-
it('posts
|
|
38
|
+
it('posts the contractId to the switch-contract route', async () => {
|
|
57
39
|
const contractId = '69b5d80e-8f61-44e0-a5cb-db54c326699c'
|
|
58
40
|
|
|
59
41
|
await expect(changeContractToken(contractId)).resolves.toBe(true)
|
|
60
42
|
|
|
61
43
|
expect(mockFetch).toHaveBeenCalledTimes(1)
|
|
62
44
|
const [url, init] = mockFetch.mock.calls[0]
|
|
63
|
-
expect(url).toBe(
|
|
64
|
-
'/api/authenticator/storefront/credential/switch-properties?an=b2bfaststoredev'
|
|
65
|
-
)
|
|
45
|
+
expect(url).toBe('/api/fs/switch-contract')
|
|
66
46
|
expect(init.method).toBe('POST')
|
|
67
47
|
expect(init.credentials).toBe('include')
|
|
68
48
|
expect(init.signal).toBeDefined()
|
|
69
|
-
expect(JSON.parse(init.body as string)).toEqual({
|
|
70
|
-
properties: { customerId: contractId },
|
|
71
|
-
})
|
|
49
|
+
expect(JSON.parse(init.body as string)).toEqual({ contractId })
|
|
72
50
|
})
|
|
73
51
|
|
|
74
|
-
it('
|
|
52
|
+
it('never writes cookies from the client', async () => {
|
|
75
53
|
await changeContractToken('contract-1')
|
|
76
54
|
|
|
77
|
-
expect(document.cookie).toContain(
|
|
78
|
-
'VtexIdclientAutCookie_b2bfaststoredev=new-contract-token'
|
|
79
|
-
)
|
|
55
|
+
expect(document.cookie).not.toContain('VtexIdclientAutCookie')
|
|
80
56
|
})
|
|
81
57
|
|
|
82
|
-
it('throws when the
|
|
58
|
+
it('throws when the route responds with a failure status', async () => {
|
|
83
59
|
mockFetch.mockResolvedValueOnce({ ok: false, status: 401 })
|
|
84
60
|
|
|
85
61
|
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
@@ -87,7 +63,7 @@ describe('changeContractToken', () => {
|
|
|
87
63
|
)
|
|
88
64
|
})
|
|
89
65
|
|
|
90
|
-
it('throws when the
|
|
66
|
+
it('throws when the request times out', async () => {
|
|
91
67
|
const timeoutError = new Error('The operation was aborted')
|
|
92
68
|
timeoutError.name = 'TimeoutError'
|
|
93
69
|
mockFetch.mockRejectedValueOnce(timeoutError)
|
|
@@ -97,61 +73,37 @@ describe('changeContractToken', () => {
|
|
|
97
73
|
)
|
|
98
74
|
})
|
|
99
75
|
|
|
100
|
-
it('throws when
|
|
101
|
-
mockFetch.
|
|
102
|
-
ok: true,
|
|
103
|
-
status: 200,
|
|
104
|
-
json: async () => ({ ...successPayload, authStatus: 'Failed' }),
|
|
105
|
-
})
|
|
76
|
+
it('throws when the network call fails', async () => {
|
|
77
|
+
mockFetch.mockRejectedValueOnce(new Error('offline'))
|
|
106
78
|
|
|
107
79
|
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
108
|
-
/
|
|
80
|
+
/Failed to switch contract: offline/
|
|
109
81
|
)
|
|
110
82
|
})
|
|
111
83
|
|
|
112
|
-
it('throws when
|
|
84
|
+
it('throws when the route reports success: false', async () => {
|
|
113
85
|
mockFetch.mockResolvedValueOnce({
|
|
114
86
|
ok: true,
|
|
115
87
|
status: 200,
|
|
116
|
-
json: async () => ({
|
|
117
|
-
authStatus: 'Success',
|
|
118
|
-
authCookie: null,
|
|
119
|
-
}),
|
|
88
|
+
json: async () => ({ success: false, error: 'nope' }),
|
|
120
89
|
})
|
|
121
90
|
|
|
122
91
|
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
123
|
-
/
|
|
92
|
+
/Contract switch was not successful/
|
|
124
93
|
)
|
|
125
94
|
})
|
|
126
|
-
})
|
|
127
95
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
document.cookie = ''
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
it('sets auth and account auth cookies when present', () => {
|
|
138
|
-
applyVtexAuthCookieFromSwitchResponse({
|
|
139
|
-
expiresIn: 3600,
|
|
140
|
-
authCookie: {
|
|
141
|
-
Name: 'VtexIdclientAutCookie_b2bfaststoredev',
|
|
142
|
-
Value: 'store-token',
|
|
143
|
-
},
|
|
144
|
-
accountAuthCookie: {
|
|
145
|
-
Name: 'VtexIdclientAutCookie_account',
|
|
146
|
-
Value: 'account-token',
|
|
96
|
+
it('throws when the response body is not valid JSON', async () => {
|
|
97
|
+
mockFetch.mockResolvedValueOnce({
|
|
98
|
+
ok: true,
|
|
99
|
+
status: 200,
|
|
100
|
+
json: async () => {
|
|
101
|
+
throw new Error('invalid json')
|
|
147
102
|
},
|
|
148
103
|
})
|
|
149
104
|
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
)
|
|
153
|
-
expect(document.cookie).toContain(
|
|
154
|
-
'VtexIdclientAutCookie_account=account-token'
|
|
105
|
+
await expect(changeContractToken('contract-1')).rejects.toThrow(
|
|
106
|
+
/Contract switch was not successful/
|
|
155
107
|
)
|
|
156
108
|
})
|
|
157
109
|
})
|
|
@@ -38,4 +38,28 @@ describe('clearPersistedSessionState', () => {
|
|
|
38
38
|
expect(sessionStorage.getItem(STORAGE_KEY_PERSON_ID)).toBeNull()
|
|
39
39
|
expect(sessionStorage.getItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE)).toBeNull()
|
|
40
40
|
})
|
|
41
|
+
|
|
42
|
+
it('also clears the persisted cart (fs::cart)', async () => {
|
|
43
|
+
await clearPersistedSessionState()
|
|
44
|
+
|
|
45
|
+
expect(mockDel).toHaveBeenCalledWith('fs::cart')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('leaves cookies alone: the HttpOnly orderForm cookies are the route\u2019s job', async () => {
|
|
49
|
+
document.cookie = 'checkout.vtex.com=__ofid=abc123; path=/'
|
|
50
|
+
|
|
51
|
+
await clearPersistedSessionState()
|
|
52
|
+
|
|
53
|
+
expect(document.cookie).toContain('checkout.vtex.com')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('still clears sessionStorage when IndexedDB deletion rejects', async () => {
|
|
57
|
+
mockDel.mockRejectedValue(new Error('blocked'))
|
|
58
|
+
|
|
59
|
+
await expect(clearPersistedSessionState()).resolves.toBeUndefined()
|
|
60
|
+
|
|
61
|
+
expect(sessionStorage.getItem(SESSION_READY_KEY)).toBeNull()
|
|
62
|
+
expect(sessionStorage.getItem(STORAGE_KEY_PERSON_ID)).toBeNull()
|
|
63
|
+
expect(sessionStorage.getItem(STORAGE_KEY_CACHE_BUST_LAST_VALUE)).toBeNull()
|
|
64
|
+
})
|
|
41
65
|
})
|
|
@@ -69,6 +69,27 @@ describe('useSwitchContract', () => {
|
|
|
69
69
|
expect(result.current.error).toBeNull()
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
+
it('still reloads when clearing the persisted state fails', async () => {
|
|
73
|
+
vi.spyOn(
|
|
74
|
+
changeContractTokenModule,
|
|
75
|
+
'changeContractToken'
|
|
76
|
+
).mockResolvedValueOnce(true)
|
|
77
|
+
mockClearPersistedSessionState.mockRejectedValueOnce(new Error('blocked'))
|
|
78
|
+
|
|
79
|
+
const { result } = renderHook(() => useSwitchContract())
|
|
80
|
+
|
|
81
|
+
let ok: boolean | undefined
|
|
82
|
+
await act(async () => {
|
|
83
|
+
ok = await result.current.switchContract('contract-2')
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
// The route already swapped the cookies, so stopping here would leave the
|
|
87
|
+
// UI on the previous contract while the browser is on the new one.
|
|
88
|
+
expect(ok).toBe(true)
|
|
89
|
+
expect(mockReload).toHaveBeenCalledTimes(1)
|
|
90
|
+
expect(result.current.error).toBeNull()
|
|
91
|
+
})
|
|
92
|
+
|
|
72
93
|
it('surfaces an error when switch-properties fails', async () => {
|
|
73
94
|
vi.spyOn(
|
|
74
95
|
changeContractTokenModule,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isSignInAreaResolved } from '../../../src/sdk/session/isSignInAreaResolved'
|
|
3
|
+
|
|
4
|
+
describe('isSignInAreaResolved', () => {
|
|
5
|
+
it('waits for the first validation of this page load', () => {
|
|
6
|
+
expect(
|
|
7
|
+
isSignInAreaResolved({ isSessionReady: true, hasValidated: false })
|
|
8
|
+
).toBe(false)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('resolves once the session is ready and validated', () => {
|
|
12
|
+
expect(
|
|
13
|
+
isSignInAreaResolved({ isSessionReady: true, hasValidated: true })
|
|
14
|
+
).toBe(true)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('never resolves before the session is ready', () => {
|
|
18
|
+
expect(
|
|
19
|
+
isSignInAreaResolved({ isSessionReady: false, hasValidated: true })
|
|
20
|
+
).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
})
|