@actuate-media/cms-core 0.29.0 → 0.31.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.
Files changed (111) hide show
  1. package/dist/__tests__/actions/seo-fields-update.test.d.ts +2 -0
  2. package/dist/__tests__/actions/seo-fields-update.test.d.ts.map +1 -0
  3. package/dist/__tests__/actions/seo-fields-update.test.js +93 -0
  4. package/dist/__tests__/actions/seo-fields-update.test.js.map +1 -0
  5. package/dist/__tests__/api/admin-contracts.test.js +10 -2
  6. package/dist/__tests__/api/admin-contracts.test.js.map +1 -1
  7. package/dist/__tests__/api/llms-txt.test.d.ts +2 -0
  8. package/dist/__tests__/api/llms-txt.test.d.ts.map +1 -0
  9. package/dist/__tests__/api/llms-txt.test.js +77 -0
  10. package/dist/__tests__/api/llms-txt.test.js.map +1 -0
  11. package/dist/__tests__/api/public-seo.test.js +2 -0
  12. package/dist/__tests__/api/public-seo.test.js.map +1 -1
  13. package/dist/__tests__/api/seo-read-access.test.d.ts +2 -0
  14. package/dist/__tests__/api/seo-read-access.test.d.ts.map +1 -0
  15. package/dist/__tests__/api/seo-read-access.test.js +101 -0
  16. package/dist/__tests__/api/seo-read-access.test.js.map +1 -0
  17. package/dist/__tests__/api/seo-summary-ssrf.test.d.ts +2 -0
  18. package/dist/__tests__/api/seo-summary-ssrf.test.d.ts.map +1 -0
  19. package/dist/__tests__/api/seo-summary-ssrf.test.js +80 -0
  20. package/dist/__tests__/api/seo-summary-ssrf.test.js.map +1 -0
  21. package/dist/__tests__/api/sitemap-pagination.test.d.ts +2 -0
  22. package/dist/__tests__/api/sitemap-pagination.test.d.ts.map +1 -0
  23. package/dist/__tests__/api/sitemap-pagination.test.js +100 -0
  24. package/dist/__tests__/api/sitemap-pagination.test.js.map +1 -0
  25. package/dist/__tests__/forms/forms-service.test.d.ts +2 -0
  26. package/dist/__tests__/forms/forms-service.test.d.ts.map +1 -0
  27. package/dist/__tests__/forms/forms-service.test.js +193 -0
  28. package/dist/__tests__/forms/forms-service.test.js.map +1 -0
  29. package/dist/__tests__/middleware.test.d.ts +2 -0
  30. package/dist/__tests__/middleware.test.d.ts.map +1 -0
  31. package/dist/__tests__/middleware.test.js +50 -0
  32. package/dist/__tests__/middleware.test.js.map +1 -0
  33. package/dist/__tests__/seo/analysis-markdown.test.d.ts +2 -0
  34. package/dist/__tests__/seo/analysis-markdown.test.d.ts.map +1 -0
  35. package/dist/__tests__/seo/analysis-markdown.test.js +57 -0
  36. package/dist/__tests__/seo/analysis-markdown.test.js.map +1 -0
  37. package/dist/__tests__/seo/audit-runner.test.d.ts +2 -0
  38. package/dist/__tests__/seo/audit-runner.test.d.ts.map +1 -0
  39. package/dist/__tests__/seo/audit-runner.test.js +135 -0
  40. package/dist/__tests__/seo/audit-runner.test.js.map +1 -0
  41. package/dist/__tests__/seo/internal-collections.test.d.ts +2 -0
  42. package/dist/__tests__/seo/internal-collections.test.d.ts.map +1 -0
  43. package/dist/__tests__/seo/internal-collections.test.js +27 -0
  44. package/dist/__tests__/seo/internal-collections.test.js.map +1 -0
  45. package/dist/__tests__/seo/page-meta.test.js +34 -2
  46. package/dist/__tests__/seo/page-meta.test.js.map +1 -1
  47. package/dist/__tests__/seo/robots.test.js +23 -2
  48. package/dist/__tests__/seo/robots.test.js.map +1 -1
  49. package/dist/actions.d.ts +24 -0
  50. package/dist/actions.d.ts.map +1 -1
  51. package/dist/actions.js +63 -0
  52. package/dist/actions.js.map +1 -1
  53. package/dist/api/handlers.d.ts.map +1 -1
  54. package/dist/api/handlers.js +744 -196
  55. package/dist/api/handlers.js.map +1 -1
  56. package/dist/config/types.d.ts +49 -0
  57. package/dist/config/types.d.ts.map +1 -1
  58. package/dist/cron/index.d.ts +1 -0
  59. package/dist/cron/index.d.ts.map +1 -1
  60. package/dist/cron/index.js +10 -1
  61. package/dist/cron/index.js.map +1 -1
  62. package/dist/forms/entries.d.ts +45 -0
  63. package/dist/forms/entries.d.ts.map +1 -0
  64. package/dist/forms/entries.js +348 -0
  65. package/dist/forms/entries.js.map +1 -0
  66. package/dist/forms/index.d.ts +4 -0
  67. package/dist/forms/index.d.ts.map +1 -1
  68. package/dist/forms/index.js +3 -0
  69. package/dist/forms/index.js.map +1 -1
  70. package/dist/forms/service.d.ts +92 -0
  71. package/dist/forms/service.d.ts.map +1 -0
  72. package/dist/forms/service.js +579 -0
  73. package/dist/forms/service.js.map +1 -0
  74. package/dist/forms/types.d.ts +244 -0
  75. package/dist/forms/types.d.ts.map +1 -0
  76. package/dist/forms/types.js +36 -0
  77. package/dist/forms/types.js.map +1 -0
  78. package/dist/index.d.ts +2 -2
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +1 -1
  81. package/dist/index.js.map +1 -1
  82. package/dist/middleware.js +12 -2
  83. package/dist/middleware.js.map +1 -1
  84. package/dist/seo/analysis.d.ts +9 -0
  85. package/dist/seo/analysis.d.ts.map +1 -1
  86. package/dist/seo/analysis.js +45 -6
  87. package/dist/seo/analysis.js.map +1 -1
  88. package/dist/seo/audit-runner.d.ts +43 -2
  89. package/dist/seo/audit-runner.d.ts.map +1 -1
  90. package/dist/seo/audit-runner.js +86 -7
  91. package/dist/seo/audit-runner.js.map +1 -1
  92. package/dist/seo/index.d.ts +3 -2
  93. package/dist/seo/index.d.ts.map +1 -1
  94. package/dist/seo/index.js +2 -2
  95. package/dist/seo/index.js.map +1 -1
  96. package/dist/seo/page-meta.d.ts +6 -0
  97. package/dist/seo/page-meta.d.ts.map +1 -1
  98. package/dist/seo/page-meta.js +22 -5
  99. package/dist/seo/page-meta.js.map +1 -1
  100. package/dist/seo/robots.d.ts +7 -0
  101. package/dist/seo/robots.d.ts.map +1 -1
  102. package/dist/seo/robots.js +10 -1
  103. package/dist/seo/robots.js.map +1 -1
  104. package/dist/seo/score.d.ts +5 -1
  105. package/dist/seo/score.d.ts.map +1 -1
  106. package/dist/seo/score.js +7 -4
  107. package/dist/seo/score.js.map +1 -1
  108. package/package.json +1 -1
  109. package/prisma/cms-schema.prisma +81 -7
  110. package/prisma/migrations/0010_forms/migration.sql +130 -0
  111. package/prisma/schema.prisma +130 -35
@@ -0,0 +1,100 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import { handleActuateAPI } from '../../api/index.js';
3
+ import { initDB } from '../../db.js';
4
+ /**
5
+ * Regression test for sitemap pagination. The old per-collection sitemap capped
6
+ * at 5,000 URLs and silently dropped the rest, and the index emitted a single
7
+ * `<sitemap>` per collection regardless of size. Collections larger than the
8
+ * 50k protocol limit must now be split across `?page=N` files referenced from
9
+ * the index.
10
+ */
11
+ const SITEMAP_PAGE_SIZE = 45000;
12
+ function createMockDB(opts) {
13
+ return {
14
+ document: {
15
+ count: vi.fn(async () => opts.count),
16
+ findMany: opts.findMany,
17
+ findFirst: vi.fn(async () => null),
18
+ },
19
+ user: {},
20
+ session: {},
21
+ media: {},
22
+ };
23
+ }
24
+ const postsConfig = {
25
+ seo: { siteUrl: 'https://example.com' },
26
+ collections: {
27
+ posts: {
28
+ slug: 'posts',
29
+ labels: { singular: 'Post', plural: 'Posts' },
30
+ urlPrefix: 'blog',
31
+ type: 'post',
32
+ fields: {},
33
+ },
34
+ },
35
+ };
36
+ describe('sitemap pagination', () => {
37
+ beforeEach(() => {
38
+ delete globalThis.__actuateConfig;
39
+ });
40
+ it('emits one <sitemap> entry per 45k-doc page in the index', async () => {
41
+ const findMany = vi.fn(async () => []);
42
+ const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2 + 10, findMany });
43
+ initDB(db);
44
+ const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
45
+ const res = await handler(new Request('https://example.com/api/cms/sitemap.xml'));
46
+ expect(res.status).toBe(200);
47
+ const text = await res.text();
48
+ // 90,010 docs / 45,000 = 3 pages → page 0 (bare) + page 1 + page 2.
49
+ expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml</loc>');
50
+ expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml?page=1</loc>');
51
+ expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml?page=2</loc>');
52
+ expect(text).not.toContain('?page=3');
53
+ });
54
+ it('emits a single entry for a small collection', async () => {
55
+ const findMany = vi.fn(async () => []);
56
+ const db = createMockDB({ count: 12, findMany });
57
+ initDB(db);
58
+ const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
59
+ const res = await handler(new Request('https://example.com/api/cms/sitemap.xml'));
60
+ const text = await res.text();
61
+ expect(text).toContain('<loc>https://example.com/api/cms/sitemaps/posts.xml</loc>');
62
+ expect(text).not.toContain('?page=1');
63
+ });
64
+ it('applies skip/take when a page is requested', async () => {
65
+ const findMany = vi.fn(async (_args) => []);
66
+ const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2, findMany });
67
+ initDB(db);
68
+ const handler = handleActuateAPI({ prismaClient: db, config: postsConfig });
69
+ await handler(new Request('https://example.com/api/cms/sitemaps/posts.xml?page=2'));
70
+ expect(findMany).toHaveBeenCalledTimes(1);
71
+ const arg = (findMany.mock.calls[0]?.[0] ?? {});
72
+ expect(arg.skip).toBe(2 * SITEMAP_PAGE_SIZE);
73
+ expect(arg.take).toBe(SITEMAP_PAGE_SIZE);
74
+ });
75
+ it('omits the archive URL on pages after the first', async () => {
76
+ const findMany = vi.fn(async () => []);
77
+ const db = createMockDB({ count: SITEMAP_PAGE_SIZE * 2, findMany });
78
+ initDB(db);
79
+ const handler = handleActuateAPI({
80
+ prismaClient: db,
81
+ config: {
82
+ seo: { siteUrl: 'https://example.com' },
83
+ collections: {
84
+ posts: {
85
+ slug: 'posts',
86
+ labels: { singular: 'Post', plural: 'Posts' },
87
+ urlPrefix: 'blog',
88
+ type: 'post',
89
+ seo: { archivePath: '/blog' },
90
+ fields: {},
91
+ },
92
+ },
93
+ },
94
+ });
95
+ const res = await handler(new Request('https://example.com/api/cms/sitemaps/posts.xml?page=1'));
96
+ const text = await res.text();
97
+ expect(text).not.toContain('<loc>https://example.com/blog</loc>');
98
+ });
99
+ });
100
+ //# sourceMappingURL=sitemap-pagination.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sitemap-pagination.test.js","sourceRoot":"","sources":["../../../src/__tests__/api/sitemap-pagination.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=forms-service.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-service.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/forms/forms-service.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,193 @@
1
+ import { describe, expect, it, vi, beforeEach } from 'vitest';
2
+ // Mutable fake Prisma client swapped per test.
3
+ const dbHolder = { current: {} };
4
+ vi.mock('../../db.js', () => ({ getDB: () => dbHolder.current }));
5
+ // Audit logging writes to the DB; no-op it so service logic is isolated.
6
+ vi.mock('../../security/audit.js', () => ({ logEvent: vi.fn() }));
7
+ import { validateFormSchema, parseFormDefinition } from '../../forms/service.js';
8
+ import { listEntries, getEntryCounts, normalizeEntry } from '../../forms/entries.js';
9
+ function field(overrides) {
10
+ return {
11
+ id: 'f1',
12
+ label: 'Label',
13
+ key: 'key',
14
+ type: 'text',
15
+ required: false,
16
+ sortOrder: 0,
17
+ ...overrides,
18
+ };
19
+ }
20
+ describe('validateFormSchema', () => {
21
+ it('accepts a valid field set', () => {
22
+ const result = validateFormSchema([
23
+ field({ key: 'name', label: 'Name', type: 'text' }),
24
+ field({ key: 'email', label: 'Email', type: 'email' }),
25
+ ]);
26
+ expect(result.valid).toBe(true);
27
+ expect(result.errors).toHaveLength(0);
28
+ });
29
+ it('rejects duplicate field keys', () => {
30
+ const result = validateFormSchema([field({ key: 'name' }), field({ key: 'name', id: 'f2' })]);
31
+ expect(result.valid).toBe(false);
32
+ expect(result.errors.some((e) => e.includes('Duplicate field key'))).toBe(true);
33
+ });
34
+ it('rejects invalid key syntax', () => {
35
+ const result = validateFormSchema([field({ key: '123-bad' })]);
36
+ expect(result.valid).toBe(false);
37
+ expect(result.errors.some((e) => e.includes('Invalid field key'))).toBe(true);
38
+ });
39
+ it('requires a label', () => {
40
+ const result = validateFormSchema([field({ key: 'ok', label: '' })]);
41
+ expect(result.valid).toBe(false);
42
+ expect(result.errors.some((e) => e.includes('missing a label'))).toBe(true);
43
+ });
44
+ it('rejects unsupported field types', () => {
45
+ const result = validateFormSchema([field({ key: 'ok', type: 'magic' })]);
46
+ expect(result.valid).toBe(false);
47
+ expect(result.errors.some((e) => e.includes('is not allowed'))).toBe(true);
48
+ });
49
+ it('requires options for select/radio fields', () => {
50
+ const result = validateFormSchema([field({ key: 'choice', type: 'select', options: [] })]);
51
+ expect(result.valid).toBe(false);
52
+ expect(result.errors.some((e) => e.includes('requires at least one option'))).toBe(true);
53
+ });
54
+ it('rejects a non-array payload', () => {
55
+ const result = validateFormSchema({ not: 'an array' });
56
+ expect(result.valid).toBe(false);
57
+ });
58
+ });
59
+ describe('parseFormDefinition', () => {
60
+ it('parses status, fields, and notify flag from document data', () => {
61
+ const def = parseFormDefinition({
62
+ id: 'form_1',
63
+ title: 'ignored',
64
+ slug: 'ignored',
65
+ createdById: 'user_1',
66
+ createdAt: new Date('2026-01-01T00:00:00Z'),
67
+ updatedAt: new Date('2026-01-02T00:00:00Z'),
68
+ data: {
69
+ name: 'Contact Us',
70
+ slug: 'contact-us',
71
+ description: 'General contact',
72
+ status: 'active',
73
+ fields: [field({ key: 'name' })],
74
+ notificationSettings: { enabled: true, recipients: ['a@b.co'] },
75
+ },
76
+ });
77
+ expect(def.name).toBe('Contact Us');
78
+ expect(def.slug).toBe('contact-us');
79
+ expect(def.status).toBe('active');
80
+ expect(def.fields).toHaveLength(1);
81
+ expect(def.notifyEnabled).toBe(true);
82
+ expect(def.createdBy).toBe('user_1');
83
+ });
84
+ it('defaults unknown status to draft', () => {
85
+ const def = parseFormDefinition({ id: 'x', data: { name: 'X', status: 'weird' } });
86
+ expect(def.status).toBe('draft');
87
+ });
88
+ });
89
+ describe('normalizeEntry', () => {
90
+ it('prefers persisted sender columns', () => {
91
+ const entry = normalizeEntry({
92
+ id: 's1',
93
+ formId: 'f1',
94
+ data: { name: 'ignored' },
95
+ senderName: 'Marcus Webb',
96
+ senderEmail: 'marcus@weave.com',
97
+ preview: 'Hello there',
98
+ unread: true,
99
+ starred: false,
100
+ archived: false,
101
+ spamStatus: 'clean',
102
+ submittedAt: new Date('2026-05-24T09:14:00Z'),
103
+ createdAt: new Date('2026-05-24T09:14:00Z'),
104
+ updatedAt: new Date('2026-05-24T09:14:00Z'),
105
+ }, { name: 'Contact Us', slug: 'contact-us' });
106
+ expect(entry.senderName).toBe('Marcus Webb');
107
+ expect(entry.senderEmail).toBe('marcus@weave.com');
108
+ expect(entry.formName).toBe('Contact Us');
109
+ expect(entry.unread).toBe(true);
110
+ });
111
+ it('derives sender/preview from data when columns are absent (legacy rows)', () => {
112
+ const entry = normalizeEntry({
113
+ id: 's2',
114
+ formId: 'f1',
115
+ data: { name: 'Priya Nair', email: 'p@nair.io', message: 'We are launching a SaaS product.' },
116
+ unread: false,
117
+ starred: false,
118
+ archived: false,
119
+ spamStatus: 'clean',
120
+ submittedAt: new Date(),
121
+ createdAt: new Date(),
122
+ updatedAt: new Date(),
123
+ });
124
+ expect(entry.senderName).toBe('Priya Nair');
125
+ expect(entry.senderEmail).toBe('p@nair.io');
126
+ expect(entry.preview).toContain('launching a SaaS');
127
+ });
128
+ });
129
+ describe('listEntries where-builder', () => {
130
+ let recordedWhere;
131
+ function fakeDb(rows) {
132
+ return {
133
+ formSubmission: {
134
+ findMany: vi.fn(async (args) => {
135
+ recordedWhere = args.where;
136
+ return rows;
137
+ }),
138
+ count: vi.fn(async () => rows.length),
139
+ },
140
+ document: { findMany: vi.fn(async () => []) },
141
+ };
142
+ }
143
+ beforeEach(() => {
144
+ recordedWhere = undefined;
145
+ });
146
+ it('excludes archived and spam by default', async () => {
147
+ dbHolder.current = fakeDb([]);
148
+ await listEntries({});
149
+ expect(recordedWhere.archived).toBe(false);
150
+ expect(recordedWhere.spamStatus).toEqual({ not: 'spam' });
151
+ });
152
+ it('filters to unread/starred when requested', async () => {
153
+ dbHolder.current = fakeDb([]);
154
+ await listEntries({ unread: true, starred: true });
155
+ expect(recordedWhere.unread).toBe(true);
156
+ expect(recordedWhere.starred).toBe(true);
157
+ });
158
+ it('builds a case-insensitive OR search across sender fields', async () => {
159
+ dbHolder.current = fakeDb([]);
160
+ await listEntries({ search: 'marcus' });
161
+ expect(Array.isArray(recordedWhere.OR)).toBe(true);
162
+ expect(recordedWhere.OR[0]).toEqual({ senderName: { contains: 'marcus', mode: 'insensitive' } });
163
+ });
164
+ it('scopes by formId and includes archived when explicitly asked', async () => {
165
+ dbHolder.current = fakeDb([]);
166
+ await listEntries({ formId: 'f1', archived: true });
167
+ expect(recordedWhere.formId).toBe('f1');
168
+ expect(recordedWhere.archived).toBe(true);
169
+ });
170
+ });
171
+ describe('getEntryCounts', () => {
172
+ it('returns chip totals and per-form unread map', async () => {
173
+ const count = vi
174
+ .fn()
175
+ .mockResolvedValueOnce(18) // total
176
+ .mockResolvedValueOnce(5) // unread
177
+ .mockResolvedValueOnce(3) // starred
178
+ .mockResolvedValueOnce(15); // this week
179
+ dbHolder.current = {
180
+ formSubmission: {
181
+ count,
182
+ groupBy: vi.fn(async () => [{ formId: 'f1', _count: { _all: 8 } }]),
183
+ },
184
+ };
185
+ const counts = await getEntryCounts({});
186
+ expect(counts.total).toBe(18);
187
+ expect(counts.unread).toBe(5);
188
+ expect(counts.starred).toBe(3);
189
+ expect(counts.thisWeek).toBe(15);
190
+ expect(counts.byForm.f1).toBe(8);
191
+ });
192
+ });
193
+ //# sourceMappingURL=forms-service.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms-service.test.js","sourceRoot":"","sources":["../../../src/__tests__/forms/forms-service.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=middleware.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/middleware.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,50 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2
+ import { createCMSMiddleware } from '../middleware.js';
3
+ /**
4
+ * Regression coverage for the admin/API route matching boundary. The guard used
5
+ * `startsWith('/admin')`, which also captured sibling public routes such as
6
+ * `/administrators` or `/admin-news`. Those must NOT be treated as admin routes.
7
+ */
8
+ describe('createCMSMiddleware route boundary', () => {
9
+ const origSecret = process.env.CMS_SECRET;
10
+ beforeEach(() => {
11
+ process.env.CMS_SECRET = 'x'.repeat(32);
12
+ });
13
+ afterEach(() => {
14
+ process.env.CMS_SECRET = origSecret;
15
+ });
16
+ function req(path) {
17
+ return new Request(`https://example.com${path}`);
18
+ }
19
+ it('does not treat /admin-news as an admin route (IP allowlist not enforced)', async () => {
20
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
21
+ // An admin route with a non-allowlisted IP would be 403; a public route
22
+ // passes through (null). /admin-news must pass through.
23
+ const res = await mw(req('/admin-news'));
24
+ expect(res).toBeNull();
25
+ });
26
+ it('does not treat /administrators as an admin route', async () => {
27
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
28
+ expect(await mw(req('/administrators'))).toBeNull();
29
+ });
30
+ it('still enforces the allowlist on the exact admin path', async () => {
31
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
32
+ const res = await mw(req('/admin'));
33
+ expect(res?.status).toBe(403);
34
+ });
35
+ it('still enforces the allowlist on nested admin paths', async () => {
36
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
37
+ const res = await mw(req('/admin/collections/posts'));
38
+ expect(res?.status).toBe(403);
39
+ });
40
+ it('does not treat /api/cmsx as a CMS API route', async () => {
41
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
42
+ expect(await mw(req('/api/cmsx/thing'))).toBeNull();
43
+ });
44
+ it('enforces the allowlist on /api/cms routes', async () => {
45
+ const mw = createCMSMiddleware({ admin: { path: '/admin', ipAllowlist: ['10.0.0.1'] } });
46
+ const res = await mw(req('/api/cms/documents'));
47
+ expect(res?.status).toBe(403);
48
+ });
49
+ });
50
+ //# sourceMappingURL=middleware.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.test.js","sourceRoot":"","sources":["../../src/__tests__/middleware.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=analysis-markdown.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-markdown.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/seo/analysis-markdown.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,57 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { analyzeContent, extractHeadings } from '../../seo/analysis.js';
3
+ import { countH1 } from '../../seo/score.js';
4
+ /**
5
+ * Regression coverage for markdown-bodied content. The heading checks used to
6
+ * match only HTML `<h1>`/`<h2>` tags, so every markdown post was wrongly flagged
7
+ * with "No H1 heading found" and "no subheadings". These tests lock in that both
8
+ * HTML and Markdown headings are recognised.
9
+ */
10
+ describe('extractHeadings (markdown-aware)', () => {
11
+ it('extracts ATX markdown headings at the requested level', () => {
12
+ const md = '# Title\n\nIntro.\n\n## Section A\n\n## Section B\n';
13
+ expect(extractHeadings(md, 1)).toEqual(['Title']);
14
+ expect(extractHeadings(md, 2)).toEqual(['Section A', 'Section B']);
15
+ });
16
+ it('extracts Setext markdown headings', () => {
17
+ const md = 'Big Title\n=========\n\nA Subhead\n---------\n';
18
+ expect(extractHeadings(md, 1)).toEqual(['Big Title']);
19
+ expect(extractHeadings(md, 2)).toEqual(['A Subhead']);
20
+ });
21
+ it('still extracts HTML headings', () => {
22
+ const html = '<h1>HTML Title</h1><h2>Sub</h2>';
23
+ expect(extractHeadings(html, 1)).toEqual(['HTML Title']);
24
+ expect(extractHeadings(html, 2)).toEqual(['Sub']);
25
+ });
26
+ it('ignores `#` inside fenced code blocks', () => {
27
+ const md = '# Real Title\n\n```\n# not a heading\n```\n';
28
+ expect(extractHeadings(md, 1)).toEqual(['Real Title']);
29
+ });
30
+ it('does not treat a higher-level ATX heading as a lower one', () => {
31
+ expect(extractHeadings('### Deep', 1)).toEqual([]);
32
+ expect(extractHeadings('### Deep', 3)).toEqual(['Deep']);
33
+ });
34
+ });
35
+ describe('countH1 (markdown-aware)', () => {
36
+ it('counts HTML h1 tags', () => {
37
+ expect(countH1('<h1>a</h1>')).toBe(1);
38
+ expect(countH1('<H1>a</H1><h1>b</h1>')).toBe(2);
39
+ });
40
+ it('counts markdown h1 headings', () => {
41
+ expect(countH1('# Only One\n\nbody')).toBe(1);
42
+ expect(countH1('# One\n\n# Two')).toBe(2);
43
+ expect(countH1('## Not an h1')).toBe(0);
44
+ });
45
+ });
46
+ describe('analyzeContent h1-present for markdown bodies', () => {
47
+ it('passes the H1 check when the markdown body has exactly one H1', () => {
48
+ const result = analyzeContent({
49
+ title: 'My Post',
50
+ slug: 'my-post',
51
+ content: '# My Post\n\nSome intro text.\n\n## Details\n\nMore text here.',
52
+ });
53
+ const h1Check = result.checks.find((c) => c.id === 'h1-present');
54
+ expect(h1Check?.status).toBe('good');
55
+ });
56
+ });
57
+ //# sourceMappingURL=analysis-markdown.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analysis-markdown.test.js","sourceRoot":"","sources":["../../../src/__tests__/seo/analysis-markdown.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=audit-runner.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-runner.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/seo/audit-runner.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,135 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { frontEndContentCollectionTypes, gatherAuditEntities, runAndPersistAudit, } from '../../seo/audit-runner.js';
3
+ /**
4
+ * Regression coverage for the SEO audit reliability fixes:
5
+ * - a new audit refuses to start while another is RUNNING (parallel runs can
6
+ * otherwise duplicate SeoIssue rows for the same issueKey), and
7
+ * - `gatherAuditEntities` bounds its document query so a large instance can't
8
+ * OOM the audit pass.
9
+ */
10
+ function baseDb(overrides = {}) {
11
+ return {
12
+ document: {
13
+ findMany: vi.fn(async () => []),
14
+ },
15
+ seoAuditRun: {
16
+ create: vi.fn(async () => ({ id: 'run-1' })),
17
+ update: vi.fn(async () => ({})),
18
+ findFirst: vi.fn(async () => null),
19
+ },
20
+ seoIssue: {
21
+ findMany: vi.fn(async () => []),
22
+ create: vi.fn(async () => ({})),
23
+ update: vi.fn(async () => ({})),
24
+ },
25
+ ...overrides,
26
+ };
27
+ }
28
+ describe('runAndPersistAudit concurrency guard', () => {
29
+ it('throws when another audit started within the stale window', async () => {
30
+ const db = baseDb();
31
+ // First findFirst call is the RUNNING check; return a fresh running run.
32
+ db.seoAuditRun.findFirst = vi.fn(async ({ where }) => {
33
+ if (where?.status === 'RUNNING')
34
+ return { startedAt: new Date() };
35
+ return null;
36
+ });
37
+ await expect(runAndPersistAudit(db)).rejects.toThrow(/already running/i);
38
+ expect(db.seoAuditRun.create).not.toHaveBeenCalled();
39
+ });
40
+ it('proceeds when the only RUNNING run is stale (crashed)', async () => {
41
+ const db = baseDb();
42
+ const stale = new Date(Date.now() - 60 * 60 * 1000); // 1h ago
43
+ db.seoAuditRun.findFirst = vi.fn(async ({ where }) => {
44
+ if (where?.status === 'RUNNING')
45
+ return { startedAt: stale };
46
+ if (where?.status === 'COMPLETED')
47
+ return { siteScoreAfter: 80 };
48
+ return null;
49
+ });
50
+ const result = await runAndPersistAudit(db);
51
+ expect(db.seoAuditRun.create).toHaveBeenCalledTimes(1);
52
+ expect(result.status).toBe('COMPLETED');
53
+ });
54
+ it('proceeds when no audit is running', async () => {
55
+ const db = baseDb();
56
+ const result = await runAndPersistAudit(db);
57
+ expect(db.seoAuditRun.create).toHaveBeenCalledTimes(1);
58
+ expect(result.status).toBe('COMPLETED');
59
+ });
60
+ });
61
+ describe('gatherAuditEntities query bounds', () => {
62
+ it('caps the document query with a take limit', async () => {
63
+ const findMany = vi.fn(async (_args) => []);
64
+ const db = { document: { findMany } };
65
+ await gatherAuditEntities(db);
66
+ expect(findMany).toHaveBeenCalledTimes(1);
67
+ const arg = (findMany.mock.calls[0]?.[0] ?? {});
68
+ expect(typeof arg.take).toBe('number');
69
+ expect(arg.take).toBeGreaterThan(0);
70
+ });
71
+ it('excludes internal/system collections from the audit set', async () => {
72
+ const findMany = vi.fn(async () => [
73
+ { id: 'a', collection: 'pages', data: { title: 'A', slug: 'a' } },
74
+ { id: 'b', collection: '__seo_config__', data: { title: 'B', slug: 'b' } },
75
+ ]);
76
+ const db = { document: { findMany } };
77
+ const entities = await gatherAuditEntities(db);
78
+ expect(entities).toHaveLength(1);
79
+ expect(entities[0].entityId).toBe('a');
80
+ });
81
+ it('scopes the query to front-end content collections when given a type map', async () => {
82
+ const findMany = vi.fn(async (_args) => []);
83
+ const db = { document: { findMany } };
84
+ await gatherAuditEntities(db, { collectionTypes: { pages: 'page', blog: 'post' } });
85
+ const arg = (findMany.mock.calls[0]?.[0] ?? {});
86
+ expect(arg.where?.collection?.in).toEqual(['pages', 'blog']);
87
+ });
88
+ it('returns nothing (without querying) when there are no front-end collections', async () => {
89
+ const findMany = vi.fn(async () => []);
90
+ const db = { document: { findMany } };
91
+ const entities = await gatherAuditEntities(db, { collectionTypes: {} });
92
+ expect(entities).toEqual([]);
93
+ expect(findMany).not.toHaveBeenCalled();
94
+ });
95
+ it('derives entityType from the configured collection type', async () => {
96
+ const findMany = vi.fn(async () => [
97
+ { id: 'p1', collection: 'pages', data: { title: 'Home', slug: 'home' } },
98
+ { id: 'b1', collection: 'blog', data: { title: 'Post', slug: 'post' } },
99
+ ]);
100
+ const db = { document: { findMany } };
101
+ const entities = await gatherAuditEntities(db, {
102
+ collectionTypes: { pages: 'page', blog: 'post' },
103
+ });
104
+ expect(entities.find((e) => e.entityId === 'p1').entityType).toBe('page');
105
+ expect(entities.find((e) => e.entityId === 'b1').entityType).toBe('post');
106
+ });
107
+ });
108
+ describe('frontEndContentCollectionTypes', () => {
109
+ const cfg = {
110
+ collections: {
111
+ pages: { type: 'page' },
112
+ blog: { type: 'post' },
113
+ 'case-studies': { type: 'post' },
114
+ // Structural / utility collections have no `type` -> excluded.
115
+ navigations: { admin: { group: 'Content' } },
116
+ 'page-tags': { admin: { hidden: true } },
117
+ // Hidden even though typed -> excluded.
118
+ drafts: { type: 'post', admin: { hidden: true } },
119
+ // Explicitly excluded from sitemap -> excluded.
120
+ legal: { type: 'page', seo: { excludeFromSitemap: true } },
121
+ },
122
+ seo: { sitemap: { excludeCollections: ['case-studies'] } },
123
+ };
124
+ it('returns only typed, visible, sitemap-eligible content collections', () => {
125
+ expect(frontEndContentCollectionTypes(cfg)).toEqual({ pages: 'page', blog: 'post' });
126
+ });
127
+ it('excludes navigation/footer (untyped) collections', () => {
128
+ expect(frontEndContentCollectionTypes(cfg)).not.toHaveProperty('navigations');
129
+ });
130
+ it('returns an empty map for missing/empty config', () => {
131
+ expect(frontEndContentCollectionTypes(null)).toEqual({});
132
+ expect(frontEndContentCollectionTypes({})).toEqual({});
133
+ });
134
+ });
135
+ //# sourceMappingURL=audit-runner.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audit-runner.test.js","sourceRoot":"","sources":["../../../src/__tests__/seo/audit-runner.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=internal-collections.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"internal-collections.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/seo/internal-collections.test.ts"],"names":[],"mappings":""}