Repository: xanzy/go-gitlab
Pull request #1131 (2021-04-16 09:37:22)

Support for managing licenses
This PR adds support for [project-level managed licenses](https://docs.gitlab.com/ee/api/managed_licenses.html).

Specifically:
- Adds a new `ProjectManagedLicensesService` service
- Adds functionality to:
  - List managed licenses
  - Get managed license
  - Edit existing managed license
  - Add/Remove managed licenses to project

All requests are covered by tests.


Patch:
diff --git a/gitlab.go b/gitlab.go
index ca7bdf2..feed610 100644
--- a/gitlab.go
+++ b/gitlab.go
@@ -141,6 +141,7 @@ type Client struct {
 	Labels                *LabelsService
 	License               *LicenseService
 	LicenseTemplates      *LicenseTemplatesService
+	ManagedLicenses       *ManagedLicensesService
 	MergeRequestApprovals *MergeRequestApprovalsService
 	MergeRequests         *MergeRequestsService
 	Milestones            *MilestonesService
@@ -313,6 +314,7 @@ func newClient(options ...ClientOptionFunc) (*Client, error) {
 	c.Labels = &LabelsService{client: c}
 	c.License = &LicenseService{client: c}
 	c.LicenseTemplates = &LicenseTemplatesService{client: c}
+	c.ManagedLicenses = &ManagedLicensesService{client: c}
 	c.MergeRequestApprovals = &MergeRequestApprovalsService{client: c}
 	c.MergeRequests = &MergeRequestsService{client: c, timeStats: timeStats}
 	c.Milestones = &MilestonesService{client: c}
diff --git a/project_managed_licenses.go b/project_managed_licenses.go
new file mode 100644
index 0000000..186bdef
--- /dev/null
+++ b/project_managed_licenses.go
@@ -0,0 +1,188 @@
+//
+// Copyright 2021, Andrea Perizzato
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"fmt"
+	"net/http"
+)
+
+// ManagedLicensesService handles communication with the managed licenses
+// methods of the GitLab API.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/managed_licenses.html
+type ManagedLicensesService struct {
+	client *Client
+}
+
+// ManagedLicense represents a managed license.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/managed_licenses.html
+type ManagedLicense struct {
+	ID             int                        `json:"id"`
+	Name           string                     `json:"name"`
+	ApprovalStatus LicenseApprovalStatusValue `json:"approval_status"`
+}
+
+// ListManagedLicenses returns a list of managed licenses from a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#list-managed-licenses
+func (s *ManagedLicensesService) ListManagedLicenses(pid interface{}, options ...RequestOptionFunc) ([]*ManagedLicense, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/managed_licenses", pathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var mls []*ManagedLicense
+	resp, err := s.client.Do(req, &mls)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return mls, resp, err
+}
+
+// GetManagedLicense returns an existing managed license.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#show-an-existing-managed-license
+func (s *ManagedLicensesService) GetManagedLicense(pid, mlid interface{}, options ...RequestOptionFunc) (*ManagedLicense, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	license, err := parseID(mlid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/managed_licenses/%s", pathEscape(project), pathEscape(license))
+
+	req, err := s.client.NewRequest(http.MethodGet, u, nil, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ml := new(ManagedLicense)
+	resp, err := s.client.Do(req, ml)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ml, resp, err
+}
+
+// AddManagedLicenseOptions represents the available AddManagedLicense() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#create-a-new-managed-license
+type AddManagedLicenseOptions struct {
+	Name           *string                     `url:"name,omitempty" json:"name,omitempty"`
+	ApprovalStatus *LicenseApprovalStatusValue `url:"approval_status,omitempty" json:"approval_status,omitempty"`
+}
+
+// AddManagedLicense adds a managed license to a project.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#create-a-new-managed-license
+func (s *ManagedLicensesService) AddManagedLicense(pid interface{}, opt *AddManagedLicenseOptions, options ...RequestOptionFunc) (*ManagedLicense, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/managed_licenses", pathEscape(project))
+
+	req, err := s.client.NewRequest(http.MethodPost, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ml := new(ManagedLicense)
+	resp, err := s.client.Do(req, ml)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ml, resp, err
+}
+
+// DeleteManagedLicense deletes a managed license with a given ID.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#delete-a-managed-license
+func (s *ManagedLicensesService) DeleteManagedLicense(pid, mlid interface{}, options ...RequestOptionFunc) (*Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, err
+	}
+	license, err := parseID(mlid)
+	if err != nil {
+		return nil, err
+	}
+	u := fmt.Sprintf("projects/%s/managed_licenses/%s", pathEscape(project), pathEscape(license))
+
+	req, err := s.client.NewRequest(http.MethodDelete, u, nil, options)
+	if err != nil {
+		return nil, err
+	}
+
+	return s.client.Do(req, nil)
+}
+
+// EditManagedLicenceOptions represents the available EditManagedLicense() options.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#edit-an-existing-managed-license
+type EditManagedLicenceOptions struct {
+	ApprovalStatus *LicenseApprovalStatusValue `url:"approval_status,omitempty" json:"approval_status,omitempty"`
+}
+
+// EditManagedLicense updates an existing managed license with a new approval
+// status.
+//
+// GitLab API docs:
+// https://docs.gitlab.com/ee/api/managed_licenses.html#edit-an-existing-managed-license
+func (s *ManagedLicensesService) EditManagedLicense(pid, mlid interface{}, opt *EditManagedLicenceOptions, options ...RequestOptionFunc) (*ManagedLicense, *Response, error) {
+	project, err := parseID(pid)
+	if err != nil {
+		return nil, nil, err
+	}
+	license, err := parseID(mlid)
+	if err != nil {
+		return nil, nil, err
+	}
+	u := fmt.Sprintf("projects/%s/managed_licenses/%s", pathEscape(project), pathEscape(license))
+
+	req, err := s.client.NewRequest(http.MethodPatch, u, opt, options)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	ml := new(ManagedLicense)
+	resp, err := s.client.Do(req, ml)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return ml, resp, err
+}
diff --git a/types.go b/types.go
index 487429a..805cd5b 100644
--- a/types.go
+++ b/types.go
@@ -249,6 +249,25 @@ func LinkType(v LinkTypeValue) *LinkTypeValue {
 	return p
 }
 
+// LicenseApprovalStatusValue describe the approval statuses of a license.
+//
+// GitLab API docs: https://docs.gitlab.com/ee/api/managed_licenses.html
+type LicenseApprovalStatusValue string
+
+// List of available license approval statuses.
+const (
+	LicenseApproved    LicenseApprovalStatusValue = "approved"
+	LicenseBlacklisted LicenseApprovalStatusValue = "blacklisted"
+)
+
+// LicenseApprovalStatus is a helper routine that allocates a new license
+// approval status value to store v and returns a pointer to it.
+func LicenseApprovalStatus(v LicenseApprovalStatusValue) *LicenseApprovalStatusValue {
+	p := new(LicenseApprovalStatusValue)
+	*p = v
+	return p
+}
+
 // MergeMethodValue represents a project merge type within GitLab.
 //
 // GitLab API docs: https://docs.gitlab.com/ce/api/projects.html#project-merge-method


Test patch:
diff --git a/project_managed_licenses_test.go b/project_managed_licenses_test.go
new file mode 100644
index 0000000..a486ae4
--- /dev/null
+++ b/project_managed_licenses_test.go
@@ -0,0 +1,156 @@
+//
+// Copyright 2021, Andrea Perizzato
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package gitlab
+
+import (
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+func TestListManagedLicenses(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/managed_licenses", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		mustWriteHTTPResponse(t, w, "testdata/list_project_managed_licenses.json")
+	})
+
+	licenses, _, err := client.ManagedLicenses.ListManagedLicenses(1)
+	if err != nil {
+		t.Errorf("ManagedLicenses.ListManagedLicenses returned error: %v", err)
+	}
+
+	want := []*ManagedLicense{
+		{
+			ID:             1,
+			Name:           "MIT",
+			ApprovalStatus: LicenseApproved,
+		},
+		{
+			ID:             3,
+			Name:           "ISC",
+			ApprovalStatus: LicenseBlacklisted,
+		},
+	}
+
+	if !reflect.DeepEqual(want, licenses) {
+		t.Errorf("ManagedLicenses.ListManagedLicenses returned %+v, want %+v", licenses, want)
+	}
+}
+
+func TestGetManagedLicenses(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/managed_licenses/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodGet)
+		mustWriteHTTPResponse(t, w, "testdata/get_project_managed_license.json")
+	})
+
+	license, _, err := client.ManagedLicenses.GetManagedLicense(1, 3)
+	if err != nil {
+		t.Errorf("ManagedLicenses.GetManagedLicense returned error: %v", err)
+	}
+
+	want := &ManagedLicense{
+		ID:             3,
+		Name:           "ISC",
+		ApprovalStatus: LicenseBlacklisted,
+	}
+
+	if !reflect.DeepEqual(want, license) {
+		t.Errorf("ManagedLicenses.GetManagedLicense returned %+v, want %+v", license, want)
+	}
+}
+
+func TestAddManagedLicenses(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/managed_licenses", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPost)
+		testBody(t, r, `{"name":"MIT","approval_status":"approved"}`)
+		mustWriteHTTPResponse(t, w, "testdata/add_project_managed_license.json")
+	})
+
+	ops := AddManagedLicenseOptions{
+		Name:           String("MIT"),
+		ApprovalStatus: LicenseApprovalStatus(LicenseApproved),
+	}
+	license, _, err := client.ManagedLicenses.AddManagedLicense(1, &ops)
+	if err != nil {
+		t.Errorf("ManagedLicenses.AddManagedLicense returned error: %v", err)
+	}
+
+	want := &ManagedLicense{
+		ID:             123,
+		Name:           "MIT",
+		ApprovalStatus: LicenseApproved,
+	}
+
+	if !reflect.DeepEqual(want, license) {
+		t.Errorf("ManagedLicenses.AddManagedLicense returned %+v, want %+v", license, want)
+	}
+}
+
+func TestDeleteManagedLicenses(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/managed_licenses/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodDelete)
+	})
+
+	_, err := client.ManagedLicenses.DeleteManagedLicense(1, 3)
+	if err != nil {
+		t.Errorf("ManagedLicenses.RemoveManagedLicense returned error: %v", err)
+	}
+}
+
+func TestEditManagedLicenses(t *testing.T) {
+	mux, server, client := setup(t)
+	defer teardown(server)
+
+	mux.HandleFunc("/api/v4/projects/1/managed_licenses/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, http.MethodPatch)
+		approvalStatus := r.URL.Query().Get("approval_status")
+		if approvalStatus != "blacklisted" {
+			t.Errorf("query param approval_status should be blacklisted but was %s", approvalStatus)
+		}
+		mustWriteHTTPResponse(t, w, "testdata/edit_project_managed_license.json")
+	})
+
+	ops := EditManagedLicenceOptions{
+		ApprovalStatus: LicenseApprovalStatus(LicenseBlacklisted),
+	}
+	license, _, err := client.ManagedLicenses.EditManagedLicense(1, 3, &ops)
+	if err != nil {
+		t.Errorf("ManagedLicenses.EditManagedLicense returned error: %v", err)
+	}
+
+	want := &ManagedLicense{
+		ID:             3,
+		Name:           "CUSTOM",
+		ApprovalStatus: LicenseBlacklisted,
+	}
+
+	if !reflect.DeepEqual(want, license) {
+		t.Errorf("ManagedLicenses.EditManagedLicense returned %+v, want %+v", license, want)
+	}
+}
diff --git a/testdata/add_project_managed_license.json b/testdata/add_project_managed_license.json
new file mode 100644
index 0000000..afe15d7
--- /dev/null
+++ b/testdata/add_project_managed_license.json
@@ -0,0 +1,5 @@
+{
+  "id": 123,
+  "name": "MIT",
+  "approval_status": "approved"
+}
diff --git a/testdata/edit_project_managed_license.json b/testdata/edit_project_managed_license.json
new file mode 100644
index 0000000..37f08c5
--- /dev/null
+++ b/testdata/edit_project_managed_license.json
@@ -0,0 +1,5 @@
+{
+  "id": 3,
+  "name": "CUSTOM",
+  "approval_status": "blacklisted"
+}
diff --git a/testdata/get_project_managed_license.json b/testdata/get_project_managed_license.json
new file mode 100644
index 0000000..18982e2
--- /dev/null
+++ b/testdata/get_project_managed_license.json
@@ -0,0 +1,5 @@
+{
+  "id": 3,
+  "name": "ISC",
+  "approval_status": "blacklisted"
+}
diff --git a/testdata/list_project_managed_licenses.json b/testdata/list_project_managed_licenses.json
new file mode 100644
index 0000000..2c3c6b7
--- /dev/null
+++ b/testdata/list_project_managed_licenses.json
@@ -0,0 +1,12 @@
+[
+  {
+    "id": 1,
+    "name": "MIT",
+    "approval_status": "approved"
+  },
+  {
+    "id": 3,
+    "name": "ISC",
+    "approval_status": "blacklisted"
+  }
+]