summaryrefslogtreecommitdiff
path: root/internal/handlers/handler_change_password_test.go
diff options
context:
space:
mode:
authorBrynn Crowley <littlehill723@gmail.com>2025-03-06 08:24:19 +0000
committerGitHub <noreply@github.com>2025-03-06 08:24:19 +0000
commitf4abcb34b757e40467344ffdd7cec9f77f46a227 (patch)
treef3cc73da2ebaa978186f6f470d5bd27b279f6a96 /internal/handlers/handler_change_password_test.go
parent5b52a9d4b18b5a07b1edb7403b6dc90b8d5c628d (diff)
feat(web): change password (#7676)
Add the ability for users to change their password from their user settings, without requiring them to use the reset password workflow. User's are required to create a elevated session in order to change their password. Users may not change their password to their current password. The user's current password is required for the password change. Users must follow any established password policies. Administrators are able to turn this feature off. Closes #3548
Diffstat (limited to 'internal/handlers/handler_change_password_test.go')
-rw-r--r--internal/handlers/handler_change_password_test.go302
1 files changed, 302 insertions, 0 deletions
diff --git a/internal/handlers/handler_change_password_test.go b/internal/handlers/handler_change_password_test.go
new file mode 100644
index 000000000..3783fef8e
--- /dev/null
+++ b/internal/handlers/handler_change_password_test.go
@@ -0,0 +1,302 @@
+package handlers
+
+import (
+ "encoding/json"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/suite"
+ "github.com/valyala/fasthttp"
+ "go.uber.org/mock/gomock"
+
+ "github.com/authelia/authelia/v4/internal/authentication"
+
+ "github.com/authelia/authelia/v4/internal/configuration/schema"
+ "github.com/authelia/authelia/v4/internal/middlewares"
+ "github.com/authelia/authelia/v4/internal/mocks"
+)
+
+const (
+ testPasswordOld = "old_password123"
+ testPasswordNew = "new_password456"
+)
+
+type ChangePasswordSuite struct {
+ suite.Suite
+ mock *mocks.MockAutheliaCtx
+}
+
+func (s *ChangePasswordSuite) SetupTest() {
+ s.mock = mocks.NewMockAutheliaCtx(s.T())
+ userSession, err := s.mock.Ctx.GetSession()
+ s.Assert().NoError(err)
+
+ userSession.Username = testUsername
+ userSession.DisplayName = testUsername
+ userSession.Emails[0] = testEmail
+ userSession.AuthenticationMethodRefs.UsernameAndPassword = true
+ s.Assert().NoError(s.mock.Ctx.SaveSession(userSession))
+}
+
+func (s *ChangePasswordSuite) TearDownTest() {
+ s.mock.Close()
+}
+
+func TestChangePasswordPOST_ShouldSucceedWithValidCredentials(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := testPasswordNew
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ mock.Ctx.Providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{})
+
+ mock.NotifierMock.EXPECT().
+ Send(mock.Ctx, gomock.Any(), "Password changed successfully", gomock.Any(), gomock.Any()).
+ Return(nil)
+
+ mock.UserProviderMock.EXPECT().
+ ChangePassword(userSession.Username, oldPassword, newPassword).
+ Return(nil)
+
+ mock.UserProviderMock.EXPECT().
+ GetDetails(testUsername).
+ Return(&authentication.UserDetails{
+ Emails: []string{testEmail},
+ }, nil)
+
+ ChangePasswordPOST(mock.Ctx)
+
+ assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+}
+
+func TestChangePasswordPOST_ShouldFailWhenPasswordPolicyNotMet(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := "weak"
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ passwordPolicy := middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{
+ Standard: schema.PasswordPolicyStandard{
+ Enabled: true,
+ MinLength: 8,
+ MaxLength: 64,
+ RequireNumber: true,
+ RequireSpecial: true,
+ RequireUppercase: true,
+ RequireLowercase: true,
+ },
+ })
+
+ mock.Ctx.Providers.PasswordPolicy = passwordPolicy
+
+ ChangePasswordPOST(mock.Ctx)
+
+ errResponse := mock.GetResponseError(t)
+
+ assert.Equal(t, "KO", errResponse.Status)
+ assert.Equal(t, "Your supplied password does not meet the password policy requirements.", errResponse.Message)
+}
+
+func TestChangePasswordPOST_ShouldFailWhenRequestBodyIsInvalid(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ mock.Ctx.Request.SetBody([]byte(`{invalid json`))
+
+ ChangePasswordPOST(mock.Ctx)
+
+ errResponse := mock.GetResponseError(t)
+ assert.Equal(t, "KO", errResponse.Status)
+ assert.Equal(t, messageUnableToChangePassword, errResponse.Message)
+}
+
+func TestChangePasswordPOST_ShouldFailWhenOldPasswordIsIncorrect(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := testPasswordNew
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ mock.Ctx.Providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{})
+
+ mock.UserProviderMock.EXPECT().
+ ChangePassword(testUsername, oldPassword, newPassword).
+ Return(authentication.ErrIncorrectPassword)
+
+ ChangePasswordPOST(mock.Ctx)
+
+ errResponse := mock.GetResponseError(t)
+ assert.Equal(t, "KO", errResponse.Status)
+ assert.Equal(t, messageIncorrectPassword, errResponse.Message)
+}
+
+func TestChangePasswordPOST_ShouldFailWhenPasswordReuseIsNotAllowed(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := testPasswordOld
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ mock.Ctx.Providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{})
+
+ mock.UserProviderMock.EXPECT().
+ ChangePassword(testUsername, oldPassword, newPassword).
+ Return(authentication.ErrPasswordWeak)
+
+ ChangePasswordPOST(mock.Ctx)
+
+ errResponse := mock.GetResponseError(t)
+ assert.Equal(t, "KO", errResponse.Status)
+ assert.Equal(t, messagePasswordWeak, errResponse.Message)
+}
+
+func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenUserHasNoEmail(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := testPasswordNew
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ mock.Ctx.Providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{})
+
+ mock.UserProviderMock.EXPECT().
+ ChangePassword(testUsername, oldPassword, newPassword).
+ Return(nil)
+
+ mock.UserProviderMock.EXPECT().
+ GetDetails(testUsername).
+ Return(&authentication.UserDetails{
+ Emails: []string{},
+ }, nil)
+
+ ChangePasswordPOST(mock.Ctx)
+
+ assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+}
+
+func TestChangePasswordPOST_ShouldSucceedButLogErrorWhenNotificationFails(t *testing.T) {
+ mock := mocks.NewMockAutheliaCtx(t)
+
+ userSession, err := mock.Ctx.GetSession()
+ assert.NoError(t, err)
+
+ userSession.Username = testUsername
+
+ assert.NoError(t, mock.Ctx.SaveSession(userSession))
+
+ oldPassword := testPasswordOld
+ newPassword := testPasswordNew
+
+ requestBody := changePasswordRequestBody{
+ OldPassword: oldPassword,
+ NewPassword: newPassword,
+ }
+
+ bodyBytes, err := json.Marshal(requestBody)
+ assert.NoError(t, err)
+ mock.Ctx.Request.SetBody(bodyBytes)
+
+ mock.Ctx.Providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(schema.PasswordPolicy{})
+
+ mock.UserProviderMock.EXPECT().
+ ChangePassword(testUsername, testPasswordOld, newPassword).
+ Return(nil)
+
+ mock.UserProviderMock.EXPECT().
+ GetDetails(testUsername).
+ Return(&authentication.UserDetails{
+ Emails: []string{testEmail},
+ }, nil)
+
+ mock.NotifierMock.EXPECT().
+ Send(mock.Ctx, gomock.Any(), "Password changed successfully", gomock.Any(), gomock.Any()).
+ Return(fmt.Errorf("failed to send notification"))
+
+ ChangePasswordPOST(mock.Ctx)
+
+ assert.Equal(t, fasthttp.StatusOK, mock.Ctx.Response.StatusCode())
+}