Rechtstexte: Hochladen-Skript für WOMS Rechtliches (ein Dokument je Aufruf, nur mit Freigabe)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
99d7e2bdca
commit
0432b7e01d
2 files changed
+418
No files matched your search
@@ -0,0 +1,325 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
// Lädt die Rechtstexte der Fassung 6 (Kundenansicht neu) nach der Freigabe in WOMS „Rechtliches“ hoch (Bucket legal-files, Collection
|
||||||
|
// legalVersions), wie es die WOMS-Oberfläche tut: neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Ein Dokument
|
||||||
|
// je Aufruf, nur mit --freigabe=JJJJ-MM-TT; schon hochgeladene Fassungen (gleiche source) werden übersprungen.
|
||||||
|
//
|
||||||
|
// node scripts/rechtliches-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Versionen und Karten; jede fehlende Karte ist ein Stopp
|
||||||
|
// node scripts/rechtliches-hochladen.mjs --karten-anlegen legt die fehlenden Karten in legalDocuments an (vorhandene nie), einmal vor dem ersten Hochladen
|
||||||
|
// node scripts/rechtliches-hochladen.mjs <documentKey> --freigabe=JJJJ-MM-TT
|
||||||
|
// lädt genau ein Dokument hoch (validFrom = Freigabedatum); ohne Karte Abbruch vor dem Schreiben
|
||||||
|
// node scripts/rechtliches-hochladen.mjs --pruefen nur lesen: Nachkontrolle, Exit 0 nur, wenn alle elf Dokumente eine aktuelle Version
|
||||||
|
// mit passender source haben
|
||||||
|
//
|
||||||
|
// Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600).
|
||||||
|
// Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch).
|
||||||
|
// Dieses Skript wird im Paket P8 nur geschrieben und getestet (mit Nachbau-deps), nie ausgeführt.
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { FUNKTIONEN_UNTERLAGEN_V6 } from '../server/services/products.js'
|
||||||
|
|
||||||
|
const ENDPOINT = 'https://ticket.webklar.com/v1'
|
||||||
|
const PROJECT = '6a1058610003c5a13a05'
|
||||||
|
const DB = 'woms-database'
|
||||||
|
const BUCKET = 'legal-files'
|
||||||
|
const KEY_DATEI = '/root/.appwrite-rechtliches.key'
|
||||||
|
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
||||||
|
const LIVE = 'https://project.webklar.com/'
|
||||||
|
|
||||||
|
const USAGE = 'Aufruf: node scripts/rechtliches-hochladen.mjs --probe | --karten-anlegen | --pruefen | <documentKey> --freigabe=JJJJ-MM-TT (genau ein Dokument je Aufruf)'
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------- Dokumente
|
||||||
|
|
||||||
|
const NAMEN = {
|
||||||
|
news: 'News-Pinnwand',
|
||||||
|
termine: 'Terminplaner',
|
||||||
|
shop: 'Webshop',
|
||||||
|
unterkunft: 'Buchungskalender',
|
||||||
|
zeiten: 'Öffnungszeiten live',
|
||||||
|
analyse: 'Website-Analyse',
|
||||||
|
}
|
||||||
|
|
||||||
|
const ABO = Object.freeze({
|
||||||
|
documentKey: 'abo-bedingungen',
|
||||||
|
dateien: Object.freeze(['public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', 'public/neu/rechtliches/abo-bedingungen.html']),
|
||||||
|
titel: 'Abo-Bedingungen',
|
||||||
|
quelle: 'public/neu/rechtliches/abo-bedingungen.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Die elf Dokumente der Fassung 6 in der Reihenfolge der Tabelle: die Abo-Bedingungen, dann je Funktion Leistungsbeschreibung und AVV wie in
|
||||||
|
* FUNKTIONEN_UNTERLAGEN_V6 (documentKey = Slug der Datei ohne Fassung). dateien = [PDF, HTML]; quelle = der HTML-Pfad, den source beim Hochladen
|
||||||
|
* trägt (verschiebt P10 die Abo-Seite, ändert sich dateien, quelle bleibt).
|
||||||
|
*/
|
||||||
|
export const DOKUMENTE_V6 = Object.freeze([
|
||||||
|
ABO,
|
||||||
|
...Object.entries(FUNKTIONEN_UNTERLAGEN_V6).flatMap(([key, unterlagen]) =>
|
||||||
|
[['lb', unterlagen.lb, `Leistungsbeschreibung ${NAMEN[key]}`], ['avv', unterlagen.avv, `AVV ${NAMEN[key]}`]]
|
||||||
|
.filter(([, dok]) => dok)
|
||||||
|
.map(([, dok, titel]) => {
|
||||||
|
const html = `public${dok.url}`
|
||||||
|
return Object.freeze({
|
||||||
|
documentKey: path.basename(dok.url, `-${dok.version}.html`),
|
||||||
|
dateien: Object.freeze([`public/${dok.pdfFile}`, html]),
|
||||||
|
titel,
|
||||||
|
quelle: html,
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
|
||||||
|
/** Eintrag der Tabelle zu einem documentKey; sonst Error('Unbekanntes Dokument: …'). */
|
||||||
|
export function dokumentFuer(documentKey) {
|
||||||
|
const d = DOKUMENTE_V6.find((x) => x.documentKey === documentKey)
|
||||||
|
if (!d) throw new Error(`Unbekanntes Dokument: ${String(documentKey).slice(0, 80)}`)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------- reine Funktionen
|
||||||
|
|
||||||
|
const MODI = { '--probe': 'probe', '--karten-anlegen': 'karten', '--pruefen': 'pruefen' }
|
||||||
|
|
||||||
|
const istKalendertag = (iso) => {
|
||||||
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false
|
||||||
|
const d = new Date(`${iso}T12:00:00Z`)
|
||||||
|
return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === iso
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kommandozeile prüfen: ein Modus allein (--probe, --karten-anlegen, --pruefen) oder genau ein documentKey mit --freigabe=JJJJ-MM-TT.
|
||||||
|
* @returns {{ modus: 'probe'|'karten'|'pruefen'|'hochladen', documentKey: string|null, freigabe: string|null }}
|
||||||
|
*/
|
||||||
|
export function parseArgs(argv) {
|
||||||
|
const optionen = argv.filter((a) => a.startsWith('--'))
|
||||||
|
const dokumente = argv.filter((a) => !a.startsWith('--'))
|
||||||
|
const modi = optionen.filter((o) => Object.hasOwn(MODI, o))
|
||||||
|
const freigaben = optionen.filter((o) => o.startsWith('--freigabe='))
|
||||||
|
const fremde = optionen.filter((o) => !Object.hasOwn(MODI, o) && !o.startsWith('--freigabe='))
|
||||||
|
if (fremde.length > 0) throw new Error(`${USAGE}\nUnbekannte Option: ${fremde[0].slice(0, 40)}`)
|
||||||
|
if (modi.length > 0) {
|
||||||
|
if (modi.length > 1 || dokumente.length > 0 || freigaben.length > 0) throw new Error(`${USAGE}\n${modi[0]} steht allein.`)
|
||||||
|
return { modus: MODI[modi[0]], documentKey: null, freigabe: null }
|
||||||
|
}
|
||||||
|
if (dokumente.length !== 1) throw new Error(`${USAGE}\nGenau ein Dokument je Aufruf.`)
|
||||||
|
if (freigaben.length !== 1) throw new Error('Freigabe fehlt: --freigabe=JJJJ-MM-TT ist beim Hochladen Pflicht.')
|
||||||
|
const freigabe = freigaben[0].slice('--freigabe='.length)
|
||||||
|
if (!istKalendertag(freigabe)) throw new Error(`Freigabe ungültig (erwartet JJJJ-MM-TT, ein echter Kalendertag): ${freigabe.slice(0, 20)}`)
|
||||||
|
dokumentFuer(dokumente[0])
|
||||||
|
return { modus: 'hochladen', documentKey: dokumente[0], freigabe }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Der Tag vor einem Datum (JJJJ-MM-TT). */
|
||||||
|
export function tagVorher(iso) {
|
||||||
|
const d = new Date(`${iso}T12:00:00Z`)
|
||||||
|
d.setUTCDate(d.getUTCDate() - 1)
|
||||||
|
return d.toISOString().slice(0, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plan für eine neue Version eines Dokuments: schon = gleiche source ist hochgeladen; nummer = vN (Zahl der Versionen mit ^v\d+$ plus eins);
|
||||||
|
* aktuellId = die bisherige gültige Version (jüngstes validFrom), die abgelöst wird; validToAltes = Tag vor validFrom, bei gleichem oder
|
||||||
|
* früherem validFrom = validFrom (wie die Vorlage des Webshop-Livegangs).
|
||||||
|
*/
|
||||||
|
export function planVersion({ alle, source, validFrom }) {
|
||||||
|
const aktuell = alle.filter((v) => v.status === 'current').sort((a, b) => b.validFrom.localeCompare(a.validFrom))[0]
|
||||||
|
return {
|
||||||
|
schon: alle.some((v) => v.source === source),
|
||||||
|
nummer: `v${alle.filter((v) => /^v\d+$/.test(v.version)).length + 1}`,
|
||||||
|
aktuellId: aktuell ? aktuell.$id : null,
|
||||||
|
validToAltes: aktuell ? (validFrom > aktuell.validFrom ? tagVorher(validFrom) : validFrom) : null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dokument-ID einer Karte: der Schlüssel, wenn er höchstens 36 Zeichen hat, sonst 27 Zeichen davon, "-" und 8 Hexzeichen von sha256(key). */
|
||||||
|
export function kartenId(key) {
|
||||||
|
if (key.length <= 36) return key
|
||||||
|
return `${key.slice(0, 27)}-${createHash('sha256').update(key).digest('hex').slice(0, 8)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Die fehlenden Karten in Tabellenreihenfolge: [{ id, daten: { key, title, liveUrl, sort } }], sort ab hoechsteSort + 1. */
|
||||||
|
export function kartenPlan({ vorhandeneKeys, hoechsteSort }) {
|
||||||
|
const vorhanden = new Set(vorhandeneKeys)
|
||||||
|
let sort = hoechsteSort
|
||||||
|
return DOKUMENTE_V6.filter((d) => !vorhanden.has(d.documentKey)).map((d) => {
|
||||||
|
sort += 1
|
||||||
|
return { id: kartenId(d.documentKey), daten: { key: d.documentKey, title: d.titel, liveUrl: `${LIVE}${d.dateien[1].replace(/^public\//, '')}`, sort } }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskiere = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
|
||||||
|
/** Nachkontrolle: { [documentKey]: Version[] } -> { fehlt: documentKey[] }; ein Dokument zählt mit einer Version status current und source kundenbereich@<Hash>:<quelle>. */
|
||||||
|
export function pruefeHochgeladen(versionenJe) {
|
||||||
|
const fehlt = DOKUMENTE_V6.filter((d) => {
|
||||||
|
const muster = new RegExp(`^kundenbereich@[0-9a-f]{7,40}:${maskiere(d.quelle)}$`)
|
||||||
|
return !(versionenJe[d.documentKey] ?? []).some((v) => v.status === 'current' && muster.test(v.source ?? ''))
|
||||||
|
}).map((d) => d.documentKey)
|
||||||
|
return { fehlt }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------- Zugriff auf WOMS
|
||||||
|
|
||||||
|
const q = (o) => encodeURIComponent(JSON.stringify(o))
|
||||||
|
const neueId = () => Array.from(crypto.getRandomValues(new Uint8Array(10)), (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 20)
|
||||||
|
|
||||||
|
/** Kleiner Appwrite-Client über deps.fetch; Fehler werden zu Error mit Methode, Pfad und Status. */
|
||||||
|
function client(fetchFn, schluessel) {
|
||||||
|
const kopf = { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': schluessel }
|
||||||
|
return async function api(method, pfad, body) {
|
||||||
|
const formular = typeof FormData !== 'undefined' && body instanceof FormData
|
||||||
|
const res = await fetchFn(`${ENDPOINT}${pfad}`, {
|
||||||
|
method,
|
||||||
|
headers: formular ? kopf : { ...kopf, 'Content-Type': 'application/json' },
|
||||||
|
body: body === undefined ? undefined : formular ? body : JSON.stringify(body),
|
||||||
|
})
|
||||||
|
const text = await res.text()
|
||||||
|
if (!res.ok) throw Object.assign(new Error(`${method} ${pfad}: ${res.status} ${text.slice(0, 200)}`), { status: res.status })
|
||||||
|
return text ? JSON.parse(text) : null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function alleDokumente(api, sammlung) {
|
||||||
|
const aus = []
|
||||||
|
for (let offset = 0; ; offset += 100) {
|
||||||
|
const r = await api('GET', `/databases/${DB}/collections/${sammlung}/documents?queries[]=${q({ method: 'limit', values: [100] })}&queries[]=${q({ method: 'offset', values: [offset] })}`)
|
||||||
|
aus.push(...r.documents)
|
||||||
|
if (r.documents.length < 100) return aus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function versionen(api, documentKey) {
|
||||||
|
const r = await api('GET', `/databases/${DB}/collections/legalVersions/documents?queries[]=${q({ method: 'equal', attribute: 'documentKey', values: [documentKey] })}&queries[]=${q({ method: 'limit', values: [100] })}`)
|
||||||
|
return r.documents
|
||||||
|
}
|
||||||
|
|
||||||
|
const ddmmjjjj = (iso) => iso.split('-').reverse().join('.')
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------------------------------------- Aufruf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einstieg für alle Aufrufe. deps = { fetch, schluessel, log, git, dateien } mit den Standards globales fetch, Datei /root/.appwrite-rechtliches.key,
|
||||||
|
* console.log, git im Projekt und das Dateisystem; der einzige Weg zu Schlüssel und Netz.
|
||||||
|
* @returns {Promise<number>} Exit-Code (0 oder 1)
|
||||||
|
*/
|
||||||
|
export async function main(argv, deps = {}) {
|
||||||
|
const log = deps.log ?? console.log
|
||||||
|
let args
|
||||||
|
try {
|
||||||
|
args = parseArgs(argv)
|
||||||
|
} catch (err) {
|
||||||
|
log(err.message)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const fetchFn = deps.fetch ?? globalThis.fetch
|
||||||
|
const api = client(fetchFn, (deps.schluessel ?? (() => readFileSync(KEY_DATEI, 'utf8').trim()))())
|
||||||
|
if (args.modus === 'probe') return await probe(api, log)
|
||||||
|
if (args.modus === 'karten') return await kartenAnlegen(api, log)
|
||||||
|
if (args.modus === 'pruefen') return await pruefen(api, log)
|
||||||
|
return await hochladen(api, log, args, deps)
|
||||||
|
} catch (err) {
|
||||||
|
log(`Fehler: ${err.message}`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --probe: nur lesen. Jede fehlende Karte ist ein Stopp. */
|
||||||
|
async function probe(api, log) {
|
||||||
|
await api('GET', `/storage/buckets/${BUCKET}/files?queries[]=${q({ method: 'limit', values: [1] })}`)
|
||||||
|
const karten = new Set((await alleDokumente(api, 'legalDocuments')).map((k) => k.key))
|
||||||
|
let stopp = 0
|
||||||
|
for (const d of DOKUMENTE_V6) {
|
||||||
|
log(`${d.documentKey}: ${(await versionen(api, d.documentKey)).map((v) => `${v.version}:${v.status}`).join(' ') || '—'}`)
|
||||||
|
if (!karten.has(d.documentKey)) {
|
||||||
|
log(`STOPP: Karte fehlt: ${d.documentKey}`)
|
||||||
|
stopp += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stopp > 0) return 1
|
||||||
|
log('Schlüssel ok (files.read + documents.read), alle Karten vorhanden.')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --karten-anlegen: legt nur die fehlenden Karten an, vorhandene nie. */
|
||||||
|
async function kartenAnlegen(api, log) {
|
||||||
|
const vorhanden = await alleDokumente(api, 'legalDocuments')
|
||||||
|
const plan = kartenPlan({ vorhandeneKeys: vorhanden.map((k) => k.key), hoechsteSort: Math.max(0, ...vorhanden.map((k) => Number(k.sort) || 0)) })
|
||||||
|
for (const karte of plan) {
|
||||||
|
await api('POST', `/databases/${DB}/collections/legalDocuments/documents`, { documentId: karte.id, data: karte.daten })
|
||||||
|
log(`+ Karte ${karte.daten.key} (ID ${karte.id})`)
|
||||||
|
}
|
||||||
|
if (plan.length === 0) log('Alle Karten sind vorhanden.')
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** --pruefen: nur lesen, Nachkontrolle nach den Uploads. */
|
||||||
|
async function pruefen(api, log) {
|
||||||
|
const je = {}
|
||||||
|
for (const d of DOKUMENTE_V6) je[d.documentKey] = await versionen(api, d.documentKey)
|
||||||
|
const { fehlt } = pruefeHochgeladen(je)
|
||||||
|
for (const d of DOKUMENTE_V6) log(`${fehlt.includes(d.documentKey) ? 'FEHLT' : 'ok'} ${d.documentKey}`)
|
||||||
|
return fehlt.length > 0 ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/** <documentKey> --freigabe=…: genau ein Dokument hochladen. Als Erstes die Kartenprüfung, dann Git- und Dateiprüfung, erst dann schreiben. */
|
||||||
|
async function hochladen(api, log, { documentKey, freigabe }, deps) {
|
||||||
|
const d = dokumentFuer(documentKey)
|
||||||
|
const karten = await alleDokumente(api, 'legalDocuments')
|
||||||
|
if (!karten.some((k) => k.key === documentKey)) {
|
||||||
|
log(`Karte fehlt: ${documentKey} (zuerst --karten-anlegen)`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
const git = deps.git ?? ((args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' }))
|
||||||
|
const dateisystem = deps.dateien ?? { existsSync: (rel) => existsSync(path.join(ROOT, rel)), readFileSync: (rel) => readFileSync(path.join(ROOT, rel)) }
|
||||||
|
for (const rel of d.dateien) {
|
||||||
|
if (!dateisystem.existsSync(rel)) {
|
||||||
|
log(`Datei fehlt: ${rel}`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (git(['status', '--porcelain', '--', ...d.dateien]).trim() !== '') {
|
||||||
|
log(`Abbruch: nicht committeter Stand in ${d.dateien.join(' und ')} (git status --porcelain ist nicht leer)`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
const commit = git(['rev-parse', '--short', 'HEAD']).trim()
|
||||||
|
const source = `kundenbereich@${commit}:${d.quelle}`
|
||||||
|
const alle = await versionen(api, documentKey)
|
||||||
|
const plan = planVersion({ alle, source, validFrom: freigabe })
|
||||||
|
if (plan.schon) {
|
||||||
|
log(`= ${documentKey}: schon hochgeladen`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const fileIds = []
|
||||||
|
for (const rel of d.dateien) {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('fileId', neueId())
|
||||||
|
form.append('file', new Blob([dateisystem.readFileSync(rel)], { type: rel.endsWith('.pdf') ? 'application/pdf' : 'text/html' }), path.basename(rel))
|
||||||
|
fileIds.push((await api('POST', `/storage/buckets/${BUCKET}/files`, form)).$id)
|
||||||
|
}
|
||||||
|
await api('POST', `/databases/${DB}/collections/legalVersions/documents`, {
|
||||||
|
documentId: neueId(),
|
||||||
|
data: {
|
||||||
|
documentKey,
|
||||||
|
version: plan.nummer,
|
||||||
|
validFrom: freigabe,
|
||||||
|
validTo: null,
|
||||||
|
status: 'current',
|
||||||
|
changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: Kundenansicht neu (Fassung 6)`,
|
||||||
|
fileIds,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (plan.aktuellId) {
|
||||||
|
await api('PATCH', `/databases/${DB}/collections/legalVersions/documents/${plan.aktuellId}`, { data: { status: 'superseded', validTo: plan.validToAltes } })
|
||||||
|
}
|
||||||
|
log(`+ ${documentKey} ${plan.nummer} (${fileIds.length} Dateien)${plan.aktuellId ? ', bisherige Version abgelöst' : ''}`)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
||||||
|
main(process.argv.slice(2)).then((code) => {
|
||||||
|
process.exitCode = code
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { test } from 'node:test'
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
|
||||||
|
// Hochladen der Fassung 6 nach WOMS "Rechtliches" (scripts/rechtliches-hochladen.mjs): die Tests rufen main nur mit Nachbau-deps auf,
|
||||||
|
// nie mit dem echten Schlüssel oder dem Netz.
|
||||||
|
const { FUNKTIONEN_UNTERLAGEN_V6, FUNKTIONEN_FASSUNG_V6 } = await import('../server/services/products.js')
|
||||||
|
const m = await import('../scripts/rechtliches-hochladen.mjs') // der Import selbst darf weder Schlüssel lesen noch das Netz nutzen
|
||||||
|
test('parseArgs: ein Dokument je Aufruf, Hochladen nur mit --freigabe=Datum, --probe und --pruefen lesen nur, --karten-anlegen legt nur Karten an', () => {
|
||||||
|
assert.deepEqual(m.parseArgs(['--probe']), { modus: 'probe', documentKey: null, freigabe: null })
|
||||||
|
assert.deepEqual(m.parseArgs(['--karten-anlegen']), { modus: 'karten', documentKey: null, freigabe: null })
|
||||||
|
assert.deepEqual(m.parseArgs(['--pruefen']), { modus: 'pruefen', documentKey: null, freigabe: null })
|
||||||
|
assert.deepEqual(m.parseArgs(['avv-webshop', '--freigabe=2026-10-20']), { modus: 'hochladen', documentKey: 'avv-webshop', freigabe: '2026-10-20' })
|
||||||
|
for (const bad of [[], ['avv-webshop'], ['--freigabe=2026-10-20'], ['avv-webshop', 'avv-terminplaner', '--freigabe=2026-10-20'], ['avv-webshop', '--freigabe=20.10.2026'], ['avv-webshop', '--freigabe=2026-13-40'], ['datenschutz', '--freigabe=2026-10-20'], ['--probe', 'avv-webshop'], ['--probe', '--freigabe=2026-10-20'], ['--karten-anlegen', 'avv-webshop'], ['--karten-anlegen', '--freigabe=2026-10-20'], ['--pruefen', '--probe'], ['--pruefen', 'avv-webshop']]) {
|
||||||
|
assert.throws(() => m.parseArgs(bad), /Aufruf|Freigabe|Dokument/, JSON.stringify(bad))
|
||||||
|
}
|
||||||
|
assert.equal(typeof m.main, 'function')
|
||||||
|
})
|
||||||
|
test('Tabelle: elf Dokumente, Dateien im Vorschaupfad, passend zu FUNKTIONEN_UNTERLAGEN_V6', () => {
|
||||||
|
assert.deepEqual(m.DOKUMENTE_V6.map((d) => d.documentKey), ['abo-bedingungen', 'leistungsbeschreibung-news', 'leistungsbeschreibung-terminplaner', 'avv-terminplaner', 'leistungsbeschreibung-webshop', 'avv-webshop', 'leistungsbeschreibung-unterkunft', 'avv-unterkunft', 'leistungsbeschreibung-zeiten', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'])
|
||||||
|
assert.deepEqual(m.dokumentFuer('abo-bedingungen').dateien, ['public/neu/rechtliches/WEBklar-Abo-Vertragsunterlagen.pdf', 'public/neu/rechtliches/abo-bedingungen.html'])
|
||||||
|
const unterlagen = Object.values(FUNKTIONEN_UNTERLAGEN_V6).flatMap((u) => [u.lb, u.avv]).filter(Boolean)
|
||||||
|
assert.deepEqual(m.DOKUMENTE_V6.slice(1).map((d) => d.dateien), unterlagen.map((u) => [`public/${u.pdfFile}`, `public${u.url}`]))
|
||||||
|
assert.deepEqual(m.DOKUMENTE_V6.map((d) => d.quelle), m.DOKUMENTE_V6.map((d) => d.dateien[1]), 'quelle = HTML-Pfad, der in source steht')
|
||||||
|
assert.throws(() => m.dokumentFuer('datenschutz'), /Unbekanntes Dokument/)
|
||||||
|
})
|
||||||
|
test('planVersion: neue Fassung als vN, bisherige gültige wird abgelöst, doppelter Upload wird erkannt', () => {
|
||||||
|
const alle = [{ $id: 'a', version: 'v1', validFrom: '2026-09-30', status: 'current', source: 'x' }, { $id: 'b', version: 'v0', validFrom: '2026-09-25', status: 'superseded', source: 'y' }]
|
||||||
|
assert.deepEqual(m.planVersion({ alle, source: 's', validFrom: '2026-10-20' }), { schon: false, nummer: 'v3', aktuellId: 'a', validToAltes: '2026-10-19' })
|
||||||
|
assert.equal(m.planVersion({ alle, source: 'x', validFrom: '2026-10-20' }).schon, true)
|
||||||
|
assert.deepEqual(m.planVersion({ alle: [], source: 's', validFrom: '2026-10-20' }), { schon: false, nummer: 'v1', aktuellId: null, validToAltes: null })
|
||||||
|
assert.equal(m.planVersion({ alle, source: 's', validFrom: '2026-09-30' }).validToAltes, '2026-09-30', 'gleicher Tag: validTo = validFrom wie in der Vorlage')
|
||||||
|
assert.equal(m.tagVorher('2026-03-01'), '2026-02-28')
|
||||||
|
})
|
||||||
|
// Nachbau des WOMS-Backends für main(): Karten als Dokumente { $id, key, sort }, Versionen und Dateien leer; zählt alle Aufrufe
|
||||||
|
function backend(karten) {
|
||||||
|
const aufrufe = [], log = [], antwort = (obj) => ({ ok: true, status: 200, text: async () => JSON.stringify(obj) })
|
||||||
|
const fetch = async (url, opt = {}) => {
|
||||||
|
const methode = opt.method ?? 'GET', pfad = new URL(url).pathname.replace(/^\/v1/, ''); aufrufe.push(`${methode} ${pfad}`)
|
||||||
|
if (methode === 'GET' && pfad.endsWith('/collections/legalDocuments/documents')) return antwort({ total: karten.length, documents: karten })
|
||||||
|
if (methode === 'GET' && pfad.endsWith('/collections/legalVersions/documents')) return antwort({ total: 0, documents: [] })
|
||||||
|
if (methode === 'GET' && pfad.startsWith('/storage/buckets/legal-files/files')) return antwort({ total: 0, files: [] })
|
||||||
|
if (methode === 'POST' && pfad.endsWith('/collections/legalDocuments/documents')) { const b = JSON.parse(opt.body); karten.push({ $id: b.documentId, ...b.data }); return antwort({ $id: b.documentId }) }
|
||||||
|
throw new Error(`unerwartet: ${methode} ${pfad}`)
|
||||||
|
}
|
||||||
|
return { karten, aufrufe, log, deps: { fetch, schluessel: () => 'test-schluessel', log: (z) => log.push(z) } }
|
||||||
|
}
|
||||||
|
const NUR_LESEN = (b) => b.aufrufe.every((a) => a.startsWith('GET '))
|
||||||
|
const FEHLEN = ['leistungsbeschreibung-news', 'leistungsbeschreibung-website-analyse', 'avv-website-analyse'] // „leistungsbeschreibung-website-analyse“ hat 37 Zeichen, eine Dokument-ID darf höchstens 36 haben
|
||||||
|
const kartenDoc = (key, i) => ({ $id: key.slice(0, 36), key, sort: i + 1 })
|
||||||
|
const VORHANDEN = m.DOKUMENTE_V6.map((d) => d.documentKey).filter((k) => !FEHLEN.includes(k)) // die Karten laut WOMS scripts/setup-legal.mjs
|
||||||
|
test('kartenPlan: genau die drei fehlenden Karten in Tabellenreihenfolge, mit zulässiger Dokument-ID', () => {
|
||||||
|
const plan = m.kartenPlan({ vorhandeneKeys: VORHANDEN, hoechsteSort: 14 })
|
||||||
|
assert.deepEqual(plan.map((k) => [k.daten.key, k.daten.sort]), [[FEHLEN[0], 15], [FEHLEN[1], 16], [FEHLEN[2], 17]])
|
||||||
|
assert.deepEqual(plan.map((k) => k.daten.title), ['Leistungsbeschreibung News-Pinnwand', 'Leistungsbeschreibung Website-Analyse', 'AVV Website-Analyse'])
|
||||||
|
assert.ok(plan.every((k) => k.daten.liveUrl.startsWith('https://project.webklar.com/neu/rechtliches/') && k.daten.liveUrl.endsWith(`-${FUNKTIONEN_FASSUNG_V6}.html`)))
|
||||||
|
assert.ok(plan.every((k) => /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/.test(k.id)), 'Dokument-ID höchstens 36 Zeichen')
|
||||||
|
assert.equal(plan[0].id, FEHLEN[0], 'kurze Schlüssel bleiben die ID, wie in setup-legal.mjs')
|
||||||
|
assert.notEqual(plan[1].id, FEHLEN[1]); assert.equal(m.kartenId(FEHLEN[1]), plan[1].id, 'lange Schlüssel: abgeleitete ID, immer dieselbe')
|
||||||
|
assert.deepEqual(m.kartenPlan({ vorhandeneKeys: m.DOKUMENTE_V6.map((d) => d.documentKey), hoechsteSort: 20 }), [])
|
||||||
|
})
|
||||||
|
test('--probe: eine fehlende Karte ist ein Stopp (Exit 1), mit allen Karten Exit 0; schreibt nie', async () => {
|
||||||
|
const b = backend(VORHANDEN.map(kartenDoc))
|
||||||
|
assert.equal(await m.main(['--probe'], b.deps), 1)
|
||||||
|
for (const k of FEHLEN) assert.ok(b.log.includes(`STOPP: Karte fehlt: ${k}`), k)
|
||||||
|
assert.ok(NUR_LESEN(b), b.aufrufe.join(', '))
|
||||||
|
assert.equal(await m.main(['--probe'], backend(m.DOKUMENTE_V6.map((d, i) => kartenDoc(d.documentKey, i))).deps), 0)
|
||||||
|
})
|
||||||
|
test('--karten-anlegen: legt nur die drei fehlenden Karten an, ein zweiter Lauf tut nichts, danach ist --probe grün', async () => {
|
||||||
|
const b = backend(VORHANDEN.map(kartenDoc))
|
||||||
|
assert.equal(await m.main(['--karten-anlegen'], b.deps), 0)
|
||||||
|
assert.deepEqual(b.aufrufe.filter((a) => !a.startsWith('GET ')), FEHLEN.map(() => 'POST /databases/woms-database/collections/legalDocuments/documents'))
|
||||||
|
assert.deepEqual(b.karten.slice(VORHANDEN.length).map((k) => k.key), FEHLEN)
|
||||||
|
b.aufrufe.length = 0
|
||||||
|
assert.equal(await m.main(['--karten-anlegen'], b.deps), 0); assert.ok(NUR_LESEN(b))
|
||||||
|
assert.equal(await m.main(['--probe'], b.deps), 0)
|
||||||
|
})
|
||||||
|
test('Hochladen ohne Karte bricht ab, bevor etwas geschrieben wird', async () => {
|
||||||
|
const b = backend(VORHANDEN.map(kartenDoc))
|
||||||
|
assert.equal(await m.main([FEHLEN[0], '--freigabe=2026-10-20'], b.deps), 1)
|
||||||
|
assert.ok(b.log.some((z) => z.includes(`Karte fehlt: ${FEHLEN[0]}`)) && NUR_LESEN(b))
|
||||||
|
})
|
||||||
|
test('pruefeHochgeladen und --pruefen: alle elf Dokumente brauchen eine aktuelle Version mit passender Quelle', async () => {
|
||||||
|
const ok = (d) => [{ $id: 'v', version: 'v2', status: 'current', source: `kundenbereich@abc1234:${d.quelle}` }]
|
||||||
|
const alle = Object.fromEntries(m.DOKUMENTE_V6.map((d) => [d.documentKey, ok(d)]))
|
||||||
|
assert.deepEqual(m.pruefeHochgeladen(alle), { fehlt: [] })
|
||||||
|
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'avv-website-analyse': [] }).fehlt, ['avv-website-analyse'])
|
||||||
|
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'abo-bedingungen': [{ ...alle['abo-bedingungen'][0], source: 'kundenbereich@7ae0ffd:public/abo-bedingungen.html' }] }).fehlt, ['abo-bedingungen'], 'Fassung 5 zählt nicht')
|
||||||
|
assert.deepEqual(m.pruefeHochgeladen({ ...alle, 'leistungsbeschreibung-news': [{ ...alle['leistungsbeschreibung-news'][0], status: 'superseded' }] }).fehlt, ['leistungsbeschreibung-news'])
|
||||||
|
assert.equal(m.pruefeHochgeladen({}).fehlt.length, 11)
|
||||||
|
const b = backend([]) // Versionen leer: alle elf fehlen
|
||||||
|
assert.equal(await m.main(['--pruefen'], b.deps), 1)
|
||||||
|
assert.equal(b.log.filter((z) => z.startsWith('FEHLT ')).length, 11); assert.ok(NUR_LESEN(b))
|
||||||
|
})
|
||||||
Reference in new issue
Block a user