Add initial project files

This commit is contained in:
Igor Martyniuk
2026-09-01 13:03:41 +03:00
parent 22e111bb44
commit 74cb5b6176
1841 changed files with 242819 additions and 30 deletions
+106
View File
@@ -0,0 +1,106 @@
import { describe, expect, it } from 'vitest';
import { safeHref } from '#web-runtime/components/links';
import type { NavigationLink } from '#web-runtime/types';
/**
* `safeHref` is the gate between CMS-authored link data and the `href`
* attribute the browser receives, so these tests pin the allowlist hard:
* anything not explicitly safe must come back as `null`.
*/
function link(overrides: Partial<NavigationLink> = {}): NavigationLink {
return {
linkType: 'external',
pageId: '',
externalUrl: '',
openInNewTab: false,
label: 'A link',
variant: 'primary',
...overrides
};
}
describe('allowed protocols', () => {
it.each([
'http://example.com/page',
'https://example.com/page?q=1#frag',
'mailto:hello@example.com',
'tel:+380441234567'
])('passes %s through untouched', (href) => {
expect(safeHref(link({ href }))).toBe(href);
});
});
describe('dangerous protocols', () => {
it.each([
'javascript:alert(1)',
'data:text/html,<script>alert(1)</script>',
'vbscript:msgbox(1)',
'file:///etc/passwd',
'blob:https://example.com/uuid',
'ftp://example.com/file'
])('rejects %s', (href) => {
expect(safeHref(link({ href }))).toBeNull();
});
it('rejects javascript: regardless of letter case', () => {
expect(safeHref(link({ href: 'JavaScript:alert(1)' }))).toBeNull();
expect(safeHref(link({ href: 'JAVASCRIPT:alert(1)' }))).toBeNull();
});
it('rejects javascript: hidden behind leading whitespace, which URL parsing strips', () => {
expect(safeHref(link({ href: ' javascript:alert(1)' }))).toBeNull();
expect(safeHref(link({ href: '\njavascript:alert(1)' }))).toBeNull();
});
it('rejects a dangerous externalUrl the same way as a dangerous href', () => {
expect(safeHref(link({ externalUrl: 'javascript:alert(1)' }))).toBeNull();
});
});
describe('malformed and missing values', () => {
it('rejects plain text that is not a URL at all', () => {
expect(safeHref(link({ href: 'not a url' }))).toBeNull();
expect(safeHref(link({ href: 'example.com/no-protocol' }))).toBeNull();
});
it('returns null when neither href nor externalUrl is set', () => {
expect(safeHref(link())).toBeNull();
expect(safeHref(link({ href: '' }))).toBeNull();
});
});
describe('href source precedence', () => {
it('prefers href over externalUrl when both exist', () => {
expect(
safeHref(
link({ href: 'https://primary.example.com', externalUrl: 'https://other.example.com' })
)
).toBe('https://primary.example.com');
});
it('falls back to externalUrl when href is empty', () => {
expect(safeHref(link({ href: '', externalUrl: 'https://fallback.example.com' }))).toBe(
'https://fallback.example.com'
);
});
});
describe('anchors and relative paths', () => {
it('passes in-page anchors through untouched', () => {
expect(safeHref(link({ href: '#pricing' }))).toBe('#pricing');
});
it('routes site-relative paths through toPublicHref, which keeps the locale prefix in this multi-locale config', () => {
// site.config.ts declares en/uk/de, so localePrefix is 'always' and
// toPublicHref is the identity function.
expect(safeHref(link({ href: '/en/about' }))).toBe('/en/about');
expect(safeHref(link({ href: '/pricing' }))).toBe('/pricing');
});
it('currently lets protocol-relative URLs through via the "/" branch (pinned behavior)', () => {
// '//evil.example.com' starts with '/', so it never reaches the protocol
// allowlist. Browsers resolve it against the current scheme (http/https),
// so it cannot smuggle javascript:, but it is an external URL in disguise.
expect(safeHref(link({ href: '//evil.example.com' }))).toBe('//evil.example.com');
});
});