Repository: google/go-github
Pull request #1595 (2020-08-07 12:25:15)

Add support for Issue Import GitHub API
This pull request implements methods provided in the Issue Import GitHub APi. These methods are primarily oriented to importing issues, and the benefits of using them are:

- No notifications are triggered when the issues are created.
- You can specify issue creation date.

More on Issue Import API [here](https://gist.github.com/jonmagic/5282384165e0f86ef105#check-status-of-multiple-issues).

google-cla[bot]: 
Thanks for your pull request. It looks like this may be your first contribution to a Google open source project (if not, look below for help). Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

:memo: **Please visit <https://cla.developers.google.com/> to sign.**

Once you've signed (or fixed any issues), please reply here with `@googlebot I signed it!` and we'll verify it.

----

#### What to do if you already signed the CLA

##### Individual signers

*   It's possible we don't have your GitHub username or you're using a different email address on your commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).

##### Corporate signers

*   Your company has a Point of Contact who decides which employees are authorized to participate. Ask your POC to be added to the group of authorized contributors. If you don't know who your Point of Contact is, direct the Google project maintainer to [go/cla#troubleshoot](http://go/cla#troubleshoot) ([Public version](https://opensource.google/docs/cla/#troubleshoot)).
*   The email used to register you as an authorized contributor must be the email used for the Git commit. Check [your existing CLA data](https://cla.developers.google.com/clas) and verify that your [email is set on your git commits](https://help.github.com/articles/setting-your-email-in-git/).
*   The email used to register you as an authorized contributor must also be [attached to your GitHub account](https://github.com/settings/emails).
		

ℹ️ **Googlers: [Go here](https://goto.google.com/prinfo/https%3A%2F%2Fgithub.com%2Fgoogle%2Fgo-github%2Fpull%2F1595) for more info**.

<!-- need_sender_cla -->
amarruedo: @googlebot I signed it!
nightlark: For the failing tests, it looks like you need to run these steps from [CONTRIBUTING.md](https://github.com/google/go-github/blob/master/CONTRIBUTING.md#submitting-a-patch):
```
go generate github.com/google/go-github/...
go test github.com/google/go-github/...
go vet github.com/google/go-github/...
```


amarruedo: Hi @nightlark 

Thanks for the tips and corrections, I'll update the code as you suggest.

I already tried to contact support@github.com and, apparently, nowadays that address does no longer work, thus, I was redirected to https://github.community/

I placed there the following [question](https://github.community/t/import-issues-from-gitlab/707/11) and so far I haven't received any feedback. I don't know if I should open a new question focusing on validity of this API or not.

On the other hand, I've tested myself (I'm working on a little migration application) the API and all I can say is that it works as described in the gist. I'll try to write tests for this API.




Patch:
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 8e35c555..0c8e260e 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -1460,6 +1460,14 @@ func (c *CombinedStatus) GetTotalCount() int {
 	return *c.TotalCount
 }
 
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (c *Comment) GetCreatedAt() time.Time {
+	if c == nil || c.CreatedAt == nil {
+		return time.Time{}
+	}
+	return *c.CreatedAt
+}
+
 // GetTotalCommitComments returns the TotalCommitComments field if it's non-nil, zero value otherwise.
 func (c *CommentStats) GetTotalCommitComments() int {
 	if c == nil || c.TotalCommitComments == nil {
@@ -5084,6 +5092,166 @@ func (i *IssueEvent) GetURL() string {
 	return *i.URL
 }
 
+// GetAssignee returns the Assignee field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetAssignee() string {
+	if i == nil || i.Assignee == nil {
+		return ""
+	}
+	return *i.Assignee
+}
+
+// GetClosed returns the Closed field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetClosed() bool {
+	if i == nil || i.Closed == nil {
+		return false
+	}
+	return *i.Closed
+}
+
+// GetClosedAt returns the ClosedAt field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetClosedAt() time.Time {
+	if i == nil || i.ClosedAt == nil {
+		return time.Time{}
+	}
+	return *i.ClosedAt
+}
+
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetCreatedAt() time.Time {
+	if i == nil || i.CreatedAt == nil {
+		return time.Time{}
+	}
+	return *i.CreatedAt
+}
+
+// GetMilestone returns the Milestone field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetMilestone() int {
+	if i == nil || i.Milestone == nil {
+		return 0
+	}
+	return *i.Milestone
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (i *IssueImport) GetUpdatedAt() time.Time {
+	if i == nil || i.UpdatedAt == nil {
+		return time.Time{}
+	}
+	return *i.UpdatedAt
+}
+
+// GetCode returns the Code field if it's non-nil, zero value otherwise.
+func (i *IssueImportError) GetCode() string {
+	if i == nil || i.Code == nil {
+		return ""
+	}
+	return *i.Code
+}
+
+// GetField returns the Field field if it's non-nil, zero value otherwise.
+func (i *IssueImportError) GetField() string {
+	if i == nil || i.Field == nil {
+		return ""
+	}
+	return *i.Field
+}
+
+// GetLocation returns the Location field if it's non-nil, zero value otherwise.
+func (i *IssueImportError) GetLocation() string {
+	if i == nil || i.Location == nil {
+		return ""
+	}
+	return *i.Location
+}
+
+// GetResource returns the Resource field if it's non-nil, zero value otherwise.
+func (i *IssueImportError) GetResource() string {
+	if i == nil || i.Resource == nil {
+		return ""
+	}
+	return *i.Resource
+}
+
+// GetValue returns the Value field if it's non-nil, zero value otherwise.
+func (i *IssueImportError) GetValue() string {
+	if i == nil || i.Value == nil {
+		return ""
+	}
+	return *i.Value
+}
+
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetCreatedAt() time.Time {
+	if i == nil || i.CreatedAt == nil {
+		return time.Time{}
+	}
+	return *i.CreatedAt
+}
+
+// GetDocumentationURL returns the DocumentationURL field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetDocumentationURL() string {
+	if i == nil || i.DocumentationURL == nil {
+		return ""
+	}
+	return *i.DocumentationURL
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetID() int {
+	if i == nil || i.ID == nil {
+		return 0
+	}
+	return *i.ID
+}
+
+// GetImportIssuesURL returns the ImportIssuesURL field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetImportIssuesURL() string {
+	if i == nil || i.ImportIssuesURL == nil {
+		return ""
+	}
+	return *i.ImportIssuesURL
+}
+
+// GetMessage returns the Message field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetMessage() string {
+	if i == nil || i.Message == nil {
+		return ""
+	}
+	return *i.Message
+}
+
+// GetRepositoryURL returns the RepositoryURL field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetRepositoryURL() string {
+	if i == nil || i.RepositoryURL == nil {
+		return ""
+	}
+	return *i.RepositoryURL
+}
+
+// GetStatus returns the Status field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetStatus() string {
+	if i == nil || i.Status == nil {
+		return ""
+	}
+	return *i.Status
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetUpdatedAt() time.Time {
+	if i == nil || i.UpdatedAt == nil {
+		return time.Time{}
+	}
+	return *i.UpdatedAt
+}
+
+// GetURL returns the URL field if it's non-nil, zero value otherwise.
+func (i *IssueImportResponse) GetURL() string {
+	if i == nil || i.URL == nil {
+		return ""
+	}
+	return *i.URL
+}
+
 // GetSince returns the Since field if it's non-nil, zero value otherwise.
 func (i *IssueListCommentsOptions) GetSince() time.Time {
 	if i == nil || i.Since == nil {
diff --git a/github/github.go b/github/github.go
index c68a80c7..b6abc8f4 100644
--- a/github/github.go
+++ b/github/github.go
@@ -43,6 +43,7 @@ const (
 	mediaTypeV3Diff            = "application/vnd.github.v3.diff"
 	mediaTypeV3Patch           = "application/vnd.github.v3.patch"
 	mediaTypeOrgPermissionRepo = "application/vnd.github.v3.repository+json"
+	mediaTypeIssueImportAPI    = "application/vnd.github.golden-comet-preview+json"
 
 	// Media Type values to access preview APIs
 
@@ -170,6 +171,7 @@ type Client struct {
 	Git            *GitService
 	Gitignores     *GitignoresService
 	Interactions   *InteractionsService
+	IssueImport    *IssueImportService
 	Issues         *IssuesService
 	Licenses       *LicensesService
 	Marketplace    *MarketplaceService
@@ -277,6 +279,7 @@ func NewClient(httpClient *http.Client) *Client {
 	c.Git = (*GitService)(&c.common)
 	c.Gitignores = (*GitignoresService)(&c.common)
 	c.Interactions = (*InteractionsService)(&c.common)
+	c.IssueImport = (*IssueImportService)(&c.common)
 	c.Issues = (*IssuesService)(&c.common)
 	c.Licenses = (*LicensesService)(&c.common)
 	c.Marketplace = &MarketplaceService{client: c}
@@ -554,7 +557,21 @@ func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Res
 
 		return nil, err
 	}
-	defer resp.Body.Close()
+
+	defer func() {
+		// Ensure the response body is fully read and closed
+		// before we reconnect, so that we reuse the same TCP connection.
+		// Close the previous response's body. But read at least some of
+		// the body so if it's small the underlying TCP connection will be
+		// re-used. No need to check for errors: if it fails, the Transport
+		// won't reuse it anyway.
+		const maxBodySlurpSize = 2 << 10
+		if resp.ContentLength == -1 || resp.ContentLength <= maxBodySlurpSize {
+			io.CopyN(ioutil.Discard, resp.Body, maxBodySlurpSize)
+		}
+
+		resp.Body.Close()
+	}()
 
 	response := newResponse(resp)
 
diff --git a/github/issue_import.go b/github/issue_import.go
new file mode 100644
index 00000000..a9810407
--- /dev/null
+++ b/github/issue_import.go
@@ -0,0 +1,152 @@
+// Copyright 2020 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 (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"time"
+)
+
+// IssueImportService handles communication with the issue import related
+// methods of the Issue Import GitHub API.
+type IssueImportService service
+
+// IssueImportRequest represents a request to create an issue.
+//
+// https://gist.github.com/jonmagic/5282384165e0f86ef105#supported-issue-and-comment-fields
+type IssueImportRequest struct {
+	IssueImport IssueImport `json:"issue"`
+	Comments    []*Comment  `json:"comments,omitempty"`
+}
+
+// IssueImport represents body of issue to import.
+type IssueImport struct {
+	Title     string     `json:"title"`
+	Body      string     `json:"body"`
+	CreatedAt *time.Time `json:"created_at,omitempty"`
+	ClosedAt  *time.Time `json:"closed_at,omitempty"`
+	UpdatedAt *time.Time `json:"updated_at,omitempty"`
+	Assignee  *string    `json:"assignee,omitempty"`
+	Milestone *int       `json:"milestone,omitempty"`
+	Closed    *bool      `json:"closed,omitempty"`
+	Labels    []string   `json:"labels,omitempty"`
+}
+
+// Comment represents comments of issue to import.
+type Comment struct {
+	CreatedAt *time.Time `json:"created_at,omitempty"`
+	Body      string     `json:"body"`
+}
+
+// IssueImportResponse represents the response of an issue import create request.
+//
+// https://gist.github.com/jonmagic/5282384165e0f86ef105#import-issue-response
+type IssueImportResponse struct {
+	ID               *int                `json:"id,omitempty"`
+	Status           *string             `json:"status,omitempty"`
+	URL              *string             `json:"url,omitempty"`
+	ImportIssuesURL  *string             `json:"import_issues_url,omitempty"`
+	RepositoryURL    *string             `json:"repository_url,omitempty"`
+	CreatedAt        *time.Time          `json:"created_at,omitempty"`
+	UpdatedAt        *time.Time          `json:"updated_at,omitempty"`
+	Message          *string             `json:"message,omitempty"`
+	DocumentationURL *string             `json:"documentation_url,omitempty"`
+	Errors           []*IssueImportError `json:"errors,omitempty"`
+}
+
+// IssueImportError represents errors of an issue import create request.
+type IssueImportError struct {
+	Location *string `json:"location,omitempty"`
+	Resource *string `json:"resource,omitempty"`
+	Field    *string `json:"field,omitempty"`
+	Value    *string `json:"value,omitempty"`
+	Code     *string `json:"code,omitempty"`
+}
+
+// Create a new imported issue on the specified repository.
+//
+// https://gist.github.com/jonmagic/5282384165e0f86ef105#start-an-issue-import
+func (s *IssueImportService) Create(ctx context.Context, owner, repo string, issue *IssueImportRequest) (*IssueImportResponse, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/import/issues", owner, repo)
+	req, err := s.client.NewRequest("POST", u, issue)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept headers when APIs fully launch.
+	req.Header.Set("Accept", mediaTypeIssueImportAPI)
+
+	i := new(IssueImportResponse)
+	resp, err := s.client.Do(ctx, req, i)
+	if err != nil {
+		aerr, ok := err.(*AcceptedError)
+		if ok {
+			decErr := json.Unmarshal(aerr.Raw, i)
+			if decErr != nil {
+				err = decErr
+			}
+
+			return i, resp, nil
+		}
+
+		return nil, resp, err
+	}
+
+	return i, resp, nil
+}
+
+// CheckStatus checks the status of an imported issue.
+//
+// https://gist.github.com/jonmagic/5282384165e0f86ef105#import-status-request
+func (s *IssueImportService) CheckStatus(ctx context.Context, owner, repo string, issueID int64) (*IssueImportResponse, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/import/issues/%v", owner, repo, issueID)
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept headers when APIs fully launch.
+	req.Header.Set("Accept", mediaTypeIssueImportAPI)
+
+	i := new(IssueImportResponse)
+	resp, err := s.client.Do(ctx, req, i)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return i, resp, nil
+}
+
+// CheckStatusSince checks the status of multiple imported issues since a given date.
+//
+// https://gist.github.com/jonmagic/5282384165e0f86ef105#check-status-of-multiple-issues
+func (s *IssueImportService) CheckStatusSince(ctx context.Context, owner, repo string, since time.Time) ([]*IssueImportResponse, *Response, error) {
+	u := fmt.Sprintf("repos/%v/%v/import/issues?since=%v", owner, repo, since.Format("2006-01-02"))
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept headers when APIs fully launch.
+	req.Header.Set("Accept", mediaTypeIssueImportAPI)
+
+	var b bytes.Buffer
+	resp, err := s.client.Do(ctx, req, &b)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	var i []*IssueImportResponse
+	err = json.Unmarshal(b.Bytes(), &i)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return i, resp, nil
+}


Test patch:
diff --git a/github/issue_import_test.go b/github/issue_import_test.go
new file mode 100644
index 00000000..5948ef44
--- /dev/null
+++ b/github/issue_import_test.go
@@ -0,0 +1,144 @@
+// Copyright 2020 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"
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestIssueImportService_Create(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	createdAt := time.Date(2020, time.August, 11, 15, 30, 0, 0, time.UTC)
+	input := &IssueImportRequest{
+		IssueImport: IssueImport{
+			Assignee:  String("developer"),
+			Body:      "Dummy description",
+			CreatedAt: &createdAt,
+			Labels:    []string{"l1", "l2"},
+			Milestone: Int(1),
+			Title:     "Dummy Issue",
+		},
+		Comments: []*Comment{{
+			CreatedAt: &createdAt,
+			Body:      "Comment body",
+		}},
+	}
+
+	mux.HandleFunc("/repos/o/r/import/issues", func(w http.ResponseWriter, r *http.Request) {
+		v := new(IssueImportRequest)
+		json.NewDecoder(r.Body).Decode(v)
+		testMethod(t, r, "POST")
+		testHeader(t, r, "Accept", mediaTypeIssueImportAPI)
+		if !reflect.DeepEqual(v, input) {
+			t.Errorf("Request body = %+v, want %+v", v, input)
+		}
+
+		w.WriteHeader(http.StatusAccepted)
+		w.Write(issueImportResponseJSON)
+	})
+
+	got, _, err := client.IssueImport.Create(context.Background(), "o", "r", input)
+	if err != nil {
+		t.Errorf("Create returned error: %v", err)
+	}
+
+	want := wantIssueImportResponse
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Create = %+v, want %+v", got, want)
+	}
+}
+
+func TestIssueImportService_Create_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	_, _, err := client.IssueImport.Create(context.Background(), "%", "r", nil)
+	testURLParseError(t, err)
+}
+
+func TestIssueImportService_CheckStatus(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/import/issues/3", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIssueImportAPI)
+		w.WriteHeader(http.StatusOK)
+		w.Write(issueImportResponseJSON)
+	})
+
+	got, _, err := client.IssueImport.CheckStatus(context.Background(), "o", "r", 3)
+	if err != nil {
+		t.Errorf("CheckStatus returned error: %v", err)
+	}
+
+	want := wantIssueImportResponse
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("CheckStatus = %+v, want %+v", got, want)
+	}
+}
+
+func TestIssueImportService_CheckStatus_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	_, _, err := client.IssueImport.CheckStatus(context.Background(), "%", "r", 1)
+	testURLParseError(t, err)
+}
+
+func TestIssueImportService_CheckStatusSince(t *testing.T) {
+	client, mux, _, teardown := setup()
+	defer teardown()
+
+	mux.HandleFunc("/repos/o/r/import/issues", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIssueImportAPI)
+		w.WriteHeader(http.StatusOK)
+		w.Write([]byte(fmt.Sprintf("[%s]", issueImportResponseJSON)))
+	})
+
+	got, _, err := client.IssueImport.CheckStatusSince(context.Background(), "o", "r", time.Now())
+	if err != nil {
+		t.Errorf("CheckStatusSince returned error: %v", err)
+	}
+
+	want := []*IssueImportResponse{wantIssueImportResponse}
+	if !reflect.DeepEqual(want, got) {
+		t.Errorf("CheckStatusSince = %v, want = %v", got, want)
+	}
+}
+
+func TestIssueImportService_CheckStatusSince_invalidOwner(t *testing.T) {
+	client, _, _, teardown := setup()
+	defer teardown()
+
+	_, _, err := client.IssueImport.CheckStatusSince(context.Background(), "%", "r", time.Now())
+	testURLParseError(t, err)
+}
+
+var issueImportResponseJSON = []byte(`{
+	"id": 3,
+	"status": "pending",
+	"url": "https://api.github.com/repos/o/r/import/issues/3",
+	"import_issues_url": "https://api.github.com/repos/o/r/import/issues",
+	"repository_url": "https://api.github.com/repos/o/r"
+}`)
+
+var wantIssueImportResponse = &IssueImportResponse{
+	ID:              Int(3),
+	Status:          String("pending"),
+	URL:             String("https://api.github.com/repos/o/r/import/issues/3"),
+	ImportIssuesURL: String("https://api.github.com/repos/o/r/import/issues"),
+	RepositoryURL:   String("https://api.github.com/repos/o/r"),
+}
diff --git a/github/licenses_test.go b/github/licenses_test.go
index a71f6a4e..5446ffd1 100644
--- a/github/licenses_test.go
+++ b/github/licenses_test.go
@@ -13,6 +13,100 @@ import (
 	"testing"
 )
 
+func TestRepositoryLicense_marshal(t *testing.T) {
+	testJSONMarshal(t, &RepositoryLicense{}, "{}")
+
+	rl := &RepositoryLicense{
+		Name:        String("n"),
+		Path:        String("p"),
+		SHA:         String("s"),
+		Size:        Int(1),
+		URL:         String("u"),
+		HTMLURL:     String("h"),
+		GitURL:      String("g"),
+		DownloadURL: String("d"),
+		Type:        String("t"),
+		Content:     String("c"),
+		Encoding:    String("e"),
+		License: &License{
+			Key:            String("k"),
+			Name:           String("n"),
+			URL:            String("u"),
+			SPDXID:         String("s"),
+			HTMLURL:        String("h"),
+			Featured:       Bool(true),
+			Description:    String("d"),
+			Implementation: String("i"),
+			Permissions:    &[]string{"p"},
+			Conditions:     &[]string{"c"},
+			Limitations:    &[]string{"l"},
+			Body:           String("b"),
+		},
+	}
+	want := `{
+		"name": "n",
+		"path": "p",
+		"sha": "s",
+		"size": 1,
+		"url": "u",
+		"html_url": "h",
+		"git_url": "g",
+		"download_url": "d",
+		"type": "t",
+		"content": "c",
+		"encoding": "e",
+		"license": {
+			"key": "k",
+			"name": "n",
+			"url": "u",
+			"spdx_id": "s",
+			"html_url": "h",
+			"featured": true,
+			"description": "d",
+			"implementation": "i",
+			"permissions": ["p"],
+			"conditions": ["c"],
+			"limitations": ["l"],
+			"body": "b"
+		}
+	}`
+	testJSONMarshal(t, rl, want)
+}
+
+func TestLicense_marshal(t *testing.T) {
+	testJSONMarshal(t, &License{}, "{}")
+
+	l := &License{
+		Key:            String("k"),
+		Name:           String("n"),
+		URL:            String("u"),
+		SPDXID:         String("s"),
+		HTMLURL:        String("h"),
+		Featured:       Bool(true),
+		Description:    String("d"),
+		Implementation: String("i"),
+		Permissions:    &[]string{"p"},
+		Conditions:     &[]string{"c"},
+		Limitations:    &[]string{"l"},
+		Body:           String("b"),
+	}
+	want := `{
+		"key": "k",
+		"name": "n",
+		"url": "u",
+		"spdx_id": "s",
+		"html_url": "h",
+		"featured": true,
+		"description": "d",
+		"implementation": "i",
+		"permissions": ["p"],
+		"conditions": ["c"],
+		"limitations": ["l"],
+		"body": "b"
+	}`
+	testJSONMarshal(t, l, want)
+}
+
 func TestLicensesService_List(t *testing.T) {
 	client, mux, _, teardown := setup()
 	defer teardown()