package web import ( "bytes" "io" "net/http" "net/http/httptest" "net/url" "path/filepath" "strings" "testing" "webfilebrowser/internal/auth" ) func TestUnauthenticatedRequestsNeverReachProtectedRoutes(t *testing.T) { _, handler := newTestApplication(t) for _, target := range []string{"/", "/files/media", "/download/media/report.pdf", "/stream/media/video.mp4", "/admin/users"} { t.Run(target, func(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, target, nil)) if response.Code != http.StatusSeeOther { t.Errorf("GET %s status = %d, want %d", target, response.Code, http.StatusSeeOther) } if location := response.Header().Get("Location"); location != loginPath { t.Errorf("GET %s Location = %q, want %q", target, location, loginPath) } if strings.Contains(response.Body.String(), "File browser is being prepared") { t.Errorf("GET %s returned protected content", target) } }) } } func TestLoginForcesPasswordChangeBeforeAccessingOtherRoutes(t *testing.T) { _, handler := newTestApplication(t) loginResponse := postForm(handler, loginPath, url.Values{ "email": {auth.SeedAdminEmail}, "password": {auth.SeedAdminPassword}, }, nil) if loginResponse.Code != http.StatusSeeOther { t.Fatalf("POST %s status = %d, want %d", loginPath, loginResponse.Code, http.StatusSeeOther) } if location := loginResponse.Header().Get("Location"); location != changePasswordPath { t.Fatalf("POST %s Location = %q, want %q", loginPath, location, changePasswordPath) } cookie := responseCookie(t, loginResponse, sessionCookieName) if !cookie.HttpOnly || !cookie.Secure || cookie.SameSite != http.SameSiteLaxMode { t.Errorf("session cookie security attributes = HttpOnly:%t Secure:%t SameSite:%v, want true:true:Lax", cookie.HttpOnly, cookie.Secure, cookie.SameSite) } for _, target := range []string{"/", "/files/media", "/download/media/report.pdf", "/stream/media/video.mp4", "/admin/users"} { response := httptest.NewRecorder() request := httptest.NewRequest(http.MethodGet, target, nil) request.AddCookie(cookie) handler.ServeHTTP(response, request) if response.Code != http.StatusSeeOther || response.Header().Get("Location") != changePasswordPath { t.Errorf("GET %s during forced change = (%d, %q), want (%d, %q)", target, response.Code, response.Header().Get("Location"), http.StatusSeeOther, changePasswordPath) } } samePasswordResponse := postForm(handler, changePasswordPath, url.Values{ "current_password": {auth.SeedAdminPassword}, "new_password": {auth.SeedAdminPassword}, }, cookie) if samePasswordResponse.Code != http.StatusBadRequest { t.Errorf("POST %s with unchanged password status = %d, want %d", changePasswordPath, samePasswordResponse.Code, http.StatusBadRequest) } changedPasswordResponse := postForm(handler, changePasswordPath, url.Values{ "current_password": {auth.SeedAdminPassword}, "new_password": {"A-new-password-123!"}, }, cookie) if changedPasswordResponse.Code != http.StatusSeeOther || changedPasswordResponse.Header().Get("Location") != "/" { t.Errorf("POST %s status/location = (%d, %q), want (%d, %q)", changePasswordPath, changedPasswordResponse.Code, changedPasswordResponse.Header().Get("Location"), http.StatusSeeOther, "/") } rootResponse := httptest.NewRecorder() rootRequest := httptest.NewRequest(http.MethodGet, "/", nil) rootRequest.AddCookie(cookie) handler.ServeHTTP(rootResponse, rootRequest) if rootResponse.Code != http.StatusOK { t.Errorf("GET / after changing password status = %d, want %d", rootResponse.Code, http.StatusOK) } } func TestLogoutDeletesTheServerSession(t *testing.T) { _, handler := newTestApplication(t) cookie := loginSeedAdmin(t, handler) logoutResponse := postForm(handler, logoutPath, nil, cookie) if logoutResponse.Code != http.StatusSeeOther || logoutResponse.Header().Get("Location") != loginPath { t.Fatalf("POST %s status/location = (%d, %q), want (%d, %q)", logoutPath, logoutResponse.Code, logoutResponse.Header().Get("Location"), http.StatusSeeOther, loginPath) } protectedResponse := httptest.NewRecorder() protectedRequest := httptest.NewRequest(http.MethodGet, "/", nil) protectedRequest.AddCookie(cookie) handler.ServeHTTP(protectedResponse, protectedRequest) if protectedResponse.Code != http.StatusSeeOther || protectedResponse.Header().Get("Location") != loginPath { t.Errorf("GET / after logout status/location = (%d, %q), want (%d, %q)", protectedResponse.Code, protectedResponse.Header().Get("Location"), http.StatusSeeOther, loginPath) } } func newTestApplication(t *testing.T) (*auth.Store, http.Handler) { t.Helper() 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 store, New(store) } func loginSeedAdmin(t *testing.T, handler http.Handler) *http.Cookie { t.Helper() response := postForm(handler, loginPath, url.Values{ "email": {auth.SeedAdminEmail}, "password": {auth.SeedAdminPassword}, }, nil) if response.Code != http.StatusSeeOther { t.Fatalf("POST %s status = %d, want %d", loginPath, response.Code, http.StatusSeeOther) } return responseCookie(t, response, sessionCookieName) } func postForm(handler http.Handler, target string, values url.Values, cookie *http.Cookie) *httptest.ResponseRecorder { var body io.Reader if values != nil { body = bytes.NewBufferString(values.Encode()) } request := httptest.NewRequest(http.MethodPost, target, body) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") if cookie != nil { request.AddCookie(cookie) } response := httptest.NewRecorder() handler.ServeHTTP(response, request) return response } func responseCookie(t *testing.T, response *httptest.ResponseRecorder, name string) *http.Cookie { t.Helper() for _, cookie := range response.Result().Cookies() { if cookie.Name == name { return cookie } } t.Fatalf("response did not set cookie %q", name) return nil }