From 3b96fb363f3e1c607f87f1a9e4051f2c4431055d Mon Sep 17 00:00:00 2001 From: Daniel Liao <10663736+liaodaniel@users.noreply.github.com> Date: Fri, 10 Nov 2023 14:36:15 +1100 Subject: [PATCH] Implement Custom Properties (#2986) Fixes: #2965. --- github/github-accessors.go | 40 ++++ github/github-accessors_test.go | 50 +++++ github/orgs_properties.go | 198 +++++++++++++++++ github/orgs_properties_test.go | 369 ++++++++++++++++++++++++++++++++ 4 files changed, 657 insertions(+) create mode 100644 github/orgs_properties.go create mode 100644 github/orgs_properties_test.go diff --git a/github/github-accessors.go b/github/github-accessors.go index bcae7636d0a..536ce5a8407 100644 --- a/github/github-accessors.go +++ b/github/github-accessors.go @@ -4990,6 +4990,46 @@ func (c *CredentialAuthorization) GetTokenLastEight() string { return *c.TokenLastEight } +// GetDefaultValue returns the DefaultValue field if it's non-nil, zero value otherwise. +func (c *CustomProperty) GetDefaultValue() string { + if c == nil || c.DefaultValue == nil { + return "" + } + return *c.DefaultValue +} + +// GetDescription returns the Description field if it's non-nil, zero value otherwise. +func (c *CustomProperty) GetDescription() string { + if c == nil || c.Description == nil { + return "" + } + return *c.Description +} + +// GetPropertyName returns the PropertyName field if it's non-nil, zero value otherwise. +func (c *CustomProperty) GetPropertyName() string { + if c == nil || c.PropertyName == nil { + return "" + } + return *c.PropertyName +} + +// GetRequired returns the Required field if it's non-nil, zero value otherwise. +func (c *CustomProperty) GetRequired() bool { + if c == nil || c.Required == nil { + return false + } + return *c.Required +} + +// GetValue returns the Value field if it's non-nil, zero value otherwise. +func (c *CustomPropertyValue) GetValue() string { + if c == nil || c.Value == nil { + return "" + } + return *c.Value +} + // GetBaseRole returns the BaseRole field if it's non-nil, zero value otherwise. func (c *CustomRepoRoles) GetBaseRole() string { if c == nil || c.BaseRole == nil { diff --git a/github/github-accessors_test.go b/github/github-accessors_test.go index 36feed7fe8e..abe96ecf98f 100644 --- a/github/github-accessors_test.go +++ b/github/github-accessors_test.go @@ -5899,6 +5899,56 @@ func TestCredentialAuthorization_GetTokenLastEight(tt *testing.T) { c.GetTokenLastEight() } +func TestCustomProperty_GetDefaultValue(tt *testing.T) { + var zeroValue string + c := &CustomProperty{DefaultValue: &zeroValue} + c.GetDefaultValue() + c = &CustomProperty{} + c.GetDefaultValue() + c = nil + c.GetDefaultValue() +} + +func TestCustomProperty_GetDescription(tt *testing.T) { + var zeroValue string + c := &CustomProperty{Description: &zeroValue} + c.GetDescription() + c = &CustomProperty{} + c.GetDescription() + c = nil + c.GetDescription() +} + +func TestCustomProperty_GetPropertyName(tt *testing.T) { + var zeroValue string + c := &CustomProperty{PropertyName: &zeroValue} + c.GetPropertyName() + c = &CustomProperty{} + c.GetPropertyName() + c = nil + c.GetPropertyName() +} + +func TestCustomProperty_GetRequired(tt *testing.T) { + var zeroValue bool + c := &CustomProperty{Required: &zeroValue} + c.GetRequired() + c = &CustomProperty{} + c.GetRequired() + c = nil + c.GetRequired() +} + +func TestCustomPropertyValue_GetValue(tt *testing.T) { + var zeroValue string + c := &CustomPropertyValue{Value: &zeroValue} + c.GetValue() + c = &CustomPropertyValue{} + c.GetValue() + c = nil + c.GetValue() +} + func TestCustomRepoRoles_GetBaseRole(tt *testing.T) { var zeroValue string c := &CustomRepoRoles{BaseRole: &zeroValue} diff --git a/github/orgs_properties.go b/github/orgs_properties.go new file mode 100644 index 00000000000..1daac811804 --- /dev/null +++ b/github/orgs_properties.go @@ -0,0 +1,198 @@ +// 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" + "fmt" +) + +// CustomProperty represents an organization custom property object. +type CustomProperty struct { + // PropertyName is required for most endpoints except when calling CreateOrUpdateCustomProperty; + // where this is sent in the path and thus can be omitted. + PropertyName *string `json:"property_name,omitempty"` + // Possible values for ValueType are: string, single_select + ValueType string `json:"value_type"` + Required *bool `json:"required,omitempty"` + DefaultValue *string `json:"default_value,omitempty"` + Description *string `json:"description,omitempty"` + AllowedValues []string `json:"allowed_values,omitempty"` +} + +// RepoCustomPropertyValue represents a repository custom property value. +type RepoCustomPropertyValue struct { + RepositoryID int64 `json:"repository_id"` + RepositoryName string `json:"repository_name"` + RepositoryFullName string `json:"repository_full_name"` + Properties []*CustomPropertyValue `json:"properties"` +} + +// CustomPropertyValue represents a custom property value. +type CustomPropertyValue struct { + PropertyName string `json:"property_name"` + Value *string `json:"value,omitempty"` +} + +// GetAllCustomProperties gets all custom properties that are defined for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#get-all-custom-properties-for-an-organization +// +//meta:operation GET /orgs/{org}/properties/schema +func (s *OrganizationsService) GetAllCustomProperties(ctx context.Context, org string) ([]*CustomProperty, *Response, error) { + u := fmt.Sprintf("orgs/%v/properties/schema", org) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var customProperties []*CustomProperty + resp, err := s.client.Do(ctx, req, &customProperties) + if err != nil { + return nil, resp, err + } + + return customProperties, resp, nil +} + +// CreateOrUpdateCustomProperties creates new or updates existing custom properties that are defined for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#create-or-update-custom-properties-for-an-organization +// +//meta:operation PATCH /orgs/{org}/properties/schema +func (s *OrganizationsService) CreateOrUpdateCustomProperties(ctx context.Context, org string, properties []*CustomProperty) ([]*CustomProperty, *Response, error) { + u := fmt.Sprintf("orgs/%v/properties/schema", org) + + params := struct { + Properties []*CustomProperty `json:"properties"` + }{ + Properties: properties, + } + + req, err := s.client.NewRequest("PATCH", u, params) + if err != nil { + return nil, nil, err + } + + var customProperties []*CustomProperty + resp, err := s.client.Do(ctx, req, &customProperties) + if err != nil { + return nil, resp, err + } + + return customProperties, resp, nil +} + +// GetCustomProperty gets a custom property that is defined for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#get-a-custom-property-for-an-organization +// +//meta:operation GET /orgs/{org}/properties/schema/{custom_property_name} +func (s *OrganizationsService) GetCustomProperty(ctx context.Context, org, name string) (*CustomProperty, *Response, error) { + u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, name) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + var customProperty *CustomProperty + resp, err := s.client.Do(ctx, req, &customProperty) + if err != nil { + return nil, resp, err + } + + return customProperty, resp, nil +} + +// CreateOrUpdateCustomProperty creates a new or updates an existing custom property that is defined for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#create-or-update-a-custom-property-for-an-organization +// +//meta:operation PUT /orgs/{org}/properties/schema/{custom_property_name} +func (s *OrganizationsService) CreateOrUpdateCustomProperty(ctx context.Context, org, customPropertyName string, property *CustomProperty) (*CustomProperty, *Response, error) { + u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, customPropertyName) + + req, err := s.client.NewRequest("PUT", u, property) + if err != nil { + return nil, nil, err + } + + var customProperty *CustomProperty + resp, err := s.client.Do(ctx, req, &customProperty) + if err != nil { + return nil, resp, err + } + + return customProperty, resp, nil +} + +// RemoveCustomProperty removes a custom property that is defined for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#remove-a-custom-property-for-an-organization +// +//meta:operation DELETE /orgs/{org}/properties/schema/{custom_property_name} +func (s *OrganizationsService) RemoveCustomProperty(ctx context.Context, org, customPropertyName string) (*Response, error) { + u := fmt.Sprintf("orgs/%v/properties/schema/%v", org, customPropertyName) + + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} + +// ListCustomPropertyValues lists all custom property values for repositories in the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#list-custom-property-values-for-organization-repositories +// +//meta:operation GET /orgs/{org}/properties/values +func (s *OrganizationsService) ListCustomPropertyValues(ctx context.Context, org string, opts *ListOptions) ([]*RepoCustomPropertyValue, *Response, error) { + u := fmt.Sprintf("orgs/%v/properties/values", 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 repoCustomPropertyValues []*RepoCustomPropertyValue + resp, err := s.client.Do(ctx, req, &repoCustomPropertyValues) + if err != nil { + return nil, resp, err + } + + return repoCustomPropertyValues, resp, nil +} + +// CreateOrUpdateRepoCustomPropertyValues creates new or updates existing custom property values across multiple repositories for the specified organization. +// +// GitHub API docs: https://docs.github.com/rest/orgs/properties#create-or-update-custom-property-values-for-organization-repositories +// +//meta:operation PATCH /orgs/{org}/properties/values +func (s *OrganizationsService) CreateOrUpdateRepoCustomPropertyValues(ctx context.Context, org string, repoNames []string, properties []*CustomProperty) (*Response, error) { + u := fmt.Sprintf("orgs/%v/properties/values", org) + + params := struct { + RepositoryNames []string `json:"repository_names"` + Properties []*CustomProperty `json:"properties"` + }{ + RepositoryNames: repoNames, + Properties: properties, + } + + req, err := s.client.NewRequest("PATCH", u, params) + if err != nil { + return nil, err + } + + return s.client.Do(ctx, req, nil) +} diff --git a/github/orgs_properties_test.go b/github/orgs_properties_test.go new file mode 100644 index 00000000000..1a372306046 --- /dev/null +++ b/github/orgs_properties_test.go @@ -0,0 +1,369 @@ +// 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" + "fmt" + "net/http" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestOrganizationsService_GetAllCustomProperties(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/schema", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `[ + { + "property_name": "name", + "value_type": "single_select", + "required": true, + "default_value": "production", + "description": "Prod or dev environment", + "allowed_values":[ + "production", + "development" + ] + }, + { + "property_name": "service", + "value_type": "string" + }, + { + "property_name": "team", + "value_type": "string", + "description": "Team owning the repository" + } + ]`) + }) + + ctx := context.Background() + properties, _, err := client.Organizations.GetAllCustomProperties(ctx, "o") + if err != nil { + t.Errorf("Organizations.GetAllCustomProperties returned error: %v", err) + } + + want := []*CustomProperty{ + { + PropertyName: String("name"), + ValueType: "single_select", + Required: Bool(true), + DefaultValue: String("production"), + Description: String("Prod or dev environment"), + AllowedValues: []string{"production", "development"}, + }, + { + PropertyName: String("service"), + ValueType: "string", + }, + { + PropertyName: String("team"), + ValueType: "string", + Description: String("Team owning the repository"), + }, + } + if !cmp.Equal(properties, want) { + t.Errorf("Organizations.GetAllCustomProperties returned %+v, want %+v", properties, want) + } + + const methodName = "GetAllCustomProperties" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.GetAllCustomProperties(ctx, "o") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_CreateOrUpdateCustomProperties(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/schema", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"properties":[{"property_name":"name","value_type":"single_select","required":true},{"property_name":"service","value_type":"string"}]}`+"\n") + fmt.Fprint(w, `[ + { + "property_name": "name", + "value_type": "single_select", + "required": true + }, + { + "property_name": "service", + "value_type": "string" + } + ]`) + }) + + ctx := context.Background() + properties, _, err := client.Organizations.CreateOrUpdateCustomProperties(ctx, "o", []*CustomProperty{ + { + PropertyName: String("name"), + ValueType: "single_select", + Required: Bool(true), + }, + { + PropertyName: String("service"), + ValueType: "string", + }, + }) + if err != nil { + t.Errorf("Organizations.CreateOrUpdateCustomProperties returned error: %v", err) + } + + want := []*CustomProperty{ + { + PropertyName: String("name"), + ValueType: "single_select", + Required: Bool(true), + }, + { + PropertyName: String("service"), + ValueType: "string", + }, + } + + if !cmp.Equal(properties, want) { + t.Errorf("Organizations.CreateOrUpdateCustomProperties returned %+v, want %+v", properties, want) + } + + const methodName = "CreateOrUpdateCustomProperties" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.CreateOrUpdateCustomProperties(ctx, "o", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_GetCustomProperty(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/schema/name", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + fmt.Fprint(w, `{ + "property_name": "name", + "value_type": "single_select", + "required": true, + "default_value": "production", + "description": "Prod or dev environment", + "allowed_values":[ + "production", + "development" + ] + }`) + }) + + ctx := context.Background() + property, _, err := client.Organizations.GetCustomProperty(ctx, "o", "name") + if err != nil { + t.Errorf("Organizations.GetCustomProperty returned error: %v", err) + } + + want := &CustomProperty{ + PropertyName: String("name"), + ValueType: "single_select", + Required: Bool(true), + DefaultValue: String("production"), + Description: String("Prod or dev environment"), + AllowedValues: []string{"production", "development"}, + } + if !cmp.Equal(property, want) { + t.Errorf("Organizations.GetCustomProperty returned %+v, want %+v", property, want) + } + + const methodName = "GetCustomProperty" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.GetCustomProperty(ctx, "o", "name") + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_CreateOrUpdateCustomProperty(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/schema/name", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + fmt.Fprint(w, `{ + "property_name": "name", + "value_type": "single_select", + "required": true, + "default_value": "production", + "description": "Prod or dev environment", + "allowed_values":[ + "production", + "development" + ] + }`) + }) + + ctx := context.Background() + property, _, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, "o", "name", &CustomProperty{ + ValueType: "single_select", + Required: Bool(true), + DefaultValue: String("production"), + Description: String("Prod or dev environment"), + AllowedValues: []string{"production", "development"}, + }) + if err != nil { + t.Errorf("Organizations.CreateOrUpdateCustomProperty returned error: %v", err) + } + + want := &CustomProperty{ + PropertyName: String("name"), + ValueType: "single_select", + Required: Bool(true), + DefaultValue: String("production"), + Description: String("Prod or dev environment"), + AllowedValues: []string{"production", "development"}, + } + if !cmp.Equal(property, want) { + t.Errorf("Organizations.CreateOrUpdateCustomProperty returned %+v, want %+v", property, want) + } + + const methodName = "CreateOrUpdateCustomProperty" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.CreateOrUpdateCustomProperty(ctx, "o", "name", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_RemoveCustomProperty(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/schema/name", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + ctx := context.Background() + _, err := client.Organizations.RemoveCustomProperty(ctx, "o", "name") + if err != nil { + t.Errorf("Organizations.RemoveCustomProperty returned error: %v", err) + } + + const methodName = "RemoveCustomProperty" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.RemoveCustomProperty(ctx, "0", "name") + }) +} + +func TestOrganizationsService_ListCustomPropertyValues(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/values", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testFormValues(t, r, values{"page": "1", "per_page": "100"}) + fmt.Fprint(w, `[{ + "repository_id": 1296269, + "repository_name": "Hello-World", + "repository_full_name": "octocat/Hello-World", + "properties": [ + { + "property_name": "environment", + "value": "production" + }, + { + "property_name": "service", + "value": "web" + } + ] + }]`) + }) + + ctx := context.Background() + repoPropertyValues, _, err := client.Organizations.ListCustomPropertyValues(ctx, "o", &ListOptions{ + Page: 1, + PerPage: 100, + }) + if err != nil { + t.Errorf("Organizations.ListCustomPropertyValues returned error: %v", err) + } + + want := []*RepoCustomPropertyValue{ + { + RepositoryID: 1296269, + RepositoryName: "Hello-World", + RepositoryFullName: "octocat/Hello-World", + Properties: []*CustomPropertyValue{ + { + PropertyName: "environment", + Value: String("production"), + }, + { + PropertyName: "service", + Value: String("web"), + }, + }, + }, + } + + if !cmp.Equal(repoPropertyValues, want) { + t.Errorf("Organizations.ListCustomPropertyValues returned %+v, want %+v", repoPropertyValues, want) + } + + const methodName = "ListCustomPropertyValues" + + testBadOptions(t, methodName, func() (err error) { + _, _, err = client.Organizations.ListCustomPropertyValues(ctx, "\n", &ListOptions{}) + return err + }) + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + got, resp, err := client.Organizations.ListCustomPropertyValues(ctx, "o", nil) + if got != nil { + t.Errorf("testNewRequestAndDoFailure %v = %#v, want nil", methodName, got) + } + return resp, err + }) +} + +func TestOrganizationsService_CreateOrUpdateRepoCustomPropertyValues(t *testing.T) { + client, mux, _, teardown := setup() + defer teardown() + + mux.HandleFunc("/orgs/o/properties/values", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + testBody(t, r, `{"repository_names":["repo"],"properties":[{"property_name":"service","value_type":"string"}]}`+"\n") + }) + + ctx := context.Background() + _, err := client.Organizations.CreateOrUpdateRepoCustomPropertyValues(ctx, "o", []string{"repo"}, []*CustomProperty{ + { + PropertyName: String("service"), + ValueType: "string", + }, + }) + if err != nil { + t.Errorf("Organizations.CreateOrUpdateCustomPropertyValuesForRepos returned error: %v", err) + } + + const methodName = "CreateOrUpdateCustomPropertyValuesForRepos" + + testNewRequestAndDoFailure(t, methodName, client, func() (*Response, error) { + return client.Organizations.CreateOrUpdateRepoCustomPropertyValues(ctx, "o", nil, nil) + }) +}