diff --git a/src/app/utils/sanitize.test.ts b/src/app/utils/sanitize.test.ts
new file mode 100644
index 000000000..170d3ff8f
--- /dev/null
+++ b/src/app/utils/sanitize.test.ts
@@ -0,0 +1,52 @@
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { sanitizeCustomHtml, sanitizeText } from './sanitize';
+
+test('sanitizeText escapes HTML metacharacters', () => {
+ assert.equal(sanitizeText('hello');
+ assert.ok(!out.includes('alert'));
+ assert.ok(out.includes('hello'));
+});
+
+test('sanitizeCustomHtml strips event-handler attributes', () => {
+ const out = sanitizeCustomHtml('hi');
+ assert.ok(!out.includes('onclick'));
+ assert.ok(out.includes('hi'));
+});
+
+test('sanitizeCustomHtml drops disallowed tags, keeps allowed ones', () => {
+ assert.ok(!sanitizeCustomHtml('').includes('iframe'));
+ assert.ok(sanitizeCustomHtml('bold').includes(''));
+});
+
+test('sanitizeCustomHtml neutralizes javascript: links', () => {
+ const out = sanitizeCustomHtml('x');
+ // eslint-disable-next-line no-script-url -- asserting the scheme was stripped
+ assert.ok(!out.includes('javascript:'));
+});
+
+test('sanitizeCustomHtml hardens anchors (noreferrer/noopener/_blank)', () => {
+ const out = sanitizeCustomHtml('x');
+ assert.ok(out.includes('rel="noreferrer noopener"'));
+ assert.ok(out.includes('target="_blank"'));
+});
+
+test('sanitizeCustomHtml converts a non-mxc
into a link', () => {
+ const out = sanitizeCustomHtml('
');
+ assert.ok(!out.includes('
class is restricted to the language-* whitelist', () => {
+ assert.ok(sanitizeCustomHtml('x
').includes('language-js'));
+ assert.ok(
+ !sanitizeCustomHtml('x
').includes('evil-site-class'),
+ );
+});