| 1 | // Auto-linking bare URLs — a plain http(s) URL in a post's federated copy becomes a
|
|---|
| 2 | // clickable link; already-linked URLs and attribute values are left alone; trailing
|
|---|
| 3 | // sentence punctuation stays outside the link. In-memory SQLite. Run: npm test
|
|---|
| 4 | import { test } from 'node:test';
|
|---|
| 5 | import assert from 'node:assert/strict';
|
|---|
| 6 |
|
|---|
| 7 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 8 | process.env.PUBLIC_BASE_URL = 'https://test.example';
|
|---|
| 9 |
|
|---|
| 10 | const dbMod = await import('../src/config/database.js');
|
|---|
| 11 | const db = dbMod.default;
|
|---|
| 12 | dbMod.initializeDatabase();
|
|---|
| 13 | const AP = (await import('../src/services/ActivityPubService.js')).default;
|
|---|
| 14 |
|
|---|
| 15 | const BASE = 'https://test.example';
|
|---|
| 16 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 17 | db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
|
|---|
| 18 | const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
|
|---|
| 19 | site.primary_slug = 'demo';
|
|---|
| 20 |
|
|---|
| 21 | const note = (content, extra = {}) => AP.buildNote(BASE, site, {
|
|---|
| 22 | id: 'u' + Math.abs(content.length * 7919 % 100000), slug: 'x', title: '', content, tags: '[]',
|
|---|
| 23 | created_at: '2026-01-01T00:00:00Z', ...extra,
|
|---|
| 24 | });
|
|---|
| 25 |
|
|---|
| 26 | test('a bare URL becomes a link; trailing punctuation stays outside', () => {
|
|---|
| 27 | const n = note('<p>check https://example.com/x?a=1&b=2, ok</p>');
|
|---|
| 28 | assert.match(n.content, /<a href="https:\/\/example\.com\/x\?a=1&b=2" rel="nofollow noopener" target="_blank">https:\/\/example\.com\/x\?a=1&b=2<\/a>,/);
|
|---|
| 29 | });
|
|---|
| 30 |
|
|---|
| 31 | test('a URL with a fragment does not get hashtag-ified', () => {
|
|---|
| 32 | const n = note('<p>see https://example.com/page#section</p>');
|
|---|
| 33 | assert.match(n.content, /<a href="https:\/\/example\.com\/page#section"/);
|
|---|
| 34 | assert.ok(!/class="mention hashtag"[^>]*>#section/.test(n.content), 'no hashtag link out of the fragment');
|
|---|
| 35 | });
|
|---|
| 36 |
|
|---|
| 37 | test('an already-linked URL is not wrapped twice', () => {
|
|---|
| 38 | const n = note('<p><a href="https://example.com/">https://example.com/</a></p>');
|
|---|
| 39 | assert.equal((n.content.match(/<a /g) || []).length, 1);
|
|---|
| 40 | });
|
|---|
| 41 |
|
|---|
| 42 | test('attribute values are untouched, standalone URLs still link (image stripped from content)', () => {
|
|---|
| 43 | const n = note('<p><img src="https://cdn.example.com/pic.png" alt=""> and https://example.org</p>');
|
|---|
| 44 | // <img> is stripped from federated content (travels as attachment) — no anchor made for its src.
|
|---|
| 45 | assert.ok(!n.content.includes('cdn.example.com'), 'img src not present in content');
|
|---|
| 46 | assert.match(n.content, /<a href="https:\/\/example\.org"/);
|
|---|
| 47 | });
|
|---|