-
Notifications
You must be signed in to change notification settings - Fork 136
tests,fix: Added unit tests for user password command #687
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -22,46 +22,71 @@ import ( | |||||
| "github.com/spf13/cobra" | ||||||
| ) | ||||||
|
|
||||||
| func UserPasswordChangeCmd() *cobra.Command { | ||||||
| var opts reset.PasswordChangeView | ||||||
| type UserPasswordChanger interface { | ||||||
| GetUserIDByName(username string) (int64, error) | ||||||
| GetUserIDFromUser() int64 | ||||||
| FillPasswordView(resetView *reset.PasswordChangeView) | ||||||
| ResetPassword(userID int64, resetView reset.PasswordChangeView) error | ||||||
| } | ||||||
|
|
||||||
| type DefaultUserPasswordChanger struct{} | ||||||
|
|
||||||
| func (d *DefaultUserPasswordChanger) GetUserIDByName(username string) (int64, error) { | ||||||
| return api.GetUsersIdByName(username) | ||||||
| } | ||||||
|
|
||||||
| func (d *DefaultUserPasswordChanger) GetUserIDFromUser() int64 { | ||||||
| return prompt.GetUserIdFromUser() | ||||||
| } | ||||||
|
|
||||||
| func (d *DefaultUserPasswordChanger) FillPasswordView(resetView *reset.PasswordChangeView) { | ||||||
| reset.ChangePasswordView(resetView) | ||||||
| } | ||||||
|
|
||||||
| func (d *DefaultUserPasswordChanger) ResetPassword(userID int64, resetView reset.PasswordChangeView) error { | ||||||
| return api.ResetPassword(userID, resetView) | ||||||
| } | ||||||
|
|
||||||
| func ChangePassword(args []string, passwordChanger UserPasswordChanger) { | ||||||
| var userID int64 | ||||||
| var err error | ||||||
| resetView := &reset.PasswordChangeView{} | ||||||
|
|
||||||
| if len(args) > 0 { | ||||||
| userID, err = passwordChanger.GetUserIDByName(args[0]) | ||||||
| if err != nil { | ||||||
| log.Errorf("failed to get user id for '%s': %v", args[0], err) | ||||||
| return | ||||||
| } | ||||||
| if userID == 0 { | ||||||
| log.Errorf("User with name '%s' not found", args[0]) | ||||||
| return | ||||||
| } | ||||||
| } else { | ||||||
| userID = passwordChanger.GetUserIDFromUser() | ||||||
| } | ||||||
|
|
||||||
| passwordChanger.FillPasswordView(resetView) | ||||||
|
|
||||||
| err = passwordChanger.ResetPassword(userID, *resetView) | ||||||
| if err != nil { | ||||||
| if isUnauthorizedError(err) { | ||||||
| log.Error("Permission denied: Admin privileges are required to execute this command.") | ||||||
| } else { | ||||||
| log.Errorf("failed to reset user password: %v", err) | ||||||
| } | ||||||
| } | ||||||
|
Comment on lines
+71
to
+78
|
||||||
| } | ||||||
|
|
||||||
| func UserPasswordChangeCmd() *cobra.Command { | ||||||
| cmd := &cobra.Command{ | ||||||
| Use: "password", | ||||||
| Short: "Reset user password by name or id", | ||||||
| Long: "Allows admin to reset the password for a specified user or select interactively if no username is provided.", | ||||||
| Args: cobra.MinimumNArgs(0), | ||||||
|
||||||
| Args: cobra.MinimumNArgs(0), | |
| Args: cobra.MaximumNArgs(1), |
Copilot
AI
Feb 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR description says the tests are for the "user list" command, but the changed files and new tests are for the user password command (cmd/harbor/root/user/password.go). Please update the PR description to match the actual scope to avoid confusion during review/release notes.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| // Copyright Project Harbor Authors | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
| package user | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "fmt" | ||
| "testing" | ||
|
|
||
| "github.com/goharbor/harbor-cli/pkg/views/password/reset" | ||
| log "github.com/sirupsen/logrus" | ||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| type MockUserPasswordChanger struct { | ||
| id map[string]int64 | ||
| passwords map[int64]string | ||
| userCnt int | ||
| expectAuthError bool | ||
| } | ||
|
Comment on lines
+26
to
+31
|
||
|
|
||
| func (m *MockUserPasswordChanger) GetUserIDByName(username string) (int64, error) { | ||
| if v, ok := m.id[username]; ok { | ||
| return v, nil | ||
| } | ||
| return 0, fmt.Errorf("username %s not found", username) | ||
| } | ||
|
|
||
| func (m *MockUserPasswordChanger) GetUserIDFromUser() int64 { | ||
| return 999 | ||
| } | ||
|
|
||
| func (m *MockUserPasswordChanger) FillPasswordView(resetView *reset.PasswordChangeView) { | ||
| resetView.NewPassword = "NewPass456" | ||
| resetView.ConfirmPassword = "NewPass456" | ||
| } | ||
|
|
||
| func (m *MockUserPasswordChanger) ResetPassword(userID int64, resetView reset.PasswordChangeView) error { | ||
| if m.expectAuthError { | ||
| return fmt.Errorf("403") | ||
| } | ||
| if _, ok := m.passwords[userID]; !ok { | ||
| return fmt.Errorf("user %d not found", userID) | ||
| } | ||
| m.passwords[userID] = resetView.NewPassword | ||
| return nil | ||
| } | ||
|
|
||
| func initMockUserPasswordChanger(userCnt int, expectAuthError bool) *MockUserPasswordChanger { | ||
| m := &MockUserPasswordChanger{ | ||
| userCnt: userCnt, | ||
| expectAuthError: expectAuthError, | ||
| id: make(map[string]int64), | ||
| passwords: make(map[int64]string), | ||
| } | ||
| for i := 0; i < userCnt; i++ { | ||
| m.id[fmt.Sprintf("test%d", i+1)] = int64(i + 1) | ||
| m.passwords[int64(i+1)] = "InitialPass123" | ||
| } | ||
| return m | ||
| } | ||
|
|
||
| func TestChangePassword(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| setup func() *MockUserPasswordChanger | ||
| args []string | ||
| expectedPasswordID int64 | ||
| expectedNewPassword string | ||
| expectedErr string | ||
| }{ | ||
| { | ||
| name: "successfully change password by username", | ||
| setup: func() *MockUserPasswordChanger { | ||
| return initMockUserPasswordChanger(5, false) | ||
| }, | ||
| args: []string{"test1"}, | ||
| expectedPasswordID: 1, | ||
| expectedNewPassword: "NewPass456", | ||
| expectedErr: "", | ||
| }, | ||
| { | ||
| name: "change password via interactive prompt", | ||
| setup: func() *MockUserPasswordChanger { | ||
| m := initMockUserPasswordChanger(5, false) | ||
| m.id["promptuser"] = 999 | ||
| m.passwords[999] = "InitialPass123" | ||
| return m | ||
|
Comment on lines
+95
to
+99
|
||
| }, | ||
| args: []string{}, | ||
| expectedPasswordID: 999, | ||
| expectedNewPassword: "NewPass456", | ||
| expectedErr: "", | ||
| }, | ||
| { | ||
| name: "user not found logs error", | ||
| setup: func() *MockUserPasswordChanger { | ||
| return initMockUserPasswordChanger(5, false) | ||
| }, | ||
| args: []string{"nonexistent"}, | ||
| expectedPasswordID: 0, | ||
| expectedNewPassword: "", | ||
| expectedErr: "failed to get user id", | ||
| }, | ||
| { | ||
| name: "permission denied error", | ||
| setup: func() *MockUserPasswordChanger { | ||
| return initMockUserPasswordChanger(5, true) | ||
| }, | ||
| args: []string{"test1"}, | ||
| expectedPasswordID: 0, | ||
| expectedNewPassword: "", | ||
| expectedErr: "Permission denied", | ||
| }, | ||
| } | ||
|
|
||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| var buf bytes.Buffer | ||
| originalLogOutput := log.StandardLogger().Out | ||
| log.SetOutput(&buf) | ||
| defer log.SetOutput(originalLogOutput) | ||
|
|
||
| m := tt.setup() | ||
| ChangePassword(tt.args, m) | ||
|
|
||
| logs := buf.String() | ||
|
|
||
| if tt.expectedErr != "" { | ||
| assert.Contains(t, logs, tt.expectedErr, "Expected error logs to contain %s but got %s", tt.expectedErr, logs) | ||
| } else { | ||
| assert.Empty(t, logs, "Expected no error logs but got: %s", logs) | ||
| } | ||
|
|
||
| if tt.expectedPasswordID != 0 { | ||
| password, exists := m.passwords[tt.expectedPasswordID] | ||
| assert.True(t, exists, "User with ID %d should exist", tt.expectedPasswordID) | ||
| assert.Equal(t, tt.expectedNewPassword, password, "Password for user %d should be changed to %s", tt.expectedPasswordID, tt.expectedNewPassword) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test coverage gap: the production implementation
api.GetUsersIdByNamereturns(0, nil)when the user is not found, andChangePasswordhas a dedicateduserID == 0branch. The tests only cover the "not found" case via an error return, so theuserID == 0branch is currently untested.