import { describe, expect, it } from 'vitest'; import { sanitizeCssColor, clampCalloutType, } from '../packages/docmost-client/src/lib/docmost-schema.js'; /** * Unit tests for the two pure guard functions of the Docmost schema. * * `sanitizeCssColor` is a SECURITY boundary: its return value is interpolated * straight into an inline `style="..."` attribute by Docmost, so any value it * accepts must be incapable of breaking out of that attribute. These tests pin * both the accept set (well-formed CSS syntax) and, more importantly, * the reject set (anything containing the characters an attacker would need to * inject: quotes, angle brackets, semicolons, colons, parentheses with letters, * or a `url(...)` / `expression(...)` payload). */ describe('sanitizeCssColor', () => { describe('accepts well-formed CSS colors', () => { it('accepts simple named colors (letters only), returned verbatim', () => { expect(sanitizeCssColor('red')).toBe('red'); expect(sanitizeCssColor('rebeccapurple')).toBe('rebeccapurple'); // Case is preserved (the regex is case-insensitive for letters). expect(sanitizeCssColor('Black')).toBe('Black'); expect(sanitizeCssColor('transparent')).toBe('transparent'); }); it('accepts #rgb and #rgba short hex', () => { expect(sanitizeCssColor('#abc')).toBe('#abc'); expect(sanitizeCssColor('#abcd')).toBe('#abcd'); expect(sanitizeCssColor('#FFF')).toBe('#FFF'); }); it('accepts #rrggbb and #rrggbbaa long hex', () => { expect(sanitizeCssColor('#aabbcc')).toBe('#aabbcc'); expect(sanitizeCssColor('#AABBCCDD')).toBe('#AABBCCDD'); expect(sanitizeCssColor('#0f0f0f')).toBe('#0f0f0f'); }); it('accepts rgb()/rgba() functional notation', () => { expect(sanitizeCssColor('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)'); expect(sanitizeCssColor('rgba(0, 128, 255, 0.5)')).toBe( 'rgba(0, 128, 255, 0.5)', ); // CSS Color 4 space-separated + slash-alpha syntax (digits, %, /, spaces). expect(sanitizeCssColor('rgb(100% 0% 0% / 50%)')).toBe( 'rgb(100% 0% 0% / 50%)', ); }); it('accepts hsl()/hsla() functional notation', () => { expect(sanitizeCssColor('hsl(120, 50%, 50%)')).toBe('hsl(120, 50%, 50%)'); expect(sanitizeCssColor('hsla(240, 100%, 50%, 0.3)')).toBe( 'hsla(240, 100%, 50%, 0.3)', ); }); }); describe('trims surrounding whitespace before validating', () => { it('trims leading/trailing whitespace and returns the trimmed value', () => { // The implementation trims first, then matches; the returned value is the // trimmed string, not the original padded input. expect(sanitizeCssColor(' red ')).toBe('red'); expect(sanitizeCssColor('\t#aabbcc\n')).toBe('#aabbcc'); expect(sanitizeCssColor(' rgb(1, 2, 3) ')).toBe('rgb(1, 2, 3)'); }); }); describe('rejects CSS-injection / breakout payloads (returns null)', () => { it('rejects a style-attribute breakout via extra declarations', () => { // The classic "set a valid color, then smuggle another declaration". expect(sanitizeCssColor('red; --x: url(x)')).toBeNull(); expect(sanitizeCssColor('red; background: url(http://x)')).toBeNull(); }); it('rejects an attribute breakout via quotes and angle brackets', () => { expect(sanitizeCssColor('red">