From 04f9a4246769c3b0a000f9a911f4b3c4aa6939fb Mon Sep 17 00:00:00 2001 From: Ary Neto Date: Sat, 20 Jul 2024 15:51:04 -0600 Subject: [PATCH 1/4] feat: Add ListFineGrainedPersonalAccessTokens API --- github/github-accessors.go | 72 +++++++++++++ github/github-accessors_test.go | 84 +++++++++++++++ github/orgs_personal_access_tokens.go | 63 +++++++++++ github/orgs_personal_access_tokens_test.go | 116 +++++++++++++++++++++ 4 files changed, 335 insertions(+) diff --git a/github/github-accessors.go b/github/github-accessors.go index 5118791cdd2..a599b605833 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -14326,6 +14326,78 @@ func (p *PagesUpdate) GetSource() *PagesSource { return p.Source } +// GetAccessGrantedAt returns the AccessGrantedAt field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetAccessGrantedAt() Timestamp { + if p == nil || p.AccessGrantedAt == nil { + return Timestamp{} + } + return *p.AccessGrantedAt +} + +// GetID returns the ID field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetID() int64 { + if p == nil || p.ID == nil { + return 0 + } + return *p.ID +} + +// GetOwner returns the Owner field. +func (p *PersonalAccessToken) GetOwner() *User { + if p == nil { + return nil + } + return p.Owner +} + +// GetPermissions returns the Permissions field. +func (p *PersonalAccessToken) GetPermissions() *PersonalAccessTokenPermissions { + if p == nil { + return nil + } + return p.Permissions +} + +// GetRepositoriesURL returns the RepositoriesURL field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetRepositoriesURL() string { + if p == nil || p.RepositoriesURL == nil { + return "" + } + return *p.RepositoriesURL +} + +// GetRepositorySelection returns the RepositorySelection field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetRepositorySelection() string { + if p == nil || p.RepositorySelection == nil { + return "" + } + return *p.RepositorySelection +} + +// GetTokenExpired returns the TokenExpired field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetTokenExpired() bool { + if p == nil || p.TokenExpired == nil { + return false + } + return *p.TokenExpired +} + +// GetTokenExpiresAt returns the TokenExpiresAt field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetTokenExpiresAt() Timestamp { + if p == nil || p.TokenExpiresAt == nil { + return Timestamp{} + } + return *p.TokenExpiresAt +} + +// GetTokenLastUsedAt returns the TokenLastUsedAt field if it's non-nil, zero value otherwise. +func (p *PersonalAccessToken) GetTokenLastUsedAt() Timestamp { + if p == nil || p.TokenLastUsedAt == nil { + return Timestamp{} + } + return *p.TokenLastUsedAt +} + // GetOrg returns the Org map if it's non-nil, an empty map otherwise. func (p *PersonalAccessTokenPermissions) GetOrg() map[string]string { if p == nil || p.Org == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 03ebada64b1..73fe57adb9e 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -16768,6 +16768,90 @@ func TestPagesUpdate_GetSource(tt *testing.T) { p.GetSource() } +func TestPersonalAccessToken_GetAccessGrantedAt(tt *testing.T) { + var zeroValue Timestamp + p := &PersonalAccessToken{AccessGrantedAt: &zeroValue} + p.GetAccessGrantedAt() + p = &PersonalAccessToken{} + p.GetAccessGrantedAt() + p = nil + p.GetAccessGrantedAt() +} + +func TestPersonalAccessToken_GetID(tt *testing.T) { + var zeroValue int64 + p := &PersonalAccessToken{ID: &zeroValue} + p.GetID() + p = &PersonalAccessToken{} + p.GetID() + p = nil + p.GetID() +} + +func TestPersonalAccessToken_GetOwner(tt *testing.T) { + p := &PersonalAccessToken{} + p.GetOwner() + p = nil + p.GetOwner() +} + +func TestPersonalAccessToken_GetPermissions(tt *testing.T) { + p := &PersonalAccessToken{} + p.GetPermissions() + p = nil + p.GetPermissions() +} + +func TestPersonalAccessToken_GetRepositoriesURL(tt *testing.T) { + var zeroValue string + p := &PersonalAccessToken{RepositoriesURL: &zeroValue} + p.GetRepositoriesURL() + p = &PersonalAccessToken{} + p.GetRepositoriesURL() + p = nil + p.GetRepositoriesURL() +} + +func TestPersonalAccessToken_GetRepositorySelection(tt *testing.T) { + var zeroValue string + p := &PersonalAccessToken{RepositorySelection: &zeroValue} + p.GetRepositorySelection() + p = &PersonalAccessToken{} + p.GetRepositorySelection() + p = nil + p.GetRepositorySelection() +} + +func TestPersonalAccessToken_GetTokenExpired(tt *testing.T) { + var zeroValue bool + p := &PersonalAccessToken{TokenExpired: &zeroValue} + p.GetTokenExpired() + p = &PersonalAccessToken{} + p.GetTokenExpired() + p = nil + p.GetTokenExpired() +} + +func TestPersonalAccessToken_GetTokenExpiresAt(tt *testing.T) { + var zeroValue Timestamp + p := &PersonalAccessToken{TokenExpiresAt: &zeroValue} + p.GetTokenExpiresAt() + p = &PersonalAccessToken{} + p.GetTokenExpiresAt() + p = nil + p.GetTokenExpiresAt() +} + +func TestPersonalAccessToken_GetTokenLastUsedAt(tt *testing.T) { + var zeroValue Timestamp + p := &PersonalAccessToken{TokenLastUsedAt: &zeroValue} + p.GetTokenLastUsedAt() + p = &PersonalAccessToken{} + p.GetTokenLastUsedAt() + p = nil + p.GetTokenLastUsedAt() +} + func TestPersonalAccessTokenPermissions_GetOrg(tt *testing.T) { zeroValue := map[string]string{} p := &PersonalAccessTokenPermissions{Org: zeroValue} diff --git a/github/orgs_personal_access_tokens.go b/github/orgs_personal_access_tokens.go index 0d786114f8c..c3ba1ac7c39 100644 --- a/github/orgs_personal_access_tokens.go +++ b/github/orgs_personal_access_tokens.go @@ -11,6 +11,69 @@ import ( "net/http" ) +// PersonalAccessToken represents the minimal representation of an organization programmatic access grant. +// +// GitHub API docs: https://docs.github.com/en/rest/orgs/personal-access-tokens?apiVersion=2022-11-28 +type PersonalAccessToken struct { + // "Unique identifier of the fine-grained personal access token. + // The `pat_id` used to get details about an approved fine-grained personal access token. + ID *int64 `json:"id"` + + // Owner is the GitHub user associated with the token. + Owner *User `json:"owner"` + + // RepositorySelection is the type of repository selection requested. + // Possible values are: "none", "all", "subset". + RepositorySelection *string `json:"repository_selection"` + + // URL to the list of repositories the fine-grained personal access token can access. + // Only follow when `repository_selection` is `subset`. + RepositoriesURL *string `json:"repositories_url"` + + // Permissions are the permissions requested, categorized by type. + Permissions *PersonalAccessTokenPermissions `json:"permissions"` + + // Date and time when the fine-grained personal access token was approved to access the organization. + AccessGrantedAt *Timestamp `json:"access_granted_at"` + + // Whether the associated fine-grained personal access token has expired. + TokenExpired *bool `json:"token_expired"` + + // Date and time when the associated fine-grained personal access token expires. + TokenExpiresAt *Timestamp `json:"token_expires_at"` + + // Date and time when the associated fine-grained personal access token was last used for authentication. + TokenLastUsedAt *Timestamp `json:"token_last_used_at"` +} + +// ListFineGrainedPersonalAccessTokens lists approved fine-grained personal access tokens owned by organization members that can access organization resources. +// Only GitHub Apps can call this API, using the `Personal access tokens` organization permissions (read). +// +// GitHub API docs: https://docs.github.com/rest/orgs/personal-access-tokens#list-fine-grained-personal-access-tokens-with-access-to-organization-resources +// +//meta:operation GET /orgs/{org}/personal-access-tokens +func (s *OrganizationsService) ListFineGrainedPersonalAccessTokens(ctx context.Context, org string, opts *ListOptions) ([]*PersonalAccessToken, *Response, error) { + u := fmt.Sprintf("orgs/%v/personal-access-tokens", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(http.MethodGet, u, &opts) + if err != nil { + return nil, nil, err + } + + var pats []*PersonalAccessToken + + resp, err := s.client.Do(ctx, req, &pats) + if err != nil { + return nil, resp, err + } + + return pats, resp, nil +} + // ReviewPersonalAccessTokenRequestOptions specifies the parameters to the ReviewPersonalAccessTokenRequest method. type ReviewPersonalAccessTokenRequestOptions struct { Action string `json:"action"` diff --git a/github/orgs_personal_access_tokens_test.go b/github/orgs_personal_access_tokens_test.go index 9426385f4a6..daad72fdb0c 100644 --- a/github/orgs_personal_access_tokens_test.go +++ b/github/orgs_personal_access_tokens_test.go @@ -8,12 +8,128 @@ package github import ( "context" "encoding/json" + "fmt" "net/http" "testing" + "time" "github.com/google/go-cmp/cmp" ) +func TestOrganizationsService_ListFineGrainedPersonalAccessTokens(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/personal-access-tokens", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"per_page": "2", "page": "2"}) + fmt.Fprint(w, ` + [ + { + "id": 25381, + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "repository_selection": "all", + "repositories_url": "https://api.github.com/organizations/652551/personal-access-tokens/25381/repositories", + "permissions": { + "organization": { + "members": "read" + }, + "repository": { + "metadata": "read" + } + }, + "access_granted_at": "2023-05-16T08:47:09.000-07:00", + "token_expired": false, + "token_expires_at": "2023-11-16T08:47:09.000-07:00", + "token_last_used_at": null + } + ]`) + }) + + opts := &ListOptions{Page: 2, PerPage: 2} + ctx := context.Background() + tokens, resp, err := client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, "o", opts) + if err != nil { + t.Errorf("Organizations.ListFineGrainedPersonalAccessTokens returned error: %v", err) + } + + want := []*PersonalAccessToken{ + { + ID: Int64(25381), + Owner: &User{ + Login: String("octocat"), + ID: Int64(1), + NodeID: String("MDQ6VXNlcjE="), + AvatarURL: String("https://github.com/images/error/octocat_happy.gif"), + GravatarID: String(""), + URL: String("https://api.github.com/users/octocat"), + HTMLURL: String("https://github.com/octocat"), + FollowersURL: String("https://api.github.com/users/octocat/followers"), + FollowingURL: String("https://api.github.com/users/octocat/following{/other_user}"), + GistsURL: String("https://api.github.com/users/octocat/gists{/gist_id}"), + StarredURL: String("https://api.github.com/users/octocat/starred{/owner}{/repo}"), + SubscriptionsURL: String("https://api.github.com/users/octocat/subscriptions"), + OrganizationsURL: String("https://api.github.com/users/octocat/orgs"), + ReposURL: String("https://api.github.com/users/octocat/repos"), + EventsURL: String("https://api.github.com/users/octocat/events{/privacy}"), + ReceivedEventsURL: String("https://api.github.com/users/octocat/received_events"), + Type: String("User"), + SiteAdmin: Bool(false), + }, + RepositorySelection: String("all"), + RepositoriesURL: String("https://api.github.com/organizations/652551/personal-access-tokens/25381/repositories"), + Permissions: &PersonalAccessTokenPermissions{ + Org: map[string]string{"members": "read"}, + Repo: map[string]string{"metadata": "read"}, + }, + AccessGrantedAt: &Timestamp{time.Date(2023, time.May, 16, 8, 47, 9, 0, time.FixedZone("PDT", -7*60*60))}, + TokenExpired: Bool(false), + TokenExpiresAt: &Timestamp{time.Date(2023, time.November, 16, 8, 47, 9, 0, time.FixedZone("PDT", -7*60*60))}, + TokenLastUsedAt: nil, + }, + } + if !cmp.Equal(tokens, want) { + t.Errorf("Organizations.ListFineGrainedPersonalAccessTokens returned %+v, want %+v", tokens, want) + } + + if resp == nil { + t.Error("Organizations.ListFineGrainedPersonalAccessTokens returned nil response") + } + + const methodName = "ListFineGrainedPersonalAccessTokens" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, "\n", opts) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, "o", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + func TestOrganizationsService_ReviewPersonalAccessTokenRequest(t *testing.T) { client, mux, _, teardown := setup() defer teardown() From 2a70832bfedbd36603fcabf3a761e62d12eba679 Mon Sep 17 00:00:00 2001 From: Ary Neto Date: Sat, 20 Jul 2024 19:08:40 -0600 Subject: [PATCH 2/4] feat: added ListFineGrainedPATOptions for ListFineGrainedPersonalAccessTokens --- github/orgs_personal_access_tokens.go | 65 +++++++++++++++++++++- github/orgs_personal_access_tokens_test.go | 21 ++++++- 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/github/orgs_personal_access_tokens.go b/github/orgs_personal_access_tokens.go index c3ba1ac7c39..16f7a7af497 100644 --- a/github/orgs_personal_access_tokens.go +++ b/github/orgs_personal_access_tokens.go @@ -9,6 +9,8 @@ import ( "context" "fmt" "net/http" + "net/url" + "strings" ) // PersonalAccessToken represents the minimal representation of an organization programmatic access grant. @@ -46,20 +48,52 @@ type PersonalAccessToken struct { TokenLastUsedAt *Timestamp `json:"token_last_used_at"` } +// ListFineGrainedPATOptions specifies optional parameters to ListFineGrainedPersonalAccessTokens. +type ListFineGrainedPATOptions struct { + // The property by which to sort the results. + // Default: created_at + // Value: created_at + Sort string `url:"sort,omitempty"` + + // The direction to sort the results by. + // Default: desc + // Value: asc, desc + Direction string `url:"direction,omitempty"` + + // A list of owner usernames to use to filter the results. + Owner []string `url:"-"` + + // The name of the repository to use to filter the results. + Repository string `url:"repository,omitempty"` + + // The permission to use to filter the results. + Permission string `url:"permission,omitempty"` + + // Only show fine-grained personal access tokens used before the given time. + // This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ. + LastUsedBefore string `url:"last_used_before,omitempty"` + + // Only show fine-grained personal access tokens used after the given time. + // This is a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ. + LastUsedAfter string `url:"last_used_after,omitempty"` + + ListOptions +} + // ListFineGrainedPersonalAccessTokens lists approved fine-grained personal access tokens owned by organization members that can access organization resources. // Only GitHub Apps can call this API, using the `Personal access tokens` organization permissions (read). // // GitHub API docs: https://docs.github.com/rest/orgs/personal-access-tokens#list-fine-grained-personal-access-tokens-with-access-to-organization-resources // //meta:operation GET /orgs/{org}/personal-access-tokens -func (s *OrganizationsService) ListFineGrainedPersonalAccessTokens(ctx context.Context, org string, opts *ListOptions) ([]*PersonalAccessToken, *Response, error) { +func (s *OrganizationsService) ListFineGrainedPersonalAccessTokens(ctx context.Context, org string, opts *ListFineGrainedPATOptions) ([]*PersonalAccessToken, *Response, error) { u := fmt.Sprintf("orgs/%v/personal-access-tokens", org) - u, err := addOptions(u, opts) + u, err := addListFineGrainedPATOptions(u, opts) if err != nil { return nil, nil, err } - req, err := s.client.NewRequest(http.MethodGet, u, &opts) + req, err := s.client.NewRequest(http.MethodGet, u, opts) if err != nil { return nil, nil, err } @@ -97,3 +131,28 @@ func (s *OrganizationsService) ReviewPersonalAccessTokenRequest(ctx context.Cont return s.client.Do(ctx, req, nil) } + +// GitHub API expects the owner parameter to be a list of strings in the `owner[]=...` format. +// This function adds the owner parameter to the URL query string with the correct format if it is set. +func addListFineGrainedPATOptions(s string, opts *ListFineGrainedPATOptions) (string, error) { + u, err := addOptions(s, opts) + if err != nil { + return s, err + } + + if len(opts.Owner) > 0 { + ownerVals := make([]string, len(opts.Owner)) + for i, owner := range opts.Owner { + ownerVals[i] = fmt.Sprintf("owner[]=%s", url.QueryEscape(owner)) + } + ownerQuery := strings.Join(ownerVals, "&") + + if strings.Contains(u, "?") { + u += "&" + ownerQuery + } else { + u += "?" + ownerQuery + } + } + + return u, nil +} diff --git a/github/orgs_personal_access_tokens_test.go b/github/orgs_personal_access_tokens_test.go index daad72fdb0c..940c309afd8 100644 --- a/github/orgs_personal_access_tokens_test.go +++ b/github/orgs_personal_access_tokens_test.go @@ -22,7 +22,19 @@ func TestOrganizationsService_ListFineGrainedPersonalAccessTokens(t *testing.T) mux.HandleFunc("/orgs/o/personal-access-tokens", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - testFormValues(t, r, values{"per_page": "2", "page": "2"}) + expectedQuery := values{ + "per_page": "2", + "page": "2", + "sort": "created_at", + "direction": "desc", + "owner[]": "octocat", + } + for key, expectedValue := range expectedQuery { + actualValue := r.URL.Query().Get(key) + if actualValue != expectedValue { + t.Errorf("Expected query param %s to be %s, got %s", key, expectedValue, actualValue) + } + } fmt.Fprint(w, ` [ { @@ -65,7 +77,12 @@ func TestOrganizationsService_ListFineGrainedPersonalAccessTokens(t *testing.T) ]`) }) - opts := &ListOptions{Page: 2, PerPage: 2} + opts := &ListFineGrainedPATOptions{ + ListOptions: ListOptions{Page: 2, PerPage: 2}, + Sort: "created_at", + Direction: "desc", + Owner: []string{"octocat"}, + } ctx := context.Background() tokens, resp, err := client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, "o", opts) if err != nil { From e4b84aaf769152905f372b5ee6d362076dc5c7ea Mon Sep 17 00:00:00 2001 From: Ary Neto Date: Sun, 21 Jul 2024 11:40:40 -0600 Subject: [PATCH 3/4] feat: Added multiple owners as query params on ListFineGranedPersonalAccessTokens test case --- github/orgs_personal_access_tokens_test.go | 30 ++++++++++++++-------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/github/orgs_personal_access_tokens_test.go b/github/orgs_personal_access_tokens_test.go index 940c309afd8..83f55023f51 100644 --- a/github/orgs_personal_access_tokens_test.go +++ b/github/orgs_personal_access_tokens_test.go @@ -22,19 +22,27 @@ func TestOrganizationsService_ListFineGrainedPersonalAccessTokens(t *testing.T) mux.HandleFunc("/orgs/o/personal-access-tokens", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") - expectedQuery := values{ - "per_page": "2", - "page": "2", - "sort": "created_at", - "direction": "desc", - "owner[]": "octocat", + expectedQuery := map[string][]string{ + "per_page": {"2"}, + "page": {"2"}, + "sort": {"created_at"}, + "direction": {"desc"}, + "owner[]": {"octocat", "octodog", "otherbot"}, } - for key, expectedValue := range expectedQuery { - actualValue := r.URL.Query().Get(key) - if actualValue != expectedValue { - t.Errorf("Expected query param %s to be %s, got %s", key, expectedValue, actualValue) + + query := r.URL.Query() + for key, expectedValues := range expectedQuery { + actualValues := query[key] + if len(actualValues) != len(expectedValues) { + t.Errorf("Expected %d values for query param %s, got %d", len(expectedValues), key, len(actualValues)) + } + for i, expectedValue := range expectedValues { + if actualValues[i] != expectedValue { + t.Errorf("Expected query param %s to be %s, got %s", key, expectedValue, actualValues[i]) + } } } + fmt.Fprint(w, ` [ { @@ -81,7 +89,7 @@ func TestOrganizationsService_ListFineGrainedPersonalAccessTokens(t *testing.T) ListOptions: ListOptions{Page: 2, PerPage: 2}, Sort: "created_at", Direction: "desc", - Owner: []string{"octocat"}, + Owner: []string{"octocat", "octodog", "otherbot"}, } ctx := context.Background() tokens, resp, err := client.Organizations.ListFineGrainedPersonalAccessTokens(ctx, "o", opts) From af4b0ab26cb60439c9e1dd13fddba3c84ca563c1 Mon Sep 17 00:00:00 2001 From: Ary Neto Date: Mon, 22 Jul 2024 15:41:14 -0600 Subject: [PATCH 4/4] feat: better comments explaining the owner parameter in ListFineGrainedPersonalAccessTokens --- github/orgs_personal_access_tokens.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/github/orgs_personal_access_tokens.go b/github/orgs_personal_access_tokens.go index 16f7a7af497..af083744e85 100644 --- a/github/orgs_personal_access_tokens.go +++ b/github/orgs_personal_access_tokens.go @@ -88,6 +88,7 @@ type ListFineGrainedPATOptions struct { //meta:operation GET /orgs/{org}/personal-access-tokens func (s *OrganizationsService) ListFineGrainedPersonalAccessTokens(ctx context.Context, org string, opts *ListFineGrainedPATOptions) ([]*PersonalAccessToken, *Response, error) { u := fmt.Sprintf("orgs/%v/personal-access-tokens", org) + // The `owner` parameter is a special case that uses the `owner[]=...` format and needs a custom function to format it correctly. u, err := addListFineGrainedPATOptions(u, opts) if err != nil { return nil, nil, err @@ -132,8 +133,16 @@ func (s *OrganizationsService) ReviewPersonalAccessTokenRequest(ctx context.Cont return s.client.Do(ctx, req, nil) } +// addListFineGrainedPATOptions adds the owner parameter to the URL query string with the correct format if it is set. +// // GitHub API expects the owner parameter to be a list of strings in the `owner[]=...` format. -// This function adds the owner parameter to the URL query string with the correct format if it is set. +// For multiple owner values, the owner parameter is repeated in the query string. +// +// Example: +// owner[]=user1&owner[]=user2 +// This will filter the results to only include fine-grained personal access tokens owned by `user1` and `user2`. +// +// This function ensures the owner parameter is formatted correctly in the URL query string. func addListFineGrainedPATOptions(s string, opts *ListFineGrainedPATOptions) (string, error) { u, err := addOptions(s, opts) if err != nil {