Commit 64197672 authored by Patrick Rice's avatar Patrick Rice ❄️
Browse files

Merge branch 'more-do-requests' into 'main'

Trim leading `@` in user ids in `do()` requests paths

See merge request !2736
parents 9bc53b1e 04595c64
Loading
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -59,10 +59,8 @@ type (

		// ListUserProjectDeployKeys gets a list of a user's deploy keys.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user

		// ListUserProjectDeployKeys gets a list of a user's deploy keys.
		// uid can be either a user ID (int) or a username (string). If a username
		// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user
@@ -225,6 +223,13 @@ type ListUserProjectDeployKeysOptions struct {
	ListOptions
}

// ListUserProjectDeployKeys gets a list of a user's deploy keys.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/deploy_keys/#list-project-deploy-keys-for-user
func (s *DeployKeysService) ListUserProjectDeployKeys(uid any, opt *ListUserProjectDeployKeysOptions, options ...RequestOptionFunc) ([]*ProjectDeployKey, *Response, error) {
	return do[[]*ProjectDeployKey](s.client,
		withPath("users/%s/project_deploy_keys", UserID{uid}),
+24 −1
Original line number Diff line number Diff line
@@ -32,8 +32,21 @@ type (
	// GitLab API docs: https://docs.gitlab.com/api/projects/
	ProjectsServiceInterface interface {
		ListProjects(opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error)
		// ListUserProjects gets a list of projects for the given user.
		//
		// uid can be either a user ID (int) or a username (string). If a username
		// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
		ListUserProjects(uid any, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error)
		// ListUserContributedProjects gets a list of visible projects a given user
		// has contributed to.
		//
		// uid can be either a user ID (int) or a username (string). If a username
		// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
		ListUserContributedProjects(uid any, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error)
		// ListUserStarredProjects gets a list of projects starred by the given user.
		//
		// uid can be either a user ID (int) or a username (string). If a username
		// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
		ListUserStarredProjects(uid any, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error)
		ListProjectsUsers(pid any, opt *ListProjectUserOptions, options ...RequestOptionFunc) ([]*ProjectUser, *Response, error)
		ListProjectsGroups(pid any, opt *ListProjectGroupOptions, options ...RequestOptionFunc) ([]*ProjectGroup, *Response, error)
@@ -464,6 +477,9 @@ func (s *ProjectsService) ListProjects(opt *ListProjectsOptions, options ...Requ

// ListUserProjects gets a list of projects for the given user.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/projects/#list-a-users-projects
func (s *ProjectsService) ListUserProjects(uid any, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error) {
@@ -474,7 +490,11 @@ func (s *ProjectsService) ListUserProjects(uid any, opt *ListProjectsOptions, op
	)
}

// ListUserContributedProjects gets a list of visible projects a given user has contributed to.
// ListUserContributedProjects gets a list of visible projects a given user has
// contributed to.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/projects/#list-projects-a-user-has-contributed-to
@@ -488,6 +508,9 @@ func (s *ProjectsService) ListUserContributedProjects(uid any, opt *ListProjects

// ListUserStarredProjects gets a list of projects starred by the given user.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/project_starring/#list-projects-starred-by-a-user
func (s *ProjectsService) ListUserStarredProjects(uid any, opt *ListProjectsOptions, options ...RequestOptionFunc) ([]*Project, *Response, error) {
+5 −1
Original line number Diff line number Diff line
@@ -5,6 +5,7 @@ import (
	"io"
	"net/http"
	"reflect"
	"strings"

	"github.com/hashicorp/go-retryablehttp"
)
@@ -52,6 +53,9 @@ func (i RunnerID) forPath() (string, error) {
	return PathEscape(id), nil
}

// UserID represents a user identifier for API paths. It accepts either a
// numeric user ID or a username string. If a username is provided with a
// leading "@" character (e.g., "@johndoe"), the "@" will be trimmed.
type UserID struct {
	Value any
}
@@ -62,7 +66,7 @@ func (i UserID) forPath() (string, error) {
		return "", err
	}

	return PathEscape(id), nil
	return PathEscape(strings.TrimPrefix(id, "@")), nil
}

type LabelID struct {
+61 −9
Original line number Diff line number Diff line
@@ -361,22 +361,74 @@ func TestDoRequestRunnerID(t *testing.T) {

func TestDoRequestUserID(t *testing.T) {
	t.Parallel()

	tests := []struct {
		name         string
		uid          any
		expectedPath string
	}{
		{
			name:         "numeric user ID",
			uid:          123,
			expectedPath: "/api/v4/users/123/status",
		},
		{
			name:         "username string",
			uid:          "johndoe",
			expectedPath: "/api/v4/users/johndoe/status",
		},
		{
			name:         "username with @ prefix is trimmed",
			uid:          "@johndoe",
			expectedPath: "/api/v4/users/johndoe/status",
		},
		{
			name:         "username with slash is escaped",
			uid:          "test/user",
			expectedPath: "/api/v4/users/test%2Fuser/status",
		},
		{
			name:         "username with @ prefix and slash",
			uid:          "@test/user",
			expectedPath: "/api/v4/users/test%2Fuser/status",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			t.Parallel()
			mux, client := setup(t)

			// GIVEN
	mux.HandleFunc("/api/v4/users/test%2Fuser", func(w http.ResponseWriter, r *http.Request) {
			mux.HandleFunc(tt.expectedPath, func(w http.ResponseWriter, r *http.Request) {
				w.WriteHeader(200)
			})

			// WHEN
			_, resp, err := do[none](
				client,
		withPath("users/%s", UserID{"test/user"}),
				withPath("users/%s/status", UserID{tt.uid}),
			)

			// THEN
			assert.NoError(t, err)
			assert.Equal(t, 200, resp.StatusCode)
		})
	}
}

func TestDoRequestUserIDInvalidType(t *testing.T) {
	t.Parallel()
	_, client := setup(t)

	// WHEN
	_, _, err := do[none](
		client,
		withPath("users/%s/status", UserID{struct{ ID int }{ID: 1}}),
	)

	// THEN
	assert.ErrorIs(t, err, ErrInvalidIDType)
}

func TestDoRequestUploadSuccess(t *testing.T) {
+21 −20
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ import (
	"io"
	"net"
	"net/http"
	"strings"
	"time"
)

@@ -87,6 +86,9 @@ type (
		ListSSHKeys(opt *ListSSHKeysOptions, options ...RequestOptionFunc) ([]*SSHKey, *Response, error)
		// ListSSHKeysForUser gets a list of a specified user's SSH keys.
		//
		// uid can be either a user ID (int) or a username (string). If a username
		// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
		//
		// GitLab API docs:
		// https://docs.gitlab.com/api/user_keys/#list-all-ssh-keys-for-a-user
		ListSSHKeysForUser(uid any, opt *ListSSHKeysForUserOptions, options ...RequestOptionFunc) ([]*SSHKey, *Response, error)
@@ -606,26 +608,18 @@ func (s *UsersService) CurrentUserStatus(options ...RequestOptionFunc) (*UserSta
	)
}

// GetUserStatus retrieves a user's status.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/users/#get-the-status-of-a-user
func (s *UsersService) GetUserStatus(uid any, options ...RequestOptionFunc) (*UserStatus, *Response, error) {
	user, err := parseID(uid)
	if err != nil {
		return nil, nil, err
	}

	u := fmt.Sprintf("users/%s/status", strings.TrimPrefix(user, "@"))

	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
	if err != nil {
		return nil, nil, err
	}

	status := new(UserStatus)
	resp, err := s.client.Do(req, status)
	if err != nil {
		return nil, resp, err
	}

	return status, resp, nil
	return do[*UserStatus](s.client,
		withPath("users/%s/status", UserID{uid}),
		withRequestOpts(options...),
	)
}

// UserStatusOptions represents the options required to set the status
@@ -705,6 +699,13 @@ type ListSSHKeysForUserOptions struct {
	ListOptions
}

// ListSSHKeysForUser gets a list of a specified user's SSH keys.
//
// uid can be either a user ID (int) or a username (string). If a username
// is provided with a leading "@" (e.g., "@johndoe"), it will be trimmed.
//
// GitLab API docs:
// https://docs.gitlab.com/api/user_keys/#list-all-ssh-keys-for-a-user
func (s *UsersService) ListSSHKeysForUser(uid any, opt *ListSSHKeysForUserOptions, options ...RequestOptionFunc) ([]*SSHKey, *Response, error) {
	return do[[]*SSHKey](s.client,
		withPath("users/%s/keys", UserID{uid}),