Gedankenstriche durch Bindestriche ersetzt
Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s

375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die
README; an keiner Stelle Bedeutung, nur Zeichensetzung.
This commit is contained in:
Harlekin
2026-09-04 08:54:52 +02:00
parent 38be773636
commit e0b03ea336
210 changed files with 368 additions and 368 deletions
+1 -1
View File
@@ -173,7 +173,7 @@ suite('Abteilungen', () => {
expect(liste.map((a) => a.id)).toContain(offenId)
expect(liste.map((a) => a.id)).not.toContain(geheimId)
// Und die Zahl im Diagramm stimmt mit der Liste überein — ein Zähler, der
// Und die Zahl im Diagramm stimmt mit der Liste überein - ein Zähler, der
// mehr zählt als die Liste zeigt, wäre selbst eine Auskunft.
const verteilung = await caseTypesOfDepartment(niedrig, abteilungId)
const summe = verteilung.reduce((s, stueck) => s + stueck.value, 0)
+1 -1
View File
@@ -92,7 +92,7 @@ suite('Vorlage des Aktenblatts', () => {
const imRaster = rows.flat().map((f) => f.key)
// Sie stehen unter der jeweiligen Unterschrift, nicht in einer eigenen
// Zelle — bearbeitbar bleiben sie trotzdem.
// Zelle - bearbeitbar bleiben sie trotzdem.
expect(imRaster).not.toContain('personalarbeiter_datum')
expect(imRaster).not.toContain('standortleiter_datum')
expect(fields.map((f) => f.key)).toContain('personalarbeiter_datum')
+2 -2
View File
@@ -9,7 +9,7 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
* Ein Anhang hat keine eigene Freigabestufe, er erbt die seines Besitzers.
* Diese Tests prüfen beide Seiten davon: dass die Stufe wirklich durchschlägt,
* und dass eine Datei, die die Prüfkette nicht bestanden hat, gar nicht erst
* ausgeliefert wird — auch nicht an einen Administrator.
* ausgeliefert wird - auch nicht an einen Administrator.
*/
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
@@ -130,7 +130,7 @@ suite('Sichtbarkeit von Anhängen', () => {
})
it('verbirgt den Anhang eines Eintrags über der Stufe des Lesers', async () => {
// Die Akte darf er sehen, den Eintrag nicht — und damit auch dessen Datei
// Die Akte darf er sehen, den Eintrag nicht - und damit auch dessen Datei
// nicht. Sonst verriete die Anhangsliste den Inhalt des Eintrags.
expect(await sichtbar(perms(nutzer.stufe1, 1))).not.toContain(anhang.imEintrag)
expect(await sichtbar(perms(nutzer.stufe4, 4))).toContain(anhang.imEintrag)
+1 -1
View File
@@ -109,7 +109,7 @@ suite('Benachrichtigungen', () => {
// Stufe 4 bekommt die Meldung.
expect(ziele).toContain(nutzer.stufe4)
// Stufe 1 nicht — die Akte gibt es für ihn nicht.
// Stufe 1 nicht - die Akte gibt es für ihn nicht.
expect(ziele).not.toContain(nutzer.stufe1)
// Wer abgeschaltet hat, bekommt nichts.
expect(ziele).not.toContain(nutzer.aus)
+2 -2
View File
@@ -9,7 +9,7 @@ import { caseText } from '@/lib/i18n/cases'
/**
* Die Zerlegung der Beteiligtenzeilen aus dem Formular „Neue Fallakte".
*
* Sie entscheidet, wer mit welcher Rolle in eine Akte kommt — und seit die
* Sie entscheidet, wer mit welcher Rolle in eine Akte kommt - und seit die
* Rolle über den Zugriff entscheidet (nur Arbeitsrollen öffnen die Akte, siehe
* lib/cases/terms.ts), darf eine erfundene Rolle hier nicht durchrutschen.
*/
@@ -55,7 +55,7 @@ describe('Beteiligte beim Anlegen', () => {
describe('Zustände einer Fallakte', () => {
it('hat für jeden Zustand einen Namen und eine Erklärung', () => {
// Der Filter zeigte fünf von sieben Zuständen — ausgerechnet „Entwurf" und
// Der Filter zeigte fünf von sieben Zuständen - ausgerechnet „Entwurf" und
// „Versiegelt" fehlten, also die beiden häufigsten in der Liste. Diese
// Prüfung fällt auf, wenn wieder einer vergessen wird.
for (const status of CASE_STATUSES) {
+3 -3
View File
@@ -10,7 +10,7 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Der Chat erbt die Sichtbarkeit seines Ortes. Der heikelste Teil ist die
* Vorschaukarte: ein Aktenzeichen im Text darf einem zu niedrig eingestuften
* Leser nichts über die Akte verraten — auch nicht, ob es sie gibt.
* Leser nichts über die Akte verraten - auch nicht, ob es sie gibt.
*/
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
const suite = url ? describe : describe.skip
@@ -149,7 +149,7 @@ suite('Aktenchat', () => {
'AfiS-1999-0001',
])
// Sichtbar: mit Titel. Verborgen: ohne alles — und genauso wie eine Akte,
// Sichtbar: mit Titel. Verborgen: ohne alles - und genauso wie eine Akte,
// die es gar nicht gibt.
expect(fuerStufe1.get(offenNummer)).toMatchObject({ visible: true, title: 'Offener Vorgang' })
expect(fuerStufe1.get(geheimNummer)).toEqual({ caseNumber: geheimNummer, visible: false })
@@ -181,7 +181,7 @@ suite('Aktenchat', () => {
await sendMessage(perms(nutzer.stufe4, 4), {
id,
scope: { kind: 'case', caseNumber: offenNummer },
// Der zweite Name gibt es nicht — er darf nicht in mentions landen.
// Der zweite Name gibt es nicht - er darf nicht in mentions landen.
body: `Bitte schauen @${nutzer.stufe1} und @niemand`,
})
+3 -3
View File
@@ -8,7 +8,7 @@ import { deliveryFileName } from '@/lib/files/delivery'
* Die Typprüfung ist die Stelle, an der eine feindselige Datei auffallen muss.
*
* Geprüft wird deshalb nicht nur, dass die erlaubten Formate durchkommen,
* sondern vor allem, dass die verbotenen es **nicht** tun — und zwar auch
* sondern vor allem, dass die verbotenen es **nicht** tun - und zwar auch
* dann, wenn sie sich als erlaubte ausgeben.
*/
@@ -46,7 +46,7 @@ describe('Erkennung erlaubter Typen', () => {
describe('Abwehr', () => {
it('lehnt SVG ab, auch wenn es wie Text aussieht', () => {
// Der eigentliche Grund für die ganze Kette: SVG führt im Browser Skripte
// aus. Es ist gültiges UTF-8 und käme sonst als text/plain durch — das ist
// aus. Es ist gültiges UTF-8 und käme sonst als text/plain durch - das ist
// in Ordnung, denn als Text wird es nie gerendert, sondern ausgeliefert
// mit Content-Disposition: attachment.
const svg = text('<svg xmlns="http://www.w3.org/2000/svg"><script>alert(1)</script></svg>')
@@ -129,7 +129,7 @@ describe('Ablagepfade', () => {
it('lässt keinen Pfad aus dem Dateiverzeichnis heraus', () => {
process.env.FILES_DIR ??= '/tmp/afis-test-files'
// Kommt aus der Datenbank, nicht aus einer Eingabe — aber ein
// Kommt aus der Datenbank, nicht aus einer Eingabe - aber ein
// Verzeichniswechsel wäre der teuerste Fehler an dieser Stelle.
expect(() => absolutePath('../../etc/passwd')).toThrow()
expect(() => absolutePath('store/../../../etc/passwd')).toThrow()
+1 -1
View File
@@ -50,7 +50,7 @@ describe('Design-Tokens', () => {
// Die Regel, aus der die Zahlen folgen: eine Beschriftung, die naeher am
// Rahmen steht als der Inhalt des Rahmens an demselben Rahmen, wirkt
// angeklebt. Also muss der Abstand nach aussen groesser sein als die
// Polsterung nach innen. Geprueft wird die Regel, nicht der Wert — wer die
// Polsterung nach innen. Geprueft wird die Regel, nicht der Wert - wer die
// Polsterung aendert, faellt hier auf.
const feld = css.slice(css.indexOf('.field {'), css.indexOf('.field:hover'))
const polsterung = /padding:\s*([\d.]+)rem\s+([\d.]+)rem/.exec(feld)
+1 -1
View File
@@ -147,7 +147,7 @@ suite('Sichtbarkeit von Dokumenten', () => {
// Das Dokument ist zu hoch eingestuft, obwohl die Akte offen liegt.
expect(treffer).not.toContain(dok.hochImOffenenFall)
// Das Dokument wäre lesbar, aber die Akte darüber nicht — die
// Das Dokument wäre lesbar, aber die Akte darüber nicht - die
// Dokumentenliste verriete sonst ihre Existenz.
expect(treffer).not.toContain(dok.imGeheimenFall)
expect(treffer).not.toContain(dok.anVerborgenerPerson)
+3 -3
View File
@@ -9,7 +9,7 @@ import { splitBody } from '@/lib/chat/mentions'
* Was passiert, wenn jemand versucht, über ein Textfeld Code einzuschleusen.
*
* Die Grundlage ist, dass React alles maskiert, was es als Text rendert, und
* dass es in dieser Anwendung genau **ein** `dangerouslySetInnerHTML` gibt —
* dass es in dieser Anwendung genau **ein** `dangerouslySetInnerHTML` gibt -
* mit einer Konstanten aus demselben Modul. Diese Datei prüft die Ränder darum
* herum: die Stellen, an denen Text nicht als Text ankommt, sondern als
* Vorlage, als Stilangabe oder als Kopfzeile.
@@ -50,7 +50,7 @@ describe('Vorlagen nehmen nur an, was sie kennen', () => {
}
})
it('lässt bösartigen Text als Beschriftung durch — als Text', () => {
it('lässt bösartigen Text als Beschriftung durch - als Text', () => {
// Das ist Absicht: eine Beschriftung darf jedes Zeichen enthalten, weil sie
// als Text gerendert wird und nie als HTML. Verboten ist die Struktur,
// nicht das Zeichen.
@@ -68,7 +68,7 @@ describe('Platzhalter bleiben Text', () => {
const text = fillPlaceholders('Hallo {{name}}', {
name: '<script>alert(1)</script>',
})
// Unverändert übernommen — und React macht daraus beim Rendern Text.
// Unverändert übernommen - und React macht daraus beim Rendern Text.
expect(text).toBe('Hallo <script>alert(1)</script>')
})
+1 -1
View File
@@ -116,7 +116,7 @@ suite('Nachträgliche Änderungen am Verlauf', () => {
const { AccessError } = await import('@/lib/cases/service')
const id = await neuerEintrag('Alter Eintrag')
// Karenzzeit serverseitig auf null setzen — nicht über den Request.
// Karenzzeit serverseitig auf null setzen - nicht über den Request.
await db.appSetting.update({ where: { id: 1 }, data: { entryEditGraceMinutes: 0 } })
await expect(
+1 -1
View File
@@ -10,7 +10,7 @@ import { invalidateUser, resolvePermissions } from '@/lib/auth/effective'
*
* Vier Wege führen dorthin: Herabstufung, Entzug der AfiS-Rolle, Rauswurf und
* selbst gehen. Der Worker deckt die Discord-Seite ab (siehe
* worker/tests/mitgliedschaft.test.ts) — hier steht die andere Hälfte: dass
* worker/tests/mitgliedschaft.test.ts) - hier steht die andere Hälfte: dass
* die Web-App den Entzug auch bemerkt.
*
* Der Kern ist, dass die **Zeile in der Datenbank** der Sitzungsnachweis ist
+1 -1
View File
@@ -97,7 +97,7 @@ describe('Zeichen für den Druckabruf', () => {
expect(claim.expiresAt).toBeGreaterThan(Date.now())
// Ein Zeichen mit vergangener Frist wird nicht angenommen, auch mit
// gültiger Unterschrift nicht — die entsteht hier gar nicht erst.
// gültiger Unterschrift nicht - die entsteht hier gar nicht erst.
expect(verifyRenderToken(`${nutzlast}.zuwenig`)).toBeNull()
})
})
+1 -1
View File
@@ -8,7 +8,7 @@ import { detectType } from '@/lib/files/magic'
* Die Neukodierung ist die wirksamste Massnahme der Kette: ein Bild, das durch
* `sharp` gelaufen ist, besteht nur noch aus Pixeln.
*
* Diese Tests prüfen genau das — nicht, dass irgendetwas herauskommt, sondern
* Diese Tests prüfen genau das - nicht, dass irgendetwas herauskommt, sondern
* dass das, was nicht Inhalt ist, **weg** ist.
*/
+3 -3
View File
@@ -14,7 +14,7 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
* Rückverweise sind der häufigste Ort, an dem ein Aktensystem leckt.
*
* Eine Personalakte der Stufe 1, auf der „3 Fälle" steht, verrät die Existenz
* von Ermittlungen, die der Leser nicht sehen darf — auch dann, wenn kein
* von Ermittlungen, die der Leser nicht sehen darf - auch dann, wenn kein
* Titel danebensteht. Deshalb prüft dieser Test nicht nur die Liste, sondern
* ausdrücklich auch die **Zahl**. Siehe docs/05-akten-und-vorlagen.md.
*/
@@ -148,7 +148,7 @@ suite('Personalakten und Rückverweise', () => {
const rollen = byRole.map((gruppe) => gruppe.role).sort()
expect(rollen).toEqual(['SUBJECT', 'WITNESS'])
// Drei Einträge: einer über die Personalakte im offenen Fall, zwei im
// geheimen — über die Personalakte und über das Discord-Konto.
// geheimen - über die Personalakte und über das Discord-Konto.
expect(total).toBe(3)
})
@@ -208,7 +208,7 @@ suite('Personalakten und Rückverweise', () => {
role: 'INVESTIGATOR',
})
// Danach schon — das ist der ausnahmsweise Zugang, wie bei der Fallakte.
// Danach schon - das ist der ausnahmsweise Zugang, wie bei der Fallakte.
expect(
await db.personnelFile.findMany({
where: { AND: [visiblePersonnelWhere(p), { id: hoheAkteId }] },
+2 -2
View File
@@ -6,7 +6,7 @@ import { queryAuditLog, AUDIT_PAGE } from '@/lib/audit/query'
/**
* Das Protokoll ist die Gegenprobe zum ganzen System: hier soll stehen, was
* passiert ist, vollständig und unverändert. Geprüft wird deshalb zweierlei —
* passiert ist, vollständig und unverändert. Geprüft wird deshalb zweierlei -
* dass die Filter genau schneiden, und dass eine Zeile nicht verschwinden
* kann, wenn ihr Ziel verschwindet.
*/
@@ -95,7 +95,7 @@ suite('Zugriffsprotokoll', () => {
expect(zurAkte?.targetHref).toBe(`/faelle/${fallNummer}`)
// Ein Auftrag ist kein Datensatz, den das Protokoll auflösen kann. Die
// Zeile bleibt trotzdem stehen — ein Protokoll mit Lücken wäre keins.
// Zeile bleibt trotzdem stehen - ein Protokoll mit Lücken wäre keins.
const zumAuftrag = rows.find((zeile) => zeile.targetType === 'ExportJob')
expect(zumAuftrag).toBeDefined()
expect(zumAuftrag?.targetLabel).toBeNull()
+1 -1
View File
@@ -5,7 +5,7 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Die Schreibgrenzen aus docs/03-auth-und-rechte.md, geprüft gegen die
* Fachlogik selbst — nicht gegen die Oberfläche. Eine Server Action ist ein
* Fachlogik selbst - nicht gegen die Oberfläche. Eine Server Action ist ein
* Endpunkt: was hier durchgeht, geht auch durch, wenn jemand das Formular
* umgeht.
*/
+1 -1
View File
@@ -6,7 +6,7 @@ import { stripRedacted } from '@/lib/redaction'
/**
* Die Schwärzung wird an zwei Stellen entfernt: in TypeScript beim Rendern und
* in SQL, bevor der Suchvektor gefüllt wird. Laufen die beiden auseinander,
* findet die Suche einen Begriff, den die Ansicht verbirgt — und der Treffer
* findet die Suche einen Begriff, den die Ansicht verbirgt - und der Treffer
* allein verrät ihn dann.
*/
const url = process.env.DIRECT_DATABASE_URL ?? process.env.DATABASE_URL
+2 -2
View File
@@ -125,7 +125,7 @@ suite('Volltextsuche', () => {
})
it('findet einen höher eingestuften Eintrag nicht, obwohl die Akte sichtbar ist', async () => {
// Der klassische Leck: die Akte darf er sehen, den Eintrag nicht — und
// Der klassische Leck: die Akte darf er sehen, den Eintrag nicht - und
// ein Treffer würde verraten, dass darin etwas über "Nachtblende" steht.
const fuerStufe1 = await searchCases(perms(nutzer.stufe1, 1), filter(wort.gestuft))
expect(fuerStufe1.hits).toHaveLength(0)
@@ -267,7 +267,7 @@ suite('Volltextsuche', () => {
})
it('macht über einen Beteiligten keine zu hoch eingestufte Akte auffindbar', async () => {
// Der Name eines Beteiligten steht in jeder Akte, die man lesen darf —
// Der Name eines Beteiligten steht in jeder Akte, die man lesen darf -
// aber die Akte selbst muss man lesen dürfen. Sonst wäre die Suche nach
// einem Namen ein Weg, die Existenz einer Akte zu erraten.
const name = `Verdeckte Person ${Date.now().toString(36)}`
+2 -2
View File
@@ -12,7 +12,7 @@ import { join } from 'node:path'
* 2. **`suppressHydrationWarning`.** Der Browser leert das Nonce-Attribut
* nach dem Parsen, React vergleicht es beim Hydrieren trotzdem und meldet
* einen Unterschied, den es nicht gibt. Die Meldung steht nur in der
* Konsole des Entwicklungsservers — im Produktionsbau meldet React sie
* Konsole des Entwicklungsservers - im Produktionsbau meldet React sie
* nicht, ein e2e-Lauf kann sie also nicht auffangen. Deshalb hier auf der
* Ebene der Quelle.
* 3. **Der Schlüssel im Speicher**, derselbe wie im Umschalter. Weichen sie
@@ -31,7 +31,7 @@ describe('Thema vor dem ersten Malen', () => {
it('benutzt denselben Speicherschlüssel wie der Umschalter', () => {
const toggle = readFileSync(join(process.cwd(), 'components/ThemeToggle.tsx'), 'utf8')
// Der Schlüssel wird importiert, nicht abgeschrieben — genau das prüft das
// Der Schlüssel wird importiert, nicht abgeschrieben - genau das prüft das
// hier: eine zweite, abgeschriebene Zeichenkette wäre der Fehler.
expect(toggle).toMatch(/export const THEME_KEY = '([^']+)'/)
expect(layout).toContain("import { THEME_KEY } from '@/components/ThemeToggle'")
+2 -2
View File
@@ -8,7 +8,7 @@ const css = readFileSync(join(process.cwd(), 'app/globals.css'), 'utf8')
* Das helle Thema ist verpflichtend, siehe docs/10-design-system.md. Diese
* Tests halten die zwei Eigenschaften fest, die es leicht verliert: dass es
* wirklich jede semantische Farbe tauscht, und dass eine Freigabestufe in
* beiden Themen dieselbe Farbe behält — sonst lernt man sie zweimal.
* beiden Themen dieselbe Farbe behält - sonst lernt man sie zweimal.
*/
describe('Helles Thema', () => {
const semantisch = [
@@ -43,7 +43,7 @@ describe('Helles Thema', () => {
it('behält die Klassifizierungsfarben als eigenes System', () => {
// Die Farbtöne dürfen für hellen Grund nachgedunkelt werden, aber es muss
// sie geben — sonst verschwindet der Stempel auf hellem Papier.
// sie geben - sonst verschwindet der Stempel auf hellem Papier.
const hell = css.slice(css.indexOf("[data-theme='light']"))
for (const stufe of [0, 1, 2, 3, 4, 5]) {
expect(hell).toContain(`--color-stamp-${stufe}`)
+1 -1
View File
@@ -75,7 +75,7 @@ suite('Übersicht und Beobachtungsliste', () => {
geheimId = geheim.id
geheimNummer = geheim.caseNumber
// Der Stufe-1-Nutzer ist an beiden Akten beteiligt — an der geheimen aber
// Der Stufe-1-Nutzer ist an beiden Akten beteiligt - an der geheimen aber
// nicht über sein Konto, sondern gar nicht. Er darf sie nicht sehen.
await db.caseParticipant.create({
data: { caseId: offen.id, userId: nutzer.stufe1, role: 'INVESTIGATOR' },
+1 -1
View File
@@ -5,7 +5,7 @@ import { uuidv7 } from '@/lib/uuidv7'
* Auf der Sortierbarkeit dieser Kennungen beruhen zwei Dinge: die
* Nachlieferung verpasster Nachrichten (`id > seit`) und die Reihenfolge im
* Chat. Bricht sie, kommen Nachrichten durcheinander an oder gehen bei einem
* Wiederverbinden verloren — beides fällt spät und unangenehm auf.
* Wiederverbinden verloren - beides fällt spät und unangenehm auf.
*/
describe('UUIDv7', () => {
it('hat die richtige Form, Version und Variante', () => {
+1 -1
View File
@@ -8,7 +8,7 @@ import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Verknüpfungen zeigen in beide Richtungen. Eine Akte, die der Leser nicht
* sehen darf, darf auch von der Gegenseite aus nicht auftauchen — sonst
* sehen darf, darf auch von der Gegenseite aus nicht auftauchen - sonst
* verrät die verknüpfte Akte ihre Existenz.
*/
loadEnvFromRepoRoot()
+1 -1
View File
@@ -190,7 +190,7 @@ suite('Sichtbarkeit von Akten', () => {
it('öffnet die Akte nicht für Zeugen, Betroffene und Täter', async () => {
// Die Beteiligtenliste beantwortet zwei Fragen: wer bearbeitet den Fall,
// und um wen geht es darin. Nur die erste verschafft Zugang — sonst läse
// und um wen geht es darin. Nur die erste verschafft Zugang - sonst läse
// der Beschuldigte die Ermittlung gegen sich selbst mit.
const vorlage = await db.documentTemplate.findFirstOrThrow({
where: { key: { startsWith: marke } },
+2 -2
View File
@@ -5,7 +5,7 @@ import { clearRecent, listRecent, noteOpened, RECENT_MAX } from '@/lib/recent/se
import type { EffectivePermissions, Permission } from '@/lib/auth/permissions'
/**
* Die Sprungliste ist eine Bequemlichkeit — und genau deshalb der Ort, an dem
* Die Sprungliste ist eine Bequemlichkeit - und genau deshalb der Ort, an dem
* eine Abkürzung besonders verlockend wäre: die Kennungen liegen ja schon da,
* warum sie nochmal prüfen? Weil eine vorgemerkte Kennung keine Berechtigung
* ist. Diese Datei prüft vor allem, dass die Liste nichts durchlässt.
@@ -127,7 +127,7 @@ suite('Zuletzt geöffnet', () => {
it('zeigt niemandem die Liste eines anderen', async () => {
await noteOpened(nutzer.b, 'CASE', offenId)
// Nutzer a hat die Akte auch geöffnet — aber b bekommt seine eigene Zeile,
// Nutzer a hat die Akte auch geöffnet - aber b bekommt seine eigene Zeile,
// und ein Administrator bekommt seine eigene, nicht die von b.
const admin = await listRecent(perms(`${marke}-admin`, 5, { isAdmin: true }))
expect(admin).toHaveLength(0)