mirror of
https://github.com/zhufuyi/sponge.git
synced 2025-12-24 10:40:55 +08:00
416 lines
13 KiB
Go
416 lines
13 KiB
Go
package staticfs
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// setupTestServer Create a gin engine and temporary directory structure for testing purposes
|
|
func setupTestServer(t *testing.T, opts ...ListDirOption) (*gin.Engine, string) {
|
|
tmpDir, err := os.MkdirTemp("", "test-staticfs-")
|
|
assert.NoError(t, err)
|
|
|
|
// Create test files and directory structure
|
|
//- /
|
|
// - sub_dir/
|
|
// - file3.txt (older)
|
|
// - .git/ (sensitive)
|
|
// - file1.txt (10 bytes)
|
|
// - file2.log (20 bytes)
|
|
// - .env (sensitive)
|
|
assert.NoError(t, os.Mkdir(filepath.Join(tmpDir, "sub_dir"), 0755))
|
|
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "sub_dir", "file3.txt"), []byte("sub"), 0644))
|
|
// Modify file time for testing sorting
|
|
twoDaysAgo := time.Now().Add(-48 * time.Hour)
|
|
assert.NoError(t, os.Chtimes(filepath.Join(tmpDir, "sub_dir", "file3.txt"), twoDaysAgo, twoDaysAgo))
|
|
|
|
assert.NoError(t, os.Mkdir(filepath.Join(tmpDir, ".git"), 0755))
|
|
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("0123456789"), 0644))
|
|
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, "file2.log"), []byte("01234567890123456789"), 0644))
|
|
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, ".env"), []byte("secret"), 0644))
|
|
|
|
gin.SetMode(gin.TestMode)
|
|
r := gin.New()
|
|
ListDir(r, opts...)
|
|
|
|
// Use t.Cleanup to automatically delete temporary directories
|
|
t.Cleanup(func() {
|
|
os.RemoveAll(tmpDir)
|
|
})
|
|
|
|
return r, tmpDir
|
|
}
|
|
|
|
func newRequest(t *testing.T, r *gin.Engine, method, url string) *httptest.ResponseRecorder {
|
|
req, err := http.NewRequest(method, url, nil)
|
|
assert.NoError(t, err)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
func TestListDirOptions(t *testing.T) {
|
|
t.Run("DefaultOptions", func(t *testing.T) {
|
|
o := defaultListDirOptions()
|
|
assert.True(t, o.enableFilter)
|
|
assert.False(t, o.enableDownload)
|
|
assert.Empty(t, o.prefixPath)
|
|
})
|
|
|
|
t.Run("WithOptions", func(t *testing.T) {
|
|
o := defaultListDirOptions()
|
|
opts := []ListDirOption{
|
|
WithListDirPrefixPath("/static"),
|
|
WithListDirDownload(),
|
|
WithListDirFilter(false),
|
|
WithListDirFilesFilter(".tmp"),
|
|
WithListDirDirsFilter("/secret"),
|
|
}
|
|
o.apply(opts...)
|
|
|
|
assert.Equal(t, "/static", o.prefixPath)
|
|
assert.True(t, o.enableDownload)
|
|
assert.False(t, o.enableFilter)
|
|
assert.Contains(t, sensitiveFiles, ".tmp")
|
|
assert.Contains(t, sensitiveDirs, "/secret")
|
|
})
|
|
}
|
|
|
|
func TestIsAllowedPath(t *testing.T) {
|
|
originalSensitiveDirs := sensitiveDirs
|
|
originalSensitiveFiles := sensitiveFiles
|
|
t.Cleanup(func() {
|
|
sensitiveDirs = originalSensitiveDirs
|
|
sensitiveFiles = originalSensitiveFiles
|
|
})
|
|
sensitiveDirs = append(sensitiveDirs, "/test_dir")
|
|
sensitiveFiles = append(sensitiveFiles, ".test_file")
|
|
|
|
testCases := []struct {
|
|
name string
|
|
path string
|
|
enableFilter bool
|
|
expected bool
|
|
}{
|
|
{"Allowed", "/home/user/file.txt", true, true},
|
|
{"DeniedDir", "/proc/1/status", true, false},
|
|
{"DeniedFile", "/home/user/.git/config", true, false},
|
|
{"CustomDeniedDir", "/test_dir/somefile", true, false},
|
|
{"CustomDeniedFile", "config.test_file", true, false},
|
|
{"FilterDisabled", "/proc/version", false, true},
|
|
{"RootDenied", "/root", true, false},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.expected, isAllowedPath(tc.path, tc.enableFilter))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatSize(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
size int64
|
|
expected string
|
|
}{
|
|
{"Bytes", 100, "100 B"},
|
|
{"KB", 2048, "2.00 KB"},
|
|
{"MB", 3 * 1024 * 1024, "3.00 MB"},
|
|
{"GB", 4 * 1024 * 1024 * 1024, "4.00 GB"},
|
|
{"FractionalKB", 1536, "1.50 KB"},
|
|
{"Zero", 0, "0 B"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.expected, formatSize(tc.size))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListDirectory(t *testing.T) {
|
|
_, tmpDir := setupTestServer(t)
|
|
|
|
t.Run("ListWithFilter", func(t *testing.T) {
|
|
files, err := listDirectory(tmpDir, true)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, files, 3) // sub_dir, file1.txt, file2.log
|
|
names := []string{}
|
|
for _, f := range files {
|
|
names = append(names, f.Name)
|
|
}
|
|
assert.Contains(t, names, "sub_dir")
|
|
assert.Contains(t, names, "file1.txt")
|
|
assert.Contains(t, names, "file2.log")
|
|
assert.NotContains(t, names, ".git")
|
|
assert.NotContains(t, names, ".env")
|
|
})
|
|
|
|
t.Run("ListWithoutFilter", func(t *testing.T) {
|
|
files, err := listDirectory(tmpDir, false)
|
|
assert.NoError(t, err)
|
|
assert.Len(t, files, 5) // all files and dirs
|
|
names := []string{}
|
|
for _, f := range files {
|
|
names = append(names, f.Name)
|
|
}
|
|
assert.Contains(t, names, ".git")
|
|
assert.Contains(t, names, ".env")
|
|
})
|
|
|
|
t.Run("NonExistentDir", func(t *testing.T) {
|
|
_, err := listDirectory(filepath.Join(tmpDir, "non-existent"), true)
|
|
assert.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestSortFiles(t *testing.T) {
|
|
now := time.Now()
|
|
files := []FileInfo{
|
|
{Name: "C", Size: 300, ModTime: now.Add(-time.Hour)},
|
|
{Name: "a", Size: 100, ModTime: now},
|
|
{Name: "B", Size: 200, ModTime: now.Add(-2 * time.Hour)},
|
|
}
|
|
|
|
t.Run("SortByNameAsc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "name", "asc")
|
|
assert.Equal(t, "a", sorted[0].Name)
|
|
assert.Equal(t, "B", sorted[1].Name)
|
|
assert.Equal(t, "C", sorted[2].Name)
|
|
})
|
|
|
|
t.Run("SortByNameDesc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "name", "desc")
|
|
assert.Equal(t, "C", sorted[0].Name)
|
|
assert.Equal(t, "B", sorted[1].Name)
|
|
assert.Equal(t, "a", sorted[2].Name)
|
|
})
|
|
|
|
t.Run("SortBySizeAsc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "size", "asc")
|
|
assert.Equal(t, int64(100), sorted[0].Size)
|
|
assert.Equal(t, int64(200), sorted[1].Size)
|
|
assert.Equal(t, int64(300), sorted[2].Size)
|
|
})
|
|
|
|
t.Run("SortBySizeDesc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "size", "desc") // default sort
|
|
assert.Equal(t, int64(300), sorted[0].Size)
|
|
assert.Equal(t, int64(200), sorted[1].Size)
|
|
assert.Equal(t, int64(100), sorted[2].Size)
|
|
})
|
|
|
|
t.Run("SortByTimeAsc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "time", "asc")
|
|
assert.Equal(t, "B", sorted[0].Name) // oldest
|
|
assert.Equal(t, "C", sorted[1].Name)
|
|
assert.Equal(t, "a", sorted[2].Name) // newest
|
|
})
|
|
|
|
t.Run("SortByTimeDesc", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "time", "desc")
|
|
assert.Equal(t, "a", sorted[0].Name) // newest
|
|
assert.Equal(t, "C", sorted[1].Name)
|
|
assert.Equal(t, "B", sorted[2].Name) // oldest
|
|
})
|
|
|
|
t.Run("DefaultSort (name asc)", func(t *testing.T) {
|
|
sorted := make([]FileInfo, len(files))
|
|
copy(sorted, files)
|
|
sortFiles(sorted, "invalid-sort-key", "asc")
|
|
assert.Equal(t, "a", sorted[0].Name)
|
|
})
|
|
}
|
|
|
|
func TestToggleOrder(t *testing.T) {
|
|
assert.Equal(t, "desc", toggleOrder("asc"))
|
|
assert.Equal(t, "asc", toggleOrder("desc"))
|
|
assert.Equal(t, "asc", toggleOrder("something-else"))
|
|
}
|
|
|
|
func TestHandleList(t *testing.T) {
|
|
r, tmpDir := setupTestServer(t)
|
|
|
|
t.Run("ValidRequest", func(t *testing.T) {
|
|
url := fmt.Sprintf("/dir/list?dir=%s", tmpDir)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
body, _ := io.ReadAll(w.Body)
|
|
assert.Contains(t, string(body), "<h1>Listing Directory</h1>")
|
|
assert.Contains(t, string(body), "file1.txt")
|
|
assert.Contains(t, string(body), "20 B") // size of file2.log
|
|
assert.NotContains(t, string(body), ".env")
|
|
})
|
|
|
|
t.Run("MissingDirParam", func(t *testing.T) {
|
|
w := newRequest(t, r, "GET", "/dir/list")
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
body, _ := io.ReadAll(w.Body)
|
|
assert.Contains(t, string(body), "dir parameter is required")
|
|
})
|
|
|
|
t.Run("NonExistentDir", func(t *testing.T) {
|
|
url := fmt.Sprintf("/dir/list?dir=%s", filepath.Join(tmpDir, "non-existent"))
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
body, _ := io.ReadAll(w.Body)
|
|
assert.Contains(t, string(body), "failed to read directory")
|
|
})
|
|
|
|
t.Run("Pagination", func(t *testing.T) {
|
|
// create more files to test pagination
|
|
for i := 0; i < 25; i++ {
|
|
assert.NoError(t, os.WriteFile(filepath.Join(tmpDir, fmt.Sprintf("page_test_%02d.txt", i)), []byte(""), 0644))
|
|
}
|
|
|
|
url := fmt.Sprintf("/dir/list?dir=%s&page=1", tmpDir)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
body, _ := io.ReadAll(w.Body)
|
|
assert.Contains(t, string(body), "Page 1 / 2") // 3 + 25 = 28 files, pageSize 20 -> 2 pages
|
|
assert.Contains(t, string(body), "page_test_15.txt") // last file on page 1, default sort by size=desc
|
|
assert.NotContains(t, string(body), "page_test_20.txt")
|
|
|
|
url = fmt.Sprintf("/dir/list?dir=%s&page=2", tmpDir)
|
|
w = newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
body, _ = io.ReadAll(w.Body)
|
|
assert.Contains(t, string(body), "Page 2 / 2")
|
|
assert.Contains(t, string(body), "page_test_24.txt")
|
|
})
|
|
|
|
t.Run("ParentDirLink", func(t *testing.T) {
|
|
subDirPath := filepath.Join(tmpDir, "sub_dir")
|
|
url := fmt.Sprintf("/dir/list?dir=%s&root=%s", subDirPath, tmpDir)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
body, _ := io.ReadAll(w.Body)
|
|
// Expect a "Back to Parent" link that points to tmpDir
|
|
assert.Contains(t, string(body), "<!DOCTYPE html>")
|
|
})
|
|
}
|
|
|
|
func TestHandleDownload(t *testing.T) {
|
|
r, tmpDir := setupTestServer(t, WithListDirDownload())
|
|
|
|
t.Run("ValidDownload", func(t *testing.T) {
|
|
filePath := filepath.Join(tmpDir, "file1.txt")
|
|
url := fmt.Sprintf("/dir/file/download?path=%s", filePath)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.Equal(t, `attachment; filename="file1.txt"`, w.Header().Get("Content-Disposition"))
|
|
body, _ := io.ReadAll(w.Body)
|
|
assert.Equal(t, "0123456789", string(body))
|
|
})
|
|
|
|
t.Run("InvalidPath", func(t *testing.T) {
|
|
w := newRequest(t, r, "GET", "/dir/file/download?path=")
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
|
|
t.Run("DisallowedPath", func(t *testing.T) {
|
|
filePath := filepath.Join(tmpDir, ".env")
|
|
url := fmt.Sprintf("/dir/file/download?path=%s", filePath)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestHandleAPIList(t *testing.T) {
|
|
r, tmpDir := setupTestServer(t)
|
|
|
|
t.Run("ValidAPIRequest", func(t *testing.T) {
|
|
url := fmt.Sprintf("/dir/list/api?dir=%s&sort=name&order=asc", tmpDir)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
|
|
var respData struct {
|
|
Dir string `json:"dir"`
|
|
Sort string `json:"sort"`
|
|
Order string `json:"order"`
|
|
Files []FileInfo `json:"files"`
|
|
}
|
|
err := json.Unmarshal(w.Body.Bytes(), &respData)
|
|
assert.NoError(t, err)
|
|
|
|
assert.Equal(t, tmpDir, respData.Dir)
|
|
assert.Equal(t, "name", respData.Sort)
|
|
assert.Equal(t, "asc", respData.Order)
|
|
assert.Len(t, respData.Files, 3)
|
|
assert.Equal(t, "file1.txt", respData.Files[0].Name) // sorted by name asc
|
|
})
|
|
|
|
t.Run("APINonExistentDir", func(t *testing.T) {
|
|
url := fmt.Sprintf("/dir/list/api?dir=%s", filepath.Join(tmpDir, "non-existent"))
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
|
})
|
|
}
|
|
|
|
func TestListDirRouterSetup(t *testing.T) {
|
|
t.Run("NoPrefix", func(t *testing.T) {
|
|
r, _ := setupTestServer(t)
|
|
w := newRequest(t, r, "GET", "/dir/list?dir=.")
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
wApi := newRequest(t, r, "GET", "/dir/list/api?dir=.")
|
|
assert.Equal(t, http.StatusOK, wApi.Code)
|
|
// Download route should not be registered by default
|
|
wDownload := newRequest(t, r, "GET", "/dir/file/download?path=.")
|
|
assert.Equal(t, http.StatusNotFound, wDownload.Code)
|
|
})
|
|
|
|
t.Run("WithPrefix", func(t *testing.T) {
|
|
r, _ := setupTestServer(t, WithListDirPrefixPath("/static/files"))
|
|
w := newRequest(t, r, "GET", "/static/files/dir/list?dir=.")
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
wApi := newRequest(t, r, "GET", "/static/files/dir/list/api?dir=.")
|
|
assert.Equal(t, http.StatusOK, wApi.Code)
|
|
})
|
|
|
|
t.Run("WithDownloadEnabled", func(t *testing.T) {
|
|
middleware := func(c *gin.Context) {
|
|
c.Header("X-Test-Header", "test-value")
|
|
c.Next()
|
|
}
|
|
r, tmpDir := setupTestServer(t,
|
|
WithListDirDownload(),
|
|
WithListDirMiddlewares(middleware))
|
|
filePath := filepath.Join(tmpDir, "file1.txt")
|
|
url := fmt.Sprintf("/dir/file/download?path=%s", filePath)
|
|
w := newRequest(t, r, "GET", url)
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
})
|
|
|
|
t.Run("WithPrefixSlashVariations", func(t *testing.T) {
|
|
testCases := []string{"/prefix", "prefix/", "/prefix/", "prefix"}
|
|
for _, prefix := range testCases {
|
|
r, _ := setupTestServer(t, WithListDirPrefixPath(prefix))
|
|
w := newRequest(t, r, "GET", "/prefix/dir/list?dir=.")
|
|
assert.Equal(t, http.StatusOK, w.Code, fmt.Sprintf("Failed with prefix: %s", prefix))
|
|
}
|
|
})
|
|
}
|