Files

509 lines
15 KiB
Go
Raw Permalink Normal View History

package storage_test
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/J3vb/OwnCord/Server/storage"
)
// newTestStorage creates a Storage instance backed by a temporary directory
// that is removed when the test ends.
func newTestStorage(t *testing.T) *storage.Storage {
t.Helper()
dir := t.TempDir()
s, err := storage.New(dir, 10)
if err != nil {
t.Fatalf("storage.New: %v", err)
}
return s
}
// ─── sanitizeFilename / path validation (tested indirectly via Save/Delete/Open) ─
// TestSave_ValidUUID verifies that a normal UUID-style filename is accepted.
func TestSave_ValidUUID(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("550e8400-e29b-41d4-a716-446655440000", strings.NewReader("hello"))
if err != nil {
t.Errorf("Save valid uuid: unexpected error: %v", err)
}
}
// TestSave_PathTraversalDotDot rejects filenames containing "..".
func TestSave_PathTraversalDotDot(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("../../etc/passwd", strings.NewReader("evil"))
if err == nil {
t.Error("Save('../../etc/passwd') returned nil error, want path traversal error")
}
}
// TestSave_DotDotFilename rejects the literal string "..".
func TestSave_DotDotFilename(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("..", strings.NewReader("evil"))
if err == nil {
t.Error("Save('..') returned nil error, want error")
}
}
// TestSave_SingleDotFilename rejects the literal string ".".
func TestSave_SingleDotFilename(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save(".", strings.NewReader("evil"))
if err == nil {
t.Error("Save('.') returned nil error, want error")
}
}
// TestSave_EmptyFilename rejects an empty string.
func TestSave_EmptyFilename(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("", strings.NewReader("data"))
if err == nil {
t.Error("Save('') returned nil error, want error")
}
}
// TestSave_DotPrefixFilename rejects filenames starting with ".".
func TestSave_DotPrefixFilename(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save(".hidden", strings.NewReader("data"))
if err == nil {
t.Error("Save('.hidden') returned nil error, want error")
}
}
// TestSave_ForwardSlashRejected rejects filenames containing a forward slash.
func TestSave_ForwardSlashRejected(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("sub/file", strings.NewReader("data"))
if err == nil {
t.Error("Save('sub/file') returned nil error, want path separator error")
}
}
// TestSave_BackslashRejected rejects filenames containing a backslash.
func TestSave_BackslashRejected(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save(`sub\file`, strings.NewReader("data"))
if err == nil {
t.Error(`Save('sub\file') returned nil error, want path separator error`)
}
}
// TestSave_ResolvedPathStaysInDir verifies the stored file is actually inside
// the storage directory (defence-in-depth after sanitisation).
func TestSave_ResolvedPathStaysInDir(t *testing.T) {
dir := t.TempDir()
s, _ := storage.New(dir, 10)
filename := "valid-file.dat"
if _, err := s.Save(filename, strings.NewReader("content")); err != nil {
t.Fatalf("Save: %v", err)
}
expectedPath := filepath.Join(dir, filename)
if _, err := os.Stat(expectedPath); errors.Is(err, os.ErrNotExist) {
t.Errorf("expected file at %s but it was not found", expectedPath)
}
}
// TestDelete_ValidUUID verifies that a saved file can be deleted by its UUID.
func TestDelete_ValidUUID(t *testing.T) {
s := newTestStorage(t)
if _, err := s.Save("abc123", strings.NewReader("data")); err != nil {
t.Fatalf("Save: %v", err)
}
if err := s.Delete("abc123"); err != nil {
t.Errorf("Delete valid uuid: unexpected error: %v", err)
}
}
// TestDelete_PathTraversal rejects path-traversal filenames.
func TestDelete_PathTraversal(t *testing.T) {
s := newTestStorage(t)
err := s.Delete("../../sensitive")
if err == nil {
t.Error("Delete('../../sensitive') returned nil error, want path traversal error")
}
}
// TestDelete_DotDot rejects "..".
func TestDelete_DotDot(t *testing.T) {
s := newTestStorage(t)
if err := s.Delete(".."); err == nil {
t.Error("Delete('..') returned nil error, want error")
}
}
// TestDelete_EmptyFilename rejects an empty string.
func TestDelete_EmptyFilename(t *testing.T) {
s := newTestStorage(t)
if err := s.Delete(""); err == nil {
t.Error("Delete('') returned nil error, want error")
}
}
// TestDelete_DotPrefixFilename rejects filenames starting with ".".
func TestDelete_DotPrefixFilename(t *testing.T) {
s := newTestStorage(t)
if err := s.Delete(".hidden"); err == nil {
t.Error("Delete('.hidden') returned nil error, want error")
}
}
// TestOpen_ValidUUID verifies that a saved file can be opened and read back.
func TestOpen_ValidUUID(t *testing.T) {
s := newTestStorage(t)
content := "hello storage"
if _, err := s.Save("myfile", strings.NewReader(content)); err != nil {
t.Fatalf("Save: %v", err)
}
f, err := s.Open("myfile")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close() //nolint:errcheck
got, err := io.ReadAll(f)
if err != nil {
t.Fatalf("reading opened file: %v", err)
}
if string(got) != content {
t.Errorf("content = %q, want %q", got, content)
}
}
// TestOpen_PathTraversal rejects path-traversal filenames.
func TestOpen_PathTraversal(t *testing.T) {
s := newTestStorage(t)
_, err := s.Open("../../etc/passwd")
if err == nil {
t.Error("Open('../../etc/passwd') returned nil error, want path traversal error")
}
}
// TestOpen_DotDot rejects "..".
func TestOpen_DotDot(t *testing.T) {
s := newTestStorage(t)
if _, err := s.Open(".."); err == nil {
t.Error("Open('..') returned nil error, want error")
}
}
// TestOpen_EmptyFilename rejects an empty string.
func TestOpen_EmptyFilename(t *testing.T) {
s := newTestStorage(t)
if _, err := s.Open(""); err == nil {
t.Error("Open('') returned nil error, want error")
}
}
// TestOpen_DotPrefixFilename rejects filenames starting with ".".
func TestOpen_DotPrefixFilename(t *testing.T) {
s := newTestStorage(t)
if _, err := s.Open(".env"); err == nil {
t.Error("Open('.env') returned nil error, want error")
}
}
// TestOpen_ForwardSlashRejected rejects filenames containing a forward slash.
func TestOpen_ForwardSlashRejected(t *testing.T) {
s := newTestStorage(t)
if _, err := s.Open("dir/file"); err == nil {
t.Error("Open('dir/file') returned nil error, want path separator error")
}
}
// TestSave_RoundTrip confirms data integrity through Save then Open.
func TestSave_RoundTrip(t *testing.T) {
s := newTestStorage(t)
payload := bytes.Repeat([]byte("abcdef"), 1000) // 6 KB
if _, err := s.Save("roundtrip", bytes.NewReader(payload)); err != nil {
t.Fatalf("Save: %v", err)
}
f, err := s.Open("roundtrip")
if err != nil {
t.Fatalf("Open: %v", err)
}
defer f.Close() //nolint:errcheck
got, _ := io.ReadAll(f)
if !bytes.Equal(got, payload) {
t.Errorf("round-trip data mismatch: got %d bytes, want %d", len(got), len(payload))
}
}
// ─── 4.2: Magic byte validation ───────────────────────────────────────────────
// TestValidateFileType_AllowsNormalContent verifies that plain file content passes.
func TestValidateFileType_AllowsNormalContent(t *testing.T) {
cases := []struct {
name string
header []byte
}{
{"PNG", []byte("\x89PNG\r\n\x1a\n")},
{"JPEG", []byte("\xff\xd8\xff\xe0")},
{"GIF87", []byte("GIF87a")},
{"GIF89", []byte("GIF89a")},
{"PDF", []byte("%PDF-1.4")},
{"ZIP", []byte("PK\x03\x04")},
{"plaintext", []byte("Hello world")},
{"empty", []byte{}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := storage.ValidateFileType(tc.header)
if err != nil {
t.Errorf("ValidateFileType(%q) = %v, want nil", tc.name, err)
}
})
}
}
// TestValidateFileType_BlocksPEExecutable verifies Windows .exe files are rejected.
func TestValidateFileType_BlocksPEExecutable(t *testing.T) {
header := []byte("MZP\x00\x02\x00\x00\x00") // PE magic "MZ"
err := storage.ValidateFileType(header)
if err == nil {
t.Error("ValidateFileType(PE header) = nil, want error")
}
}
// TestValidateFileType_BlocksELFBinary verifies Linux ELF binaries are rejected.
func TestValidateFileType_BlocksELFBinary(t *testing.T) {
header := []byte("\x7fELF\x02\x01\x01\x00")
err := storage.ValidateFileType(header)
if err == nil {
t.Error("ValidateFileType(ELF header) = nil, want error")
}
}
// TestValidateFileType_BlocksMachO64 verifies macOS 64-bit Mach-O binaries are rejected.
func TestValidateFileType_BlocksMachO64(t *testing.T) {
header := []byte("\xcf\xfa\xed\xfe\x07\x00\x00\x01")
err := storage.ValidateFileType(header)
if err == nil {
t.Error("ValidateFileType(Mach-O 64 header) = nil, want error")
}
}
// TestValidateFileType_BlocksMachO32 verifies macOS 32-bit Mach-O binaries are rejected.
func TestValidateFileType_BlocksMachO32(t *testing.T) {
header := []byte("\xce\xfa\xed\xfe\x07\x00\x00\x01")
err := storage.ValidateFileType(header)
if err == nil {
t.Error("ValidateFileType(Mach-O 32 header) = nil, want error")
}
}
// TestValidateFileType_BlocksShellScript verifies shebang scripts are rejected.
func TestValidateFileType_BlocksShellScript(t *testing.T) {
cases := []struct {
name string
header []byte
}{
{"bash", []byte("#!/bin/bash\necho hi")},
{"sh", []byte("#!/bin/sh\necho hi")},
{"python", []byte("#!/usr/bin/env python3\nprint('x')")},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
err := storage.ValidateFileType(tc.header)
if err == nil {
t.Errorf("ValidateFileType(script %q) = nil, want error", tc.name)
}
})
}
}
// TestValidateFileType_ErrorMessageContainsFormat verifies the error names the blocked type.
func TestValidateFileType_ErrorMessageContainsFormat(t *testing.T) {
header := []byte("MZ\x90\x00") // PE executable
err := storage.ValidateFileType(header)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "PE executable") {
t.Errorf("error message %q does not mention 'PE executable'", err.Error())
}
}
// TestSave_BlocksExecutable verifies Save rejects PE executable content.
func TestSave_BlocksExecutable(t *testing.T) {
s := newTestStorage(t)
// Construct content with PE magic followed by padding.
content := append([]byte("MZ"), make([]byte, 100)...)
_, err := s.Save("malware.exe", bytes.NewReader(content))
if err == nil {
t.Error("Save(PE executable) = nil, want error")
}
}
// TestSave_BlocksELF verifies Save rejects ELF binary content.
func TestSave_BlocksELF(t *testing.T) {
s := newTestStorage(t)
content := append([]byte("\x7fELF"), make([]byte, 100)...)
_, err := s.Save("linux-binary", bytes.NewReader(content))
if err == nil {
t.Error("Save(ELF binary) = nil, want error")
}
}
// TestSave_BlocksShellScript verifies Save rejects script content.
func TestSave_BlocksShellScript(t *testing.T) {
s := newTestStorage(t)
content := []byte("#!/bin/bash\nrm -rf /\n")
_, err := s.Save("nasty.sh", bytes.NewReader(content))
if err == nil {
t.Error("Save(shell script) = nil, want error")
}
}
// TestSave_AllowsPNG verifies Save still accepts legitimate image content after magic check.
func TestSave_AllowsPNG(t *testing.T) {
s := newTestStorage(t)
content := append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, 100)...)
_, err := s.Save("image.png", bytes.NewReader(content))
if err != nil {
t.Errorf("Save(PNG) = %v, want nil", err)
}
}
// TestSave_EmptyFileAllowed verifies that an empty file (no content) is accepted.
func TestSave_EmptyFileAllowed(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("empty-file", bytes.NewReader([]byte{}))
if err != nil {
t.Errorf("Save(empty) = %v, want nil", err)
}
}
// ─── New edge cases ──────────────────────────────────────────────────────────
func TestNew_CreatesDirectory(t *testing.T) {
tmpDir := t.TempDir()
newDir := filepath.Join(tmpDir, "nested", "storage")
s, err := storage.New(newDir, 10)
if err != nil {
t.Fatalf("New: %v", err)
}
if s == nil {
t.Fatal("New returned nil")
}
// Directory should exist.
info, statErr := os.Stat(newDir)
if statErr != nil {
t.Fatalf("directory not created: %v", statErr)
}
if !info.IsDir() {
t.Error("expected directory, got file")
}
}
// ─── Save large file ────────────────────────────────────────────────────────
func TestSave_ExceedsMaxSize(t *testing.T) {
tmpDir := t.TempDir()
// 1 MB max.
s, err := storage.New(tmpDir, 1)
if err != nil {
t.Fatalf("New: %v", err)
}
// Create reader with >1MB of data.
bigData := bytes.Repeat([]byte("x"), 1024*1024+100)
_, err = s.Save("big-file", bytes.NewReader(bigData))
if err == nil {
t.Error("Save should reject file exceeding max size")
}
// File should be removed.
if _, statErr := os.Stat(filepath.Join(tmpDir, "big-file")); !os.IsNotExist(statErr) {
t.Error("oversized file should be removed after rejection")
}
}
func TestSave_ReadError(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save("read-err", &failReader{})
if err == nil {
t.Error("Save with failing reader should return error")
}
}
type failReader struct{}
func (f *failReader) Read([]byte) (int, error) {
return 0, errors.New("simulated read error")
}
// midCopyFailReader serves a valid header, then fails — reaching the
// copy-phase error branch after the destination file already exists
// (a write-side disk error like ENOSPC fails at the same branch).
type midCopyFailReader struct{ served bool }
func (r *midCopyFailReader) Read(p []byte) (int, error) {
if !r.served {
r.served = true
return copy(p, "plaintext"), nil
}
return 0, errors.New("simulated mid-copy failure")
}
// A Save that fails after creating the file must not leave a partial file
// behind: the orphan sweep is DB-row-driven and nothing walks the storage
// dir, so a row-less file would be leaked forever. TestSave_ExceedsMaxSize
// locks the same contract for the oversize branch.
func TestSave_MidCopyError_RemovesPartialFile(t *testing.T) {
tmpDir := t.TempDir()
s, err := storage.New(tmpDir, 10)
if err != nil {
t.Fatalf("New: %v", err)
}
if _, err = s.Save("partial-file", &midCopyFailReader{}); err == nil {
t.Fatal("Save with mid-copy failure should return error")
}
if _, statErr := os.Stat(filepath.Join(tmpDir, "partial-file")); !os.IsNotExist(statErr) {
t.Error("partial file should be removed after a failed save")
}
}
// ─── resolvedPath edge case (via Save with dot prefix) ──────────────────────
func TestSave_HiddenFilename(t *testing.T) {
s := newTestStorage(t)
_, err := s.Save(".hidden", strings.NewReader("data"))
if err == nil {
t.Error("Save should reject hidden filenames starting with '.'")
}
}
func TestOpen_NotFound(t *testing.T) {
s := newTestStorage(t)
_, err := s.Open("nonexistent-file")
if err == nil {
t.Error("Open should return error for nonexistent file")
}
}
func TestDelete_NotFound(t *testing.T) {
s := newTestStorage(t)
err := s.Delete("nonexistent-file")
if err == nil {
t.Error("Delete should return error for nonexistent file")
}
}