diff --git a/github/copilot.go b/github/copilot.go new file mode 100644 index 0000000000..3f3d73b25b --- /dev/null +++ b/github/copilot.go @@ -0,0 +1,311 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "encoding/json" + "fmt" +) + +// CopilotService provides access to the Copilot-related functions +// in the GitHub API. +// +// GitHub API docs: https://docs.github.com/en/rest/copilot/ +type CopilotService service + +// CopilotOrganizationDetails represents the details of an organization's Copilot for Business subscription. +type CopilotOrganizationDetails struct { + SeatBreakdown *CopilotSeatBreakdown `json:"seat_breakdown"` + PublicCodeSuggestions string `json:"public_code_suggestions"` + CopilotChat string `json:"copilot_chat"` + SeatManagementSetting string `json:"seat_management_setting"` +} + +// CopilotSeatBreakdown represents the breakdown of Copilot for Business seats for the organization. +type CopilotSeatBreakdown struct { + Total int `json:"total"` + AddedThisCycle int `json:"added_this_cycle"` + PendingCancellation int `json:"pending_cancellation"` + PendingInvitation int `json:"pending_invitation"` + ActiveThisCycle int `json:"active_this_cycle"` + InactiveThisCycle int `json:"inactive_this_cycle"` +} + +// ListCopilotSeatsResponse represents the Copilot for Business seat assignments for an organization. +type ListCopilotSeatsResponse struct { + TotalSeats int64 `json:"total_seats"` + Seats []*CopilotSeatDetails `json:"seats"` +} + +// CopilotSeatDetails represents the details of a Copilot for Business seat. +type CopilotSeatDetails struct { + // Assignee can either be a User, Team, or Organization. + Assignee interface{} `json:"assignee"` + AssigningTeam *Team `json:"assigning_team,omitempty"` + PendingCancellationDate *string `json:"pending_cancellation_date,omitempty"` + LastActivityAt *Timestamp `json:"last_activity_at,omitempty"` + LastActivityEditor *string `json:"last_activity_editor,omitempty"` + CreatedAt *Timestamp `json:"created_at"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` +} + +// SeatAssignments represents the number of seats assigned. +type SeatAssignments struct { + SeatsCreated int `json:"seats_created"` +} + +// SeatCancellations represents the number of seats cancelled. +type SeatCancellations struct { + SeatsCancelled int `json:"seats_cancelled"` +} + +func (cp *CopilotSeatDetails) UnmarshalJSON(data []byte) error { + // Using an alias to avoid infinite recursion when calling json.Unmarshal + type alias CopilotSeatDetails + var seatDetail alias + + if err := json.Unmarshal(data, &seatDetail); err != nil { + return err + } + + cp.AssigningTeam = seatDetail.AssigningTeam + cp.PendingCancellationDate = seatDetail.PendingCancellationDate + cp.LastActivityAt = seatDetail.LastActivityAt + cp.LastActivityEditor = seatDetail.LastActivityEditor + cp.CreatedAt = seatDetail.CreatedAt + cp.UpdatedAt = seatDetail.UpdatedAt + + switch v := seatDetail.Assignee.(type) { + case map[string]interface{}: + jsonData, err := json.Marshal(seatDetail.Assignee) + if err != nil { + return err + } + + if v["type"] == nil { + return fmt.Errorf("assignee type field is not set") + } + + if t, ok := v["type"].(string); ok && t == "User" { + user := &User{} + if err := json.Unmarshal(jsonData, user); err != nil { + return err + } + cp.Assignee = user + } else if t, ok := v["type"].(string); ok && t == "Team" { + team := &Team{} + if err := json.Unmarshal(jsonData, team); err != nil { + return err + } + cp.Assignee = team + } else if t, ok := v["type"].(string); ok && t == "Organization" { + organization := &Organization{} + if err := json.Unmarshal(jsonData, organization); err != nil { + return err + } + cp.Assignee = organization + } else { + return fmt.Errorf("unsupported assignee type %v", v["type"]) + } + default: + return fmt.Errorf("unsupported assignee type %T", v) + } + + return nil +} + +// GetUser gets the User from the CopilotSeatDetails if the assignee is a user. +func (cp *CopilotSeatDetails) GetUser() (*User, bool) { u, ok := cp.Assignee.(*User); return u, ok } + +// GetTeam gets the Team from the CopilotSeatDetails if the assignee is a team. +func (cp *CopilotSeatDetails) GetTeam() (*Team, bool) { t, ok := cp.Assignee.(*Team); return t, ok } + +// GetOrganization gets the Organization from the CopilotSeatDetails if the assignee is an organization. +func (cp *CopilotSeatDetails) GetOrganization() (*Organization, bool) { + o, ok := cp.Assignee.(*Organization) + return o, ok +} + +// GetCopilotBilling gets Copilot for Business billing information and settings for an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#get-copilot-business-seat-information-and-settings-for-an-organization +// +//meta:operation GET /orgs/{org}/copilot/billing +func (s *CopilotService) GetCopilotBilling(ctx context.Context, org string) (*CopilotOrganizationDetails, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing", org) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var copilotDetails *CopilotOrganizationDetails + resp, err := s.client.Do(ctx, req, &copilotDetails) + if err != nil { + return nil, resp, err + } + + return copilotDetails, resp, nil +} + +// ListCopilotSeats lists Copilot for Business seat assignments for an organization. +// +// To paginate through all seats, populate 'Page' with the number of the last page. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#list-all-copilot-business-seat-assignments-for-an-organization +// +//meta:operation GET /orgs/{org}/copilot/billing/seats +func (s *CopilotService) ListCopilotSeats(ctx context.Context, org string, opts *ListOptions) (*ListCopilotSeatsResponse, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing/seats", org) + + req, err := s.client.NewRequest("GET", u, opts) + if err != nil { + return nil, nil, err + } + + var copilotSeats *ListCopilotSeatsResponse + resp, err := s.client.Do(ctx, req, &copilotSeats) + if err != nil { + return nil, resp, err + } + + return copilotSeats, resp, nil +} + +// AddCopilotTeams adds teams to the Copilot for Business subscription for an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#add-teams-to-the-copilot-business-subscription-for-an-organization +// +//meta:operation POST /orgs/{org}/copilot/billing/selected_teams +func (s *CopilotService) AddCopilotTeams(ctx context.Context, org string, teamNames []string) (*SeatAssignments, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing/selected_teams", org) + + body := struct { + SelectedTeams []string `json:"selected_teams"` + }{ + SelectedTeams: teamNames, + } + + req, err := s.client.NewRequest("POST", u, body) + if err != nil { + return nil, nil, err + } + + var seatAssignments *SeatAssignments + resp, err := s.client.Do(ctx, req, &seatAssignments) + if err != nil { + return nil, resp, err + } + + return seatAssignments, resp, nil +} + +// RemoveCopilotTeams removes teams from the Copilot for Business subscription for an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#remove-teams-from-the-copilot-business-subscription-for-an-organization +// +//meta:operation DELETE /orgs/{org}/copilot/billing/selected_teams +func (s *CopilotService) RemoveCopilotTeams(ctx context.Context, org string, teamNames []string) (*SeatCancellations, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing/selected_teams", org) + + body := struct { + SelectedTeams []string `json:"selected_teams"` + }{ + SelectedTeams: teamNames, + } + + req, err := s.client.NewRequest("DELETE", u, body) + if err != nil { + return nil, nil, err + } + + var seatCancellations *SeatCancellations + resp, err := s.client.Do(ctx, req, &seatCancellations) + if err != nil { + return nil, resp, err + } + + return seatCancellations, resp, nil +} + +// AddCopilotUsers adds users to the Copilot for Business subscription for an organization +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#add-users-to-the-copilot-business-subscription-for-an-organization +// +//meta:operation POST /orgs/{org}/copilot/billing/selected_users +func (s *CopilotService) AddCopilotUsers(ctx context.Context, org string, users []string) (*SeatAssignments, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing/selected_users", org) + + body := struct { + SelectedUsers []string `json:"selected_users"` + }{ + SelectedUsers: users, + } + + req, err := s.client.NewRequest("POST", u, body) + if err != nil { + return nil, nil, err + } + + var seatAssignments *SeatAssignments + resp, err := s.client.Do(ctx, req, &seatAssignments) + if err != nil { + return nil, resp, err + } + + return seatAssignments, resp, nil +} + +// RemoveCopilotUsers removes users from the Copilot for Business subscription for an organization. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#remove-users-from-the-copilot-business-subscription-for-an-organization +// +//meta:operation DELETE /orgs/{org}/copilot/billing/selected_users +func (s *CopilotService) RemoveCopilotUsers(ctx context.Context, org string, users []string) (*SeatCancellations, *Response, error) { + u := fmt.Sprintf("orgs/%v/copilot/billing/selected_users", org) + + body := struct { + SelectedUsers []string `json:"selected_users"` + }{ + SelectedUsers: users, + } + + req, err := s.client.NewRequest("DELETE", u, body) + if err != nil { + return nil, nil, err + } + + var seatCancellations *SeatCancellations + resp, err := s.client.Do(ctx, req, &seatCancellations) + if err != nil { + return nil, resp, err + } + + return seatCancellations, resp, nil +} + +// GetSeatDetails gets Copilot for Business seat assignment details for a user. +// +// GitHub API docs: https://docs.github.com/rest/copilot/copilot-business#get-copilot-business-seat-assignment-details-for-a-user +// +//meta:operation GET /orgs/{org}/members/{username}/copilot +func (s *CopilotService) GetSeatDetails(ctx context.Context, org, user string) (*CopilotSeatDetails, *Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/copilot", org, user) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var seatDetails *CopilotSeatDetails + resp, err := s.client.Do(ctx, req, &seatDetails) + if err != nil { + return nil, resp, err + } + + return seatDetails, resp, nil +} diff --git a/github/copilot_test.go b/github/copilot_test.go new file mode 100644 index 0000000000..9df2c9a497 --- /dev/null +++ b/github/copilot_test.go @@ -0,0 +1,887 @@ +// Copyright 2023 The go-github AUTHORS. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package github + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + "time" + + "github.com/google/go-cmp/cmp" +) + +// Test invalid JSON responses, vlaid responses are covered in the other tests +func TestCopilotSeatDetails_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + data string + want *CopilotSeatDetails + wantErr bool + }{ + { + name: "Invalid JSON", + data: `{`, + want: &CopilotSeatDetails{ + Assignee: nil, + }, + wantErr: true, + }, + { + name: "Invalid top level type", + data: `{ + "assignee": { + "type": "User", + "name": "octokittens", + "id": 1 + }, + "assigning_team": "this should be an object" + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "No Type Field", + data: `{ + "assignee": { + "name": "octokittens", + "id": 1 + } + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "Invalid Assignee Field Type", + data: `{ + "assignee": "test" + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "Invalid Assignee Type", + data: `{ + "assignee": { + "name": "octokittens", + "id": 1, + "type": [] + } + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "Invalid User", + data: `{ + "assignee": { + "type": "User", + "id": "bad" + } + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "Invalid Team", + data: `{ + "assignee": { + "type": "Team", + "id": "bad" + } + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + { + name: "Invalid Organization", + data: `{ + "assignee": { + "type": "Organization", + "id": "bad" + } + }`, + want: &CopilotSeatDetails{}, + wantErr: true, + }, + } + + for _, tc := range tests { + seatDetails := &CopilotSeatDetails{} + + t.Run(tc.name, func(t *testing.T) { + err := json.Unmarshal([]byte(tc.data), seatDetails) + if err == nil && tc.wantErr { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned nil instead of an error") + } + if err != nil && !tc.wantErr { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + if !cmp.Equal(tc.want, seatDetails) { + t.Errorf("CopilotSeatDetails.UnmarshalJSON expected %+v, got %+v", tc.want, seatDetails) + } + }) + } +} + +func TestCopilotService_GetSeatDetailsUser(t *testing.T) { + data := `{ + "assignee": { + "type": "User", + "id": 1 + } + }` + + seatDetails := &CopilotSeatDetails{} + + err := json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + want := &User{ + ID: Int64(1), + Type: String("User"), + } + + if got, ok := seatDetails.GetUser(); ok && !cmp.Equal(got, want) { + t.Errorf("CopilotSeatDetails.GetTeam returned %+v, want %+v", got, want) + } else if !ok { + t.Errorf("CopilotSeatDetails.GetUser returned false, expected true") + } + + data = `{ + "assignee": { + "type": "Organization", + "id": 1 + } + }` + + bad := &Organization{ + ID: Int64(1), + Type: String("Organization"), + } + + err = json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + if got, ok := seatDetails.GetUser(); ok { + t.Errorf("CopilotSeatDetails.GetUser returned true, expected false. Returned %v, expected %v", got, bad) + } +} + +func TestCopilotService_GetSeatDetailsTeam(t *testing.T) { + data := `{ + "assignee": { + "type": "Team", + "id": 1 + } + }` + + seatDetails := &CopilotSeatDetails{} + + err := json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + want := &Team{ + ID: Int64(1), + } + + if got, ok := seatDetails.GetTeam(); ok && !cmp.Equal(got, want) { + t.Errorf("CopilotSeatDetails.GetTeam returned %+v, want %+v", got, want) + } else if !ok { + t.Errorf("CopilotSeatDetails.GetTeam returned false, expected true") + } + + data = `{ + "assignee": { + "type": "User", + "id": 1 + } + }` + + bad := &User{ + ID: Int64(1), + Type: String("User"), + } + + err = json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + if got, ok := seatDetails.GetTeam(); ok { + t.Errorf("CopilotSeatDetails.GetTeam returned true, expected false. Returned %v, expected %v", got, bad) + } +} + +func TestCopilotService_GetSeatDetailsOrganization(t *testing.T) { + data := `{ + "assignee": { + "type": "Organization", + "id": 1 + } + }` + + seatDetails := &CopilotSeatDetails{} + + err := json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + want := &Organization{ + ID: Int64(1), + Type: String("Organization"), + } + + if got, ok := seatDetails.GetOrganization(); ok && !cmp.Equal(got, want) { + t.Errorf("CopilotSeatDetails.GetOrganization returned %+v, want %+v", got, want) + } else if !ok { + t.Errorf("CopilotSeatDetails.GetOrganization returned false, expected true") + } + + data = `{ + "assignee": { + "type": "Team", + "id": 1 + } + }` + + bad := &Team{ + ID: Int64(1), + } + + err = json.Unmarshal([]byte(data), seatDetails) + if err != nil { + t.Errorf("CopilotSeatDetails.UnmarshalJSON returned an unexpected error: %v", err) + } + + if got, ok := seatDetails.GetOrganization(); ok { + t.Errorf("CopilotSeatDetails.GetOrganization returned true, expected false. Returned %v, expected %v", got, bad) + } +} + +func TestCopilotService_GetCopilotBilling(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "seat_breakdown": { + "total": 12, + "added_this_cycle": 9, + "pending_invitation": 0, + "pending_cancellation": 0, + "active_this_cycle": 12, + "inactive_this_cycle": 11 + }, + "seat_management_setting": "assign_selected", + "public_code_suggestions": "block" + }`) + }) + + ctx := context.Background() + got, _, err := client.Copilot.GetCopilotBilling(ctx, "o") + if err != nil { + t.Errorf("Copilot.GetCopilotBilling returned error: %v", err) + } + + want := &CopilotOrganizationDetails{ + SeatBreakdown: &CopilotSeatBreakdown{ + Total: 12, + AddedThisCycle: 9, + PendingInvitation: 0, + PendingCancellation: 0, + ActiveThisCycle: 12, + InactiveThisCycle: 11, + }, + PublicCodeSuggestions: "block", + CopilotChat: "", + SeatManagementSetting: "assign_selected", + } + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetCopilotBilling returned %+v, want %+v", got, want) + } + + const methodName = "GetCopilotBilling" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetCopilotBilling(ctx, "\n") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetCopilotBilling(ctx, "o") + if got != nil { + t.Errorf("Copilot.GetCopilotBilling returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_ListCopilotSeats(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing/seats", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "total_seats": 4, + "seats": [ + { + "created_at": "2021-08-03T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": null, + "last_activity_at": "2021-10-14T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "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 + }, + "assigning_team": { + "id": 1, + "node_id": "MDQ6VGVhbTE=", + "url": "https://api.github.com/teams/1", + "html_url": "https://github.com/orgs/github/teams/justice-league", + "name": "Justice League", + "slug": "justice-league", + "description": "A great team.", + "privacy": "closed", + "notification_setting": "notifications_enabled", + "permission": "admin", + "members_url": "https://api.github.com/teams/1/members{/member}", + "repositories_url": "https://api.github.com/teams/1/repos", + "parent": null + } + }, + { + "created_at": "2021-09-23T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": "2021-11-01", + "last_activity_at": "2021-10-13T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "login": "octokitten", + "id": 1, + "node_id": "MDQ76VNlcjE=", + "avatar_url": "https://github.com/images/error/octokitten_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octokitten", + "html_url": "https://github.com/octokitten", + "followers_url": "https://api.github.com/users/octokitten/followers", + "following_url": "https://api.github.com/users/octokitten/following{/other_user}", + "gists_url": "https://api.github.com/users/octokitten/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octokitten/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octokitten/subscriptions", + "organizations_url": "https://api.github.com/users/octokitten/orgs", + "repos_url": "https://api.github.com/users/octokitten/repos", + "events_url": "https://api.github.com/users/octokitten/events{/privacy}", + "received_events_url": "https://api.github.com/users/octokitten/received_events", + "type": "User", + "site_admin": false + } + }, + { + "created_at": "2021-09-23T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": "2021-11-01", + "last_activity_at": "2021-10-13T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "name": "octokittens", + "id": 1, + "type": "Team" + } + }, + { + "created_at": "2021-09-23T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": "2021-11-01", + "last_activity_at": "2021-10-13T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "name": "octocats", + "id": 1, + "type": "Organization" + } + } + ] + }`) + }) + + tmp, err := time.Parse(time.RFC3339, "2021-08-03T18:00:00-06:00") + if err != nil { + panic(err) + } + createdAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T15:00:00-06:00") + if err != nil { + panic(err) + } + updatedAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-10-14T00:53:32-06:00") + if err != nil { + panic(err) + } + lastActivityAt1 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T18:00:00-06:00") + if err != nil { + panic(err) + } + createdAt2 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T15:00:00-06:00") + if err != nil { + panic(err) + } + updatedAt2 := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-10-13T00:53:32-06:00") + if err != nil { + panic(err) + } + lastActivityAt2 := Timestamp{tmp} + + ctx := context.Background() + got, _, err := client.Copilot.ListCopilotSeats(ctx, "o", nil) + if err != nil { + t.Errorf("Copilot.ListCopilotSeats returned error: %v", err) + } + + want := &ListCopilotSeatsResponse{ + TotalSeats: 4, + Seats: []*CopilotSeatDetails{ + { + Assignee: &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), + }, + AssigningTeam: &Team{ + ID: Int64(1), + NodeID: String("MDQ6VGVhbTE="), + URL: String("https://api.github.com/teams/1"), + HTMLURL: String("https://github.com/orgs/github/teams/justice-league"), + Name: String("Justice League"), + Slug: String("justice-league"), + Description: String("A great team."), + Privacy: String("closed"), + Permission: String("admin"), + MembersURL: String("https://api.github.com/teams/1/members{/member}"), + RepositoriesURL: String("https://api.github.com/teams/1/repos"), + Parent: nil, + }, + CreatedAt: &createdAt1, + UpdatedAt: &updatedAt1, + PendingCancellationDate: nil, + LastActivityAt: &lastActivityAt1, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + }, + { + Assignee: &User{ + Login: String("octokitten"), + ID: Int64(1), + NodeID: String("MDQ76VNlcjE="), + AvatarURL: String("https://github.com/images/error/octokitten_happy.gif"), + GravatarID: String(""), + URL: String("https://api.github.com/users/octokitten"), + HTMLURL: String("https://github.com/octokitten"), + FollowersURL: String("https://api.github.com/users/octokitten/followers"), + FollowingURL: String("https://api.github.com/users/octokitten/following{/other_user}"), + GistsURL: String("https://api.github.com/users/octokitten/gists{/gist_id}"), + StarredURL: String("https://api.github.com/users/octokitten/starred{/owner}{/repo}"), + SubscriptionsURL: String("https://api.github.com/users/octokitten/subscriptions"), + OrganizationsURL: String("https://api.github.com/users/octokitten/orgs"), + ReposURL: String("https://api.github.com/users/octokitten/repos"), + EventsURL: String("https://api.github.com/users/octokitten/events{/privacy}"), + ReceivedEventsURL: String("https://api.github.com/users/octokitten/received_events"), + Type: String("User"), + SiteAdmin: Bool(false), + }, + AssigningTeam: nil, + CreatedAt: &createdAt2, + UpdatedAt: &updatedAt2, + PendingCancellationDate: String("2021-11-01"), + LastActivityAt: &lastActivityAt2, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + }, + { + Assignee: &Team{ + ID: Int64(1), + Name: String("octokittens"), + }, + AssigningTeam: nil, + CreatedAt: &createdAt2, + UpdatedAt: &updatedAt2, + PendingCancellationDate: String("2021-11-01"), + LastActivityAt: &lastActivityAt2, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + }, + { + Assignee: &Organization{ + ID: Int64(1), + Name: String("octocats"), + Type: String("Organization"), + }, + AssigningTeam: nil, + CreatedAt: &createdAt2, + UpdatedAt: &updatedAt2, + PendingCancellationDate: String("2021-11-01"), + LastActivityAt: &lastActivityAt2, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + }, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.ListCopilotSeats returned %+v, want %+v", got, want) + } + + const methodName = "ListCopilotSeats" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.ListCopilotSeats(ctx, "\n", nil) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.ListCopilotSeats(ctx, "", nil) + if got != nil { + t.Errorf("Copilot.ListCopilotSeats returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_AddCopilotTeams(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing/selected_teams", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"selected_teams":["team1","team2"]}`+"\n") + fmt.Fprint(w, `{"seats_created": 2}`) + }) + + ctx := context.Background() + got, _, err := client.Copilot.AddCopilotTeams(ctx, "o", []string{"team1", "team2"}) + if err != nil { + t.Errorf("Copilot.AddCopilotTeams returned error: %v", err) + } + + want := &SeatAssignments{SeatsCreated: 2} + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.AddCopilotTeams returned %+v, want %+v", got, want) + } + + const methodName = "AddCopilotTeams" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.AddCopilotTeams(ctx, "\n", []string{"team1", "team2"}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.AddCopilotTeams(ctx, "o", []string{"team1", "team2"}) + if got != nil { + t.Errorf("Copilot.AddCopilotTeams returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_RemoveCopilotTeams(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing/selected_teams", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBody(t, r, `{"selected_teams":["team1","team2"]}`+"\n") + fmt.Fprint(w, `{"seats_cancelled": 2}`) + }) + + ctx := context.Background() + got, _, err := client.Copilot.RemoveCopilotTeams(ctx, "o", []string{"team1", "team2"}) + if err != nil { + t.Errorf("Copilot.RemoveCopilotTeams returned error: %v", err) + } + + want := &SeatCancellations{SeatsCancelled: 2} + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.RemoveCopilotTeams returned %+v, want %+v", got, want) + } + + const methodName = "RemoveCopilotTeams" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.RemoveCopilotTeams(ctx, "\n", []string{"team1", "team2"}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.RemoveCopilotTeams(ctx, "o", []string{"team1", "team2"}) + if got != nil { + t.Errorf("Copilot.RemoveCopilotTeams returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_AddCopilotUsers(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"selected_users":["user1","user2"]}`+"\n") + fmt.Fprint(w, `{"seats_created": 2}`) + }) + + ctx := context.Background() + got, _, err := client.Copilot.AddCopilotUsers(ctx, "o", []string{"user1", "user2"}) + if err != nil { + t.Errorf("Copilot.AddCopilotUsers returned error: %v", err) + } + + want := &SeatAssignments{SeatsCreated: 2} + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.AddCopilotUsers returned %+v, want %+v", got, want) + } + + const methodName = "AddCopilotUsers" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.AddCopilotUsers(ctx, "\n", []string{"user1", "user2"}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.AddCopilotUsers(ctx, "o", []string{"user1", "user2"}) + if got != nil { + t.Errorf("Copilot.AddCopilotUsers returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_RemoveCopilotUsers(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/copilot/billing/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBody(t, r, `{"selected_users":["user1","user2"]}`+"\n") + fmt.Fprint(w, `{"seats_cancelled": 2}`) + }) + + ctx := context.Background() + got, _, err := client.Copilot.RemoveCopilotUsers(ctx, "o", []string{"user1", "user2"}) + if err != nil { + t.Errorf("Copilot.RemoveCopilotUsers returned error: %v", err) + } + + want := &SeatCancellations{SeatsCancelled: 2} + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.RemoveCopilotUsers returned %+v, want %+v", got, want) + } + + const methodName = "RemoveCopilotUsers" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.RemoveCopilotUsers(ctx, "\n", []string{"user1", "user2"}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.RemoveCopilotUsers(ctx, "o", []string{"user1", "user2"}) + if got != nil { + t.Errorf("Copilot.RemoveCopilotUsers returned %+v, want nil", got) + } + return resp, err + }) +} + +func TestCopilotService_GetSeatDetails(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/members/u/copilot", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "created_at": "2021-08-03T18:00:00-06:00", + "updated_at": "2021-09-23T15:00:00-06:00", + "pending_cancellation_date": null, + "last_activity_at": "2021-10-14T00:53:32-06:00", + "last_activity_editor": "vscode/1.77.3/copilot/1.86.82", + "assignee": { + "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 + }, + "assigning_team": { + "id": 1, + "node_id": "MDQ6VGVhbTE=", + "url": "https://api.github.com/teams/1", + "html_url": "https://github.com/orgs/github/teams/justice-league", + "name": "Justice League", + "slug": "justice-league", + "description": "A great team.", + "privacy": "closed", + "notification_setting": "notifications_enabled", + "permission": "admin", + "members_url": "https://api.github.com/teams/1/members{/member}", + "repositories_url": "https://api.github.com/teams/1/repos", + "parent": null + } + }`) + }) + + tmp, err := time.Parse(time.RFC3339, "2021-08-03T18:00:00-06:00") + if err != nil { + panic(err) + } + createdAt := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-09-23T15:00:00-06:00") + if err != nil { + panic(err) + } + updatedAt := Timestamp{tmp} + + tmp, err = time.Parse(time.RFC3339, "2021-10-14T00:53:32-06:00") + if err != nil { + panic(err) + } + lastActivityAt := Timestamp{tmp} + + ctx := context.Background() + got, _, err := client.Copilot.GetSeatDetails(ctx, "o", "u") + if err != nil { + t.Errorf("Copilot.GetSeatDetails returned error: %v", err) + } + + want := &CopilotSeatDetails{ + Assignee: &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), + }, + AssigningTeam: &Team{ + ID: Int64(1), + NodeID: String("MDQ6VGVhbTE="), + URL: String("https://api.github.com/teams/1"), + HTMLURL: String("https://github.com/orgs/github/teams/justice-league"), + Name: String("Justice League"), + Slug: String("justice-league"), + Description: String("A great team."), + Privacy: String("closed"), + Permission: String("admin"), + MembersURL: String("https://api.github.com/teams/1/members{/member}"), + RepositoriesURL: String("https://api.github.com/teams/1/repos"), + Parent: nil, + }, + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + PendingCancellationDate: nil, + LastActivityAt: &lastActivityAt, + LastActivityEditor: String("vscode/1.77.3/copilot/1.86.82"), + } + + if !cmp.Equal(got, want) { + t.Errorf("Copilot.GetSeatDetails returned %+v, want %+v", got, want) + } + + const methodName = "GetSeatDetails" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Copilot.GetSeatDetails(ctx, "\n", "u") + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Copilot.GetSeatDetails(ctx, "o", "u") + if got != nil { + t.Errorf("Copilot.GetSeatDetails returned %+v, want nil", got) + } + return resp, err + }) +} diff --git a/github/github-accessors.go b/github/github-accessors.go index f41409f9e6..3f24a795e5 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4014,6 +4014,62 @@ func (c *ContributorStats) GetTotal() int { return *c.Total } +// GetSeatBreakdown returns the SeatBreakdown field. +func (c *CopilotOrganizationDetails) GetSeatBreakdown() *CopilotSeatBreakdown { + if c == nil { + return nil + } + return c.SeatBreakdown +} + +// GetAssigningTeam returns the AssigningTeam field. +func (c *CopilotSeatDetails) GetAssigningTeam() *Team { + if c == nil { + return nil + } + return c.AssigningTeam +} + +// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetCreatedAt() Timestamp { + if c == nil || c.CreatedAt == nil { + return Timestamp{} + } + return *c.CreatedAt +} + +// GetLastActivityAt returns the LastActivityAt field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetLastActivityAt() Timestamp { + if c == nil || c.LastActivityAt == nil { + return Timestamp{} + } + return *c.LastActivityAt +} + +// GetLastActivityEditor returns the LastActivityEditor field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetLastActivityEditor() string { + if c == nil || c.LastActivityEditor == nil { + return "" + } + return *c.LastActivityEditor +} + +// GetPendingCancellationDate returns the PendingCancellationDate field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetPendingCancellationDate() string { + if c == nil || c.PendingCancellationDate == nil { + return "" + } + return *c.PendingCancellationDate +} + +// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise. +func (c *CopilotSeatDetails) GetUpdatedAt() Timestamp { + if c == nil || c.UpdatedAt == nil { + return Timestamp{} + } + return *c.UpdatedAt +} + // GetCompletedAt returns the CompletedAt field if it's non-nil, zero value otherwise. func (c *CreateCheckRunOptions) GetCompletedAt() Timestamp { if c == nil || c.CompletedAt == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 5b0b9911bd..e575fea252 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -4709,6 +4709,70 @@ func TestContributorStats_GetTotal(tt *testing.T) { c.GetTotal() } +func TestCopilotOrganizationDetails_GetSeatBreakdown(tt *testing.T) { + c := &CopilotOrganizationDetails{} + c.GetSeatBreakdown() + c = nil + c.GetSeatBreakdown() +} + +func TestCopilotSeatDetails_GetAssigningTeam(tt *testing.T) { + c := &CopilotSeatDetails{} + c.GetAssigningTeam() + c = nil + c.GetAssigningTeam() +} + +func TestCopilotSeatDetails_GetCreatedAt(tt *testing.T) { + var zeroValue Timestamp + c := &CopilotSeatDetails{CreatedAt: &zeroValue} + c.GetCreatedAt() + c = &CopilotSeatDetails{} + c.GetCreatedAt() + c = nil + c.GetCreatedAt() +} + +func TestCopilotSeatDetails_GetLastActivityAt(tt *testing.T) { + var zeroValue Timestamp + c := &CopilotSeatDetails{LastActivityAt: &zeroValue} + c.GetLastActivityAt() + c = &CopilotSeatDetails{} + c.GetLastActivityAt() + c = nil + c.GetLastActivityAt() +} + +func TestCopilotSeatDetails_GetLastActivityEditor(tt *testing.T) { + var zeroValue string + c := &CopilotSeatDetails{LastActivityEditor: &zeroValue} + c.GetLastActivityEditor() + c = &CopilotSeatDetails{} + c.GetLastActivityEditor() + c = nil + c.GetLastActivityEditor() +} + +func TestCopilotSeatDetails_GetPendingCancellationDate(tt *testing.T) { + var zeroValue string + c := &CopilotSeatDetails{PendingCancellationDate: &zeroValue} + c.GetPendingCancellationDate() + c = &CopilotSeatDetails{} + c.GetPendingCancellationDate() + c = nil + c.GetPendingCancellationDate() +} + +func TestCopilotSeatDetails_GetUpdatedAt(tt *testing.T) { + var zeroValue Timestamp + c := &CopilotSeatDetails{UpdatedAt: &zeroValue} + c.GetUpdatedAt() + c = &CopilotSeatDetails{} + c.GetUpdatedAt() + c = nil + c.GetUpdatedAt() +} + func TestCreateCheckRunOptions_GetCompletedAt(tt *testing.T) { var zeroValue Timestamp c := &CreateCheckRunOptions{CompletedAt: &zeroValue} diff --git a/github/github.go b/github/github.go index da4f2d067e..0b00655243 100644 --- a/github/github.go +++ b/github/github.go @@ -186,6 +186,7 @@ type Client struct { CodeScanning *CodeScanningService CodesOfConduct *CodesOfConductService Codespaces *CodespacesService + Copilot *CopilotService Dependabot *DependabotService DependencyGraph *DependencyGraphService Emojis *EmojisService @@ -414,6 +415,7 @@ func (c *Client) initialize() { c.CodeScanning = (*CodeScanningService)(&c.common) c.Codespaces = (*CodespacesService)(&c.common) c.CodesOfConduct = (*CodesOfConductService)(&c.common) + c.Copilot = (*CopilotService)(&c.common) c.Dependabot = (*DependabotService)(&c.common) c.DependencyGraph = (*DependencyGraphService)(&c.common) c.Emojis = (*EmojisService)(&c.common)