syncova-backup/packages/platform/logging/logging_test.go
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

121 lines
4.3 KiB
Go

package logging
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
)
// decodeSingleLogEntry parst genau eine JSON-Logzeile aus dem Puffer.
func decodeSingleLogEntry(testInstance *testing.T, logBuffer *bytes.Buffer) map[string]any {
testInstance.Helper()
logLine := strings.TrimSpace(logBuffer.String())
if logLine == "" {
testInstance.Fatal("es wurde kein Logeintrag geschrieben")
}
var decodedEntry map[string]any
if decodeError := json.Unmarshal([]byte(logLine), &decodedEntry); decodeError != nil {
testInstance.Fatalf("Logeintrag ist kein gültiges JSON (%v): %s", decodeError, logLine)
}
return decodedEntry
}
func TestNewWritesServiceAndLevel(testInstance *testing.T) {
var logBuffer bytes.Buffer
testLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
testLogger.Info("dienst gestartet")
decodedEntry := decodeSingleLogEntry(testInstance, &logBuffer)
if decodedEntry[FieldService] != "syncova-api" {
testInstance.Errorf("Feld %q soll den Dienstnamen tragen, war %v", FieldService, decodedEntry[FieldService])
}
if decodedEntry["level"] != "INFO" {
testInstance.Errorf("Level soll INFO sein, war %v", decodedEntry["level"])
}
}
func TestSecretAttributesAreRedacted(testInstance *testing.T) {
var logBuffer bytes.Buffer
testLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
const secretValue = "streng-geheim"
// Alle vier Schlüssel benennen Geheimnisse in unterschiedlicher Schreibweise.
testLogger.Info("verbindungsaufbau",
"db_password", secretValue,
"agentToken", secretValue,
"Authorization", secretValue,
"connection_string", secretValue,
)
logOutput := logBuffer.String()
if strings.Contains(logOutput, secretValue) {
testInstance.Fatalf("das Log enthält ein Geheimnis im Klartext: %s", logOutput)
}
decodedEntry := decodeSingleLogEntry(testInstance, &logBuffer)
for _, secretFieldName := range []string{"db_password", "agentToken", "Authorization", "connection_string"} {
if decodedEntry[secretFieldName] != redactedPlaceholder {
testInstance.Errorf("Feld %q soll redigiert sein, war %v", secretFieldName, decodedEntry[secretFieldName])
}
}
}
func TestHarmlessFieldsAreNotRedacted(testInstance *testing.T) {
var logBuffer bytes.Buffer
testLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
// Eine zu aggressive Redaction würde die Logs unbrauchbar machen.
testLogger.Info("backup abgeschlossen", "repository_name", "Repository-01", "bytes_written", 4096)
decodedEntry := decodeSingleLogEntry(testInstance, &logBuffer)
if decodedEntry["repository_name"] != "Repository-01" {
testInstance.Errorf("harmloses Feld wurde verändert: %v", decodedEntry["repository_name"])
}
}
func TestWithContextAddsCorrelationID(testInstance *testing.T) {
var logBuffer bytes.Buffer
baseLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
const expectedCorrelationID = "5f8f7a1e-0d3a-4c2b-9a41-3a2b1c0d9e8f"
contextWithID := ContextWithCorrelationID(context.Background(), expectedCorrelationID)
WithContext(contextWithID, baseLogger).Info("request bearbeitet")
decodedEntry := decodeSingleLogEntry(testInstance, &logBuffer)
if decodedEntry[FieldCorrelationID] != expectedCorrelationID {
testInstance.Errorf("Correlation ID fehlt oder ist falsch: %v", decodedEntry[FieldCorrelationID])
}
}
func TestWithContextWithoutCorrelationIDLeavesLoggerUnchanged(testInstance *testing.T) {
var logBuffer bytes.Buffer
baseLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
WithContext(context.Background(), baseLogger).Info("hintergrundaufgabe")
decodedEntry := decodeSingleLogEntry(testInstance, &logBuffer)
if _, isPresent := decodedEntry[FieldCorrelationID]; isPresent {
testInstance.Error("ohne Correlation ID im Context darf das Feld nicht gesetzt werden")
}
}
func TestDebugIsSuppressedAtInfoLevel(testInstance *testing.T) {
var logBuffer bytes.Buffer
testLogger := New(&logBuffer, Options{ServiceName: "syncova-api", Level: "info", Format: "json"})
testLogger.Debug("interner zwischenschritt")
if logBuffer.Len() != 0 {
testInstance.Fatalf("Debug-Einträge dürfen bei Level info nicht erscheinen: %s", logBuffer.String())
}
}