Files
Aether/backend/internal/storage/localfs_test.go
T
HoneyandClaude Opus 5 31a027c93a feat(aether): initial commit – self-hosted Cloud, Phasen 1–7
Go-Backend (chi, pgx, golang-migrate) + React/TS-Frontend als PWA.
Enthalten: Auth inkl. OIDC, Dateien mit Versionen und Papierkorb,
Freigaben (Links + intern), WebDAV, Gruppenordner, Admin/Theming,
Volltextsuche mit OCR, eigener PDF-Betrachter und WOPI-Office
(Collabora/OnlyOffice).

Nicht im Repo: deploy/.env (Sitzungsschlüssel, DB- und Admin-Passwort)
sowie alle Daten- und Build-Verzeichnisse – siehe .gitignore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 09:25:36 +02:00

224 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}