package handlers_test import ( "context" "net/http" "net/http/httptest" "net/url" "strings" "testing" "github.com/go-chi/chi" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/config" "sneak.berlin/go/webhooker/internal/database" "sneak.berlin/go/webhooker/internal/handlers" "sneak.berlin/go/webhooker/internal/logger" "sneak.berlin/go/webhooker/internal/middleware" "sneak.berlin/go/webhooker/internal/session" ) // authenticatedCookies creates an authenticated session for the given // user and returns the resulting cookies for use on a later request. func authenticatedCookies( t *testing.T, sess *session.Session, userID, username string, ) []*http.Cookie { t.Helper() setupReq := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/setup", nil, ) setupW := httptest.NewRecorder() s, err := sess.Get(setupReq) require.NoError(t, err) sess.SetUser(s, userID, username) require.NoError(t, sess.Save(setupReq, setupW, s)) cookies := setupW.Result().Cookies() require.NotEmpty(t, cookies, "session cookie should be set") return cookies } // profileRequest builds a GET request for the given profile username, // attaching the supplied cookies and the chi URL parameter that the // handler reads via chi.URLParam. func profileRequest( username string, cookies []*http.Cookie, ) *http.Request { req := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/user/"+username, nil, ) for _, c := range cookies { req.AddCookie(c) } rctx := chi.NewRouteContext() rctx.URLParams.Add("username", username) return req.WithContext( context.WithValue(req.Context(), chi.RouteCtxKey, rctx), ) } func TestHandleProfile_OwnProfile_OK(t *testing.T) { t.Parallel() var h *handlers.Handlers var sess *session.Session app := newTestApp(t, &h, &sess) app.RequireStart() t.Cleanup(app.RequireStop) cookies := authenticatedCookies(t, sess, "test-user-id", "testuser") req := profileRequest("testuser", cookies) w := httptest.NewRecorder() h.HandleProfile().ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) } func TestHandleProfile_OtherProfile_Forbidden(t *testing.T) { t.Parallel() var h *handlers.Handlers var sess *session.Session app := newTestApp(t, &h, &sess) app.RequireStart() t.Cleanup(app.RequireStop) cookies := authenticatedCookies(t, sess, "test-user-id", "testuser") req := profileRequest("otheruser", cookies) w := httptest.NewRecorder() h.HandleProfile().ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code) } // TestUserRoute_Unauthenticated_RedirectedByMiddleware exercises the // /user/{username} route group's middleware chain (CSRF then // RequireAuth, matching setupUserRoutes) and proves that an // unauthenticated request is redirected to /pages/login at the // middleware layer, never reaching the endpoint handler. func TestUserRoute_Unauthenticated_RedirectedByMiddleware(t *testing.T) { t.Parallel() var log *logger.Logger var cfg *config.Config var sess *session.Session app := newTestApp(t, &log, &cfg, &sess) app.RequireStart() t.Cleanup(app.RequireStop) mw := middleware.NewForTest(log.Get(), cfg, sess) var handlerReached bool router := chi.NewRouter() router.Route("/user/{username}", func(r chi.Router) { r.Use(mw.CSRF()) r.Use(mw.RequireAuth()) r.Get("/", func(w http.ResponseWriter, _ *http.Request) { handlerReached = true w.WriteHeader(http.StatusOK) }) }) req := httptest.NewRequestWithContext( context.Background(), http.MethodGet, "/user/testuser", nil, ) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.False( t, handlerReached, "handler must not be reached for unauthenticated request", ) assert.Equal(t, http.StatusSeeOther, w.Code) assert.Equal(t, "/pages/login", w.Header().Get("Location")) } // passwordChangeRequest builds a POST request to the password-change // endpoint for the given username, attaching the supplied cookies, an // urlencoded form body, and the chi URL parameter the handler reads. func passwordChangeRequest( username string, cookies []*http.Cookie, form url.Values, ) *http.Request { req := httptest.NewRequestWithContext( context.Background(), http.MethodPost, "/user/"+username+"/password", strings.NewReader(form.Encode()), ) req.Header.Set( "Content-Type", "application/x-www-form-urlencoded", ) for _, c := range cookies { req.AddCookie(c) } rctx := chi.NewRouteContext() rctx.URLParams.Add("username", username) return req.WithContext( context.WithValue(req.Context(), chi.RouteCtxKey, rctx), ) } func TestHandlePasswordChange_Success(t *testing.T) { t.Parallel() var h *handlers.Handlers var sess *session.Session var db *database.Database app := newTestApp(t, &h, &sess, &db) app.RequireStart() t.Cleanup(app.RequireStop) oldHash, err := database.HashPassword("oldpassword") require.NoError(t, err) user := &database.User{Username: "pwuser", Password: oldHash} require.NoError(t, db.DB().Create(user).Error) cookies := authenticatedCookies(t, sess, user.ID, "pwuser") form := url.Values{} form.Set("current_password", "oldpassword") form.Set("new_password", "newpassword") form.Set("confirm_password", "newpassword") req := passwordChangeRequest("pwuser", cookies, form) w := httptest.NewRecorder() h.HandlePasswordChange().ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains( t, w.Body.String(), "Password changed successfully.", ) var updated database.User require.NoError(t, db.DB().Where("username = ?", "pwuser").First(&updated).Error, ) assert.NotEqual(t, oldHash, updated.Password) valid, err := database.VerifyPassword( "newpassword", updated.Password, ) require.NoError(t, err) assert.True(t, valid, "new password should verify against new hash") } func TestHandlePasswordChange_WrongCurrentPassword(t *testing.T) { t.Parallel() var h *handlers.Handlers var sess *session.Session var db *database.Database app := newTestApp(t, &h, &sess, &db) app.RequireStart() t.Cleanup(app.RequireStop) oldHash, err := database.HashPassword("oldpassword") require.NoError(t, err) user := &database.User{Username: "pwuser2", Password: oldHash} require.NoError(t, db.DB().Create(user).Error) cookies := authenticatedCookies(t, sess, user.ID, "pwuser2") form := url.Values{} form.Set("current_password", "wrongpassword") form.Set("new_password", "newpassword") form.Set("confirm_password", "newpassword") req := passwordChangeRequest("pwuser2", cookies, form) w := httptest.NewRecorder() h.HandlePasswordChange().ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code) assert.Contains( t, w.Body.String(), "Current password is incorrect.", ) var unchanged database.User require.NoError(t, db.DB().Where( "username = ?", "pwuser2", ).First(&unchanged).Error, ) assert.Equal( t, oldHash, unchanged.Password, "stored hash must be unchanged after a rejected change", ) }