Ein Driver-Interface, zwei austauschbare Treiber: LocalDriver (Entwicklung, Dateisystem, HMAC-signierte URLs) und S3Driver (Produktion, S3-kompatibel via aws-sdk-go-v2, echte presigned URLs). Pfadschema documents/<id>/ revisions/<id> innerhalb des mandantenspezifischen Buckets. Service verbindet Driver mit Nutzungsmeldung an Core LIC-05 (HTTPUsageReporter, Vertrag von internal/resync.Handler nachgebildet, DMS kann Cores internal/- Pakete als eigenes Modul nicht importieren). Auf 192.168.1.131 verifiziert, S3-Treiber gegen echtes lokal installiertes MinIO (kein Mock): Round-Trip beide Treiber, abgelaufene presigned URL real mit 403 abgewiesen (manuell zusaetzlich zum Unit-Test verifiziert), klare ErrNotFound bei fehlendem Objekt beide Treiber, Nutzungsmeldung mit korrektem Tenant/Metrik/Delta bei Put/Delete. Befund dokumentiert: Cores internal/resync.Handler (Gegenstelle fuer die Nutzungsmeldung) ist noch in keinem cmd/*/main.go verdrahtet (dieselbe Fehlerklasse wie QA-05/AUD-06) - HTTPUsageReporter daher gegen den dokumentierten Vertrag getestet, nicht gegen eine laufende Core-Instanz. Siehe dms/docs/FDN-03-PRUEFPROTOKOLL.md. golangci-lint auf v2.1.6 aktualisiert (v1.63.4 konnte go1.24-Zielstand nicht linten), .golangci.yml auf v2-Konfigurationsformat migriert. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
109 lines
3.2 KiB
Go
109 lines
3.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"net/url"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func newTestLocalDriver(t *testing.T) *LocalDriver {
|
|
t.Helper()
|
|
return NewLocalDriver(t.TempDir(), []byte("test-signing-secret"), "https://files.example.test")
|
|
}
|
|
|
|
// TestLocalDriver_RoundTrip ist Pruefung 1 fuer den lokalen Treiber:
|
|
// Upload/Download-Roundtrip.
|
|
func TestLocalDriver_RoundTrip(t *testing.T) {
|
|
d := newTestLocalDriver(t)
|
|
ctx := context.Background()
|
|
key := "documents/doc-1/revisions/rev-1"
|
|
content := []byte("hallo welt")
|
|
|
|
written, err := d.Put(ctx, key, bytes.NewReader(content), int64(len(content)), "text/plain")
|
|
if err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
if written != int64(len(content)) {
|
|
t.Fatalf("geschriebene groesse = %d, want %d", written, len(content))
|
|
}
|
|
|
|
rc, err := d.Get(ctx, key)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
defer func() { _ = rc.Close() }()
|
|
got, err := io.ReadAll(rc)
|
|
if err != nil {
|
|
t.Fatalf("lesen: %v", err)
|
|
}
|
|
if !bytes.Equal(got, content) {
|
|
t.Fatalf("gelesener inhalt = %q, want %q", got, content)
|
|
}
|
|
}
|
|
|
|
// TestLocalDriver_MissingObject ist Pruefung 3: klarer Fehler bei
|
|
// fehlendem Objekt, sowohl fuer Get als auch Delete.
|
|
func TestLocalDriver_MissingObject(t *testing.T) {
|
|
d := newTestLocalDriver(t)
|
|
ctx := context.Background()
|
|
|
|
if _, err := d.Get(ctx, "nie-angelegt"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("get eines fehlenden objekts: erwartet ErrNotFound, habe %v", err)
|
|
}
|
|
if err := d.Delete(ctx, "nie-angelegt"); !errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("delete eines fehlenden objekts: erwartet ErrNotFound, habe %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLocalDriver_SignedURL_ExpiredIsRejected ist Pruefung 2: eine
|
|
// abgelaufene signierte URL wird abgewiesen.
|
|
func TestLocalDriver_SignedURL_ExpiredIsRejected(t *testing.T) {
|
|
d := newTestLocalDriver(t)
|
|
ctx := context.Background()
|
|
key := "documents/doc-2/revisions/rev-1"
|
|
|
|
// Gueltige, noch nicht abgelaufene URL wird akzeptiert.
|
|
urlValid, err := d.SignedURL(ctx, key, time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("signedurl (gueltig): %v", err)
|
|
}
|
|
expiry, sig := parseSignedURLQuery(t, urlValid)
|
|
if err := d.VerifySignedURL(key, expiry, sig); err != nil {
|
|
t.Fatalf("gueltige url wurde abgewiesen: %v", err)
|
|
}
|
|
|
|
// Bereits abgelaufene URL (negative TTL) wird abgewiesen.
|
|
urlExpired, err := d.SignedURL(ctx, key, -time.Hour)
|
|
if err != nil {
|
|
t.Fatalf("signedurl (abgelaufen): %v", err)
|
|
}
|
|
expiredExpiry, expiredSig := parseSignedURLQuery(t, urlExpired)
|
|
if err := d.VerifySignedURL(key, expiredExpiry, expiredSig); !errors.Is(err, ErrURLExpired) {
|
|
t.Fatalf("abgelaufene url: erwartet ErrURLExpired, habe %v", err)
|
|
}
|
|
|
|
// Manipulierte Signatur wird abgewiesen.
|
|
if err := d.VerifySignedURL(key, expiry, "manipuliert"); !errors.Is(err, ErrInvalidSignature) {
|
|
t.Fatalf("manipulierte signatur: erwartet ErrInvalidSignature, habe %v", err)
|
|
}
|
|
}
|
|
|
|
func parseSignedURLQuery(t *testing.T, rawURL string) (expiry int64, sig string) {
|
|
t.Helper()
|
|
parsed, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
t.Fatalf("signierte url parsen: %v (%s)", err, rawURL)
|
|
}
|
|
q := parsed.Query()
|
|
expInt, err := strconv.ParseInt(q.Get("exp"), 10, 64)
|
|
if err != nil {
|
|
t.Fatalf("exp parsen: %v", err)
|
|
}
|
|
return expInt, q.Get("sig")
|
|
}
|