From 4a64b87072ca451d94fe8856aa090476ba8f8923 Mon Sep 17 00:00:00 2001 From: CaIon Date: Sun, 5 Jul 2026 13:53:25 +0800 Subject: [PATCH] test(user): cover self-service password update guard Pin down the checkUpdatePassword contract: - changing a password requires the correct current password - OAuth/passwordless accounts (empty password hash) cannot set a password through the self-service endpoint and must use the password reset flow instead - setupLogin never writes back the password column These guard against regressing the password-change path back into a short-circuit that lets passwordless accounts set a password directly. --- controller/model_list_test.go | 81 +++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/controller/model_list_test.go b/controller/model_list_test.go index 9fba5b4a..3d09956b 100644 --- a/controller/model_list_test.go +++ b/controller/model_list_test.go @@ -14,8 +14,11 @@ import ( "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting/config" "github.com/QuantumNous/new-api/setting/operation_setting" + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gorm.io/gorm" ) @@ -284,3 +287,81 @@ func TestListModelsTokenLimitIncludesTieredBillingModel(t *testing.T) { require.NotContains(t, ids, "zz-token-tiered-missing-expr-model") require.NotContains(t, ids, "zz-token-unpriced-model") } + +func TestCheckUpdatePasswordRequiresCurrentPassword(t *testing.T) { + db := setupModelListControllerTestDB(t) + hashedPassword, err := common.Password2Hash("CurrentPassword123") + require.NoError(t, err) + user := &model.User{ + Username: "password-user", + Password: hashedPassword, + Status: common.UserStatusEnabled, + } + require.NoError(t, db.Create(user).Error) + + updatePassword, err := checkUpdatePassword("", "", user.Id) + require.NoError(t, err) + assert.False(t, updatePassword) + + updatePassword, err = checkUpdatePassword("", "NewPassword123", user.Id) + require.Error(t, err) + assert.False(t, updatePassword) + assert.ErrorIs(t, err, errOriginalPasswordFail) + + updatePassword, err = checkUpdatePassword("CurrentPassword123", "NewPassword123", user.Id) + require.NoError(t, err) + assert.True(t, updatePassword) +} + +func TestCheckUpdatePasswordRejectsHistoricalEmptyPassword(t *testing.T) { + db := setupModelListControllerTestDB(t) + user := &model.User{ + Username: "legacy-passwordless-user", + Password: "", + Status: common.UserStatusEnabled, + } + require.NoError(t, db.Create(user).Error) + + updatePassword, err := checkUpdatePassword("", "NewPassword123", user.Id) + require.Error(t, err) + assert.False(t, updatePassword) + assert.ErrorIs(t, err, errUserPasswordUnset) +} + +func TestSetupLoginDoesNotTouchPasswordWhenPasswordFieldOmitted(t *testing.T) { + db := setupModelListControllerTestDB(t) + require.NoError(t, db.AutoMigrate(&model.Log{})) + + hashedPassword, err := common.Password2Hash("CurrentPassword123") + require.NoError(t, err) + user := &model.User{ + Username: "twofa-user", + Password: hashedPassword, + Role: common.RoleCommonUser, + Status: common.UserStatusEnabled, + Group: "default", + } + require.NoError(t, db.Create(user).Error) + + router := gin.New() + store := cookie.NewStore([]byte("test-session-secret")) + router.Use(sessions.Sessions("session", store)) + router.GET("/", func(c *gin.Context) { + setupLogin(&model.User{ + Id: user.Id, + Username: user.Username, + Role: user.Role, + Status: user.Status, + Group: user.Group, + }, c) + }) + + recorder := httptest.NewRecorder() + request := httptest.NewRequest(http.MethodGet, "/", nil) + router.ServeHTTP(recorder, request) + + require.Equal(t, http.StatusOK, recorder.Code) + var stored model.User + require.NoError(t, db.First(&stored, user.Id).Error) + assert.Equal(t, hashedPassword, stored.Password) +}