package dossier_test import ( "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "math/big" "testing" "time" "github.com/digitorus/timestamp" "github.com/netcell-it/deklarix/internal/dossier" "github.com/netcell-it/deklarix/internal/evidence" "github.com/netcell-it/deklarix/internal/rules" ) // fakeTimestampToken erzeugt einen strukturell gültigen, selbstsignierten // RFC-3161-Token für hash — offline, ohne echte TSA. Damit lassen sich // BuildContent/Render testen, ohne bei jedem Testlauf eine echte // Time-Stamp Authority anzufragen (die Echtheit/Vertrauenswürdigkeit // des Zertifikats spielt für diese Tests keine Rolle, nur dass der Token // strukturell parsbar ist wie ein echter). func fakeTimestampToken(t *testing.T, hash []byte, at time.Time) []byte { t.Helper() key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { t.Fatalf("generate key: %v", err) } certTemplate := &x509.Certificate{ SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "deklarix-test-tsa"}, NotBefore: at.Add(-time.Hour), NotAfter: at.Add(time.Hour), KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageTimeStamping}, } certDER, err := x509.CreateCertificate(rand.Reader, certTemplate, certTemplate, &key.PublicKey, key) if err != nil { t.Fatalf("create certificate: %v", err) } cert, err := x509.ParseCertificate(certDER) if err != nil { t.Fatalf("parse certificate: %v", err) } ts := timestamp.Timestamp{ HashAlgorithm: crypto.SHA256, HashedMessage: hash, Time: at, SerialNumber: big.NewInt(1), Policy: asn1.ObjectIdentifier{1, 2, 3}, } respDER, err := ts.CreateResponse(cert, key) if err != nil { t.Fatalf("create timestamp response: %v", err) } parsed, err := timestamp.ParseResponse(respDER) if err != nil { t.Fatalf("parse fake timestamp response: %v", err) } return parsed.RawToken } func validData(t *testing.T) dossier.Data { t.Helper() hash := evidence.HashBytes([]byte("caption+bild")) at := time.Date(2026, 8, 27, 12, 0, 0, 0, time.UTC) return dossier.Data{ Submission: dossier.Submission{ Platform: "instagram", PostType: "reel", Caption: "Werbung für ein Produkt äöüß", CreatedAt: at, }, Facts: rules.Facts{ DisclosurePresent: true, DisclosureWording: "Werbung", DisclosureBeforeCut: false, }, Findings: []rules.Finding{ {RuleID: "WK-004", RuleVersion: 1, Severity: rules.SeverityHigh, Title: "t", Fix: "f", Sources: []string{"§ 5a Abs. 4 UWG"}}, }, Participants: []dossier.Participant{ {Role: "creator", Name: "Max Mustermann", Vorgegeben: false, Freigegeben: true}, }, AssetHash: hash, MetadataHash: evidence.HashBytes([]byte("metadata")), TimestampToken: fakeTimestampToken(t, hash, at), GeneratedAt: at, } } func TestBuildContentRejectsMissingPlatform(t *testing.T) { data := validData(t) data.Submission.Platform = "" if _, err := dossier.BuildContent(data); err == nil { t.Fatal("expected error for missing platform, got nil") } } func TestBuildContentRejectsMissingTimestampToken(t *testing.T) { data := validData(t) data.TimestampToken = nil if _, err := dossier.BuildContent(data); err == nil { t.Fatal("expected error for missing timestamp token, got nil") } } func TestBuildContentRejectsMissingMetadataHash(t *testing.T) { data := validData(t) data.MetadataHash = nil if _, err := dossier.BuildContent(data); err == nil { t.Fatal("expected error for missing metadata hash, got nil") } } func TestBuildContentAllowsMissingAssetHash(t *testing.T) { // Reine Caption-Pruefungen ohne hochgeladenes Bild/Video haben kein // Asset zum Hashen — das ist kein Fehlerfall. data := validData(t) data.AssetHash = nil content, err := dossier.BuildContent(data) if err != nil { t.Fatalf("BuildContent: %v", err) } if content.AssetHashHex != "" { t.Fatalf("AssetHashHex = %q, want empty when no asset was provided", content.AssetHashHex) } } func TestBuildContentSuccess(t *testing.T) { data := validData(t) content, err := dossier.BuildContent(data) if err != nil { t.Fatalf("BuildContent: %v", err) } if content.Platform != "instagram" { t.Errorf("Platform = %q, want instagram", content.Platform) } if len(content.Findings) != 1 || content.Findings[0].RuleID != "WK-004" { t.Errorf("Findings = %+v, want one WK-004 row", content.Findings) } if len(content.Participants) != 1 || content.Participants[0].Name != "Max Mustermann" { t.Errorf("Participants = %+v, want one Max-Mustermann row", content.Participants) } if len(content.AssetHashHex) != 64 { t.Errorf("AssetHashHex length = %d, want 64 (hex of 32-byte SHA-256)", len(content.AssetHashHex)) } if !content.TimestampedAt.Equal(data.Submission.CreatedAt) { t.Errorf("TimestampedAt = %v, want %v", content.TimestampedAt, data.Submission.CreatedAt) } if content.Disclaimer == "" { t.Error("expected a non-empty disclaimer") } }