/** * Payload is ESM-only and next/jest will not transform it, so the collections * cannot be imported and their sanitised config inspected here (see * lib/payloadRoutes.ts for the full reasoning). These assert the source of the * collection definitions instead — enough to catch the settings whose loss is * silent, and cheap. Behaviour is proved by the e2e journeys against staging. */ import fs from 'fs'; import path from 'path'; const read = (file: string) => fs.readFileSync(path.join(__dirname, '..', '..', 'collections', file), 'utf8'); const POSTS = read('Posts.ts'); const MEDIA = read('Media.ts'); const CONFIG = fs.readFileSync( path.join(__dirname, '..', '..', 'payload.config.ts'), 'utf8', ); describe('posts collection', () => { it('supports drafts, so saving is not publishing', () => { expect(POSTS).toMatch(/drafts:\s*true/); }); it('has a unique, indexed slug for stable URLs', () => { const slugField = POSTS.slice(POSTS.indexOf("name: 'slug'")); expect(slugField).toMatch(/unique:\s*true/); expect(slugField).toMatch(/index:\s*true/); }); it('hides drafts from anonymous readers at the access layer', () => { // Payload's docs are explicit: "The `draft` argument alone does not // restrict documents with _status: 'draft' from being returned by the // API." The blog pages' where-clause is not enforcement — a direct GET // /cms-api/posts would return unpublished drafts to anyone. Access // control returning a query constraint is the only thing that stops it. expect(POSTS).toMatch(/_status:\s*\{\s*equals:\s*'published'\s*\}/); expect(POSTS).toMatch(/if\s*\(req\.user\)\s*return true/); }); it('revalidates the post page when a post changes or is deleted', () => { // /blog/[slug] is ISR — generated on first request and cached — so an edit // to an already-published post would otherwise not appear until the // revalidate window expired, up to an hour of a writer concluding that // saving is broken. The index and feeds are force-dynamic and need no hook. expect(POSTS).toContain('afterChange'); expect(POSTS).toContain('afterDelete'); expect(POSTS).toMatch(/revalidatePath\(`\/blog\/\$\{[^}]+\}`\)/); }); }); describe('media collection', () => { it('writes uploads to the mounted volume, by absolute path', () => { // Must match the payload_media mount in docker-compose.portainer.yml. // Payload 3 requires staticDir to be absolute. expect(MEDIA).toMatch(/staticDir:\s*'\/app\/media'/); }); it('requires alt text on every upload', () => { const altField = MEDIA.slice(MEDIA.indexOf("name: 'alt'")); expect(altField).toMatch(/required:\s*true/); }); }); describe('payload config', () => { it('registers every collection', () => { expect(CONFIG).toMatch(/collections:\s*\[Users,\s*Posts,\s*Media\]/); }); });