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.
 
 
 

155 lines
6.0 KiB

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
}