feat: integrate XSLT Tools with toolbox portal

This commit is contained in:
2026-07-20 18:16:18 +02:00
parent 3193cc395c
commit 8f8a59e76c
36 changed files with 1737 additions and 243 deletions

View File

@@ -0,0 +1,80 @@
import { describe, expect, it, vi } from 'vitest';
import { isStylesheetExecutionTrusted } from '../src/transform/stylesheetTrust';
import { runTransformation } from '../src/transform/transformService';
import {
TRANSFORM_LIMITS,
assertTransformInputWithinLimits,
assertTransformOutputWithinLimits,
withTransformationTimeout,
} from '../src/transform/transformLimits';
describe('stylesheet execution trust', () => {
it('requires both the exact SHA-256 and the current edit revision', () => {
const trust = { hash: 'sha256-a', revision: 4 };
expect(isStylesheetExecutionTrusted(trust, 'sha256-a', 4)).toBe(true);
expect(isStylesheetExecutionTrusted(trust, 'sha256-b', 4)).toBe(false);
expect(isStylesheetExecutionTrusted(trust, 'sha256-a', 5)).toBe(false);
expect(isStylesheetExecutionTrusted(null, 'sha256-a', 4)).toBe(false);
});
it.each(['saxon-js-dynamic', 'native-xsltprocessor'] as const)(
'refuses the %s service path when authorization does not match',
async (engine) => {
await expect(
runTransformation(
{
engine,
xmlText: '<root/>',
xsltText: '<xsl:stylesheet/>',
},
{
trustedStylesheet: { hash: 'not-the-stylesheet-hash', revision: 7 },
currentRevision: 7,
}
)
).rejects.toThrow(/review and trust this exact stylesheet/i);
}
);
});
describe('transformation resource limits', () => {
it('rejects oversized XML, XSLT, and result strings', () => {
expect(() =>
assertTransformInputWithinLimits({
engine: 'native-xsltprocessor',
xmlText: 'x'.repeat(TRANSFORM_LIMITS.maxXmlBytes + 1),
xsltText: '<xsl:stylesheet/>',
})
).toThrow(/XML input exceeds/i);
expect(() =>
assertTransformInputWithinLimits({
engine: 'saxon-js-dynamic',
xmlText: '<root/>',
xsltText: 'x'.repeat(TRANSFORM_LIMITS.maxXsltBytes + 1),
})
).toThrow(/stylesheet exceeds/i);
expect(() =>
assertTransformOutputWithinLimits({
engine: 'saxon-js-dynamic',
output: 'x'.repeat(TRANSFORM_LIMITS.maxOutputBytes + 1),
diagnostics: [],
transformedAt: '2026-01-01T00:00:00.000Z',
})
).toThrow(/output exceeds/i);
});
it('rejects an asynchronous execution that exceeds its deadline', async () => {
vi.useFakeTimers();
try {
const result = withTransformationTimeout(
() => new Promise<never>(() => undefined),
25
);
const rejection = expect(result).rejects.toThrow(/25-millisecond/i);
await vi.advanceTimersByTimeAsync(25);
await rejection;
} finally {
vi.useRealTimers();
}
});
});