You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
120 lines
4.1 KiB
120 lines
4.1 KiB
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"webfilebrowser/internal/auth"
|
|
"webfilebrowser/internal/config"
|
|
)
|
|
|
|
func TestAuthenticatedBrowserListsRootsAndProvidesBreadcrumbNavigation(t *testing.T) {
|
|
rootPath, handler := newBrowsingApplication(t)
|
|
if err := os.Mkdir(filepath.Join(rootPath, "documents"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(rootPath, "documents", "report.txt"), []byte("report"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cookie := completeSeedPasswordChange(t, handler)
|
|
|
|
rootResponse := getWithCookie(handler, "/", cookie)
|
|
if rootResponse.Code != http.StatusOK {
|
|
t.Fatalf("GET / status = %d, want %d", rootResponse.Code, http.StatusOK)
|
|
}
|
|
if body := rootResponse.Body.String(); !strings.Contains(body, "Media") || !strings.Contains(body, "/browse?root=Media") {
|
|
t.Errorf("GET / did not render the configured root: %s", body)
|
|
}
|
|
|
|
directoryResponse := getWithCookie(handler, "/browse?root=Media&path=documents", cookie)
|
|
if directoryResponse.Code != http.StatusOK {
|
|
t.Fatalf("GET nested directory status = %d, want %d", directoryResponse.Code, http.StatusOK)
|
|
}
|
|
body := directoryResponse.Body.String()
|
|
for _, text := range []string{"Media", "documents", "report.txt", "File"} {
|
|
if !strings.Contains(body, text) {
|
|
t.Errorf("GET nested directory body is missing %q: %s", text, body)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBrowserRejectsTraversalAndSymlinkEscapeRequests(t *testing.T) {
|
|
rootPath, handler := newBrowsingApplication(t)
|
|
outsidePath := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(outsidePath, "secret.txt"), []byte("outside-root"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outsidePath, filepath.Join(rootPath, "escape")); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cookie := completeSeedPasswordChange(t, handler)
|
|
|
|
for _, target := range []string{"/browse?root=Media&path=../outside", "/browse?root=Media&path=escape"} {
|
|
t.Run(target, func(t *testing.T) {
|
|
response := getWithCookie(handler, target, cookie)
|
|
if response.Code != http.StatusNotFound {
|
|
t.Errorf("GET %s status = %d, want %d", target, response.Code, http.StatusNotFound)
|
|
}
|
|
if strings.Contains(response.Body.String(), "outside-root") {
|
|
t.Errorf("GET %s exposed content outside the configured root", target)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBrowserThemeTogglePersistsInLocalStorage(t *testing.T) {
|
|
_, handler := newBrowsingApplication(t)
|
|
cookie := completeSeedPasswordChange(t, handler)
|
|
|
|
response := getWithCookie(handler, "/", cookie)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("GET / status = %d, want %d", response.Code, http.StatusOK)
|
|
}
|
|
body := response.Body.String()
|
|
for _, text := range []string{`id="theme-toggle"`, "localStorage.getItem", "localStorage.setItem", "data-theme"} {
|
|
if !strings.Contains(body, text) {
|
|
t.Errorf("GET / is missing theme persistence behavior %q", text)
|
|
}
|
|
}
|
|
}
|
|
|
|
func newBrowsingApplication(t *testing.T) (string, http.Handler) {
|
|
t.Helper()
|
|
rootPath := t.TempDir()
|
|
store, err := auth.Open(filepath.Join(t.TempDir(), "webfilebrowser.db"), auth.Options{})
|
|
if err != nil {
|
|
t.Fatalf("Open() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = store.Close() })
|
|
return rootPath, New(store, config.Root{Name: "Media", Path: rootPath})
|
|
}
|
|
|
|
func completeSeedPasswordChange(t *testing.T, handler http.Handler) *http.Cookie {
|
|
t.Helper()
|
|
loginResponse := postForm(handler, loginPath, url.Values{
|
|
"email": {auth.SeedAdminEmail},
|
|
"password": {auth.SeedAdminPassword},
|
|
}, nil)
|
|
cookie := responseCookie(t, loginResponse, sessionCookieName)
|
|
changeResponse := postForm(handler, changePasswordPath, url.Values{
|
|
"current_password": {auth.SeedAdminPassword},
|
|
"new_password": {"A-new-password-123!"},
|
|
}, cookie)
|
|
if changeResponse.Code != http.StatusSeeOther {
|
|
t.Fatalf("POST %s status = %d, want %d", changePasswordPath, changeResponse.Code, http.StatusSeeOther)
|
|
}
|
|
return cookie
|
|
}
|
|
|
|
func getWithCookie(handler http.Handler, target string, cookie *http.Cookie) *httptest.ResponseRecorder {
|
|
request := httptest.NewRequest(http.MethodGet, target, nil)
|
|
request.AddCookie(cookie)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
return response
|
|
}
|
|
|