diff --git a/github/codespaces_orgs.go b/github/codespaces_orgs.go new file mode 100644 index 00000000000..3aeb05f86cb --- /dev/null +++ b/github/codespaces_orgs.go @@ -0,0 +1,173 @@ +// Copyright 2025 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" + "fmt" +) + +// CodespacesOrgAccessControlRequest represent request for SetOrgAccessControl. +type CodespacesOrgAccessControlRequest struct { + // Visibility represent which users can access codespaces in the organization. + // Can be one of: disabled, selected_members, all_members, all_members_and_outside_collaborators. + Visibility string `json:"visibility"` + // SelectedUsernames represent the usernames of the organization members who should have access to codespaces in the organization. + // Required when visibility is selected_members. + SelectedUsernames []string `json:"selected_usernames,omitzero"` +} + +// ListInOrg lists the codespaces associated to a specified organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#list-codespaces-for-the-organization +// +//meta:operation GET /orgs/{org}/codespaces +func (s *CodespacesService) ListInOrg(ctx context.Context, org string, opts *ListOptions) (*ListCodespaces, *Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces", org) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var codespaces *ListCodespaces + resp, err := s.client.Do(ctx, req, &codespaces) + if err != nil { + return nil, resp, err + } + + return codespaces, resp, nil +} + +// SetOrgAccessControl sets which users can access codespaces in an organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#manage-access-control-for-organization-codespaces +// +//meta:operation PUT /orgs/{org}/codespaces/access +func (s *CodespacesService) SetOrgAccessControl(ctx context.Context, org string, request CodespacesOrgAccessControlRequest) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access", org) + req, err := s.client.NewRequest("PUT", u, request) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// AddUsersToOrgAccess adds users to Codespaces access for an organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#add-users-to-codespaces-access-for-an-organization +// +//meta:operation POST /orgs/{org}/codespaces/access/selected_users +func (s *CodespacesService) AddUsersToOrgAccess(ctx context.Context, org string, usernames []string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access/selected_users", org) + req, err := s.client.NewRequest("POST", u, map[string][]string{"selected_usernames": usernames}) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// RemoveUsersFromOrgAccess removes users from Codespaces access for an organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#remove-users-from-codespaces-access-for-an-organization +// +//meta:operation DELETE /orgs/{org}/codespaces/access/selected_users +func (s *CodespacesService) RemoveUsersFromOrgAccess(ctx context.Context, org string, usernames []string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/codespaces/access/selected_users", org) + req, err := s.client.NewRequest("DELETE", u, map[string][]string{"selected_usernames": usernames}) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// ListUserCodespacesInOrg lists the codespaces that a member of an organization has for repositories in that organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#list-codespaces-for-a-user-in-organization +// +//meta:operation GET /orgs/{org}/members/{username}/codespaces +func (s *CodespacesService) ListUserCodespacesInOrg(ctx context.Context, org, username string, opts *ListOptions) (*ListCodespaces, *Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces", org, username) + u, err := addOptions(u, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var codespaces *ListCodespaces + resp, err := s.client.Do(ctx, req, &codespaces) + if err != nil { + return nil, resp, err + } + + return codespaces, resp, nil +} + +// DeleteUserCodespaceInOrg deletes a user's codespace from the organization. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#delete-a-codespace-from-the-organization +// +//meta:operation DELETE /orgs/{org}/members/{username}/codespaces/{codespace_name} +func (s *CodespacesService) DeleteUserCodespaceInOrg(ctx context.Context, org, username, codespaceName string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces/%v", org, username, codespaceName) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} + +// StopUserCodespaceInOrg stops a codespace for an organization user. +// +// GitHub API docs: https://docs.github.com/rest/codespaces/organizations#stop-a-codespace-for-an-organization-user +// +//meta:operation POST /orgs/{org}/members/{username}/codespaces/{codespace_name}/stop +func (s *CodespacesService) StopUserCodespaceInOrg(ctx context.Context, org, username, codespaceName string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/members/%v/codespaces/%v/stop", org, username, codespaceName) + req, err := s.client.NewRequest("POST", u, nil) + if err != nil { + return nil, err + } + + resp, err := s.client.Do(ctx, req, nil) + if err != nil { + return resp, err + } + + return resp, nil +} diff --git a/github/codespaces_orgs_test.go b/github/codespaces_orgs_test.go new file mode 100644 index 00000000000..c9b674b7215 --- /dev/null +++ b/github/codespaces_orgs_test.go @@ -0,0 +1,241 @@ +// Copyright 2025 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 ( + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestCodespacesService_ListInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"total_count":1,"codespaces":[{"id":1}]}`) + }) + + opts := &ListOptions{Page: 1, PerPage: 10} + ctx := t.Context() + got, _, err := client.Codespaces.ListInOrg(ctx, "o1", opts) + if err != nil { + t.Fatalf("Codespaces.ListInOrg returned error: %v", err) + } + + want := &ListCodespaces{ + TotalCount: Ptr(1), + Codespaces: []*Codespace{ + {ID: Ptr(int64(1))}, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListInOrg = %+v, want %+v", got, want) + } + const methodName = "ListInOrg" + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListInOrg(ctx, "\n", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListInOrg(ctx, "o1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestCodespacesService_SetOrgAccessControl(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + testBody(t, r, `{"visibility":"selected_members","selected_usernames":["u1","u2"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := CodespacesOrgAccessControlRequest{ + Visibility: "selected_members", + SelectedUsernames: []string{"u1", "u2"}, + } + + _, err := client.Codespaces.SetOrgAccessControl(ctx, "o1", req) + if err != nil { + t.Fatalf("Codespaces.SetOrgAccessControl returned error: %v", err) + } + + const methodName = "SetOrgAccessControl" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.SetOrgAccessControl(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.SetOrgAccessControl(ctx, "o1", req) + }) +} + +func TestEnterpriseService_AddUsersToOrgAccess(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBody(t, r, `{"selected_usernames":["u1"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := []string{"u1"} + resp, err := client.Codespaces.AddUsersToOrgAccess(ctx, "o1", req) + if err != nil { + t.Fatalf("AddUsersToOrgAccess returned error: %v", err) + } + if resp == nil { + t.Fatal("AddUsersToOrgAccess returned nil Response") + } + + const methodName = "AddUsersToOrgAccess" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.AddUsersToOrgAccess(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.AddUsersToOrgAccess(ctx, "o1", req) + }) +} + +func TestEnterpriseService_RemoveUsersFromOrgAccess(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/codespaces/access/selected_users", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBody(t, r, `{"selected_usernames":["u1"]}`+"\n") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + req := []string{"u1"} + resp, err := client.Codespaces.RemoveUsersFromOrgAccess(ctx, "o1", req) + if err != nil { + t.Fatalf("RemoveUsersFromOrgAccess returned error: %v", err) + } + if resp == nil { + t.Fatal("RemoveUsersFromOrgAccess returned nil Response") + } + + const methodName = "RemoveUsersFromOrgAccess" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.RemoveUsersFromOrgAccess(ctx, "\n", req) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.RemoveUsersFromOrgAccess(ctx, "o1", req) + }) +} + +func TestCodespacesService_ListUserCodespacesInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{"total_count":1,"codespaces":[{"id":1}]}`) + }) + + opts := &ListOptions{Page: 1, PerPage: 10} + ctx := t.Context() + got, _, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "o1", "u1", opts) + if err != nil { + t.Fatalf("Codespaces.ListUserCodespacesInOrg returned error: %v", err) + } + + want := &ListCodespaces{ + TotalCount: Ptr(1), + Codespaces: []*Codespace{ + {ID: Ptr(int64(1))}, + }, + } + + if !cmp.Equal(got, want) { + t.Errorf("Codespaces.ListUserCodespacesInOrg = %+v, want %+v", got, want) + } + const methodName = "ListUserCodespacesInOrg" + testBadOptions(t, methodName, func() error { + _, _, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "\n", "\n", opts) + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Codespaces.ListUserCodespacesInOrg(ctx, "o1", "u1", opts) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestEnterpriseService_DeleteUserCodespaceInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces/c1", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Codespaces.DeleteUserCodespaceInOrg(ctx, "o1", "u1", "c1") + if err != nil { + t.Fatalf("DeleteUserCodespaceInOrg returned error: %v", err) + } + if resp == nil { + t.Fatal("DeleteUserCodespaceInOrg returned nil Response") + } + + const methodName = "DeleteUserCodespaceInOrg" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.DeleteUserCodespaceInOrg(ctx, "\n", "u1", "c1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.DeleteUserCodespaceInOrg(ctx, "o1", "u1", "c1") + }) +} + +func TestEnterpriseService_StopUserCodespaceInOrg(t *testing.T) { + t.Parallel() + client, mux, _ := setup(t) + + mux.HandleFunc("/orgs/o1/members/u1/codespaces/c1/stop", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + w.WriteHeader(http.StatusNoContent) + }) + + ctx := t.Context() + resp, err := client.Codespaces.StopUserCodespaceInOrg(ctx, "o1", "u1", "c1") + if err != nil { + t.Fatalf("StopUserCodespaceInOrg returned error: %v", err) + } + if resp == nil { + t.Fatal("StopUserCodespaceInOrg returned nil Response") + } + + const methodName = "StopUserCodespaceInOrg" + testBadOptions(t, methodName, func() error { + _, err := client.Codespaces.StopUserCodespaceInOrg(ctx, "\n", "u1", "c1") + return err + }) + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Codespaces.StopUserCodespaceInOrg(ctx, "o1", "u1", "c1") + }) +}