@setzkasten-cms/astro-admin 1.4.2 → 1.5.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/dist/api-routes/_auth-guard.d.ts +27 -3
- package/dist/api-routes/_auth-guard.js +5 -2
- package/dist/api-routes/_dev-session-secret.d.ts +8 -0
- package/dist/api-routes/_dev-session-secret.js +8 -0
- package/dist/api-routes/_github-token.js +1 -1
- package/dist/api-routes/_role-resolver.js +6 -3
- package/dist/api-routes/_session-secret.d.ts +19 -0
- package/dist/api-routes/_session-secret.js +7 -0
- package/dist/api-routes/_session-signing.d.ts +45 -0
- package/dist/api-routes/_session-signing.js +8 -0
- package/dist/api-routes/_webhook-dispatcher.js +4 -4
- package/dist/api-routes/asset-proxy.js +1 -1
- package/dist/api-routes/auth-callback.js +12 -5
- package/dist/api-routes/auth-logout.d.ts +4 -4
- package/dist/api-routes/auth-logout.js +8 -2
- package/dist/api-routes/auth-session.d.ts +6 -0
- package/dist/api-routes/auth-session.js +19 -19
- package/dist/api-routes/auth-setzkasten-login.js +14 -7
- package/dist/api-routes/catalog-add.js +59 -17
- package/dist/api-routes/catalog-export.js +14 -4
- package/dist/api-routes/config.d.ts +10 -3
- package/dist/api-routes/config.js +26 -4
- package/dist/api-routes/deploy-hook.js +8 -8
- package/dist/api-routes/editors.d.ts +1 -1
- package/dist/api-routes/editors.js +5 -2
- package/dist/api-routes/github-proxy.js +30 -8
- package/dist/api-routes/global-config.js +6 -3
- package/dist/api-routes/history-rollback.js +31 -14
- package/dist/api-routes/history-version.js +8 -6
- package/dist/api-routes/history.js +5 -2
- package/dist/api-routes/icons-local.js +1 -1
- package/dist/api-routes/init-add-section.js +150 -48
- package/dist/api-routes/init-apply.js +56 -42
- package/dist/api-routes/init-migrate.js +43 -36
- package/dist/api-routes/init-scan-page.d.ts +1 -1
- package/dist/api-routes/init-scan-page.js +59 -13
- package/dist/api-routes/init-scan.js +22 -7
- package/dist/api-routes/migrate-to-multi.js +5 -2
- package/dist/api-routes/pages.js +15 -4
- package/dist/api-routes/section-add.js +68 -16
- package/dist/api-routes/section-commit-pending.js +70 -22
- package/dist/api-routes/section-delete.js +49 -14
- package/dist/api-routes/section-duplicate.js +65 -16
- package/dist/api-routes/section-prepare-copy.js +15 -2
- package/dist/api-routes/section-prepare.js +25 -4
- package/dist/api-routes/setup-github-app-bounce.js +15 -1
- package/dist/api-routes/setup-github-app-branches.js +9 -6
- package/dist/api-routes/setup-github-app-callback.js +24 -1
- package/dist/api-routes/setup-github-app-credentials.d.ts +27 -0
- package/dist/api-routes/setup-github-app-credentials.js +43 -0
- package/dist/api-routes/setup-github-app-installed.js +22 -1
- package/dist/api-routes/setup-github-app-repos.js +5 -2
- package/dist/api-routes/setup-github-app.d.ts +4 -0
- package/dist/api-routes/setup-github-app.js +19 -2
- package/dist/api-routes/updater-register.js +7 -1
- package/dist/api-routes/webhooks-status.js +5 -2
- package/dist/api-routes/webhooks-test.js +9 -8
- package/dist/api-routes/webhooks.js +12 -14
- package/dist/api-routes/websites-add.js +5 -2
- package/dist/api-routes/websites-remove.js +5 -2
- package/dist/{chunk-ZQDGGWJP.js → chunk-5KMGSFCZ.js} +2 -2
- package/dist/{chunk-RHJONMLK.js → chunk-CDXCYYQR.js} +222 -5
- package/dist/{chunk-NKDATSPA.js → chunk-DP6RTINQ.js} +1 -1
- package/dist/chunk-KENFINT4.js +76 -0
- package/dist/chunk-ONP6BRZO.js +47 -0
- package/dist/{chunk-INIWFKQ3.js → chunk-Q5HV47DW.js} +33 -19
- package/dist/chunk-QVCW6EF3.js +26 -0
- package/dist/{chunk-K22A4ZBS.js → chunk-UHI6323G.js} +293 -174
- package/dist/{chunk-AM4DZXXM.js → chunk-UJAFZEX2.js} +76 -9
- package/package.json +12 -6
- package/src/api-routes/__tests__/_session-signing.test.ts +114 -0
- package/src/api-routes/__tests__/_session-test-helper.ts +27 -0
- package/src/api-routes/__tests__/add-section-helpers.test.ts +91 -97
- package/src/api-routes/__tests__/auth-guard.test.ts +46 -7
- package/src/api-routes/__tests__/catalog-api.test.ts +14 -6
- package/src/api-routes/__tests__/commit-trailers.test.ts +5 -5
- package/src/api-routes/__tests__/deferred-operations.test.ts +9 -12
- package/src/api-routes/__tests__/deploy-hook.test.ts +3 -8
- package/src/api-routes/__tests__/feature-gate.test.ts +1 -1
- package/src/api-routes/__tests__/github-cache.test.ts +1 -1
- package/src/api-routes/__tests__/github-token.test.ts +1 -1
- package/src/api-routes/__tests__/global-config-theme.test.ts +4 -4
- package/src/api-routes/__tests__/history-rollback.test.ts +6 -3
- package/src/api-routes/__tests__/history.test.ts +9 -6
- package/src/api-routes/__tests__/init-scan-page-resolve-config.test.ts +11 -3
- package/src/api-routes/__tests__/migrate-to-multi.test.ts +5 -1
- package/src/api-routes/__tests__/pages-meta-store.test.ts +10 -5
- package/src/api-routes/__tests__/pages.test.ts +7 -2
- package/src/api-routes/__tests__/patch-page-file.test.ts +71 -19
- package/src/api-routes/__tests__/route-registry.test.ts +11 -18
- package/src/api-routes/__tests__/scan-page-helpers.test.ts +13 -10
- package/src/api-routes/__tests__/section-management.test.ts +28 -28
- package/src/api-routes/__tests__/setup-github-app-callback.test.ts +58 -16
- package/src/api-routes/__tests__/setup-github-app-repos.test.ts +4 -5
- package/src/api-routes/__tests__/setup-github-app.test.ts +27 -7
- package/src/api-routes/__tests__/storage-config-for-request.test.ts +83 -0
- package/src/api-routes/__tests__/updater-register.test.ts +230 -0
- package/src/api-routes/__tests__/webhook-signing.test.ts +1 -1
- package/src/api-routes/__tests__/webhooks.test.ts +19 -7
- package/src/api-routes/__tests__/websites-add.test.ts +2 -1
- package/src/api-routes/__tests__/websites-remove.test.ts +2 -1
- package/src/api-routes/_auth-guard.ts +47 -15
- package/src/api-routes/_commit-trailers.ts +3 -2
- package/src/api-routes/_dev-session-secret.ts +79 -0
- package/src/api-routes/_github-token.ts +1 -1
- package/src/api-routes/_pages-meta-store.ts +2 -2
- package/src/api-routes/_role-resolver.ts +7 -5
- package/src/api-routes/_session-secret.ts +46 -0
- package/src/api-routes/_session-signing.ts +135 -0
- package/src/api-routes/_vercel-origin.ts +2 -6
- package/src/api-routes/_webhook-dispatcher.ts +12 -16
- package/src/api-routes/_website-resolver.ts +3 -10
- package/src/api-routes/auth-callback.ts +9 -5
- package/src/api-routes/auth-login.ts +5 -3
- package/src/api-routes/auth-logout.ts +18 -1
- package/src/api-routes/auth-session.ts +13 -21
- package/src/api-routes/auth-setzkasten-login.ts +12 -9
- package/src/api-routes/catalog-add.ts +89 -31
- package/src/api-routes/catalog-export.ts +30 -10
- package/src/api-routes/config.ts +39 -6
- package/src/api-routes/deploy-hook.ts +13 -11
- package/src/api-routes/editors.ts +33 -22
- package/src/api-routes/github-proxy.ts +25 -11
- package/src/api-routes/global-config.ts +103 -18
- package/src/api-routes/history-rollback.ts +41 -14
- package/src/api-routes/history-version.ts +5 -6
- package/src/api-routes/history.ts +3 -3
- package/src/api-routes/icons-local.ts +2 -2
- package/src/api-routes/init-add-section.ts +218 -88
- package/src/api-routes/init-apply.ts +71 -56
- package/src/api-routes/init-migrate.ts +54 -48
- package/src/api-routes/init-scan-page.ts +77 -30
- package/src/api-routes/init-scan.ts +19 -11
- package/src/api-routes/pages.ts +16 -11
- package/src/api-routes/section-add.ts +98 -27
- package/src/api-routes/section-commit-pending.ts +87 -34
- package/src/api-routes/section-delete.ts +76 -27
- package/src/api-routes/section-duplicate.ts +95 -28
- package/src/api-routes/section-management.ts +3 -7
- package/src/api-routes/section-prepare-copy.ts +29 -8
- package/src/api-routes/section-prepare.ts +38 -10
- package/src/api-routes/setup-github-app-bounce.ts +7 -1
- package/src/api-routes/setup-github-app-branches.ts +6 -7
- package/src/api-routes/setup-github-app-callback.ts +18 -1
- package/src/api-routes/setup-github-app-credentials.ts +55 -0
- package/src/api-routes/setup-github-app-installed.ts +12 -1
- package/src/api-routes/setup-github-app-repos.ts +2 -3
- package/src/api-routes/setup-github-app.ts +14 -5
- package/src/api-routes/updater-check.ts +6 -4
- package/src/api-routes/updater-register.ts +34 -20
- package/src/api-routes/updater-transfer.ts +8 -6
- package/src/api-routes/updater-unbind.ts +14 -10
- package/src/api-routes/webhooks-test.ts +9 -11
- package/src/api-routes/webhooks.ts +15 -19
- package/src/init/__tests__/page-level.test.ts +279 -105
- package/src/init/__tests__/page-list-coverage.test.ts +70 -70
- package/src/init/__tests__/patcher-child-component.test.ts +126 -0
- package/src/init/__tests__/patcher-edge-cases.test.ts +47 -23
- package/src/init/__tests__/patcher-mixed-content-wrapper.test.ts +16 -6
- package/src/init/__tests__/patcher-page-mode.test.ts +30 -20
- package/src/init/__tests__/section-pipeline.test.ts +102 -16
- package/src/init/astro-config-patcher.ts +4 -18
- package/src/init/astro-detector.ts +2 -7
- package/src/init/astro-section-analyzer-v2.ts +475 -193
- package/src/init/field-label-enricher.ts +6 -6
- package/src/init/template-patcher-v2.ts +490 -56
|
@@ -1,17 +1,24 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { parseSession, guardPageAccess } from '../_auth-guard'
|
|
3
1
|
import type { AuthSession, SetzKastenConfig } from '@setzkasten-cms/core'
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { guardPageAccess, parseSession } from '../_auth-guard'
|
|
4
|
+
import { makeTestSessionCookie } from './_session-test-helper'
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Helpers
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
|
|
9
10
|
function adminSession(): AuthSession {
|
|
10
|
-
return {
|
|
11
|
+
return {
|
|
12
|
+
user: { id: '1', email: 'admin@example.com', provider: 'github', role: 'admin' },
|
|
13
|
+
expiresAt: Date.now() + 86400_000,
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
function editorSession(email = 'editor@example.com'): AuthSession {
|
|
14
|
-
return {
|
|
18
|
+
return {
|
|
19
|
+
user: { id: '2', email, provider: 'google', role: 'editor' },
|
|
20
|
+
expiresAt: Date.now() + 86400_000,
|
|
21
|
+
}
|
|
15
22
|
}
|
|
16
23
|
|
|
17
24
|
const baseConfig: SetzKastenConfig = {
|
|
@@ -33,9 +40,37 @@ describe('parseSession', () => {
|
|
|
33
40
|
expect(parseSession('not-json')).toBeNull()
|
|
34
41
|
})
|
|
35
42
|
|
|
36
|
-
it('
|
|
43
|
+
it('rejects an unsigned plain-JSON cookie (legacy / forged format)', () => {
|
|
44
|
+
// Pre-C1 the cookie was JSON.stringify(session) — anyone could forge
|
|
45
|
+
// any role. parseSession must refuse the old format outright.
|
|
37
46
|
const session = adminSession()
|
|
38
|
-
expect(parseSession(JSON.stringify(session))).
|
|
47
|
+
expect(parseSession(JSON.stringify(session))).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('accepts a properly signed session cookie', () => {
|
|
51
|
+
const session = adminSession()
|
|
52
|
+
expect(parseSession(makeTestSessionCookie(session))).toEqual(session)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('rejects an expired signed session', () => {
|
|
56
|
+
const expired: AuthSession = {
|
|
57
|
+
user: { id: '1', email: 'admin@example.com', provider: 'github', role: 'admin' },
|
|
58
|
+
expiresAt: Date.now() - 1,
|
|
59
|
+
}
|
|
60
|
+
expect(parseSession(makeTestSessionCookie(expired))).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('rejects a tampered payload (role escalation forgery)', () => {
|
|
64
|
+
const editor: AuthSession = {
|
|
65
|
+
user: { id: '2', email: 'e@example.com', provider: 'google', role: 'editor' },
|
|
66
|
+
expiresAt: Date.now() + 60_000,
|
|
67
|
+
}
|
|
68
|
+
const cookie = makeTestSessionCookie(editor)
|
|
69
|
+
const [, sig] = cookie.split('.')
|
|
70
|
+
const forgedPayload = Buffer.from(
|
|
71
|
+
JSON.stringify({ ...editor, user: { ...editor.user, role: 'admin' } }),
|
|
72
|
+
).toString('base64url')
|
|
73
|
+
expect(parseSession(`${forgedPayload}.${sig}`)).toBeNull()
|
|
39
74
|
})
|
|
40
75
|
})
|
|
41
76
|
|
|
@@ -103,7 +138,11 @@ describe('guardPageAccess – dynamic editors', () => {
|
|
|
103
138
|
kind: 'present',
|
|
104
139
|
editors: [{ email: 'editor@example.com' }],
|
|
105
140
|
})
|
|
106
|
-
const res = await guardPageAccess(
|
|
141
|
+
const res = await guardPageAccess(
|
|
142
|
+
editorSession('editor@example.com'),
|
|
143
|
+
'secret-page',
|
|
144
|
+
baseConfig,
|
|
145
|
+
)
|
|
107
146
|
expect(res).toBeNull()
|
|
108
147
|
})
|
|
109
148
|
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
* 3. buildCatalogAddCommit — builds file paths for a catalog add commit
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { describe,
|
|
13
|
+
import { describe, expect, it } from 'vitest'
|
|
14
14
|
import {
|
|
15
|
+
buildCatalogAddCommit,
|
|
15
16
|
buildCatalogResponse,
|
|
16
17
|
validateCatalogAddBody,
|
|
17
|
-
buildCatalogAddCommit,
|
|
18
18
|
} from '../catalog-helpers'
|
|
19
19
|
|
|
20
20
|
// ---------------------------------------------------------------------------
|
|
@@ -31,7 +31,7 @@ describe('buildCatalogResponse', () => {
|
|
|
31
31
|
})
|
|
32
32
|
|
|
33
33
|
it('includes hero, features, cta', () => {
|
|
34
|
-
const names = buildCatalogResponse().map(t => t.name)
|
|
34
|
+
const names = buildCatalogResponse().map((t) => t.name)
|
|
35
35
|
expect(names).toContain('hero')
|
|
36
36
|
expect(names).toContain('features')
|
|
37
37
|
expect(names).toContain('cta')
|
|
@@ -62,7 +62,9 @@ describe('validateCatalogAddBody', () => {
|
|
|
62
62
|
})
|
|
63
63
|
|
|
64
64
|
it('accepts body with sectionKey override', () => {
|
|
65
|
-
expect(() =>
|
|
65
|
+
expect(() =>
|
|
66
|
+
validateCatalogAddBody({ templateName: 'hero', pageKey: 'index', sectionKey: 'hero--top' }),
|
|
67
|
+
).not.toThrow()
|
|
66
68
|
})
|
|
67
69
|
|
|
68
70
|
it('throws when templateName is missing', () => {
|
|
@@ -74,7 +76,9 @@ describe('validateCatalogAddBody', () => {
|
|
|
74
76
|
})
|
|
75
77
|
|
|
76
78
|
it('throws when templateName is not in registry', () => {
|
|
77
|
-
expect(() =>
|
|
79
|
+
expect(() =>
|
|
80
|
+
validateCatalogAddBody({ templateName: 'nonexistent-xyz', pageKey: 'index' }),
|
|
81
|
+
).toThrow()
|
|
78
82
|
})
|
|
79
83
|
})
|
|
80
84
|
|
|
@@ -109,7 +113,11 @@ describe('buildCatalogAddCommit', () => {
|
|
|
109
113
|
})
|
|
110
114
|
|
|
111
115
|
it('pageConfigPath matches the pageKey', () => {
|
|
112
|
-
const result = buildCatalogAddCommit({
|
|
116
|
+
const result = buildCatalogAddCommit({
|
|
117
|
+
...opts,
|
|
118
|
+
pageKey: 'about',
|
|
119
|
+
pageConfigPath: 'content/pages/_about.json',
|
|
120
|
+
})
|
|
113
121
|
expect(result.pageConfigPath).toBe('content/pages/_about.json')
|
|
114
122
|
})
|
|
115
123
|
})
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* @vitest-environment node
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { describe,
|
|
7
|
-
import {
|
|
6
|
+
import { describe, expect, it } from 'vitest'
|
|
7
|
+
import { SETZKASTEN_CO_AUTHOR, withTrailers } from '../_commit-trailers'
|
|
8
8
|
|
|
9
9
|
describe('SETZKASTEN_CO_AUTHOR', () => {
|
|
10
10
|
it('is a valid Co-authored-by trailer', () => {
|
|
@@ -28,20 +28,20 @@ describe('withTrailers', () => {
|
|
|
28
28
|
|
|
29
29
|
it('does not add editor trailer when editorEmail is not provided', () => {
|
|
30
30
|
const result = withTrailers('chore: something')
|
|
31
|
-
const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
|
|
31
|
+
const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
|
|
32
32
|
expect(lines).toHaveLength(1)
|
|
33
33
|
expect(lines[0]).toBe(SETZKASTEN_CO_AUTHOR)
|
|
34
34
|
})
|
|
35
35
|
|
|
36
36
|
it('does not add editor trailer when editorEmail is null', () => {
|
|
37
37
|
const result = withTrailers('chore: something', null)
|
|
38
|
-
const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
|
|
38
|
+
const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
|
|
39
39
|
expect(lines).toHaveLength(1)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
it('adds editor Co-authored-by when editorEmail is provided', () => {
|
|
43
43
|
const result = withTrailers('content: update', 'jane.doe@example.com')
|
|
44
|
-
const lines = result.split('\n').filter(l => l.startsWith('Co-authored-by:'))
|
|
44
|
+
const lines = result.split('\n').filter((l) => l.startsWith('Co-authored-by:'))
|
|
45
45
|
expect(lines).toHaveLength(2)
|
|
46
46
|
expect(lines[1]).toContain('jane.doe@example.com')
|
|
47
47
|
})
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* - Commits triggered on every duplicate/delete instead of batched
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { describe,
|
|
12
|
-
import {
|
|
11
|
+
import { describe, expect, it } from 'vitest'
|
|
12
|
+
import { duplicateInPageConfig, generateDuplicateKey } from '../section-management'
|
|
13
13
|
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
15
15
|
// 1. Duplicate key generation (reused by prepare-copy)
|
|
@@ -25,8 +25,9 @@ describe('generateDuplicateKey (used by prepare-copy)', () => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
it('increments suffix until unique', () => {
|
|
28
|
-
expect(generateDuplicateKey(['hero', 'hero--copy', 'hero--copy2', 'hero--copy3'], 'hero'))
|
|
29
|
-
|
|
28
|
+
expect(generateDuplicateKey(['hero', 'hero--copy', 'hero--copy2', 'hero--copy3'], 'hero')).toBe(
|
|
29
|
+
'hero--copy4',
|
|
30
|
+
)
|
|
30
31
|
})
|
|
31
32
|
|
|
32
33
|
it('generates copy of a copy', () => {
|
|
@@ -82,9 +83,7 @@ describe('duplicateInPageConfig (used by prepare-copy)', () => {
|
|
|
82
83
|
|
|
83
84
|
it('preserves type field from original entry when present', () => {
|
|
84
85
|
const configWithType = {
|
|
85
|
-
sections: [
|
|
86
|
-
{ key: 'hero--about', type: 'hero', enabled: true, order: 0 },
|
|
87
|
-
],
|
|
86
|
+
sections: [{ key: 'hero--about', type: 'hero', enabled: true, order: 0 }],
|
|
88
87
|
}
|
|
89
88
|
const result = duplicateInPageConfig(configWithType, 'hero--about', 'hero--about--copy')
|
|
90
89
|
const copy = result.sections.find((s: any) => s.key === 'hero--about--copy')
|
|
@@ -98,9 +97,7 @@ describe('duplicateInPageConfig (used by prepare-copy)', () => {
|
|
|
98
97
|
// getSectionDef looks up 'testPricing--copy' in the catalog → not found → empty editor.
|
|
99
98
|
// resolveSectionType returns 'testPricing--copy' → no component → not in preview.
|
|
100
99
|
const configNoType = {
|
|
101
|
-
sections: [
|
|
102
|
-
{ key: 'testPricing', enabled: true, order: 0 },
|
|
103
|
-
],
|
|
100
|
+
sections: [{ key: 'testPricing', enabled: true, order: 0 }],
|
|
104
101
|
}
|
|
105
102
|
const result = duplicateInPageConfig(configNoType, 'testPricing', 'testPricing--copy')
|
|
106
103
|
const copy = result.sections.find((s: any) => s.key === 'testPricing--copy')
|
|
@@ -169,7 +166,7 @@ describe('commit-pending: commit message', () => {
|
|
|
169
166
|
function buildCommitMessage(pageKey: string, sections: Array<{ key: string }>): string {
|
|
170
167
|
const parts: string[] = []
|
|
171
168
|
if (sections.length > 0) {
|
|
172
|
-
const keys = sections.map(s => s.key).join(', ')
|
|
169
|
+
const keys = sections.map((s) => s.key).join(', ')
|
|
173
170
|
parts.push(`add ${sections.length} section${sections.length > 1 ? 's' : ''} (${keys})`)
|
|
174
171
|
}
|
|
175
172
|
return `content: ${parts.length > 0 ? parts.join(', ') : 'update page config'} on ${pageKey}`
|
|
@@ -210,7 +207,7 @@ describe('prepare-copy: type resolution', () => {
|
|
|
210
207
|
sections: Array<{ key: string; type?: string }>,
|
|
211
208
|
sectionKey: string,
|
|
212
209
|
): string {
|
|
213
|
-
const entry = sections.find(s => s.key === sectionKey)
|
|
210
|
+
const entry = sections.find((s) => s.key === sectionKey)
|
|
214
211
|
return entry?.type ?? sectionKey
|
|
215
212
|
}
|
|
216
213
|
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* 6. Secret header is sent when configured
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
14
14
|
|
|
15
15
|
// We import the POST handler directly; Astro's APIRoute type is just a function.
|
|
16
16
|
// We construct a minimal mock context manually.
|
|
@@ -40,9 +40,7 @@ describe('deploy-hook POST handler', () => {
|
|
|
40
40
|
return {
|
|
41
41
|
cookies: {
|
|
42
42
|
get: (name: string) =>
|
|
43
|
-
name === 'setzkasten_session' && sessionValue
|
|
44
|
-
? { value: sessionValue }
|
|
45
|
-
: undefined,
|
|
43
|
+
name === 'setzkasten_session' && sessionValue ? { value: sessionValue } : undefined,
|
|
46
44
|
},
|
|
47
45
|
}
|
|
48
46
|
}
|
|
@@ -64,10 +62,7 @@ describe('deploy-hook POST handler', () => {
|
|
|
64
62
|
;(globalThis as any).__SETZKASTEN_CONFIG__ = {
|
|
65
63
|
deployHook: { url: 'https://example.com/hook' },
|
|
66
64
|
}
|
|
67
|
-
vi.stubGlobal(
|
|
68
|
-
'fetch',
|
|
69
|
-
vi.fn().mockResolvedValue({ ok: true, status: 200 } as Response),
|
|
70
|
-
)
|
|
65
|
+
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: true, status: 200 } as Response))
|
|
71
66
|
|
|
72
67
|
const res = await POST(makeCtx('tok'))
|
|
73
68
|
expect(res.status).toBe(200)
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* 2. Nicht alle Vars gesetzt → auth error Result
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
10
9
|
import { generateKeyPairSync } from 'node:crypto'
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
11
|
|
|
12
12
|
const { privateKey: TEST_KEY } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
13
13
|
const TEST_PRIVATE_KEY_PEM = TEST_KEY.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
2
|
|
|
3
3
|
vi.mock('../_storage-config', () => ({
|
|
4
4
|
resolveStorageConfig: vi.fn(() => ({ owner: 'o', repo: 'r', branch: 'main' })),
|
|
@@ -8,7 +8,7 @@ vi.stubGlobal('__SETZKASTEN_CONFIG__', { storage: { contentPath: 'content' } })
|
|
|
8
8
|
|
|
9
9
|
describe('GlobalConfig – theme field', () => {
|
|
10
10
|
it('GlobalConfig type accepts a theme object', async () => {
|
|
11
|
-
const {
|
|
11
|
+
const {} = await import('../global-config')
|
|
12
12
|
// Type-level check: if this compiles the type is correct
|
|
13
13
|
const cfg = {
|
|
14
14
|
theme: { primaryColor: '#ff0000', brandName: 'Acme', logo: '/logo.png' },
|
|
@@ -43,7 +43,7 @@ describe('config.ts – theme merge', () => {
|
|
|
43
43
|
|
|
44
44
|
const { GET } = await import('../config')
|
|
45
45
|
const res = await GET({} as any)
|
|
46
|
-
const body = await res.json() as any
|
|
46
|
+
const body = (await res.json()) as any
|
|
47
47
|
|
|
48
48
|
expect(body.theme.primaryColor).toBe('#abc123')
|
|
49
49
|
expect(body.theme.brandName).toBe('Dynamic Brand')
|
|
@@ -63,7 +63,7 @@ describe('config.ts – theme merge', () => {
|
|
|
63
63
|
|
|
64
64
|
const { GET } = await import('../config')
|
|
65
65
|
const res = await GET({} as any)
|
|
66
|
-
const body = await res.json() as any
|
|
66
|
+
const body = (await res.json()) as any
|
|
67
67
|
|
|
68
68
|
expect(body.theme.primaryColor).toBe('#ffffff')
|
|
69
69
|
expect(body.theme.brandName).toBe('Static Brand')
|
|
@@ -4,16 +4,17 @@
|
|
|
4
4
|
|
|
5
5
|
import { generateKeyPairSync } from 'node:crypto'
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { makeTestSessionCookie } from './_session-test-helper'
|
|
7
8
|
|
|
8
9
|
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
9
10
|
const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
10
11
|
|
|
11
|
-
const ADMIN_SESSION =
|
|
12
|
+
const ADMIN_SESSION = makeTestSessionCookie({
|
|
12
13
|
user: { id: 'u1', email: 'admin@example.com', role: 'admin', provider: 'github' },
|
|
13
14
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
14
15
|
})
|
|
15
16
|
|
|
16
|
-
const EDITOR_SESSION =
|
|
17
|
+
const EDITOR_SESSION = makeTestSessionCookie({
|
|
17
18
|
user: { id: 'u2', email: 'editor@example.com', role: 'editor', provider: 'google' },
|
|
18
19
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
19
20
|
})
|
|
@@ -88,7 +89,9 @@ describe('POST /api/setzkasten/history/rollback', () => {
|
|
|
88
89
|
|
|
89
90
|
it('returns 400 when path or sha missing', async () => {
|
|
90
91
|
const { POST } = await import('../history-rollback')
|
|
91
|
-
const res = await (POST as (ctx: unknown) => Promise<Response>)(
|
|
92
|
+
const res = await (POST as (ctx: unknown) => Promise<Response>)(
|
|
93
|
+
makeCtx({ path: ROLLBACK_PATH }),
|
|
94
|
+
)
|
|
92
95
|
expect(res.status).toBe(400)
|
|
93
96
|
})
|
|
94
97
|
|
|
@@ -8,17 +8,22 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
8
8
|
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
9
9
|
const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import { makeTestSessionCookie } from './_session-test-helper'
|
|
12
|
+
|
|
13
|
+
const ADMIN_SESSION = makeTestSessionCookie({
|
|
12
14
|
user: { id: 'u1', email: 'admin@example.com', role: 'admin', provider: 'github' },
|
|
13
15
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
14
16
|
})
|
|
15
17
|
|
|
16
|
-
const EDITOR_SESSION =
|
|
18
|
+
const EDITOR_SESSION = makeTestSessionCookie({
|
|
17
19
|
user: { id: 'u2', email: 'editor@example.com', role: 'editor', provider: 'google' },
|
|
18
20
|
expiresAt: Date.now() + 60 * 60 * 1000,
|
|
19
21
|
})
|
|
20
22
|
|
|
21
|
-
function makeCtx(
|
|
23
|
+
function makeCtx(
|
|
24
|
+
searchParams: Record<string, string>,
|
|
25
|
+
sessionValue: string | null = ADMIN_SESSION,
|
|
26
|
+
) {
|
|
22
27
|
const url = new URL('https://cms.example.com/api/setzkasten/history')
|
|
23
28
|
for (const [k, v] of Object.entries(searchParams)) url.searchParams.set(k, v)
|
|
24
29
|
const request = new Request(url, { method: 'GET' })
|
|
@@ -136,9 +141,7 @@ describe('GET /api/setzkasten/history', () => {
|
|
|
136
141
|
authorEmail: 'maria@example.com',
|
|
137
142
|
message: 'Update hero',
|
|
138
143
|
})
|
|
139
|
-
expect(body.commits[0].coAuthors).toEqual([
|
|
140
|
-
{ name: 'Editor', email: 'editor@example.com' },
|
|
141
|
-
])
|
|
144
|
+
expect(body.commits[0].coAuthors).toEqual([{ name: 'Editor', email: 'editor@example.com' }])
|
|
142
145
|
})
|
|
143
146
|
|
|
144
147
|
it('returns empty list when GitHub returns 404 for the path', async () => {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* @vitest-environment node
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
18
18
|
import { resolveFullConfig } from '../init-scan-page'
|
|
19
19
|
|
|
20
20
|
const SAMPLE_CONFIG = {
|
|
@@ -23,8 +23,16 @@ const SAMPLE_CONFIG = {
|
|
|
23
23
|
website: {
|
|
24
24
|
label: 'Website',
|
|
25
25
|
sections: {
|
|
26
|
-
_layout_header: {
|
|
27
|
-
|
|
26
|
+
_layout_header: {
|
|
27
|
+
label: 'header',
|
|
28
|
+
icon: 'panel-top',
|
|
29
|
+
fields: { items: { type: 'array' } },
|
|
30
|
+
},
|
|
31
|
+
_layout_footer: {
|
|
32
|
+
label: 'footer',
|
|
33
|
+
icon: 'file-text',
|
|
34
|
+
fields: { description: { type: 'text' } },
|
|
35
|
+
},
|
|
28
36
|
},
|
|
29
37
|
},
|
|
30
38
|
},
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { generateKeyPairSync } from 'node:crypto'
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { makeTestSessionCookie } from './_session-test-helper'
|
|
7
8
|
|
|
8
9
|
const { privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 })
|
|
9
10
|
const PEM = privateKey.export({ type: 'pkcs8', format: 'pem' }) as string
|
|
@@ -15,7 +16,10 @@ function makeCtx(body: unknown, sessionValue = 'valid', role: 'admin' | 'editor'
|
|
|
15
16
|
headers: { 'content-type': 'application/json' },
|
|
16
17
|
})
|
|
17
18
|
const sessionPayload = sessionValue
|
|
18
|
-
?
|
|
19
|
+
? makeTestSessionCookie({
|
|
20
|
+
user: { id: 'test-user', email: 'a@b.com', role, provider: 'github' },
|
|
21
|
+
expiresAt: Date.now() + 60_000,
|
|
22
|
+
})
|
|
19
23
|
: ''
|
|
20
24
|
return {
|
|
21
25
|
request,
|
|
@@ -21,7 +21,9 @@ afterEach(() => {
|
|
|
21
21
|
vi.unstubAllEnvs()
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
function fetchSequence(
|
|
24
|
+
function fetchSequence(
|
|
25
|
+
steps: Array<(url: string, init?: RequestInit) => Response | Promise<Response>>,
|
|
26
|
+
) {
|
|
25
27
|
let i = 0
|
|
26
28
|
const fetchMock = vi.fn(async (url: string, init?: RequestInit) => {
|
|
27
29
|
const handler = steps[Math.min(i++, steps.length - 1)]
|
|
@@ -51,7 +53,10 @@ describe('readPagesMeta', () => {
|
|
|
51
53
|
fetchSequence([
|
|
52
54
|
() =>
|
|
53
55
|
new Response(
|
|
54
|
-
JSON.stringify({
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
content: Buffer.from(JSON.stringify(meta)).toString('base64'),
|
|
58
|
+
sha: 'abc',
|
|
59
|
+
}),
|
|
55
60
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
56
61
|
),
|
|
57
62
|
])
|
|
@@ -156,9 +161,9 @@ describe('recordPageEdit', () => {
|
|
|
156
161
|
() =>
|
|
157
162
|
new Response(
|
|
158
163
|
JSON.stringify({
|
|
159
|
-
content: Buffer.from(
|
|
160
|
-
|
|
161
|
-
),
|
|
164
|
+
content: Buffer.from(
|
|
165
|
+
JSON.stringify({ version: 1, pages: { x: { lastModified: 2 } } }),
|
|
166
|
+
).toString('base64'),
|
|
162
167
|
sha: 'second',
|
|
163
168
|
}),
|
|
164
169
|
{ status: 200, headers: { 'content-type': 'application/json' } },
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* @vitest-environment node
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
17
17
|
import { resolvePages } from '../pages'
|
|
18
18
|
|
|
19
19
|
interface PageInfo {
|
|
@@ -63,7 +63,12 @@ describe('resolvePages', () => {
|
|
|
63
63
|
it('handles multiple pages including nested paths', () => {
|
|
64
64
|
const pages: PageInfo[] = [
|
|
65
65
|
{ path: '/', pageKey: 'index', label: 'Startseite', hasConfig: true },
|
|
66
|
-
{
|
|
66
|
+
{
|
|
67
|
+
path: '/docs/architecture',
|
|
68
|
+
pageKey: 'docs/architecture',
|
|
69
|
+
label: 'Architecture',
|
|
70
|
+
hasConfig: false,
|
|
71
|
+
},
|
|
67
72
|
]
|
|
68
73
|
;(globalThis as Record<string, unknown>).__SETZKASTEN_PAGES__ = pages
|
|
69
74
|
expect(resolvePages()).toHaveLength(2)
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* Bugs here mean sections either don't appear in the page or appear twice.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { describe,
|
|
13
|
-
import {
|
|
12
|
+
import { describe, expect, it } from 'vitest'
|
|
13
|
+
import { calculateRelativePath, patchPageFile } from '../init-add-section'
|
|
14
14
|
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// Fixtures
|
|
@@ -64,29 +64,59 @@ describe('patchPageFile — new section injection', () => {
|
|
|
64
64
|
const componentPath = 'src/components/sections/FeaturesSection.astro'
|
|
65
65
|
|
|
66
66
|
it('should return a non-null result', () => {
|
|
67
|
-
const result = patchPageFile(
|
|
67
|
+
const result = patchPageFile(
|
|
68
|
+
PAGE_WITH_SECTIONS,
|
|
69
|
+
'features',
|
|
70
|
+
'FeaturesSection',
|
|
71
|
+
componentPath,
|
|
72
|
+
pagePath,
|
|
73
|
+
)
|
|
68
74
|
expect(result).not.toBeNull()
|
|
69
75
|
})
|
|
70
76
|
|
|
71
77
|
it('should add the import statement', () => {
|
|
72
|
-
const result = patchPageFile(
|
|
73
|
-
|
|
78
|
+
const result = patchPageFile(
|
|
79
|
+
PAGE_WITH_SECTIONS,
|
|
80
|
+
'features',
|
|
81
|
+
'FeaturesSection',
|
|
82
|
+
componentPath,
|
|
83
|
+
pagePath,
|
|
84
|
+
)!
|
|
85
|
+
expect(result).toContain('import FeaturesSection from')
|
|
74
86
|
})
|
|
75
87
|
|
|
76
88
|
it('should include the correct relative path', () => {
|
|
77
|
-
const result = patchPageFile(
|
|
89
|
+
const result = patchPageFile(
|
|
90
|
+
PAGE_WITH_SECTIONS,
|
|
91
|
+
'features',
|
|
92
|
+
'FeaturesSection',
|
|
93
|
+
componentPath,
|
|
94
|
+
pagePath,
|
|
95
|
+
)!
|
|
78
96
|
// From src/pages to src/components/sections → one level up → ../components/sections/...
|
|
79
97
|
expect(result).toContain('../components/sections/FeaturesSection.astro')
|
|
80
98
|
})
|
|
81
99
|
|
|
82
100
|
it('should add the section key to SECTION_COMPONENTS', () => {
|
|
83
|
-
const result = patchPageFile(
|
|
101
|
+
const result = patchPageFile(
|
|
102
|
+
PAGE_WITH_SECTIONS,
|
|
103
|
+
'features',
|
|
104
|
+
'FeaturesSection',
|
|
105
|
+
componentPath,
|
|
106
|
+
pagePath,
|
|
107
|
+
)!
|
|
84
108
|
expect(result).toContain('features')
|
|
85
109
|
expect(result).toContain('FeaturesSection')
|
|
86
110
|
})
|
|
87
111
|
|
|
88
112
|
it('should preserve existing sections in the registry', () => {
|
|
89
|
-
const result = patchPageFile(
|
|
113
|
+
const result = patchPageFile(
|
|
114
|
+
PAGE_WITH_SECTIONS,
|
|
115
|
+
'features',
|
|
116
|
+
'FeaturesSection',
|
|
117
|
+
componentPath,
|
|
118
|
+
pagePath,
|
|
119
|
+
)!
|
|
90
120
|
expect(result).toContain('HeroSection')
|
|
91
121
|
expect(result).toContain("normalize('hero')")
|
|
92
122
|
})
|
|
@@ -122,13 +152,25 @@ describe('patchPageFile — hardcoded tag removal', () => {
|
|
|
122
152
|
it('should remove the hardcoded <HeroSection /> when adding to registry', () => {
|
|
123
153
|
// PAGE_WITH_HARDCODED_COMPONENT has HeroSection imported but NOT in registry
|
|
124
154
|
// → patchPageFile adds it to registry AND removes the hardcoded tag
|
|
125
|
-
const result = patchPageFile(
|
|
155
|
+
const result = patchPageFile(
|
|
156
|
+
PAGE_WITH_HARDCODED_COMPONENT,
|
|
157
|
+
'hero',
|
|
158
|
+
'HeroSection',
|
|
159
|
+
componentPath,
|
|
160
|
+
pagePath,
|
|
161
|
+
)
|
|
126
162
|
expect(result).not.toBeNull()
|
|
127
163
|
expect(result).not.toMatch(/<HeroSection\s*\/>/)
|
|
128
164
|
})
|
|
129
165
|
|
|
130
166
|
it('should add the section to SECTION_COMPONENTS when removing hardcoded tag', () => {
|
|
131
|
-
const result = patchPageFile(
|
|
167
|
+
const result = patchPageFile(
|
|
168
|
+
PAGE_WITH_HARDCODED_COMPONENT,
|
|
169
|
+
'hero',
|
|
170
|
+
'HeroSection',
|
|
171
|
+
componentPath,
|
|
172
|
+
pagePath,
|
|
173
|
+
)!
|
|
132
174
|
expect(result).toContain('hero')
|
|
133
175
|
expect(result).toContain('HeroSection')
|
|
134
176
|
})
|
|
@@ -157,7 +199,13 @@ const SECTION_COMPONENTS = {
|
|
|
157
199
|
const pagePath = 'src/pages/docs/architecture.astro'
|
|
158
200
|
const componentPath = 'src/components/sections/FeaturesSection.astro'
|
|
159
201
|
|
|
160
|
-
const result = patchPageFile(
|
|
202
|
+
const result = patchPageFile(
|
|
203
|
+
PAGE_NESTED,
|
|
204
|
+
'features',
|
|
205
|
+
'FeaturesSection',
|
|
206
|
+
componentPath,
|
|
207
|
+
pagePath,
|
|
208
|
+
)
|
|
161
209
|
if (result) {
|
|
162
210
|
// From src/pages/docs to src/components/sections = ../../components/sections
|
|
163
211
|
expect(result).toContain('../../components/sections/FeaturesSection.astro')
|
|
@@ -175,19 +223,23 @@ describe('calculateRelativePath', () => {
|
|
|
175
223
|
})
|
|
176
224
|
|
|
177
225
|
it('one level up', () => {
|
|
178
|
-
expect(calculateRelativePath('src/pages', 'src/components/Hero.astro'))
|
|
179
|
-
|
|
226
|
+
expect(calculateRelativePath('src/pages', 'src/components/Hero.astro')).toBe(
|
|
227
|
+
'../components/Hero.astro',
|
|
228
|
+
)
|
|
180
229
|
})
|
|
181
230
|
|
|
182
231
|
it('two levels up', () => {
|
|
183
|
-
expect(calculateRelativePath('src/pages/docs', 'src/components/sections/Hero.astro'))
|
|
184
|
-
|
|
232
|
+
expect(calculateRelativePath('src/pages/docs', 'src/components/sections/Hero.astro')).toBe(
|
|
233
|
+
'../../components/sections/Hero.astro',
|
|
234
|
+
)
|
|
185
235
|
})
|
|
186
236
|
|
|
187
237
|
it('deeper nesting in monorepo', () => {
|
|
188
|
-
expect(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
238
|
+
expect(
|
|
239
|
+
calculateRelativePath(
|
|
240
|
+
'apps/website/src/pages',
|
|
241
|
+
'apps/website/src/components/sections/FooterSection.astro',
|
|
242
|
+
),
|
|
243
|
+
).toBe('../components/sections/FooterSection.astro')
|
|
192
244
|
})
|
|
193
245
|
})
|