package storage import ( "bytes" "context" "crypto/sha256" "encoding/hex" "io" "os" "strings" "testing" ) func newTestFS(t *testing.T) *LocalFS { t.Helper() root := t.TempDir() tmp := t.TempDir() fs, err := NewLocalFS(root, tmp) if err != nil { t.Fatalf("NewLocalFS: %v", err) } return fs } func TestPutAndGet(t *testing.T) { fs := newTestFS(t) ctx := context.Background() content := []byte("hello aether") checksum, size, err := fs.Put(ctx, "testfile", bytes.NewReader(content)) if err != nil { t.Fatalf("Put: %v", err) } if size != int64(len(content)) { t.Errorf("size: got %d want %d", size, len(content)) } // verify checksum h := sha256.Sum256(content) expected := hex.EncodeToString(h[:]) if checksum != expected { t.Errorf("checksum: got %s want %s", checksum, expected) } // get and read back rc, err := fs.Get(ctx, "testfile") if err != nil { t.Fatalf("Get: %v", err) } defer rc.Close() got, err := io.ReadAll(rc) if err != nil { t.Fatalf("ReadAll: %v", err) } if !bytes.Equal(got, content) { t.Errorf("content mismatch") } } func TestChecksumVerification(t *testing.T) { fs := newTestFS(t) ctx := context.Background() content := []byte("important data") storedChecksum, _, err := fs.Put(ctx, "file", bytes.NewReader(content)) if err != nil { t.Fatalf("Put: %v", err) } info, err := fs.Stat(ctx, "file") if err != nil { t.Fatalf("Stat: %v", err) } if info.Checksum != storedChecksum { t.Errorf("checksum mismatch after stat: got %s want %s", info.Checksum, storedChecksum) } } func TestPathTraversal(t *testing.T) { fs := newTestFS(t) ctx := context.Background() cases := []string{ "../escape", "../../etc/passwd", "/absolute/path", "a/../../b", "foo/../../../bar", } for _, key := range cases { _, _, err := fs.Put(ctx, key, strings.NewReader("data")) if err == nil { t.Errorf("expected error for traversal key %q but got none", key) } } } func TestAtomicWrite_NoPartialFile(t *testing.T) { fs := newTestFS(t) ctx := context.Background() // Simulate an interrupted reader (fails mid-stream). bad := &failReader{data: []byte("partial"), failAt: 3} _, _, err := fs.Put(ctx, "partial", bad) if err == nil { t.Fatal("expected error from failing reader") } // The destination file must NOT exist. dest := fs.root + "/partial" if _, statErr := os.Stat(dest); !os.IsNotExist(statErr) { t.Error("partial file must not exist after failed write") } } func TestDelete(t *testing.T) { fs := newTestFS(t) ctx := context.Background() if _, _, err := fs.Put(ctx, "todelete", strings.NewReader("bye")); err != nil { t.Fatalf("Put: %v", err) } if err := fs.Delete(ctx, "todelete"); err != nil { t.Fatalf("Delete: %v", err) } if _, err := fs.Get(ctx, "todelete"); err == nil { t.Error("expected error getting deleted file") } // Double-delete should be a no-op. if err := fs.Delete(ctx, "todelete"); err != nil { t.Errorf("second Delete should be no-op, got: %v", err) } } // failReader returns data normally up to failAt bytes, then errors. type failReader struct { data []byte pos int failAt int } func (r *failReader) Read(p []byte) (int, error) { if r.pos >= r.failAt { return 0, io.ErrUnexpectedEOF } n := copy(p, r.data[r.pos:min(r.failAt, len(r.data))]) r.pos += n return n, nil } func min(a, b int) int { if a < b { return a } return b } // Walk ist die Grundlage der Blob-Bereinigung: Übersieht es Dateien, bleiben // Waisen liegen; meldet es Schlüssel in einer anderen Form als die Datenbank // sie speichert, hält der GC gesunde Blobs für verwaist und löscht sie. func TestWalkReportsEveryBlobWithDBStyleKeys(t *testing.T) { fs := newTestFS(t) ctx := context.Background() want := map[string]bool{ "users/alice/home/notiz.txt": true, "users/alice/home/Unterordner/bild.png": true, "users/alice/.versions/abc-123": true, "users/alice/.trash/deadbeef_alt.txt": true, } for key := range want { if _, _, err := fs.Put(ctx, key, strings.NewReader("inhalt")); err != nil { t.Fatalf("Put(%q): %v", key, err) } } // Außerhalb des Präfixes – darf nicht mitkommen. if _, _, err := fs.Put(ctx, "thumbs/xyz.jpg", strings.NewReader("x")); err != nil { t.Fatalf("Put thumb: %v", err) } got := map[string]bool{} if err := fs.Walk(ctx, "users", func(key string, info FileInfo) error { got[key] = true if info.Size != int64(len("inhalt")) { t.Errorf("Walk reported size %d for %q, want %d", info.Size, key, len("inhalt")) } if info.ModTime.IsZero() { t.Errorf("Walk reported zero ModTime for %q – the GC grace period relies on it", key) } return nil }); err != nil { t.Fatalf("Walk: %v", err) } for key := range want { if !got[key] { t.Errorf("Walk missed %q – the GC would never reclaim it", key) } } if got["thumbs/xyz.jpg"] { t.Error("Walk crossed the prefix boundary") } if len(got) != len(want) { t.Errorf("Walk returned %d keys, want %d: %v", len(got), len(want), got) } } // Ein fehlender Präfix ist kein Fehler – sonst bräche der GC auf einer frischen // Installation ab, bevor er irgendetwas tut. func TestWalkMissingPrefixIsNotAnError(t *testing.T) { fs := newTestFS(t) called := false if err := fs.Walk(context.Background(), "gibtesnicht", func(string, FileInfo) error { called = true return nil }); err != nil { t.Fatalf("Walk on missing prefix: %v", err) } if called { t.Error("callback ran although the prefix does not exist") } }