import { test } from 'node:test';
import assert from 'node:assert/strict';
import { analyzeLink, hostFromText, previewUrlsFor, registrableDomain } from './linkSafety';
test('visible text that names another site is a mismatch', () => {
const r = analyzeLink('https://matrix.lotusguild.org/login', 'https://evil.example/login');
assert.equal(r?.mismatch, true);
assert.equal(r?.shownHost, 'matrix.lotusguild.org');
assert.equal(r?.realHost, 'evil.example');
});
test('honest links are not flagged: same registrable domain, plain words, non-http', () => {
assert.equal(
analyzeLink('youtube.com/watch?v=1', 'https://www.youtube.com/watch?v=1')?.mismatch,
false,
);
assert.equal(analyzeLink('bbc.co.uk', 'https://news.bbc.co.uk/x')?.mismatch, false);
assert.equal(analyzeLink('click here', 'https://evil.example')?.mismatch, false);
assert.equal(analyzeLink('evil.example', 'mailto:someone@evil.example'), null);
assert.equal(analyzeLink('elsewhere.org', 'https://matrix.to/#/#room:x'), null);
assert.equal(
analyzeLink('lotusguild.org', 'https://chat.lotusguild.org/home/!r:x')?.mismatch,
false,
);
});
test('registrable domain handles two-label suffixes', () => {
assert.equal(registrableDomain('news.bbc.co.uk'), 'bbc.co.uk');
assert.equal(registrableDomain('www.example.com'), 'example.com');
assert.equal(registrableDomain('example.com'), 'example.com');
});
test('hostFromText only accepts URL/host-shaped text; IDN becomes punycode', () => {
assert.equal(hostFromText('Visit https://a.example/path'), null);
assert.equal(hostFromText('a.example/path'), 'a.example');
assert.equal(hostFromText('user@a.example:8448/x'), 'a.example');
assert.equal(hostFromText('pаypal.com'), 'xn--pypal-4ve.com'); // Cyrillic а
assert.equal(analyzeLink('paypal.com', 'https://xn--pypal-4ve.com/')?.punycode, true);
assert.equal(analyzeLink('paypal.com', 'https://xn--pypal-4ve.com/')?.mismatch, true);
});
test('a trusted name inside a sentence still exposes a different destination (#335)', () => {
const a = analyzeLink('Log in at chat.lotusguild.org', 'https://evil.example/');
assert.equal(a?.mismatch, true);
assert.equal(a?.shownHost, 'chat.lotusguild.org');
assert.equal(
analyzeLink('chat.lotusguild.org (official)', 'https://evil.example/')?.mismatch,
true,
);
assert.equal(
analyzeLink('Open https://paypal.com/login now', 'https://evil.example/')?.mismatch,
true,
);
// honest sentences are not flagged
assert.equal(
analyzeLink('Log in at chat.lotusguild.org', 'https://chat.lotusguild.org/login')?.mismatch,
false,
);
assert.equal(analyzeLink('read the docs', 'https://evil.example/')?.mismatch, false);
assert.equal(analyzeLink('see main.py and report.pdf', 'https://github.com/x')?.mismatch, false);
assert.equal(analyzeLink('release v1.2.3 notes', 'https://github.com/x')?.mismatch, false);
});
test('shared hosting suffixes are separate sites (#335)', () => {
assert.equal(registrableDomain('alice.github.io'), 'alice.github.io');
assert.equal(registrableDomain('www.alice.github.io'), 'alice.github.io');
assert.equal(registrableDomain('bucket.s3.amazonaws.com'), 'bucket.s3.amazonaws.com');
assert.equal(analyzeLink('alice.github.io', 'https://evil.github.io/')?.mismatch, true);
assert.equal(analyzeLink('lotusguild.pages.dev', 'https://evil.pages.dev/')?.mismatch, true);
assert.equal(
analyzeLink('alice.github.io/blog', 'https://alice.github.io/blog/1')?.mismatch,
false,
);
assert.equal(registrableDomain('news.bbc.co.uk'), 'bbc.co.uk');
});
test('previewUrlsFor: no preview for a site a deceptive link only claims (#286)', () => {
const urls = ['https://paypal.com'];
assert.deepEqual(
previewUrlsFor(urls, 'https://paypal.com'),
[],
);
// An honest link keeps its preview.
assert.deepEqual(
previewUrlsFor(urls, 'https://paypal.com'),
urls,
);
assert.deepEqual(previewUrlsFor(urls, undefined), urls);
// Other links in the message keep theirs.
assert.deepEqual(
previewUrlsFor(
['https://paypal.com', 'https://lotusguild.org'],
'see paypal.com and https://lotusguild.org',
),
['https://lotusguild.org'],
);
});