import assert from 'node:assert/strict'; import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { join } from 'node:path'; import test from 'node:test'; const root = new URL('..', import.meta.url).pathname; const postsDir = join(root, 'src/content/post/en'); const read = (path) => readFileSync(join(root, path), 'utf8'); const readPost = (name) => readFileSync(join(postsDir, name), 'utf8'); test('uses a verified organizational byline for the reviewed publication batch', () => { const affected = readdirSync(postsDir) .filter((name) => name.endsWith('.md') || name.endsWith('.mdx')) .filter((name) => readPost(name).includes('author: Hoabing Ziu, CTO')); assert.deepEqual(affected, []); }); test('renders post FAQ data visibly and attributes RustDesk Team to the organization schema', () => { const route = read('src/pages/[...blog]/index.astro'); const faqComponent = read('src/components/blog/BlogFaq.astro'); const structuredData = read('src/components/common/StructuredData.astro'); assert.match(route, //); assert.match(faqComponent, /]*>Frequently asked questions<\/h2>/); assert.match(faqComponent, /item\.question/); assert.match(faqComponent, /item\.answer/); assert.match(structuredData, /article\.author === 'RustDesk Team'/); assert.match(structuredData, /author: articleAuthorSchema/); }); test('requires a protected path before entering initial web-console credentials', () => { const article = readPost('rustdesk-web-console-custom-client-generator-port-21114.md'); assert.match(article, /do not enter credentials over plain HTTP on a public or untrusted network\./i); assert.match(article, /localhost, an SSH tunnel, or a trusted private network/); }); test('limits no-inbound-port and iOS host claims in the RDP comparison', () => { const article = readPost('rustdesk-vs-rdp.md'); assert.match(article, /endpoint clients initiate outbound connections/); assert.match(article, /self-hosted server still accepts inbound connections/); assert.match(article, /iOS acts as a controller only/); assert.doesNotMatch(article, /controlling and being controlled/); assert.doesNotMatch(article, /Host platforms\s*\|[^\n]*iOS/); }); test('does not claim that self-hosted RustDesk exposes nothing inbound', () => { const affected = readdirSync(postsDir) .filter((name) => name.endsWith('.md') || name.endsWith('.mdx')) .filter((name) => /nothing inbound is exposed/i.test(readPost(name))); assert.deepEqual(affected, []); }); test('uses protocol-specific RustDesk server port guidance', () => { const article = readPost('rustdesk-connected-waiting-for-image.md'); assert.doesNotMatch(article, /TCP\/UDP 21115-21119/); assert.match(article, /TCP 21115-21117 and UDP 21116/); assert.match(article, /TCP 21118-21119 only if you use WebSocket clients/); }); test('states the evidence and limits behind the large-fleet capacity example', () => { const article = readPost('rustdesk-scale-50000-200000-devices.md'); assert.match(article, /more than two million online endpoints/); assert.match(article, /12-core CPU and 32 GB of RAM/); assert.match(article, /not two million simultaneous remote-control sessions/); assert.match(article, /production observation, not a reproducible Server Pro benchmark/); assert.doesNotMatch(article, /one million|million-plus/i); }); test('lists first-party ScreenConnect sources promised by the comparison', () => { const article = readPost('rustdesk-vs-screenconnect.md'); assert.match(article, /## Sources/); assert.match(article, /https:\/\/www\.screenconnect\.com\/pricing/); assert.match( article, /https:\/\/docs\.connectwise\.com\/ScreenConnect_Documentation\/Get_started\/Host_client\/Host_client_requirements/ ); assert.match( article, /https:\/\/www\.connectwise\.com\/company\/trust\/security-bulletins\/connectwise-screenconnect-23\.9\.8/ ); }); test('opens only the RustDesk ports required by each deployed feature', () => { const article = readPost('why-self-host-remote-desktop-software.md'); assert.doesNotMatch(article, /default ports are \*\*21114–21119\*\*/); assert.match(article, /TCP 21115-21117 and UDP 21116/); assert.match(article, /expose only TCP 443 publicly/); assert.match(article, /keep 21114 and 21118-21119 internal/); }); test('limits the no-code scam advice to the typical social-engineering flow', () => { const article = readPost('avoid-remote-desktop-scams.md'); assert.doesNotMatch(article, /they cannot simply connect/); assert.match(article, /typical social-engineering flow/); assert.match( article, /does not rule out existing unattended access, stolen credentials, malware, or exposed services/ ); }); test('distinguishes Splashtop cloud plans from Splashtop On-Prem', () => { const article = readPost('rustdesk-vs-splashtop.md'); assert.match(article, /managed SaaS plans and a \*\*separately licensed On-Prem\*\* product/); assert.match(article, /https:\/\/www\.splashtop\.com\/products\/on-prem/); assert.match(article, /customer-operated Splashtop (?:On-Prem )?Gateway/i); assert.doesNotMatch(article, /It remains a managed cloud service/); assert.doesNotMatch(article, /Splashtop is a cloud service/); }); test('consolidates Splashtop comparisons without erasing the On-Prem product', () => { const removedSlug = 'teamviewer-vs-anydesk-vs-splashtop'; const teamViewerComparison = readPost('teamviewer-vs-splashtop.md'); const rustDeskComparison = readPost('rustdesk-vs-splashtop.md'); const mspComparison = readPost('teamviewer-vs-anydesk-for-msps.md'); const netlify = read('netlify.toml'); const vercel = read('vercel.json'); assert.equal(existsSync(join(postsDir, `${removedSlug}.md`)), false); assert.equal(existsSync(join(root, `src/assets/images/blog/${removedSlug}-og.png`)), false); assert.doesNotMatch(mspComparison, new RegExp(`/blog/${removedSlug}`)); assert.match( netlify, new RegExp(`from = "/blog/${removedSlug}"[\\s\\S]*?to = "/blog/teamviewer-vs-splashtop"[\\s\\S]*?status = 301`) ); assert.match( vercel, new RegExp( `"source": "/blog/${removedSlug}"[\\s\\S]*?"destination": "/blog/teamviewer-vs-splashtop"[\\s\\S]*?"permanent": true` ) ); for (const article of [teamViewerComparison, rustDeskComparison]) { assert.match(article, /separately licensed On-Prem/i); assert.match(article, /Splashtop (?:On-Prem )?Gateway/i); assert.doesNotMatch(article, /Splashtop[^\n]*cloud only/i); assert.doesNotMatch(article, /(?:almost|hardly|nearly) (?:no one|nobody)|few customers use/i); } }); test('positions the Splashtop alternative page as an evidence-bounded IT switching guide', () => { const article = readPost('rustdesk-vs-splashtop.md'); assert.match(article, /title: 'Self-Hosted Splashtop Alternative/); assert.match(article, /excerpt: 'A self-hosted Splashtop alternative evaluation guide/); assert.match(article, /## Why IT teams evaluate alternatives to Splashtop/); assert.match(article, /do not establish how common/i); assert.match(article, /integration-specific/i); assert.match(article, /remote audio/i); assert.match(article, /monitor mapping/i); assert.match(article, /parallel pilot/i); assert.match(article, /rollback/i); assert.doesNotMatch(article, /72 (?:signals|customers|emails)|3\/72/); assert.doesNotMatch(article, /Kent Tec|Kimray|KinderCare|Liberty Tax|WaliaTech/); assert.doesNotMatch(article, /£50|£200|\$8K|\$8000/); }); test('describes the exploited ScreenConnect vulnerability without calling it a ConnectWise hack', () => { const article = readPost('best-screenconnect-alternative-msps.md'); assert.doesNotMatch(article, /ConnectWise hack|remote-access vendor gets breached/i); assert.match(article, /exploitation of CVE-2024-1709 in affected ScreenConnect servers/); }); test('does not impose a personal-use-only license on Chrome Remote Desktop', () => { const article = readPost('best-free-remote-desktop-software.md'); assert.doesNotMatch( article, /Chrome Remote Desktop is free but licensed for personal use|terms center on personal use/ ); assert.match(article, /Google documents enterprise administration policies/); assert.match(article, /https:\/\/support\.google\.com\/chrome\/a\/answer\/2799701/); }); test('separates Chrome Remote Desktop signaling from direct and relayed session traffic', () => { const article = readPost('chrome-remote-desktop-alternative.md'); assert.doesNotMatch(article, /every session runs through Google/i); assert.match(article, /initially negotiated through Google services/); assert.match(article, /Direct, STUN, or TURN\/relay/); assert.match(article, /https:\/\/support\.google\.com\/chrome\/a\/answer\/16364503/); }); test('does not lean on a competitor blog for the Chrome Remote Desktop limitations section', () => { const article = readPost('chrome-remote-desktop-alternative.md'); assert.doesNotMatch(article, /helpwire\.app\/blog\/chrome-remote-desktop/i); assert.match(article, /https:\/\/support\.google\.com\/chrome\/answer\/1649523/); assert.match(article, /https:\/\/support\.google\.com\/chrome\/a\/answer\/16364503/); }); test('does not present RustDesk iOS as an unattended host in the Chrome Remote Desktop comparison', () => { const article = readPost('chrome-remote-desktop-alternative.md'); assert.doesNotMatch( article, /Real file transfer and permanent-password unattended access come standard, across Windows, macOS, Linux, Android, and iOS\./ ); assert.match(article, /the iOS app is controller-only/); }); test('does not explain AnyDesk commercial-use detection through mandatory cloud relay', () => { const article = readPost('anydesk-commercial-use-detected.md'); assert.doesNotMatch(article, /every session runs through _their_ cloud/); assert.match(article, /AnyDesk supports direct client-to-client connections/); assert.match(article, /https:\/\/support\.anydesk\.com\/docs\/settings/); }); test('labels the public-server capacity number as a non-audited point-in-time internal observation', () => { const article = readPost('rustdesk-scale-50000-200000-devices.md'); assert.match(article, /point-in-time internal production observation/); assert.match(article, /not independently audited/); assert.match(article, /no public monitoring dashboard/); assert.doesNotMatch(article, /has operated stably/); }); test('sources AnyDesk plan limits from the current official pricing page', () => { const article = readPost('rustdesk-vs-anydesk.md'); assert.match(article, /https:\/\/anydesk\.com\/en\/pricing/); assert.match(article, /checked July 7, 2026/); assert.doesNotMatch(article, /third-party pricing trackers|Independent trackers/); }); test('splits RustDesk iOS controller support from host features in the free-software roundup', () => { const article = readPost('best-free-remote-desktop-software.md'); assert.doesNotMatch( article, /cross-platform \(Windows, macOS, Linux, Android, iOS\), with file transfer and unattended access/i ); assert.match(article, /Windows, macOS, Linux, and Android hosts; iOS is a controller app/); }); test('sources and bounds TeamViewer and Splashtop security claims', () => { const article = readPost('teamviewer-vs-splashtop.md'); assert.match(article, /https:\/\/www\.splashtop\.com\/security/); assert.match(article, /https:\/\/www\.splashtop\.com\/press\/splashtop-achieves-iso-iec-27001-2022-certification/); assert.match(article, /https:\/\/www\.teamviewer\.com\/en\/resources\/trust-center/); assert.match(article, /tv-2024-1005/); assert.doesNotMatch(article, /no publicly disclosed breach/); }); test('removes the unsupported claim that AnyDesk detection became stricter in 2025', () => { const article = readPost('anydesk-commercial-use-detected.md'); assert.doesNotMatch(article, /got stricter in 2025|became stricter in 2025/i); }); test('uses AnyDesk official pricing instead of unnamed trackers in the MSP FAQ', () => { const article = readPost('teamviewer-vs-anydesk-for-msps.md'); assert.doesNotMatch(article, /Multiple trackers|multiple trackers reporting/i); assert.match(article, /https:\/\/anydesk\.com\/en\/pricing/); assert.match(article, /official pricing page, checked July 7, 2026/); }); test('explains why RustDesk is unavailable on Google Play and gives current Android sources', () => { const article = readPost('rustdesk-remote-control-android-ios.md'); assert.doesNotMatch(article, /install (?:the app )?from Google Play/i); assert.doesNotMatch(article, /Android client is on Google Play/i); assert.match(article, /not currently distributed through Google Play/i); assert.match(article, /\/blog\/rustdesk-and-remote-access-scams/); assert.match(article, /github\.com\/rustdesk\/rustdesk\/releases/); assert.match(article, /f-droid\.org\/packages\/com\.carriez\.flutter_hbb/); }); test('documents RustDesk anti-scam actions and controlled-device defenses', () => { const article = readPost('rustdesk-and-remote-access-scams.md'); assert.match(article, /x\.com\/rustdesk\/status\/1698372220379349421/); assert.match(article, /github\.com\/rustdesk\/rustdesk\/discussions\/5660/); assert.match(article, /Login-required-for-public-server/); assert.match(article, /strong, unique permanent password/i); assert.match(article, /controlled device[^\n]*2FA|2FA[^\n]*controlled device/i); assert.match(article, /IP allowlist/i); assert.match(article, /\/32/); assert.match(article, /\/24/); assert.match(article, /\/128/); assert.match(article, /\/64/); assert.doesNotMatch(article, /Server Pro (?:account|web-console) 2FA/i); }); test('uses Server Pro Access Control rather than address-book visibility for authorization', () => { const article = readPost('rustdesk-per-user-access-control-device-groups-shared-address-book.md'); assert.match(article, /https:\/\/rustdesk\.com\/docs\/en\/self-host\/rustdesk-server-pro\/permissions\//); assert.match(article, /address book[^\n]*(?:visibility|convenience)[^\n]*not[^\n]*authorization/i); assert.match(article, /user assignments, device groups, and cross-group access settings/i); assert.match(article, /permissions are cumulative/i); assert.match(article, /known device ID/i); assert.doesNotMatch(article, /working approach today[^\n]*shared address book/i); assert.doesNotMatch(article, /per-user access control today[^\n]*shared address book/i); }); test('points license-management posts at the self-service portal with a support fallback', () => { const portalPosts = [ 'rustdesk-not-connecting-troubleshooting.md', 'upgrade-rustdesk-license-mid-subscription.md', 'rustdesk-custom-quote-minimum-users-invoice-fees.md', 'rustdesk-pro-license-cost-how-to-pay.md', ]; for (const name of portalPosts) { const article = readPost(name); assert.match(article, /rustdesk\.com\/self-host\/account/, `${name} should link the self-service license portal`); assert.match(article, /support@rustdesk\.com/, `${name} should give the support@ fallback for a forgotten checkout email`); } }); test('states the air-gapped license-validation grace window', () => { const article = readPost('rustdesk-server-pro-offline-air-gapped.md'); assert.match(article, /seven days/i); assert.match(article, /grace/i); }); test('adds the missing competitor migration drivers to the comparison posts', () => { const screenconnect = readPost('rustdesk-vs-screenconnect.md'); assert.match(screenconnect, /code-signing/i); assert.match(screenconnect, /July 7, 2025/); const teamviewer = readPost('rustdesk-vs-teamviewer.md'); assert.match(teamviewer, /perpetual license/i); assert.match(teamviewer, /subscription-only/i); const logmein = readPost('rustdesk-vs-logmein.md'); assert.match(logmein, /LDAP\/Active Directory/); assert.match(logmein, /single sign-on via OIDC/i); }); test('answers the enterprise mass-deployment and REST API questions', () => { const article = readPost('rustdesk-for-enterprise.md'); assert.match(article, /MSI/); assert.match(article, /GPO|Intune/); assert.match(article, /REST API/); }); test('frames RustDesk compliance around the self-hosted model, not a certification deficit', () => { const article = readPost('remote-desktop-data-sovereignty-gdpr.md'); assert.match(article, /ISO 27001, SOC 2, or HIPAA/); assert.match(article, /ISO 27001 or HIPAA scope/); assert.match(article, /sales@rustdesk\.com/); assert.doesNotMatch(article, /does not market formal/i); }); test('every head-to-head comparison post ships FAQ structured data', () => { const comparisons = [ 'rustdesk-vs-teamviewer.md', 'rustdesk-vs-anydesk.md', 'rustdesk-vs-screenconnect.md', 'rustdesk-vs-splashtop.md', 'rustdesk-vs-logmein.md', 'rustdesk-vs-rdp.md', 'rustdesk-vs-vnc.md', ]; for (const name of comparisons) { assert.match(readPost(name), /^faq:/m, `${name} should define an faq: block`); } }); test('shows concrete self-hosted GDPR controls, not just obligations', () => { const article = readPost('remote-desktop-data-sovereignty-gdpr.md'); assert.match(article, /built-in log rotation/i); assert.match(article, /stays on your own server when you self-host/i); assert.match(article, /Control Role/); }); test('explains custom-client build server, retention window, and config non-retention', () => { const article = readPost('rustdesk-web-console-custom-client-generator-port-21114.md'); assert.match(article, /compiled on RustDesk's build server/i); assert.match(article, /cross-compile and sign all of those/i); assert.match(article, /automatically deleted from the build server/i); assert.match(article, /deleted automatically once the build finishes/i); });