Files
lage-chat-control/tests/unit/dist-links.test.ts
T
Aurealibe 8ebc1da9e4 Rebuild du site sur Astro : i18n multilingue, faits verifies, Docker, CI, tests
- Astro statique, EN par defaut + /fr/ + /nl/, detection langue navigateur
- i18n par fichiers JSON, ajouter une langue = ajouter des traductions
- Contenu original porte a l'identique (diff d'inventaire par langue)
- Chronologie legislative corrigee sur sources primaires (PE, Conseil, votes)
- Timeline 23 precedents + observatoire 35 items + annuaire 66 outils FOSS,
  chaque affirmation verifiee et sourcee
- Zero requete tierce (teste), lisible JS coupe, axe WCAG AA x2 themes
- Version offline monofichier par langue a chaque build
- Docker multi-stage vers nginx + CSP stricte auto-generee
- CI GitHub Actions (SHA-pinnees) + verification hebdo des liens
- CONTRIBUTING : divulgation d'affiliation obligatoire, allowlist de
  domaines testee en CI
2026-07-10 03:52:49 +01:00

75 lines
2.6 KiB
TypeScript

import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import allowlist from '../../src/data/domains-allowlist.json'
// The anti-backlink gate, run against the BUILT site (ground truth).
// PR #1 taught us the lesson: a smuggled external link must never be
// mergeable silently. Every external hostname in dist/ must be declared
// in src/data/domains-allowlist.json (a reviewable diff), and every
// external anchor must carry the full safe rel.
//
// Requires `pnpm build` first (CI builds before testing).
const DIST = 'dist'
function htmlFiles(dir: string): string[] {
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) return htmlFiles(path)
return entry.name.endsWith('.html') ? [path] : []
})
}
const built = existsSync(DIST)
describe.skipIf(!built)('built site external links', () => {
const allowed = new Set<string>(allowlist.domains)
const anchorRe = /<a\s[^>]*href="(https?:\/\/[^"]+)"[^>]*>/gi
const anchors: { file: string; href: string; tag: string }[] = []
for (const file of htmlFiles(DIST)) {
const html = readFileSync(file, 'utf8')
for (const m of html.matchAll(anchorRe)) {
anchors.push({ file, href: m[1]!, tag: m[0]! })
}
}
it('found external anchors to audit (sanity)', () => {
expect(anchors.length).toBeGreaterThan(0)
})
it('every external hostname is on the allowlist', () => {
const offenders = anchors
.map((a) => ({ ...a, host: new URL(a.href).hostname }))
.filter((a) => !allowed.has(a.host) && a.host !== 'exitchatcontrol.org')
expect(
offenders.map((o) => `${o.host} (${o.file})`),
'unlisted external domain in built output — add it to domains-allowlist.json ONLY after review',
).toEqual([])
})
it('every external anchor is https and carries rel="noopener noreferrer"', () => {
const offenders = anchors.filter(
(a) =>
!a.href.startsWith('https://') ||
!/rel="[^"]*noopener[^"]*"/.test(a.tag) ||
!/rel="[^"]*noreferrer[^"]*"/.test(a.tag),
)
expect(offenders.map((o) => o.tag)).toEqual([])
})
it('never links the domains PR #1 tried to smuggle in', () => {
for (const { tag } of anchors) {
expect(tag).not.toMatch(/nika\.sh|vpn-gratuit\.fr/)
}
})
})
describe('allowlist hygiene', () => {
it('is sorted and unique (reviewable diffs)', () => {
const domains = allowlist.domains
expect([...new Set(domains)].sort()).toEqual(domains)
})
})