@faststore/core 4.10.1-dev.1 → 4.10.1-dev.3

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.
@@ -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 type {
6
- ContentOptions,
7
- ContentParams,
6
+ import {
7
+ type ContentOptions,
8
+ type ContentParams,
9
+ ContentSourceType,
8
10
  } from '../../../src/server/content/types'
9
- import type * as ContentUtils from '../../../src/server/content/utils'
10
- import { isContentPlatformSource } from '../../../src/server/content/utils'
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
- isContentPlatformSource: vi.fn(actual.isContentPlatformSource),
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) => ({ id, name: id }) as ContentEntry
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
- vi.mocked(isContentPlatformSource).mockReturnValue(true)
201
+ useContentPlatform()
167
202
  vi.spyOn(console, 'warn').mockImplementation(() => {})
168
203
  })
169
204
 
170
205
  afterEach(() => {
171
- vi.mocked(isContentPlatformSource).mockReset()
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
  })