feat: Standbild-Upload bei der Pre-Publish-Prüfung

CLAUDE.md beschreibt die Prüfung seit dem ersten Commit als "Caption,
Standbild und Vertragslage rein" — bisher wurde nur die Caption
verarbeitet, das asset-Schema aus Migration 0001 blieb ungenutzt.

- internal/store/asset.go: CreateAsset/GetLatestAssetForSubmission.
  Migration 0005 macht asset append-only (Trigger fehlte seit 0001,
  weil bis jetzt nichts hineinschrieb) — ein hochgeladenes Beweisstück
  wird nicht nachträglich ausgetauscht, aus demselben Grund wie bei
  extraction/finding/evidence_package.
- handleCheck liest ein optionales "standbild"-Formularfeld (Bild-
  Upload, max. 8 MiB, Content-Type muss image/* sein), validiert es
  VOR dem Anlegen der Submission (ein ungültiger Upload hinterlässt so
  keine leere Beitrags-Zeile), speichert es danach unter ASSET_DIR und
  legt die Asset-Zeile an.
- handleArchive bindet den Asset-Hash (falls vorhanden) in den
  Metadaten-Hash und ins PDF-Dossier ein (dossier.Data.AssetHash war
  bereits vorbereitet, wurde aber nie befüllt).
- index.html: Formular auf multipart/form-data umgestellt
  (hx-encoding + enctype), neues optionales Dateifeld. handleCheck
  bleibt abwärtskompatibel zu urlencoded-Requests (ParseMultipartForm
  liefert ErrNotMultipart, das wird wie "kein Bild hochgeladen"
  behandelt, nicht wie ein Fehler).
- ASSET_DIR neue Konfigurationsvariable (Default "assets", wie
  DOSSIER_DIR relativ zu WorkingDirectory=/var/lib/deklarix — kein
  postinst-Healing nötig, anders als bei RULES_DIR, dessen Default
  nicht zum installierten Pfad passt).

Volle Testsuite inkl. echter Postgres-Tests grün; End-to-End gegen
einen laufenden Server verifiziert (Upload, Hash in DB, Hash im
erzeugten PDF via pdftotext, Ablehnung bei falschem Dateityp).
This commit is contained in:
noroot
2026-08-27 21:45:03 +02:00
parent ae59e745c0
commit 790ab20651
12 changed files with 538 additions and 17 deletions

View File

@@ -0,0 +1,99 @@
package store_test
import (
"context"
"errors"
"testing"
"github.com/netcell-it/deklarix/internal/store"
)
func TestAssetCreateAndGetLatest(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
accID := testAccountID(t, s)
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
if err != nil {
t.Fatalf("CreateSubmission: %v", err)
}
a, err := s.CreateAsset(ctx, sub.ID, "image", "/var/lib/deklarix/assets/abc.jpg", "deadbeef")
if err != nil {
t.Fatalf("CreateAsset: %v", err)
}
if a.SubmissionID != sub.ID || a.Kind != "image" {
t.Fatalf("CreateAsset = %+v, unerwartete Werte", a)
}
got, err := s.GetLatestAssetForSubmission(ctx, sub.ID)
if err != nil {
t.Fatalf("GetLatestAssetForSubmission: %v", err)
}
if got.ID != a.ID || got.SHA256 != "deadbeef" {
t.Fatalf("GetLatestAssetForSubmission = %+v, want %+v", got, a)
}
}
func TestGetLatestAssetForSubmissionNotFoundWhenNoneUploaded(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
accID := testAccountID(t, s)
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
if err != nil {
t.Fatalf("CreateSubmission: %v", err)
}
_, err = s.GetLatestAssetForSubmission(ctx, sub.ID)
if !errors.Is(err, store.ErrNotFound) {
t.Fatalf("err = %v, want store.ErrNotFound", err)
}
}
func TestGetLatestAssetForSubmissionReturnsNewestWhenMultiple(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
accID := testAccountID(t, s)
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
if err != nil {
t.Fatalf("CreateSubmission: %v", err)
}
if _, err := s.CreateAsset(ctx, sub.ID, "image", "/tmp/erstes.jpg", "erstehash"); err != nil {
t.Fatalf("CreateAsset (1): %v", err)
}
second, err := s.CreateAsset(ctx, sub.ID, "image", "/tmp/zweites.jpg", "zweitehash")
if err != nil {
t.Fatalf("CreateAsset (2): %v", err)
}
got, err := s.GetLatestAssetForSubmission(ctx, sub.ID)
if err != nil {
t.Fatalf("GetLatestAssetForSubmission: %v", err)
}
if got.ID != second.ID {
t.Fatalf("expected the newest asset, got %+v", got)
}
}
func TestAssetIsAppendOnly(t *testing.T) {
s := openTestStore(t)
ctx := context.Background()
accID := testAccountID(t, s)
sub, err := s.CreateSubmission(ctx, accID, "instagram", "feed", "...")
if err != nil {
t.Fatalf("CreateSubmission: %v", err)
}
a, err := s.CreateAsset(ctx, sub.ID, "image", "/tmp/x.jpg", "hash")
if err != nil {
t.Fatalf("CreateAsset: %v", err)
}
_, err = s.Pool.Exec(ctx, `UPDATE asset SET sha256 = 'geaendert' WHERE id = $1`, a.ID)
if err == nil {
t.Fatal("expected UPDATE on asset to be rejected by the append-only trigger")
}
_, err = s.Pool.Exec(ctx, `DELETE FROM asset WHERE id = $1`, a.ID)
if err == nil {
t.Fatal("expected DELETE on asset to be rejected by the append-only trigger")
}
}