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
90 lines
2.8 KiB
Go
90 lines
2.8 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// fakeUsageReporter zeichnet jeden Report-Aufruf auf, damit Tests
|
|
// nachweisen koennen, dass Service tatsaechlich meldet (Akzeptanzkriterium
|
|
// 4 / Pruefung 4) — ohne echten HTTP-Aufruf gegen Core.
|
|
type fakeUsageReporter struct {
|
|
mu sync.Mutex
|
|
calls []reportCall
|
|
failOn int // wenn >0, schlaegt der reportCall-te Aufruf fehl
|
|
}
|
|
|
|
type reportCall struct {
|
|
tenantSlug string
|
|
metric string
|
|
delta int64
|
|
}
|
|
|
|
func (f *fakeUsageReporter) Report(ctx context.Context, tenantSlug, metric string, delta int64) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.calls = append(f.calls, reportCall{tenantSlug, metric, delta})
|
|
if f.failOn > 0 && len(f.calls) == f.failOn {
|
|
return context.DeadlineExceeded
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// TestService_PutReportsPositiveDelta ist Pruefung 4 (Schreibvorgang):
|
|
// Melde-Aufruf an Core wird bei Put ausgeloest, mit korrekter Groesse.
|
|
func TestService_PutReportsPositiveDelta(t *testing.T) {
|
|
driver := NewLocalDriver(t.TempDir(), []byte("secret"), "https://files.example.test")
|
|
usage := &fakeUsageReporter{}
|
|
svc := NewService(driver, usage, "acme")
|
|
|
|
content := []byte("zwoelf bytes")
|
|
if _, err := svc.Put(context.Background(), "documents/d1/revisions/r1", bytes.NewReader(content), int64(len(content)), "text/plain"); err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
|
|
if len(usage.calls) != 1 {
|
|
t.Fatalf("erwartet 1 nutzungsmeldung, habe %d", len(usage.calls))
|
|
}
|
|
call := usage.calls[0]
|
|
if call.tenantSlug != "acme" || call.metric != UsageMetric || call.delta != int64(len(content)) {
|
|
t.Fatalf("unerwarteter meldungsinhalt: %+v", call)
|
|
}
|
|
}
|
|
|
|
// TestService_DeleteReportsNegativeDelta ist Pruefung 4 (Loeschvorgang).
|
|
func TestService_DeleteReportsNegativeDelta(t *testing.T) {
|
|
driver := NewLocalDriver(t.TempDir(), []byte("secret"), "https://files.example.test")
|
|
usage := &fakeUsageReporter{}
|
|
svc := NewService(driver, usage, "acme")
|
|
ctx := context.Background()
|
|
key := "documents/d2/revisions/r1"
|
|
|
|
if _, err := svc.Put(ctx, key, bytes.NewReader([]byte("abc")), 3, "text/plain"); err != nil {
|
|
t.Fatalf("put: %v", err)
|
|
}
|
|
if err := svc.Delete(ctx, key, 3); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
|
|
if len(usage.calls) != 2 {
|
|
t.Fatalf("erwartet 2 nutzungsmeldungen (put+delete), habe %d", len(usage.calls))
|
|
}
|
|
del := usage.calls[1]
|
|
if del.delta != -3 {
|
|
t.Fatalf("delete-delta = %d, want -3", del.delta)
|
|
}
|
|
}
|
|
|
|
// TestService_GetMissingObjectReturnsClearError ist Pruefung 3 auf
|
|
// Service-Ebene.
|
|
func TestService_GetMissingObjectReturnsClearError(t *testing.T) {
|
|
driver := NewLocalDriver(t.TempDir(), []byte("secret"), "https://files.example.test")
|
|
svc := NewService(driver, &fakeUsageReporter{}, "acme")
|
|
|
|
if _, err := svc.Get(context.Background(), "nie-angelegt"); err == nil {
|
|
t.Fatal("get eines fehlenden objekts haette einen fehler liefern muessen")
|
|
}
|
|
}
|