source: Klonkt/test/url-linkify.test.js@ 3ccca13

main
Last change on this file since 3ccca13 was fc229f5, checked in by roboburr <roboburr@…>, 2 months ago

fix(fediverse): bracketed mentions/hashtags/URLs link and notify

The mention/hashtag/URL prefix classes required whitespace or '>' before the
token, so "(@user@server)", "(#tag)" and "(https://…)" federated as plain text —
and a bracketed mention's target was never notified (real-world miss on a reply).
Opening brackets are now valid prefixes. Quote characters are deliberately NOT in
the class: a quote precedes attribute values, which must never match.

  • src/services/ActivityPubService.js — prefix class [\s>] → [\s>([{] in linkHashtags, linkUrls, and both mention regexes (scan + per-handle replace).
  • test/url-linkify.test.js — bracketed URL + hashtag link; attribute values stay untouched.
  • CHANGELOG(.nl/.de).md — under Fixed.

Closes prutfolio-src-ovj.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 3.2 KB
Line 
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
4import { test } from 'node:test';
5import assert from 'node:assert/strict';
6
7process.env.DATABASE_PATH = ':memory:';
8process.env.PUBLIC_BASE_URL = 'https://test.example';
9
10const dbMod = await import('../src/config/database.js');
11const db = dbMod.default;
12dbMod.initializeDatabase();
13const AP = (await import('../src/services/ActivityPubService.js')).default;
14
15const BASE = 'https://test.example';
16db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@test', 'x', 'god');
17db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)').run('s1', 'demo', 'Demo', 'u1', 1);
18const site = db.prepare('SELECT * FROM sites WHERE id = ?').get('s1');
19site.primary_slug = 'demo';
20
21const 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
26test('a bare URL becomes a link; trailing punctuation stays outside', () => {
27 const n = note('<p>check https://example.com/x?a=1&amp;b=2, ok</p>');
28 assert.match(n.content, /<a href="https:\/\/example\.com\/x\?a=1&amp;b=2" rel="nofollow noopener" target="_blank">https:\/\/example\.com\/x\?a=1&amp;b=2<\/a>,/);
29});
30
31test('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
37test('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
42test('a URL inside parentheses links, closing paren stays outside', () => {
43 const n = note('<p>site (https://example.com/a) hier</p>');
44 assert.match(n.content, /\(<a href="https:\/\/example\.com\/a"[^>]*>https:\/\/example\.com\/a<\/a>\)/);
45});
46
47test('a hashtag inside parentheses still links; quoted attribute values never match', () => {
48 const n = note('<p>leuk (#jazz) toch</p>');
49 assert.match(n.content, /\(<a [^>]*class="mention hashtag"[^>]*>#jazz<\/a>\)/);
50 // A quote precedes attribute values — the prefix class must NOT include quotes.
51 const n2 = note('<p>plaatje <a href="https://x.example/#frag">link</a></p>');
52 assert.equal((n2.content.match(/<a /g) || []).length, 1, 'href value untouched');
53});
54
55test('attribute values are untouched, standalone URLs still link (image stripped from content)', () => {
56 const n = note('<p><img src="https://cdn.example.com/pic.png" alt=""> and https://example.org</p>');
57 // <img> is stripped from federated content (travels as attachment) — no anchor made for its src.
58 assert.ok(!n.content.includes('cdn.example.com'), 'img src not present in content');
59 assert.match(n.content, /<a href="https:\/\/example\.org"/);
60});
Note: See TracBrowser for help on using the repository browser.