import { afterEach, describe, expect, it } from 'vitest';
import { setActuateConfig } from '../../config/runtime.js';
import { sanitizeDocumentHtml, sanitizeLayoutTree } from '../../security/sanitize-document.js';
/**
 * Regression tests for write-time HTML sanitization (security audit H1).
 *
 * Before this fix only TOP-LEVEL `richText` fields were sanitized on write, so
 * HTML nested inside `array`/`blocks` fields, the canonical `sections[]`
 * rich-text slots, and the page-builder `_layout` tree was stored raw — a
 * stored-XSS path reachable by any authenticated writer, since the public
 * renderer trusts stored HTML by default. These tests pin that every reachable
 * HTML sink is sanitized and that plain-text fields are left untouched.
 */
const XSS = '<p>ok</p><script>alert(1)</script><img src=x onerror=alert(2)>';
function registerPostsConfig() {
    setActuateConfig({
        collections: {
            posts: {
                slug: 'posts',
                fields: {
                    title: { type: 'text', label: 'Title' },
                    body: { type: 'richText', label: 'Body' },
                    sectionsList: {
                        type: 'array',
                        label: 'Sections',
                        fields: {
                            heading: { type: 'text', label: 'Heading' },
                            copy: { type: 'richText', label: 'Copy' },
                        },
                    },
                    flexible: {
                        type: 'blocks',
                        label: 'Flexible',
                        blocks: [
                            {
                                slug: 'prose',
                                labels: { singular: 'Prose', plural: 'Prose' },
                                fields: {
                                    label: { type: 'text', label: 'Label' },
                                    html: { type: 'richText', label: 'HTML' },
                                },
                            },
                        ],
                    },
                },
            },
        },
    });
}
describe('sanitizeDocumentHtml', () => {
    afterEach(() => {
        delete globalThis.__actuateConfig;
    });
    it('sanitizes top-level richText but leaves plain text fields untouched', () => {
        registerPostsConfig();
        const out = sanitizeDocumentHtml({ title: 'A < B plain', body: XSS }, 'posts');
        expect(out.body).not.toContain('<script>');
        expect(out.body).not.toContain('onerror');
        expect(out.body).toContain('<p>ok</p>');
        // Plain-text field must be byte-for-byte preserved (no HTML re-parsing).
        expect(out.title).toBe('A < B plain');
    });
    it('sanitizes richText nested inside array field items', () => {
        registerPostsConfig();
        const out = sanitizeDocumentHtml({ sectionsList: [{ heading: 'keep < me', copy: XSS }] }, 'posts');
        const item = out.sectionsList[0];
        expect(item.copy).not.toContain('<script>');
        expect(item.heading).toBe('keep < me');
    });
    it('sanitizes richText inside blocks field items via the block discriminator', () => {
        registerPostsConfig();
        const out = sanitizeDocumentHtml({ flexible: [{ blockType: 'prose', label: 'x < y', html: XSS }] }, 'posts');
        const block = out.flexible[0];
        expect(block.html).not.toContain('<script>');
        expect(block.label).toBe('x < y');
    });
    it('sanitizes canonical section rich-text slots from the registry', () => {
        registerPostsConfig();
        const out = sanitizeDocumentHtml({
            sections: [
                {
                    id: 's1',
                    sectionType: 'article-body',
                    content: { source: 'override', body: XSS, width: 'normal' },
                },
            ],
        }, 'posts');
        const section = out.sections[0];
        const content = section.content;
        expect(content.body).not.toContain('<script>');
        expect(content.body).not.toContain('onerror');
        // Non-richText section content is preserved verbatim.
        expect(content.width).toBe('normal');
    });
    it('does not throw on unknown section types or malformed nested values', () => {
        registerPostsConfig();
        expect(() => sanitizeDocumentHtml({
            body: 123,
            sectionsList: 'not-an-array',
            sections: [{ sectionType: 'does-not-exist', content: { body: XSS } }, null],
        }, 'posts')).not.toThrow();
    });
    it('is a no-op when the collection has no field config', () => {
        delete globalThis.__actuateConfig;
        const out = sanitizeDocumentHtml({ body: XSS }, 'unknown');
        // No field schema → nothing declared as HTML → untouched.
        expect(out.body).toBe(XSS);
    });
});
describe('sanitizeLayoutTree', () => {
    it('sanitizes rich-text block bodies and recurses through children', () => {
        const tree = {
            type: 'page',
            children: [
                {
                    type: 'row',
                    children: [
                        {
                            type: 'column',
                            children: [
                                { type: 'block', settings: { blockType: 'richText' }, data: { content: XSS } },
                                {
                                    type: 'block',
                                    settings: { blockType: 'heading' },
                                    data: { text: 'Plain < text' },
                                },
                            ],
                        },
                    ],
                },
            ],
        };
        const out = sanitizeLayoutTree(tree);
        const column = out.children[0].children[0];
        const blocks = column.children;
        expect(blocks[0].data.content).not.toContain('<script>');
        expect(blocks[0].data.content).toContain('<p>ok</p>');
        // Plain-text block fields (keys not in the HTML allow-list) are untouched.
        expect(blocks[1].data.text).toBe('Plain < text');
    });
    it('returns non-object layouts unchanged', () => {
        expect(sanitizeLayoutTree(null)).toBeNull();
        expect(sanitizeLayoutTree('nope')).toBe('nope');
    });
});
//# sourceMappingURL=sanitize-document.test.js.map