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 }