package wormstore import ( "context" "errors" "os" "reflect" "strings" "testing" "github.com/jackc/pgx/v5/pgxpool" ) func setupTest(t *testing.T) (*Store, *pgxpool.Pool) { t.Helper() dsn := os.Getenv("TEST_TENANT_DSN") if dsn == "" { t.Skip("TEST_TENANT_DSN nicht gesetzt, Integrationstest uebersprungen") } ctx := context.Background() pool, err := pgxpool.New(ctx, dsn) if err != nil { t.Fatalf("pool: %v", err) } t.Cleanup(func() { pool.Close() }) if _, err := pool.Exec(ctx, ` CREATE TABLE IF NOT EXISTS worm_objects ( storage_key TEXT PRIMARY KEY, checksum_sha256 TEXT NOT NULL, size_bytes BIGINT NOT NULL, written_at TIMESTAMPTZ NOT NULL DEFAULT now() ); `); err != nil { t.Fatalf("schema: %v", err) } t.Cleanup(func() { _, _ = pool.Exec(context.Background(), `DELETE FROM worm_objects WHERE storage_key LIKE 'wormtest_%'`) }) return New(t.TempDir(), pool), pool } // TestPut_DirectOverwriteAttemptIsRejected ist die geforderte // Pflichtprüfung 1: Objekt mit aktivem Schreibschutz widersteht einem // direkten Überschreibversuch — sowohl über die Store-API als auch // direkt am Dateisystem (echter Nachweis der Datei-Schreibsperre, nicht // nur der Anwendungslogik). func TestPut_DirectOverwriteAttemptIsRejected(t *testing.T) { store, _ := setupTest(t) ctx := context.Background() sum1, size1, err := store.Put(ctx, "wormtest_a", strings.NewReader("erster inhalt")) if err != nil { t.Fatalf("erster put: %v", err) } // Überschreibversuch über die API. _, _, err = store.Put(ctx, "wormtest_a", strings.NewReader("manipulierter inhalt")) if !errors.Is(err, ErrAlreadyExists) { t.Fatalf("erwartet ErrAlreadyExists beim zweiten put, habe: %v", err) } // Direkter Überschreibversuch am Dateisystem (umgeht die API) — muss // an der echten OS-Schreibsperre (chmod 0400) scheitern. Nur // aussagekräftig, wenn NICHT als root getestet wird: root umgeht // Unix-Dateirechte grundsätzlich (DAC-Bypass), das ist kein Defekt // dieses Pakets, sondern eine bekannte Grenze von chmod-basiertem // Schutz (siehe Paket-Dokumentation "kein absoluter Schutz gegen // root"). Auf 131 laufen Tests als root — dieser Teilnachweis wird // dort bewusst übersprungen, die API-seitige Sperre (oben) bleibt // die primäre, root-unabhängige Prüfung. if os.Geteuid() != 0 { full := store.path("wormtest_a") f, openErr := os.OpenFile(full, os.O_WRONLY, 0) if openErr == nil { _ = f.Close() t.Fatal("erwartet permission-fehler beim direkten öffnen zum schreiben, datei war schreibbar") } if !os.IsPermission(openErr) { t.Fatalf("erwartet permission-fehler, habe: %v", openErr) } } else { t.Log("laeuft als root: OS-Schreibsperren-Teilnachweis uebersprungen (root umgeht Unix-Dateirechte grundsaetzlich)") } // Ursprünglicher Inhalt unverändert. data, err := store.GetVerified(ctx, "wormtest_a") if err != nil { t.Fatalf("getverified: %v", err) } if string(data) != "erster inhalt" { t.Fatalf("inhalt wurde veraendert: %q", data) } if size1 != int64(len("erster inhalt")) || sum1 == "" { t.Fatalf("unerwartete put-metadaten: sum=%q size=%d", sum1, size1) } } // TestGetVerified_DetectsTamperedFile ist die geforderte Pflichtprüfung // 2: Prüfsummenabgleich erkennt eine manipulierte Testdatei zuverlässig. func TestGetVerified_DetectsTamperedFile(t *testing.T) { store, _ := setupTest(t) ctx := context.Background() if _, _, err := store.Put(ctx, "wormtest_b", strings.NewReader("unveraenderter inhalt")); err != nil { t.Fatalf("put: %v", err) } // Manipulation simulieren: Schreibschutz temporär aufheben (root/ // Betreiber-Zugriff außerhalb der API, genau der Fall, den die // Prüfsumme abfangen soll) und den Inhalt verändern. full := store.path("wormtest_b") if err := os.Chmod(full, 0o600); err != nil { t.Fatalf("chmod fuer testmanipulation: %v", err) } if err := os.WriteFile(full, []byte("MANIPULIERTER INHALT"), 0o600); err != nil { t.Fatalf("testmanipulation schreiben: %v", err) } _, err := store.GetVerified(ctx, "wormtest_b") if !errors.Is(err, ErrChecksumMismatch) { t.Fatalf("erwartet ErrChecksumMismatch bei manipulierter datei, habe: %v", err) } } // TestStore_HasNoDeleteMethod ist die geforderte Pflichtprüfung 3 // (präzisiert): Löschung ist über die Go-API des Pakets technisch // unmöglich, weil KEINE Delete-Funktion existiert — strukturell per // Reflection nachgewiesen, kein bloßer Verzicht per Konvention/Kommentar. func TestStore_HasNoDeleteMethod(t *testing.T) { storeType := reflect.TypeOf(&Store{}) for i := 0; i < storeType.NumMethod(); i++ { name := storeType.Method(i).Name if strings.Contains(strings.ToLower(name), "delete") || strings.Contains(strings.ToLower(name), "remove") { t.Fatalf("wormstore.Store darf keine Lösch-Methode besitzen, gefunden: %s", name) } } }