diff --git a/github/enterprise_licenses.go b/github/enterprise_licenses.go index 73da6c5e66f..e1cc35c2fb2 100644 --- a/github/enterprise_licenses.go +++ b/github/enterprise_licenses.go @@ -89,6 +89,34 @@ type LastLicenseSyncProperties struct { Error string `json:"error"` } +// VisualStudioSubscriptionAssignment represents a user's Visual Studio subscription assignment. +type VisualStudioSubscriptionAssignment struct { + VisualStudioSubscriptionEmail *string `json:"visual_studio_subscription_email,omitempty"` + SubscriptionID *string `json:"subscription_id,omitempty"` + Username *string `json:"username,omitempty"` + ManualMatch *bool `json:"manual_match,omitempty"` +} + +// VisualStudioSubscriptions represents a list of Visual Studio subscriptions for an enterprise. +type VisualStudioSubscriptions struct { + TotalCount *int `json:"total_count,omitempty"` + VisualStudioSubscriptions []*VisualStudioSubscriptionAssignment `json:"visual_studio_subscriptions,omitempty"` +} + +// VisualStudioSubscriptionAssignmentRequest represents the request body to add or update a subscription assignment. +type VisualStudioSubscriptionAssignmentRequest struct { + UserIdentifier *string `json:"user_identifier,omitempty"` +} + +// ListVisualStudioSubscriptionsOptions specifies the optional parameters to +// EnterpriseService.ListVisualStudioSubscriptions. +type ListVisualStudioSubscriptionsOptions struct { + ListOptions + + // IsUnmatchedOnly filters results to return only unmatched subscriptions. + IsUnmatchedOnly bool `url:"is_unmatched_only,omitempty"` +} + // ListConsumedLicenses collect information about the number of consumed licenses and a collection with all the users with consumed enterprise licenses. // // GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/licensing?apiVersion=2022-11-28#list-enterprise-consumed-licenses @@ -136,3 +164,67 @@ func (s *EnterpriseService) GetLicenseSyncStatus(ctx context.Context, enterprise return syncStatus, resp, nil } + +// ListVisualStudioSubscriptions gets a list of Visual Studio subscriptions for an enterprise. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/licensing?apiVersion=2022-11-28#get-a-list-of-visual-studio-subscriptions-for-the-enterprise +// +//meta:operation GET /enterprises/{enterprise}/visual-studio-subscriptions +func (s *EnterpriseService) ListVisualStudioSubscriptions(ctx context.Context, enterprise string, opts *ListVisualStudioSubscriptionsOptions) (*VisualStudioSubscriptions, *Response, error) { + u := fmt.Sprintf("enterprises/%v/visual-studio-subscriptions", enterprise) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, "GET", u, nil) + if err != nil { + return nil, nil, err + } + + var subscriptions *VisualStudioSubscriptions + resp, err := s.client.Do(req, &subscriptions) + if err != nil { + return nil, resp, err + } + + return subscriptions, resp, nil +} + +// AddOrUpdateVisualStudioSubscriptionAssignment adds or updates a manual match between a user and a Visual Studio subscription. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/licensing?apiVersion=2022-11-28#add-or-update-a-visual-studio-subscription-user-match +// +//meta:operation PUT /enterprises/{enterprise}/visual-studio-subscriptions/{visual_studio_subscription_id} +func (s *EnterpriseService) AddOrUpdateVisualStudioSubscriptionAssignment(ctx context.Context, enterprise, subscriptionID string, body VisualStudioSubscriptionAssignmentRequest) (*VisualStudioSubscriptionAssignment, *Response, error) { + u := fmt.Sprintf("enterprises/%v/visual-studio-subscriptions/%v", enterprise, subscriptionID) + + req, err := s.client.NewRequest(ctx, "PUT", u, body) + if err != nil { + return nil, nil, err + } + + var assignment *VisualStudioSubscriptionAssignment + resp, err := s.client.Do(req, &assignment) + if err != nil { + return nil, resp, err + } + + return assignment, resp, nil +} + +// DeleteVisualStudioSubscriptionAssignment deletes a manual match between a user and a Visual Studio subscription. +// +// GitHub API docs: https://docs.github.com/enterprise-cloud@latest/rest/enterprise-admin/licensing?apiVersion=2022-11-28#delete-a-visual-studio-subscription-user-match +// +//meta:operation DELETE /enterprises/{enterprise}/visual-studio-subscriptions/{visual_studio_subscription_id} +func (s *EnterpriseService) DeleteVisualStudioSubscriptionAssignment(ctx context.Context, enterprise, subscriptionID string) (*Response, error) { + u := fmt.Sprintf("enterprises/%v/visual-studio-subscriptions/%v", enterprise, subscriptionID) + + req, err := s.client.NewRequest(ctx, "DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/github/enterprise_licenses_test.go b/github/enterprise_licenses_test.go index 288e8572e88..f6418c78d94 100644 --- a/github/enterprise_licenses_test.go +++ b/github/enterprise_licenses_test.go @@ -180,3 +180,143 @@ func TestEnterpriseService_GetLicenseSyncStatus(t *testing.T) { return resp, err }) } + +func TestEnterpriseService_ListVisualStudioSubscriptions(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/visual-studio-subscriptions", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "1", "per_page": "10", "is_unmatched_only": "true"}) + fmt.Fprint(w, `{ + "total_count": 1, + "visual_studio_subscriptions": [{ + "visual_studio_subscription_email": "user@example.com", + "subscription_id": "sub-123", + "username": "monalisa", + "manual_match": true + }] + }`) + }) + + opt := &ListVisualStudioSubscriptionsOptions{ + ListOptions: ListOptions{Page: 1, PerPage: 10}, + IsUnmatchedOnly: true, + } + ctx := t.Context() + subscriptions, _, err := client.Enterprise.ListVisualStudioSubscriptions(ctx, "e", opt) + if err != nil { + t.Errorf("Enterprise.ListVisualStudioSubscriptions returned error: %v", err) + } + + want := &VisualStudioSubscriptions{ + TotalCount: Ptr(1), + VisualStudioSubscriptions: []*VisualStudioSubscriptionAssignment{ + { + VisualStudioSubscriptionEmail: Ptr("user@example.com"), + SubscriptionID: Ptr("sub-123"), + Username: Ptr("monalisa"), + ManualMatch: Ptr(true), + }, + }, + } + + if !cmp.Equal(subscriptions, want) { + t.Errorf("Enterprise.ListVisualStudioSubscriptions returned %+v, want %+v", subscriptions, want) + } + + const methodName = "ListVisualStudioSubscriptions" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.ListVisualStudioSubscriptions(ctx, "\n", opt) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.ListVisualStudioSubscriptions(ctx, "e", opt) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_AddOrUpdateVisualStudioSubscriptionAssignment(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + input := VisualStudioSubscriptionAssignmentRequest{ + UserIdentifier: Ptr("monalisa"), + } + + mux.HandleFunc("/enterprises/e/visual-studio-subscriptions/sub-123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testJSONBody(t, r, input) + fmt.Fprint(w, `{ + "visual_studio_subscription_email": "user@example.com", + "subscription_id": "sub-123", + "username": "monalisa", + "manual_match": true + }`) + }) + + ctx := t.Context() + assignment, _, err := client.Enterprise.AddOrUpdateVisualStudioSubscriptionAssignment(ctx, "e", "sub-123", input) + if err != nil { + t.Errorf("Enterprise.AddOrUpdateVisualStudioSubscriptionAssignment returned error: %v", err) + } + + want := &VisualStudioSubscriptionAssignment{ + VisualStudioSubscriptionEmail: Ptr("user@example.com"), + SubscriptionID: Ptr("sub-123"), + Username: Ptr("monalisa"), + ManualMatch: Ptr(true), + } + + if !cmp.Equal(assignment, want) { + t.Errorf("Enterprise.AddOrUpdateVisualStudioSubscriptionAssignment returned %+v, want %+v", assignment, want) + } + + const methodName = "AddOrUpdateVisualStudioSubscriptionAssignment" + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Enterprise.AddOrUpdateVisualStudioSubscriptionAssignment(ctx, "\n", "sub-123", input) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Enterprise.AddOrUpdateVisualStudioSubscriptionAssignment(ctx, "e", "sub-123", input) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_DeleteVisualStudioSubscriptionAssignment(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/enterprises/e/visual-studio-subscriptions/sub-123", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Enterprise.DeleteVisualStudioSubscriptionAssignment(ctx, "e", "sub-123") + if err != nil { + t.Errorf("Enterprise.DeleteVisualStudioSubscriptionAssignment returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Enterprise.DeleteVisualStudioSubscriptionAssignment status code = %v, want %v", resp.StatusCode, http.StatusNoContent) + } + + const methodName = "DeleteVisualStudioSubscriptionAssignment" + testBadOptions(t, methodName, func() (err error) { + _, err = client.Enterprise.DeleteVisualStudioSubscriptionAssignment(ctx, "\n", "sub-123") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Enterprise.DeleteVisualStudioSubscriptionAssignment(ctx, "e", "sub-123") + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index ab14c72673c..278ba074271 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -24270,6 +24270,14 @@ func (l *ListUserIssuesOptions) GetState() string { return l.State } +// GetIsUnmatchedOnly returns the IsUnmatchedOnly field. +func (l *ListVisualStudioSubscriptionsOptions) GetIsUnmatchedOnly() bool { + if l == nil { + return false + } + return l.IsUnmatchedOnly +} + // GetFilter returns the Filter field. func (l *ListWorkflowJobsOptions) GetFilter() string { if l == nil { @@ -45918,6 +45926,62 @@ func (u *UserUpdateRequest) GetTwitterUsername() string { return *u.TwitterUsername } +// GetManualMatch returns the ManualMatch field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptionAssignment) GetManualMatch() bool { + if v == nil || v.ManualMatch == nil { + return false + } + return *v.ManualMatch +} + +// GetSubscriptionID returns the SubscriptionID field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptionAssignment) GetSubscriptionID() string { + if v == nil || v.SubscriptionID == nil { + return "" + } + return *v.SubscriptionID +} + +// GetUsername returns the Username field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptionAssignment) GetUsername() string { + if v == nil || v.Username == nil { + return "" + } + return *v.Username +} + +// GetVisualStudioSubscriptionEmail returns the VisualStudioSubscriptionEmail field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptionAssignment) GetVisualStudioSubscriptionEmail() string { + if v == nil || v.VisualStudioSubscriptionEmail == nil { + return "" + } + return *v.VisualStudioSubscriptionEmail +} + +// GetUserIdentifier returns the UserIdentifier field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptionAssignmentRequest) GetUserIdentifier() string { + if v == nil || v.UserIdentifier == nil { + return "" + } + return *v.UserIdentifier +} + +// GetTotalCount returns the TotalCount field if it's non-nil, zero value otherwise. +func (v *VisualStudioSubscriptions) GetTotalCount() int { + if v == nil || v.TotalCount == nil { + return 0 + } + return *v.TotalCount +} + +// GetVisualStudioSubscriptions returns the VisualStudioSubscriptions slice if it's non-nil, nil otherwise. +func (v *VisualStudioSubscriptions) GetVisualStudioSubscriptions() []*VisualStudioSubscriptionAssignment { + if v == nil || v.VisualStudioSubscriptions == nil { + return nil + } + return v.VisualStudioSubscriptions +} + // GetEcosystem returns the Ecosystem field if it's non-nil, zero value otherwise. func (v *VulnerabilityPackage) GetEcosystem() string { if v == nil || v.Ecosystem == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index beb42687d75..40c8007ea4a 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -30403,6 +30403,14 @@ func TestListUserIssuesOptions_GetState(tt *testing.T) { l.GetState() } +func TestListVisualStudioSubscriptionsOptions_GetIsUnmatchedOnly(tt *testing.T) { + tt.Parallel() + l := &ListVisualStudioSubscriptionsOptions{} + l.GetIsUnmatchedOnly() + l = nil + l.GetIsUnmatchedOnly() +} + func TestListWorkflowJobsOptions_GetFilter(tt *testing.T) { tt.Parallel() l := &ListWorkflowJobsOptions{} @@ -57556,6 +57564,83 @@ func TestUserUpdateRequest_GetTwitterUsername(tt *testing.T) { u.GetTwitterUsername() } +func TestVisualStudioSubscriptionAssignment_GetManualMatch(tt *testing.T) { + tt.Parallel() + var zeroValue bool + v := &VisualStudioSubscriptionAssignment{ManualMatch: &zeroValue} + v.GetManualMatch() + v = &VisualStudioSubscriptionAssignment{} + v.GetManualMatch() + v = nil + v.GetManualMatch() +} + +func TestVisualStudioSubscriptionAssignment_GetSubscriptionID(tt *testing.T) { + tt.Parallel() + var zeroValue string + v := &VisualStudioSubscriptionAssignment{SubscriptionID: &zeroValue} + v.GetSubscriptionID() + v = &VisualStudioSubscriptionAssignment{} + v.GetSubscriptionID() + v = nil + v.GetSubscriptionID() +} + +func TestVisualStudioSubscriptionAssignment_GetUsername(tt *testing.T) { + tt.Parallel() + var zeroValue string + v := &VisualStudioSubscriptionAssignment{Username: &zeroValue} + v.GetUsername() + v = &VisualStudioSubscriptionAssignment{} + v.GetUsername() + v = nil + v.GetUsername() +} + +func TestVisualStudioSubscriptionAssignment_GetVisualStudioSubscriptionEmail(tt *testing.T) { + tt.Parallel() + var zeroValue string + v := &VisualStudioSubscriptionAssignment{VisualStudioSubscriptionEmail: &zeroValue} + v.GetVisualStudioSubscriptionEmail() + v = &VisualStudioSubscriptionAssignment{} + v.GetVisualStudioSubscriptionEmail() + v = nil + v.GetVisualStudioSubscriptionEmail() +} + +func TestVisualStudioSubscriptionAssignmentRequest_GetUserIdentifier(tt *testing.T) { + tt.Parallel() + var zeroValue string + v := &VisualStudioSubscriptionAssignmentRequest{UserIdentifier: &zeroValue} + v.GetUserIdentifier() + v = &VisualStudioSubscriptionAssignmentRequest{} + v.GetUserIdentifier() + v = nil + v.GetUserIdentifier() +} + +func TestVisualStudioSubscriptions_GetTotalCount(tt *testing.T) { + tt.Parallel() + var zeroValue int + v := &VisualStudioSubscriptions{TotalCount: &zeroValue} + v.GetTotalCount() + v = &VisualStudioSubscriptions{} + v.GetTotalCount() + v = nil + v.GetTotalCount() +} + +func TestVisualStudioSubscriptions_GetVisualStudioSubscriptions(tt *testing.T) { + tt.Parallel() + zeroValue := []*VisualStudioSubscriptionAssignment{} + v := &VisualStudioSubscriptions{VisualStudioSubscriptions: zeroValue} + v.GetVisualStudioSubscriptions() + v = &VisualStudioSubscriptions{} + v.GetVisualStudioSubscriptions() + v = nil + v.GetVisualStudioSubscriptions() +} + func TestVulnerabilityPackage_GetEcosystem(tt *testing.T) { tt.Parallel() var zeroValue string diff --git a/github/github-iterators.go b/github/github-iterators.go index 6be130a1ec1..86edb9a47af 100644 --- a/github/github-iterators.go +++ b/github/github-iterators.go @@ -3327,6 +3327,41 @@ func (s *EnterpriseService) ListTeamsIter(ctx context.Context, enterprise string } } +// ListVisualStudioSubscriptionsIter returns an iterator that paginates through all results of ListVisualStudioSubscriptions. +func (s *EnterpriseService) ListVisualStudioSubscriptionsIter(ctx context.Context, enterprise string, opts *ListVisualStudioSubscriptionsOptions) iter.Seq2[*VisualStudioSubscriptionAssignment, error] { + return func(yield func(*VisualStudioSubscriptionAssignment, error) bool) { + // Create a copy of opts to avoid mutating the caller's struct + if opts == nil { + opts = &ListVisualStudioSubscriptionsOptions{} + } else { + opts = Ptr(*opts) + } + + for { + results, resp, err := s.ListVisualStudioSubscriptions(ctx, enterprise, opts) + if err != nil { + yield(nil, err) + return + } + + var iterItems []*VisualStudioSubscriptionAssignment + if results != nil { + iterItems = results.VisualStudioSubscriptions + } + for _, item := range iterItems { + if !yield(item, nil) { + return + } + } + + if resp.NextPage == 0 { + break + } + opts.ListOptions.Page = resp.NextPage + } + } +} + // ListIter returns an iterator that paginates through all results of List. func (s *GistsService) ListIter(ctx context.Context, user string, opts *GistListOptions) iter.Seq2[*Gist, error] { return func(yield func(*Gist, error) bool) { diff --git a/github/github-iterators_test.go b/github/github-iterators_test.go index 00ea4167da9..2ebf3a55f40 100644 --- a/github/github-iterators_test.go +++ b/github/github-iterators_test.go @@ -7143,6 +7143,78 @@ func TestEnterpriseService_ListTeamsIter(t *testing.T) { } } +func TestEnterpriseService_ListVisualStudioSubscriptionsIter(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + var callNum int + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + callNum++ + switch callNum { + case 1: + w.Header().Set("Link", `; rel="next"`) + fmt.Fprint(w, `{"visual_studio_subscriptions": [{},{},{}]}`) + case 2: + fmt.Fprint(w, `{"visual_studio_subscriptions": [{},{},{},{}]}`) + case 3: + fmt.Fprint(w, `{"visual_studio_subscriptions": [{},{}]}`) + case 4: + w.WriteHeader(http.StatusNotFound) + case 5: + fmt.Fprint(w, `{"visual_studio_subscriptions": [{},{}]}`) + } + }) + + iter := client.Enterprise.ListVisualStudioSubscriptionsIter(t.Context(), "", nil) + var gotItems int + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 7; gotItems != want { + t.Errorf("client.Enterprise.ListVisualStudioSubscriptionsIter call 1 got %v items; want %v", gotItems, want) + } + + opts := &ListVisualStudioSubscriptionsOptions{} + iter = client.Enterprise.ListVisualStudioSubscriptionsIter(t.Context(), "", opts) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + if want := 2; gotItems != want { + t.Errorf("client.Enterprise.ListVisualStudioSubscriptionsIter call 2 got %v items; want %v", gotItems, want) + } + + iter = client.Enterprise.ListVisualStudioSubscriptionsIter(t.Context(), "", nil) + gotItems = 0 + for _, err := range iter { + gotItems++ + if err == nil { + t.Error("expected error; got nil") + } + } + if gotItems != 1 { + t.Errorf("client.Enterprise.ListVisualStudioSubscriptionsIter call 3 got %v items; want 1 (an error)", gotItems) + } + + iter = client.Enterprise.ListVisualStudioSubscriptionsIter(t.Context(), "", nil) + gotItems = 0 + iter(func(item *VisualStudioSubscriptionAssignment, err error) bool { + gotItems++ + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + return false + }) + if gotItems != 1 { + t.Errorf("client.Enterprise.ListVisualStudioSubscriptionsIter call 4 got %v items; want 1 (an error)", gotItems) + } +} + func TestGistsService_ListIter(t *testing.T) { t.Parallel() client, mux, _ := setup(t)