Content assembly (BuildContent) is separate from PDF drawing (Render), so the actual business logic — what goes into the evidence dossier, in what form, with which mandatory fields — is unit-testable without parsing PDF bytes. BuildContent refuses to produce a dossier missing its evidentiary fields (timestamp token, asset/metadata hash, platform) rather than emitting one with silently empty proof sections. Every dossier carries the "this is not legal advice" disclaimer required by CLAUDE.md's guardrails. Uses github.com/go-pdf/fpdf (actively maintained fork of jung-kurt/ gofpdf, no dependencies beyond the Go stdlib) for rendering. Its core fonts use cp1252 internally, so a small cp1252.map (copied from the fpdf module, embedded via go:embed) drives UnicodeTranslator — German umlauts render correctly without needing an external font file at runtime, keeping Deklarix a single binary. Verified visually with pdftotext/pdfinfo against a generated sample. Tests build a real, structurally valid RFC-3161 token offline (a throwaway self-signed cert + timestamp.Timestamp.CreateResponse), so BuildContent/Render/Generate are fully tested without hitting a real TSA — unlike the network-gated integration test in internal/evidence. Also adds evidence.TimestampTime(), extracted from the parsing logic already used by the TSA client, since the dossier needs to show the timestamped time to a human reader. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package evidence_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
"github.com/digitorus/timestamp"
|
|
|
|
"github.com/netcell-it/deklarix/internal/evidence"
|
|
)
|
|
|
|
func TestTimestampRejectsWrongHashSize(t *testing.T) {
|
|
called := false
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
called = true
|
|
}))
|
|
defer server.Close()
|
|
|
|
ts := evidence.NewHTTPTimestamper(server.URL)
|
|
_, err := ts.Timestamp(context.Background(), []byte("zu kurz"))
|
|
if err == nil {
|
|
t.Fatal("expected error for a non-SHA-256-sized hash, got nil")
|
|
}
|
|
if called {
|
|
t.Fatal("expected no HTTP call for an invalid hash size")
|
|
}
|
|
}
|
|
|
|
// TestTimestampSendsValidRequest prüft serverseitig, dass Timestamp()
|
|
// einen tatsächlich gültigen, RFC-3161-konformen Request schickt (parsbar,
|
|
// korrekter Hash, korrekter Algorithmus, Nonce gesetzt) — ohne dafür eine
|
|
// echte signierte Antwort fälschen zu müssen.
|
|
func TestTimestampSendsValidRequest(t *testing.T) {
|
|
hash := evidence.HashBytes([]byte("test-content"))
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if ct := r.Header.Get("Content-Type"); ct != "application/timestamp-query" {
|
|
t.Errorf("Content-Type = %q, want application/timestamp-query", ct)
|
|
}
|
|
body, err := io.ReadAll(r.Body)
|
|
if err != nil {
|
|
t.Errorf("read request body: %v", err)
|
|
}
|
|
|
|
req, err := timestamp.ParseRequest(body)
|
|
if err != nil {
|
|
t.Errorf("ParseRequest: %v", err)
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
if !bytes.Equal(req.HashedMessage, hash) {
|
|
t.Errorf("HashedMessage = %x, want %x", req.HashedMessage, hash)
|
|
}
|
|
if req.Nonce == nil {
|
|
t.Error("expected a nonce to be set on the request")
|
|
}
|
|
|
|
// Keine echte TSA hier — nur die Request-Validierung interessiert
|
|
// dieser Test. Ein absichtlich ungültiger Response-Body lässt
|
|
// Timestamp() mit einem Parse-Fehler zurückkommen, was für diesen
|
|
// Test in Ordnung ist.
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("not a valid timestamp response"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
ts := evidence.NewHTTPTimestamper(server.URL)
|
|
_, err := ts.Timestamp(context.Background(), hash)
|
|
if err == nil {
|
|
t.Fatal("expected a parse error for the fake response, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTimestampRejectsNon200(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
w.Write([]byte("tsa down"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
ts := evidence.NewHTTPTimestamper(server.URL)
|
|
hash := evidence.HashBytes([]byte("x"))
|
|
if _, err := ts.Timestamp(context.Background(), hash); err == nil {
|
|
t.Fatal("expected error for non-200 TSA status, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTimestampRejectsMalformedResponse(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Write([]byte("garbage, not ASN.1 DER"))
|
|
}))
|
|
defer server.Close()
|
|
|
|
ts := evidence.NewHTTPTimestamper(server.URL)
|
|
hash := evidence.HashBytes([]byte("x"))
|
|
if _, err := ts.Timestamp(context.Background(), hash); err == nil {
|
|
t.Fatal("expected error for malformed TSA response, got nil")
|
|
}
|
|
}
|
|
|
|
// TestTimestampIntegration ruft die echte Standard-TSA (FreeTSA.org) auf.
|
|
// Läuft nur, wenn DEKLARIX_TSA_INTEGRATION gesetzt ist — kein Netzwerkzugriff
|
|
// in normalen Testläufen (siehe internal/store für dasselbe Muster mit
|
|
// DATABASE_URL).
|
|
func TestTimestampIntegration(t *testing.T) {
|
|
if os.Getenv("DEKLARIX_TSA_INTEGRATION") == "" {
|
|
t.Skip("DEKLARIX_TSA_INTEGRATION nicht gesetzt, überspringe echten TSA-Aufruf")
|
|
}
|
|
|
|
hash := evidence.HashBytes([]byte(t.Name()))
|
|
ts := evidence.NewHTTPTimestamper("")
|
|
|
|
token, err := ts.Timestamp(context.Background(), hash)
|
|
if err != nil {
|
|
t.Fatalf("Timestamp: %v", err)
|
|
}
|
|
|
|
parsed, err := timestamp.Parse(token)
|
|
if err != nil {
|
|
t.Fatalf("Parse(token): %v", err)
|
|
}
|
|
if !bytes.Equal(parsed.HashedMessage, hash) {
|
|
t.Fatalf("token hash = %x, want %x", parsed.HashedMessage, hash)
|
|
}
|
|
if parsed.Time.IsZero() {
|
|
t.Fatal("expected a non-zero timestamp time")
|
|
}
|
|
|
|
tm, err := evidence.TimestampTime(token)
|
|
if err != nil {
|
|
t.Fatalf("TimestampTime: %v", err)
|
|
}
|
|
if !tm.Equal(parsed.Time) {
|
|
t.Fatalf("TimestampTime() = %v, want %v", tm, parsed.Time)
|
|
}
|
|
}
|