@faststore/core 4.10.1 → 4.10.2-dev.0
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 +304 -252
- package/@generated/graphql.ts +1 -0
- package/@generated/schema.graphql +1 -1
- package/CHANGELOG.md +82 -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 +18 -4
- 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 +101 -60
- package/src/pages/pvt/account/403.tsx +5 -4
- 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 +15 -11
- package/src/sdk/product/isProductNotFound.ts +13 -0
- package/src/sdk/product/usePageProductsQuery.ts +80 -21
- package/src/sdk/product/useProductNotFoundRedirect.ts +43 -0
- package/src/sdk/product/useProductQuery.ts +29 -0
- package/src/sdk/session/index.ts +46 -34
- package/src/server/cms/index.ts +59 -7
- package/src/server/cms/pdp.ts +10 -5
- package/src/server/cms/plp.ts +10 -5
- package/src/server/content/promiseCache.ts +68 -0
- package/src/server/content/service.ts +71 -51
- package/src/utils/getCheckoutParentDomain.ts +53 -0
- package/src/utils/isLocalHost.ts +2 -16
- package/src/utils/shouldBypassRefreshToken.ts +63 -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/graphql.test.ts +175 -0
- package/test/pages/api/logout.test.ts +106 -0
- package/test/pages/api/switch-contract.test.ts +443 -15
- package/test/pages/pvt/account/403.test.ts +67 -0
- 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 +72 -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/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 +111 -5
- package/test/server/cms/index.test.ts +102 -9
- package/test/server/content/promiseCache.test.ts +150 -0
- package/test/server/content/service.test.ts +277 -11
- package/test/utils/shouldBypassRefreshToken.browser.test.ts +38 -0
- package/test/utils/shouldBypassRefreshToken.test.ts +98 -0
- package/test/utils/trustedForwardedHost.test.ts +21 -0
- package/src/sdk/account/getIsRepresentative.ts +0 -20
- package/test/utils/isLocalHost.browser.test.ts +0 -37
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { vi } from 'vitest'
|
|
2
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
clientCMS,
|
|
4
|
+
cmsPageCache,
|
|
5
|
+
getCMSPage,
|
|
6
|
+
getPage,
|
|
7
|
+
} from '../../../src/server/cms'
|
|
3
8
|
import type { ContentData } from '@vtex/client-cms'
|
|
4
9
|
|
|
5
10
|
describe('CMS Integration', () => {
|
|
@@ -18,15 +23,22 @@ describe('CMS Integration', () => {
|
|
|
18
23
|
return data
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
const mockListing = (count = 1) =>
|
|
27
|
+
vi.fn((_contentType: string) =>
|
|
28
|
+
Promise.resolve({
|
|
29
|
+
data: mockData(count),
|
|
30
|
+
hasNextPage: false,
|
|
31
|
+
totalItems: count,
|
|
32
|
+
})
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
cmsPageCache.clear()
|
|
37
|
+
})
|
|
38
|
+
|
|
21
39
|
describe('getCMSPage', () => {
|
|
22
40
|
it('returns the first page if there is only one page', async () => {
|
|
23
|
-
const mockFunction =
|
|
24
|
-
return Promise.resolve({
|
|
25
|
-
data: mockData(3),
|
|
26
|
-
hasNextPage: false,
|
|
27
|
-
totalItems: 3,
|
|
28
|
-
})
|
|
29
|
-
})
|
|
41
|
+
const mockFunction = mockListing(3)
|
|
30
42
|
clientCMS.getCMSPagesByContentType = mockFunction
|
|
31
43
|
|
|
32
44
|
const result = await getCMSPage({ contentType: 'plp' }, clientCMS)
|
|
@@ -61,4 +73,85 @@ describe('CMS Integration', () => {
|
|
|
61
73
|
expect(result.data.length).toBe(15)
|
|
62
74
|
})
|
|
63
75
|
})
|
|
76
|
+
|
|
77
|
+
describe('cache', () => {
|
|
78
|
+
it('shares a content type listing across concurrent callers', async () => {
|
|
79
|
+
const mockFunction = mockListing(3)
|
|
80
|
+
clientCMS.getCMSPagesByContentType = mockFunction
|
|
81
|
+
|
|
82
|
+
await Promise.all([
|
|
83
|
+
getCMSPage({ contentType: 'plp' }),
|
|
84
|
+
getCMSPage({ contentType: 'plp' }),
|
|
85
|
+
getCMSPage({ contentType: 'plp' }),
|
|
86
|
+
])
|
|
87
|
+
await getCMSPage({ contentType: 'pdp' })
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
mockFunction.mock.calls.map(([contentType]) => contentType)
|
|
91
|
+
).toEqual(['plp', 'pdp'])
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not cache filtered listings', async () => {
|
|
95
|
+
const mockFunction = mockListing(1)
|
|
96
|
+
clientCMS.getCMSPagesByContentType = mockFunction
|
|
97
|
+
const options = {
|
|
98
|
+
contentType: 'landingPage',
|
|
99
|
+
filters: { 'settings.seo.slug': '/summer-sale' },
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
await getCMSPage(options)
|
|
103
|
+
await getCMSPage(options)
|
|
104
|
+
|
|
105
|
+
expect(mockFunction).toHaveBeenCalledTimes(2)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('fetches a locator on every call by default', async () => {
|
|
109
|
+
const mockFunction = vi.fn(() => Promise.resolve(mockData(1)[0]))
|
|
110
|
+
clientCMS.getCMSPage = mockFunction
|
|
111
|
+
const locator = {
|
|
112
|
+
contentType: 'globalSections',
|
|
113
|
+
documentId: 'doc-1',
|
|
114
|
+
versionId: 'version-1',
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await getPage(locator)
|
|
118
|
+
await getPage(locator)
|
|
119
|
+
|
|
120
|
+
expect(mockFunction).toHaveBeenCalledTimes(2)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('reuses a locator fetch when caching is enabled', async () => {
|
|
124
|
+
const mockFunction = vi.fn(() => Promise.resolve(mockData(1)[0]))
|
|
125
|
+
clientCMS.getCMSPage = mockFunction
|
|
126
|
+
const locator = {
|
|
127
|
+
contentType: 'globalSections',
|
|
128
|
+
documentId: 'doc-1',
|
|
129
|
+
versionId: 'version-1',
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [first, second] = await Promise.all([
|
|
133
|
+
getPage(locator, { cache: true }),
|
|
134
|
+
getPage(locator, { cache: true }),
|
|
135
|
+
])
|
|
136
|
+
await getPage({ ...locator, versionId: 'version-2' }, { cache: true })
|
|
137
|
+
|
|
138
|
+
expect(first).toEqual(second)
|
|
139
|
+
expect(mockFunction).toHaveBeenCalledTimes(2)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('does not cache failures', async () => {
|
|
143
|
+
const error = new Error('Request failed with status code 500')
|
|
144
|
+
const mockFunction = vi
|
|
145
|
+
.fn()
|
|
146
|
+
.mockRejectedValueOnce(error)
|
|
147
|
+
.mockResolvedValueOnce({ data: mockData(1), totalItems: 1 })
|
|
148
|
+
clientCMS.getCMSPagesByContentType = mockFunction
|
|
149
|
+
|
|
150
|
+
await expect(getCMSPage({ contentType: 'plp' })).rejects.toBe(error)
|
|
151
|
+
const result = await getCMSPage({ contentType: 'plp' })
|
|
152
|
+
|
|
153
|
+
expect(result.data).toHaveLength(1)
|
|
154
|
+
expect(mockFunction).toHaveBeenCalledTimes(2)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
64
157
|
})
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { createPromiseCache } from '../../../src/server/content/promiseCache'
|
|
4
|
+
|
|
5
|
+
describe('createPromiseCache', () => {
|
|
6
|
+
afterEach(() => {
|
|
7
|
+
vi.useRealTimers()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('shares one in-flight load across concurrent callers', async () => {
|
|
11
|
+
const cache = createPromiseCache<string>(1000)
|
|
12
|
+
const load = vi.fn(async () => 'value')
|
|
13
|
+
|
|
14
|
+
const results = await Promise.all([
|
|
15
|
+
cache.get('key', load),
|
|
16
|
+
cache.get('key', load),
|
|
17
|
+
cache.get('key', load),
|
|
18
|
+
])
|
|
19
|
+
|
|
20
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
21
|
+
expect(results).toEqual(['value', 'value', 'value'])
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('keeps separate entries per key', async () => {
|
|
25
|
+
const cache = createPromiseCache<string>(1000)
|
|
26
|
+
const load = vi.fn(async () => 'value')
|
|
27
|
+
|
|
28
|
+
await cache.get('a', load)
|
|
29
|
+
await cache.get('b', load)
|
|
30
|
+
|
|
31
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('loads again after the TTL', async () => {
|
|
35
|
+
vi.useFakeTimers()
|
|
36
|
+
const cache = createPromiseCache<string>(1000)
|
|
37
|
+
const load = vi.fn(async () => 'value')
|
|
38
|
+
|
|
39
|
+
await cache.get('key', load)
|
|
40
|
+
vi.advanceTimersByTime(999)
|
|
41
|
+
await cache.get('key', load)
|
|
42
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
43
|
+
|
|
44
|
+
vi.advanceTimersByTime(1)
|
|
45
|
+
await cache.get('key', load)
|
|
46
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('does not cache failures', async () => {
|
|
50
|
+
const cache = createPromiseCache<string>(1000)
|
|
51
|
+
const error = new Error('boom')
|
|
52
|
+
const load = vi
|
|
53
|
+
.fn<() => Promise<string>>()
|
|
54
|
+
.mockRejectedValueOnce(error)
|
|
55
|
+
.mockResolvedValueOnce('value')
|
|
56
|
+
|
|
57
|
+
await expect(cache.get('key', load)).rejects.toBe(error)
|
|
58
|
+
await expect(cache.get('key', load)).resolves.toBe('value')
|
|
59
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('does not let a late failure evict a newer load', async () => {
|
|
63
|
+
vi.useFakeTimers()
|
|
64
|
+
const cache = createPromiseCache<string>(1000)
|
|
65
|
+
let rejectFirst: (err: Error) => void = () => {}
|
|
66
|
+
const load = vi
|
|
67
|
+
.fn<() => Promise<string>>()
|
|
68
|
+
.mockImplementationOnce(
|
|
69
|
+
() =>
|
|
70
|
+
new Promise((_, reject) => {
|
|
71
|
+
rejectFirst = reject
|
|
72
|
+
})
|
|
73
|
+
)
|
|
74
|
+
.mockResolvedValue('value')
|
|
75
|
+
|
|
76
|
+
const first = cache.get('key', load)
|
|
77
|
+
const firstResult = expect(first).rejects.toThrow('late failure')
|
|
78
|
+
vi.advanceTimersByTime(1000)
|
|
79
|
+
await cache.get('key', load)
|
|
80
|
+
|
|
81
|
+
rejectFirst(new Error('late failure'))
|
|
82
|
+
await firstResult
|
|
83
|
+
await cache.get('key', load)
|
|
84
|
+
|
|
85
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('does not keep values rejected by shouldCache', async () => {
|
|
89
|
+
const cache = createPromiseCache<string>(1000)
|
|
90
|
+
const load = vi.fn(async () => '')
|
|
91
|
+
const shouldCache = (value: string) => value !== ''
|
|
92
|
+
|
|
93
|
+
const concurrent = await Promise.all([
|
|
94
|
+
cache.get('key', load, shouldCache),
|
|
95
|
+
cache.get('key', load, shouldCache),
|
|
96
|
+
])
|
|
97
|
+
await cache.get('key', load, shouldCache)
|
|
98
|
+
|
|
99
|
+
expect(concurrent).toEqual(['', ''])
|
|
100
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('keeps values accepted by shouldCache', async () => {
|
|
104
|
+
const cache = createPromiseCache<string>(1000)
|
|
105
|
+
const load = vi.fn(async () => 'value')
|
|
106
|
+
|
|
107
|
+
await cache.get('key', load, (value) => value !== '')
|
|
108
|
+
await cache.get('key', load, (value) => value !== '')
|
|
109
|
+
|
|
110
|
+
expect(load).toHaveBeenCalledTimes(1)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('drops expired entries when a new key is stored', async () => {
|
|
114
|
+
vi.useFakeTimers()
|
|
115
|
+
const cache = createPromiseCache<string>(1000)
|
|
116
|
+
const load = vi.fn(async () => 'value')
|
|
117
|
+
|
|
118
|
+
await cache.get('old-version', load)
|
|
119
|
+
await cache.get('other', load)
|
|
120
|
+
expect(cache.size).toBe(2)
|
|
121
|
+
|
|
122
|
+
vi.advanceTimersByTime(1000)
|
|
123
|
+
await cache.get('new-version', load)
|
|
124
|
+
|
|
125
|
+
expect(cache.size).toBe(1)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('keeps entries that have not expired when a new key is stored', async () => {
|
|
129
|
+
vi.useFakeTimers()
|
|
130
|
+
const cache = createPromiseCache<string>(1000)
|
|
131
|
+
const load = vi.fn(async () => 'value')
|
|
132
|
+
|
|
133
|
+
await cache.get('a', load)
|
|
134
|
+
vi.advanceTimersByTime(999)
|
|
135
|
+
await cache.get('b', load)
|
|
136
|
+
|
|
137
|
+
expect(cache.size).toBe(2)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('drops every entry on clear', async () => {
|
|
141
|
+
const cache = createPromiseCache<string>(1000)
|
|
142
|
+
const load = vi.fn(async () => 'value')
|
|
143
|
+
|
|
144
|
+
await cache.get('key', load)
|
|
145
|
+
cache.clear()
|
|
146
|
+
await cache.get('key', load)
|
|
147
|
+
|
|
148
|
+
expect(load).toHaveBeenCalledTimes(2)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -1,22 +1,45 @@
|
|
|
1
1
|
import type { ContentEntry, EntryPathParams } from '@vtex/client-cp'
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
|
+
import { clientCMS, cmsPageCache } from '../../../src/server/cms'
|
|
4
5
|
import { ContentService } from '../../../src/server/content/service'
|
|
5
|
-
import
|
|
6
|
-
ContentOptions,
|
|
7
|
-
ContentParams,
|
|
6
|
+
import {
|
|
7
|
+
type ContentOptions,
|
|
8
|
+
type ContentParams,
|
|
9
|
+
ContentSourceType,
|
|
8
10
|
} from '../../../src/server/content/types'
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
+
import { contentSource } from '../../../discovery.config'
|
|
12
|
+
|
|
13
|
+
type ContentSourceConfig = { type: string }
|
|
14
|
+
type DiscoveryConfigModule = {
|
|
15
|
+
default: Record<string, unknown>
|
|
16
|
+
contentSource: ContentSourceConfig
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
vi.mock('../../../discovery.config', async (importOriginal) => {
|
|
20
|
+
const actual = await importOriginal<DiscoveryConfigModule>()
|
|
21
|
+
const base = actual.default ?? actual
|
|
22
|
+
const mutableContentSource = {
|
|
23
|
+
...(base.contentSource as ContentSourceConfig),
|
|
24
|
+
}
|
|
11
25
|
|
|
12
|
-
vi.mock('../../../src/server/content/utils', async (importOriginal) => {
|
|
13
|
-
const actual = await importOriginal<typeof ContentUtils>()
|
|
14
26
|
return {
|
|
15
27
|
...actual,
|
|
16
|
-
|
|
28
|
+
default: { ...base, contentSource: mutableContentSource },
|
|
29
|
+
contentSource: mutableContentSource,
|
|
17
30
|
}
|
|
18
31
|
})
|
|
19
32
|
|
|
33
|
+
const DEFAULT_CONTENT_SOURCE_TYPE = contentSource.type
|
|
34
|
+
|
|
35
|
+
const useContentPlatform = () => {
|
|
36
|
+
contentSource.type = ContentSourceType.ContentPlatform
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const useHeadlessCms = () => {
|
|
40
|
+
contentSource.type = DEFAULT_CONTENT_SOURCE_TYPE
|
|
41
|
+
}
|
|
42
|
+
|
|
20
43
|
type ContentServiceInternals = {
|
|
21
44
|
createContentOptions: (params: ContentParams) => ContentOptions
|
|
22
45
|
convertOptionsToParams: (options: ContentOptions) => EntryPathParams
|
|
@@ -39,7 +62,17 @@ const entryData: Record<string, Record<string, unknown>> = {
|
|
|
39
62
|
},
|
|
40
63
|
}
|
|
41
64
|
|
|
42
|
-
const toEntry = (id: string) => ({
|
|
65
|
+
const toEntry = (id: string): ContentEntry => ({
|
|
66
|
+
id,
|
|
67
|
+
name: id,
|
|
68
|
+
account: 'example-account',
|
|
69
|
+
storeId: 'faststore',
|
|
70
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
71
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
72
|
+
createdBy: 'editor@example.com',
|
|
73
|
+
updatedBy: 'other@example.com',
|
|
74
|
+
slug: id,
|
|
75
|
+
})
|
|
43
76
|
|
|
44
77
|
function createFakeClientCP(pages: string[][]) {
|
|
45
78
|
return {
|
|
@@ -52,6 +85,8 @@ function createFakeClientCP(pages: string[][]) {
|
|
|
52
85
|
}),
|
|
53
86
|
getEntry: vi.fn(async ({ entryId }: EntryPathParams) => ({
|
|
54
87
|
...entryData[entryId as string],
|
|
88
|
+
createdBy: 'payload@example.com',
|
|
89
|
+
updatedBy: 'payload@example.com',
|
|
55
90
|
})),
|
|
56
91
|
previewEntryById: vi.fn(async ({ entryId }: EntryPathParams) => ({
|
|
57
92
|
...entryData[entryId as string],
|
|
@@ -163,16 +198,43 @@ describe('ContentService', () => {
|
|
|
163
198
|
const FIVE_MINUTES = 5 * 60 * 1000
|
|
164
199
|
|
|
165
200
|
beforeEach(() => {
|
|
166
|
-
|
|
201
|
+
useContentPlatform()
|
|
167
202
|
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
168
203
|
})
|
|
169
204
|
|
|
170
205
|
afterEach(() => {
|
|
171
|
-
|
|
206
|
+
useHeadlessCms()
|
|
172
207
|
vi.restoreAllMocks()
|
|
173
208
|
vi.useRealTimers()
|
|
174
209
|
})
|
|
175
210
|
|
|
211
|
+
it('keeps Content Platform author emails out of public page content', async () => {
|
|
212
|
+
const { service, clientCP } = createServiceWithClient()
|
|
213
|
+
clientCP.getEntryBySlug.mockResolvedValueOnce({
|
|
214
|
+
sections: [{ name: 'Direct' }],
|
|
215
|
+
createdBy: 'editor@example.com',
|
|
216
|
+
updatedBy: 'other@example.com',
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const directPage = await service.getSingleContent({
|
|
220
|
+
contentType: 'pdp',
|
|
221
|
+
slug: 'product-slug',
|
|
222
|
+
})
|
|
223
|
+
const { data } = await service.getMultipleContent({ contentType: 'pdp' })
|
|
224
|
+
const listedPage = data[0]
|
|
225
|
+
|
|
226
|
+
expect(directPage.sections).toEqual([{ name: 'Direct' }])
|
|
227
|
+
expect(directPage).not.toHaveProperty('createdBy')
|
|
228
|
+
expect(directPage).not.toHaveProperty('updatedBy')
|
|
229
|
+
expect(listedPage.id).toBe('generic')
|
|
230
|
+
expect(listedPage.name).toBe('generic')
|
|
231
|
+
expect(listedPage.sections).toEqual([{ name: 'Generic' }])
|
|
232
|
+
expect(listedPage).not.toHaveProperty('createdBy')
|
|
233
|
+
expect(listedPage).not.toHaveProperty('updatedBy')
|
|
234
|
+
expect(listedPage).not.toHaveProperty('account')
|
|
235
|
+
expect(listedPage).not.toHaveProperty('storeId')
|
|
236
|
+
})
|
|
237
|
+
|
|
176
238
|
it('shares one list and entry fetch across concurrent PLPs with different slugs', async () => {
|
|
177
239
|
const { service, clientCP } = createServiceWithClient()
|
|
178
240
|
|
|
@@ -334,4 +396,208 @@ describe('ContentService', () => {
|
|
|
334
396
|
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
335
397
|
})
|
|
336
398
|
})
|
|
399
|
+
|
|
400
|
+
describe('Content Platform single entry cache', () => {
|
|
401
|
+
const FIVE_MINUTES = 5 * 60 * 1000
|
|
402
|
+
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
useContentPlatform()
|
|
405
|
+
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
afterEach(() => {
|
|
409
|
+
useHeadlessCms()
|
|
410
|
+
vi.restoreAllMocks()
|
|
411
|
+
vi.useRealTimers()
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
it('shares one list and entry fetch across concurrent pages', async () => {
|
|
415
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
416
|
+
|
|
417
|
+
const results = await Promise.all(
|
|
418
|
+
Array.from({ length: 20 }, () =>
|
|
419
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
|
|
424
|
+
expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
|
|
425
|
+
for (const result of results) {
|
|
426
|
+
expect(result.sections).toEqual([{ name: 'Generic' }])
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
it('keeps separate entries per content type and locale', async () => {
|
|
431
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
432
|
+
|
|
433
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
434
|
+
await service.getSingleContent({ contentType: 'globalHeaderSections' })
|
|
435
|
+
await service.getSingleContent({
|
|
436
|
+
contentType: 'globalSections',
|
|
437
|
+
locale: 'en-US',
|
|
438
|
+
})
|
|
439
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
440
|
+
|
|
441
|
+
expect(
|
|
442
|
+
clientCP.listEntries.mock.calls.map(([params]) => [
|
|
443
|
+
params.contentType,
|
|
444
|
+
params.locale,
|
|
445
|
+
])
|
|
446
|
+
).toEqual([
|
|
447
|
+
['globalSections', undefined],
|
|
448
|
+
['globalHeaderSections', undefined],
|
|
449
|
+
['globalSections', 'en-US'],
|
|
450
|
+
])
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
it('caches an empty optional content type', async () => {
|
|
454
|
+
const { service, clientCP } = createServiceWithClient([[]])
|
|
455
|
+
|
|
456
|
+
const first = await service.getSingleContent({
|
|
457
|
+
contentType: 'globalFooterSections',
|
|
458
|
+
})
|
|
459
|
+
await service.getSingleContent({ contentType: 'globalFooterSections' })
|
|
460
|
+
|
|
461
|
+
expect(first).toEqual({})
|
|
462
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(1)
|
|
463
|
+
expect(clientCP.getEntry).not.toHaveBeenCalled()
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it('does not cache an empty required content type', async () => {
|
|
467
|
+
const { service, clientCP } = createServiceWithClient([[]])
|
|
468
|
+
|
|
469
|
+
const first = await service.getSingleContent({
|
|
470
|
+
contentType: 'globalSections',
|
|
471
|
+
})
|
|
472
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
473
|
+
|
|
474
|
+
expect(first).toEqual({})
|
|
475
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('picks up a required content type published after an empty lookup', async () => {
|
|
479
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
480
|
+
clientCP.listEntries.mockResolvedValueOnce({ entries: [], scroll: null })
|
|
481
|
+
|
|
482
|
+
const before = await service.getSingleContent({
|
|
483
|
+
contentType: 'globalSections',
|
|
484
|
+
})
|
|
485
|
+
const after = await service.getSingleContent({
|
|
486
|
+
contentType: 'globalSections',
|
|
487
|
+
})
|
|
488
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
489
|
+
|
|
490
|
+
expect(before).toEqual({})
|
|
491
|
+
expect(after.sections).toEqual([{ name: 'Generic' }])
|
|
492
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
493
|
+
})
|
|
494
|
+
|
|
495
|
+
it('does not cache lookups by slug', async () => {
|
|
496
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
497
|
+
|
|
498
|
+
await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
|
|
499
|
+
await service.getSingleContent({ contentType: 'landingPage', slug: 'a' })
|
|
500
|
+
|
|
501
|
+
expect(clientCP.getEntryBySlug).toHaveBeenCalledTimes(2)
|
|
502
|
+
expect(clientCP.listEntries).not.toHaveBeenCalled()
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
it('bypasses the cache for branch previews', async () => {
|
|
506
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
507
|
+
const branchPreview: ContentParams = {
|
|
508
|
+
contentType: 'globalSections',
|
|
509
|
+
previewData: {
|
|
510
|
+
contentType: 'landingPage',
|
|
511
|
+
documentId: 'some-landing-page',
|
|
512
|
+
versionId: 'branch-1',
|
|
513
|
+
},
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
517
|
+
await service.getSingleContent(branchPreview)
|
|
518
|
+
await service.getSingleContent(branchPreview)
|
|
519
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
520
|
+
|
|
521
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(3)
|
|
522
|
+
expect(clientCP.getEntry).toHaveBeenCalledTimes(1)
|
|
523
|
+
expect(clientCP.previewEntryById).toHaveBeenCalledTimes(2)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it('does not cache a multiple-entries error', async () => {
|
|
527
|
+
const { service, clientCP } = createServiceWithClient([
|
|
528
|
+
['generic', 'collection'],
|
|
529
|
+
])
|
|
530
|
+
|
|
531
|
+
await expect(
|
|
532
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
533
|
+
).rejects.toThrow()
|
|
534
|
+
await expect(
|
|
535
|
+
service.getSingleContent({ contentType: 'globalSections' })
|
|
536
|
+
).rejects.toThrow()
|
|
537
|
+
|
|
538
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
it('fetches again after the TTL', async () => {
|
|
542
|
+
vi.useFakeTimers()
|
|
543
|
+
const { service, clientCP } = createServiceWithClient([['generic']])
|
|
544
|
+
|
|
545
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
546
|
+
vi.advanceTimersByTime(FIVE_MINUTES)
|
|
547
|
+
await service.getSingleContent({ contentType: 'globalSections' })
|
|
548
|
+
|
|
549
|
+
expect(clientCP.listEntries).toHaveBeenCalledTimes(2)
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
describe('Headless CMS single content cache', () => {
|
|
554
|
+
const locator = {
|
|
555
|
+
contentType: 'globalSections',
|
|
556
|
+
documentId: 'doc-1',
|
|
557
|
+
versionId: 'version-1',
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
beforeEach(() => {
|
|
561
|
+
useHeadlessCms()
|
|
562
|
+
cmsPageCache.clear()
|
|
563
|
+
vi.spyOn(clientCMS, 'getCMSPage').mockResolvedValue({
|
|
564
|
+
id: 'doc-1',
|
|
565
|
+
name: 'doc-1',
|
|
566
|
+
status: 'published',
|
|
567
|
+
type: 'globalSections',
|
|
568
|
+
releaseId: 'release-1',
|
|
569
|
+
sections: [{ name: 'Children' }],
|
|
570
|
+
} as Awaited<ReturnType<typeof clientCMS.getCMSPage>>)
|
|
571
|
+
})
|
|
572
|
+
|
|
573
|
+
afterEach(() => {
|
|
574
|
+
useHeadlessCms()
|
|
575
|
+
vi.restoreAllMocks()
|
|
576
|
+
})
|
|
577
|
+
|
|
578
|
+
it('reuses a published document fetched by locator', async () => {
|
|
579
|
+
const service = new ContentService()
|
|
580
|
+
|
|
581
|
+
await Promise.all([
|
|
582
|
+
service.getSingleContent(locator),
|
|
583
|
+
service.getSingleContent(locator),
|
|
584
|
+
])
|
|
585
|
+
await service.getSingleContent(locator)
|
|
586
|
+
|
|
587
|
+
expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(1)
|
|
588
|
+
})
|
|
589
|
+
|
|
590
|
+
it('fetches the document on every preview request', async () => {
|
|
591
|
+
const service = new ContentService()
|
|
592
|
+
const previewParams: ContentParams = {
|
|
593
|
+
...locator,
|
|
594
|
+
previewData: locator,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
await service.getSingleContent(previewParams)
|
|
598
|
+
await service.getSingleContent(previewParams)
|
|
599
|
+
|
|
600
|
+
expect(clientCMS.getCMSPage).toHaveBeenCalledTimes(2)
|
|
601
|
+
})
|
|
602
|
+
})
|
|
337
603
|
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { shouldBypassRefreshTokenBrowser } from '../../src/utils/shouldBypassRefreshToken'
|
|
3
|
+
|
|
4
|
+
const originalLocation = window.location
|
|
5
|
+
|
|
6
|
+
function stubHostname(hostname: string) {
|
|
7
|
+
// jsdom forbids assigning to `window.location` directly, so we redefine the
|
|
8
|
+
// descriptor with a minimal stub that exposes the hostname we want to test.
|
|
9
|
+
Object.defineProperty(window, 'location', {
|
|
10
|
+
configurable: true,
|
|
11
|
+
value: { ...originalLocation, hostname },
|
|
12
|
+
})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('shouldBypassRefreshTokenBrowser', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
Object.defineProperty(window, 'location', {
|
|
18
|
+
configurable: true,
|
|
19
|
+
value: originalLocation,
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it.each(['localhost', '127.0.0.1', 'brandless-abc123.preview.vtex.app'])(
|
|
24
|
+
'returns true when window.location.hostname is "%s"',
|
|
25
|
+
(hostname) => {
|
|
26
|
+
stubHostname(hostname)
|
|
27
|
+
expect(shouldBypassRefreshTokenBrowser()).toBe(true)
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
it.each(['brandless.vtex.app', 'brandless.fast.store'])(
|
|
32
|
+
'returns false for the production hostname "%s"',
|
|
33
|
+
(hostname) => {
|
|
34
|
+
stubHostname(hostname)
|
|
35
|
+
expect(shouldBypassRefreshTokenBrowser()).toBe(false)
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
})
|