Repository: google/go-github
Pull request #763 (2017-10-26 21:04:22)

NewEnterpriseClient method with custom urls
With this PR I have completed the following, addressing the issues discussed in #756:

- Introduced new func to create a new enterprise client
- Ensured base and upload urls are formatted with a trailing slash
- Added appropriate comment documentation
- Extended test cases to cover this new creation method

Did I cover everything? :)


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 68a23f00..8da635a6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -55,7 +55,7 @@ again.
 
 [forking]: https://help.github.com/articles/fork-a-repo
 [golint]: https://github.com/golang/lint
-[golint readme]: https://github.com/golang/lint/blob/master/README
+[golint readme]: https://github.com/golang/lint/blob/master/README.md
 [gocov]: https://github.com/axw/gocov
 [gocov-html]: https://github.com/matm/gocov-html
 [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html
diff --git a/github/apps.go b/github/apps.go
index ff338938..a83ea7c2 100644
--- a/github/apps.go
+++ b/github/apps.go
@@ -5,7 +5,11 @@
 
 package github
 
-import "context"
+import (
+	"context"
+	"fmt"
+	"time"
+)
 
 // AppsService provides access to the installation related functions
 // in the GitHub API.
@@ -13,6 +17,57 @@ import "context"
 // GitHub API docs: https://developer.github.com/v3/apps/
 type AppsService service
 
+// App represents a GitHub App.
+type App struct {
+	ID          *int       `json:"id,omitempty"`
+	Owner       *User      `json:"owner,omitempty"`
+	Name        *string    `json:"name,omitempty"`
+	Description *string    `json:"description,omitempty"`
+	ExternalURL *string    `json:"external_url,omitempty"`
+	HTMLURL     *string    `json:"html_url,omitempty"`
+	CreatedAt   *time.Time `json:"created_at,omitempty"`
+	UpdatedAt   *time.Time `json:"updated_at,omitempty"`
+}
+
+// InstallationToken represents an installation token.
+type InstallationToken struct {
+	Token     *string    `json:"token,omitempty"`
+	ExpiresAt *time.Time `json:"expires_at,omitempty"`
+}
+
+// Get a single GitHub App. Passing the empty string will get
+// the authenticated GitHub App.
+//
+// Note: appSlug is just the URL-friendly name of your GitHub App.
+// You can find this on the settings page for your GitHub App
+// (e.g., https://github.com/settings/apps/:app_slug).
+//
+// GitHub API docs: https://developer.github.com/v3/apps/#get-a-single-github-app
+func (s *AppsService) Get(ctx context.Context, appSlug string) (*App, *Response, error) {
+	var u string
+	if appSlug != "" {
+		u = fmt.Sprintf("apps/%v", appSlug)
+	} else {
+		u = "app"
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeIntegrationPreview)
+
+	app := new(App)
+	resp, err := s.client.Do(ctx, req, app)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return app, resp, nil
+}
+
 // ListInstallations lists the installations that the current GitHub App has.
 //
 // GitHub API docs: https://developer.github.com/v3/apps/#find-installations
@@ -38,3 +93,77 @@ func (s *AppsService) ListInstallations(ctx context.Context, opt *ListOptions) (
 
 	return i, resp, nil
 }
+
+// GetInstallation returns the specified installation.
+//
+// GitHub API docs: https://developer.github.com/v3/apps/#get-a-single-installation
+func (s *AppsService) GetInstallation(ctx context.Context, id int) (*Installation, *Response, error) {
+	u := fmt.Sprintf("app/installations/%v", id)
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeIntegrationPreview)
+
+	i := new(Installation)
+	resp, err := s.client.Do(ctx, req, i)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return i, resp, nil
+}
+
+// ListUserInstallations lists installations that are accessible to the authenticated user.
+//
+// GitHub API docs: https://developer.github.com/v3/apps/#list-installations-for-user
+func (s *AppsService) ListUserInstallations(ctx context.Context, opt *ListOptions) ([]*Installation, *Response, error) {
+	u, err := addOptions("user/installations", opt)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeIntegrationPreview)
+
+	var i struct {
+		Installations []*Installation `json:"installations"`
+	}
+	resp, err := s.client.Do(ctx, req, &i)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return i.Installations, resp, nil
+}
+
+// CreateInstallationToken creates a new installation token.
+//
+// GitHub API docs: https://developer.github.com/v3/apps/#create-a-new-installation-token
+func (s *AppsService) CreateInstallationToken(ctx context.Context, id int) (*InstallationToken, *Response, error) {
+	u := fmt.Sprintf("installations/%v/access_tokens", id)
+
+	req, err := s.client.NewRequest("POST", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// TODO: remove custom Accept header when this API fully launches.
+	req.Header.Set("Accept", mediaTypeIntegrationPreview)
+
+	t := new(InstallationToken)
+	resp, err := s.client.Do(ctx, req, t)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return t, resp, nil
+}
diff --git a/github/apps_installation.go b/github/apps_installation.go
index 5c932919..ccac316c 100644
--- a/github/apps_installation.go
+++ b/github/apps_installation.go
@@ -51,6 +51,33 @@ func (s *AppsService) ListRepos(ctx context.Context, opt *ListOptions) ([]*Repos
 	return r.Repositories, resp, nil
 }
 
+// ListUserRepos lists repositories that are accessible
+// to the authenticated user for an installation.
+//
+// GitHub API docs: https://developer.github.com/v3/apps/installations/#list-repositories-accessible-to-the-user-for-an-installation
+func (s *AppsService) ListUserRepos(ctx context.Context, id int, opt *ListOptions) ([]*Repository, *Response, error) {
+	u := fmt.Sprintf("user/installations/%v/repositories", id)
+	u, err := addOptions(u, opt)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	req, err := s.client.NewRequest("GET", u, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	var r struct {
+		Repositories []*Repository `json:"repositories"`
+	}
+	resp, err := s.client.Do(ctx, req, &r)
+	if err != nil {
+		return nil, resp, err
+	}
+
+	return r.Repositories, resp, nil
+}
+
 // AddRepository adds a single repository to an installation.
 //
 // GitHub API docs: https://developer.github.com/v3/apps/installations/#add-repository-to-installation
diff --git a/github/github-accessors.go b/github/github-accessors.go
index 6da009ba..4e0ce600 100644
--- a/github/github-accessors.go
+++ b/github/github-accessors.go
@@ -36,6 +36,62 @@ func (a *APIMeta) GetVerifiablePasswordAuthentication() bool {
 	return *a.VerifiablePasswordAuthentication
 }
 
+// GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
+func (a *App) GetCreatedAt() time.Time {
+	if a == nil || a.CreatedAt == nil {
+		return time.Time{}
+	}
+	return *a.CreatedAt
+}
+
+// GetDescription returns the Description field if it's non-nil, zero value otherwise.
+func (a *App) GetDescription() string {
+	if a == nil || a.Description == nil {
+		return ""
+	}
+	return *a.Description
+}
+
+// GetExternalURL returns the ExternalURL field if it's non-nil, zero value otherwise.
+func (a *App) GetExternalURL() string {
+	if a == nil || a.ExternalURL == nil {
+		return ""
+	}
+	return *a.ExternalURL
+}
+
+// GetHTMLURL returns the HTMLURL field if it's non-nil, zero value otherwise.
+func (a *App) GetHTMLURL() string {
+	if a == nil || a.HTMLURL == nil {
+		return ""
+	}
+	return *a.HTMLURL
+}
+
+// GetID returns the ID field if it's non-nil, zero value otherwise.
+func (a *App) GetID() int {
+	if a == nil || a.ID == nil {
+		return 0
+	}
+	return *a.ID
+}
+
+// GetName returns the Name field if it's non-nil, zero value otherwise.
+func (a *App) GetName() string {
+	if a == nil || a.Name == nil {
+		return ""
+	}
+	return *a.Name
+}
+
+// GetUpdatedAt returns the UpdatedAt field if it's non-nil, zero value otherwise.
+func (a *App) GetUpdatedAt() time.Time {
+	if a == nil || a.UpdatedAt == nil {
+		return time.Time{}
+	}
+	return *a.UpdatedAt
+}
+
 // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
 func (a *Authorization) GetCreatedAt() Timestamp {
 	if a == nil || a.CreatedAt == nil {
@@ -2036,6 +2092,22 @@ func (i *InstallationRepositoriesEvent) GetRepositorySelection() string {
 	return *i.RepositorySelection
 }
 
+// GetExpiresAt returns the ExpiresAt field if it's non-nil, zero value otherwise.
+func (i *InstallationToken) GetExpiresAt() time.Time {
+	if i == nil || i.ExpiresAt == nil {
+		return time.Time{}
+	}
+	return *i.ExpiresAt
+}
+
+// GetToken returns the Token field if it's non-nil, zero value otherwise.
+func (i *InstallationToken) GetToken() string {
+	if i == nil || i.Token == nil {
+		return ""
+	}
+	return *i.Token
+}
+
 // GetCreatedAt returns the CreatedAt field if it's non-nil, zero value otherwise.
 func (i *Invitation) GetCreatedAt() time.Time {
 	if i == nil || i.CreatedAt == nil {
diff --git a/github/github.go b/github/github.go
index b33f70a4..87af91de 100644
--- a/github/github.go
+++ b/github/github.go
@@ -235,6 +235,37 @@ func NewClient(httpClient *http.Client) *Client {
 	return c
 }
 
+// NewEnterpriseClient returns a new GitHub API client with provided
+// base URL and upload URL (often the same URL).
+// If either URL does not have a trailing slash, one is added automatically.
+// If a nil httpClient is provided, http.DefaultClient will be used.
+//
+// Note that NewEnterpriseClient is a convenience helper only;
+// its behavior is equivalent to using NewClient, followed by setting
+// the BaseURL and UploadURL fields.
+func NewEnterpriseClient(baseURL, uploadURL string, httpClient *http.Client) (*Client, error) {
+	baseEndpoint, err := url.Parse(baseURL)
+	if err != nil {
+		return nil, err
+	}
+	if !strings.HasSuffix(baseEndpoint.Path, "/") {
+		baseEndpoint.Path += "/"
+	}
+
+	uploadEndpoint, err := url.Parse(uploadURL)
+	if err != nil {
+		return nil, err
+	}
+	if !strings.HasSuffix(uploadEndpoint.Path, "/") {
+		uploadEndpoint.Path += "/"
+	}
+
+	c := NewClient(httpClient)
+	c.BaseURL = baseEndpoint
+	c.UploadURL = uploadEndpoint
+	return c, nil
+}
+
 // NewRequest creates an API request. A relative URL can be provided in urlStr,
 // in which case it is resolved relative to the BaseURL of the Client.
 // Relative URLs should always be specified without a preceding slash. If
diff --git a/github/orgs_teams.go b/github/orgs_teams.go
index 684e2daf..a1413221 100644
--- a/github/orgs_teams.go
+++ b/github/orgs_teams.go
@@ -196,6 +196,9 @@ func (s *OrganizationsService) ListTeamMembers(ctx context.Context, team int, op
 // IsTeamMember checks if a user is a member of the specified team.
 //
 // GitHub API docs: https://developer.github.com/v3/orgs/teams/#get-team-member
+//
+// Deprecated: This API has been marked as deprecated in the Github API docs,
+// OrganizationsService.GetTeamMembership method should be used instead.
 func (s *OrganizationsService) IsTeamMember(ctx context.Context, team int, user string) (bool, *Response, error) {
 	u := fmt.Sprintf("teams/%v/members/%v", team, user)
 	req, err := s.client.NewRequest("GET", u, nil)


Test patch:
diff --git a/github/apps_installation_test.go b/github/apps_installation_test.go
index 69598cfe..ec796cd3 100644
--- a/github/apps_installation_test.go
+++ b/github/apps_installation_test.go
@@ -39,6 +39,31 @@ func TestAppsService_ListRepos(t *testing.T) {
 	}
 }
 
+func TestAppsService_ListUserRepos(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/user/installations/1/repositories", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testFormValues(t, r, values{
+			"page":     "1",
+			"per_page": "2",
+		})
+		fmt.Fprint(w, `{"repositories": [{"id":1}]}`)
+	})
+
+	opt := &ListOptions{Page: 1, PerPage: 2}
+	repositories, _, err := client.Apps.ListUserRepos(context.Background(), 1, opt)
+	if err != nil {
+		t.Errorf("Apps.ListUserRepos returned error: %v", err)
+	}
+
+	want := []*Repository{{ID: Int(1)}}
+	if !reflect.DeepEqual(repositories, want) {
+		t.Errorf("Apps.ListUserRepos returned %+v, want %+v", repositories, want)
+	}
+}
+
 func TestAppsService_AddRepository(t *testing.T) {
 	setup()
 	defer teardown()
diff --git a/github/apps_test.go b/github/apps_test.go
index f847b842..9ef1f468 100644
--- a/github/apps_test.go
+++ b/github/apps_test.go
@@ -13,6 +13,48 @@ import (
 	"testing"
 )
 
+func TestAppsService_Get_authenticatedApp(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/app", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIntegrationPreview)
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	app, _, err := client.Apps.Get(context.Background(), "")
+	if err != nil {
+		t.Errorf("Apps.Get returned error: %v", err)
+	}
+
+	want := &App{ID: Int(1)}
+	if !reflect.DeepEqual(app, want) {
+		t.Errorf("Apps.Get returned %+v, want %+v", app, want)
+	}
+}
+
+func TestAppsService_Get_specifiedApp(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/apps/a", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIntegrationPreview)
+		fmt.Fprint(w, `{"html_url":"https://github.com/apps/a"}`)
+	})
+
+	app, _, err := client.Apps.Get(context.Background(), "a")
+	if err != nil {
+		t.Errorf("Apps.Get returned error: %v", err)
+	}
+
+	want := &App{HTMLURL: String("https://github.com/apps/a")}
+	if !reflect.DeepEqual(app, want) {
+		t.Errorf("Apps.Get returned %+v, want %+v", *app.HTMLURL, *want.HTMLURL)
+	}
+}
+
 func TestAppsService_ListInstallations(t *testing.T) {
 	setup()
 	defer teardown()
@@ -38,3 +80,71 @@ func TestAppsService_ListInstallations(t *testing.T) {
 		t.Errorf("Apps.ListInstallations returned %+v, want %+v", installations, want)
 	}
 }
+
+func TestAppsService_GetInstallation(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/app/installations/1", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIntegrationPreview)
+		fmt.Fprint(w, `{"id":1}`)
+	})
+
+	installation, _, err := client.Apps.GetInstallation(context.Background(), 1)
+	if err != nil {
+		t.Errorf("Apps.GetInstallation returned error: %v", err)
+	}
+
+	want := &Installation{ID: Int(1)}
+	if !reflect.DeepEqual(installation, want) {
+		t.Errorf("Apps.GetInstallation returned %+v, want %+v", installation, want)
+	}
+}
+
+func TestAppsService_ListUserInstallations(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/user/installations", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "GET")
+		testHeader(t, r, "Accept", mediaTypeIntegrationPreview)
+		testFormValues(t, r, values{
+			"page":     "1",
+			"per_page": "2",
+		})
+		fmt.Fprint(w, `{"installations":[{"id":1}]}`)
+	})
+
+	opt := &ListOptions{Page: 1, PerPage: 2}
+	installations, _, err := client.Apps.ListUserInstallations(context.Background(), opt)
+	if err != nil {
+		t.Errorf("Apps.ListUserInstallations returned error: %v", err)
+	}
+
+	want := []*Installation{{ID: Int(1)}}
+	if !reflect.DeepEqual(installations, want) {
+		t.Errorf("Apps.ListUserInstallations returned %+v, want %+v", installations, want)
+	}
+}
+
+func TestAppsService_CreateInstallationToken(t *testing.T) {
+	setup()
+	defer teardown()
+
+	mux.HandleFunc("/installations/1/access_tokens", func(w http.ResponseWriter, r *http.Request) {
+		testMethod(t, r, "POST")
+		testHeader(t, r, "Accept", mediaTypeIntegrationPreview)
+		fmt.Fprint(w, `{"token":"t"}`)
+	})
+
+	token, _, err := client.Apps.CreateInstallationToken(context.Background(), 1)
+	if err != nil {
+		t.Errorf("Apps.CreateInstallationToken returned error: %v", err)
+	}
+
+	want := &InstallationToken{Token: String("t")}
+	if !reflect.DeepEqual(token, want) {
+		t.Errorf("Apps.CreateInstallationToken returned %+v, want %+v", token, want)
+	}
+}
diff --git a/github/github_test.go b/github/github_test.go
index aff436a6..cc62fc6b 100644
--- a/github/github_test.go
+++ b/github/github_test.go
@@ -186,6 +186,41 @@ func TestNewClient(t *testing.T) {
 	}
 }
 
+func TestNewEnterpriseClient(t *testing.T) {
+	baseURL := "https://custom-url/"
+	uploadURL := "https://custom-upload-url/"
+	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
+	if err != nil {
+		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
+	}
+
+	if got, want := c.BaseURL.String(), baseURL; got != want {
+		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
+	}
+	if got, want := c.UploadURL.String(), uploadURL; got != want {
+		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
+	}
+}
+
+func TestNewEnterpriseClient_addsTrailingSlashToURLs(t *testing.T) {
+	baseURL := "https://custom-url"
+	uploadURL := "https://custom-upload-url"
+	formattedBaseURL := baseURL + "/"
+	formattedUploadURL := uploadURL + "/"
+
+	c, err := NewEnterpriseClient(baseURL, uploadURL, nil)
+	if err != nil {
+		t.Fatalf("NewEnterpriseClient returned unexpected error: %v", err)
+	}
+
+	if got, want := c.BaseURL.String(), formattedBaseURL; got != want {
+		t.Errorf("NewClient BaseURL is %v, want %v", got, want)
+	}
+	if got, want := c.UploadURL.String(), formattedUploadURL; got != want {
+		t.Errorf("NewClient UploadURL is %v, want %v", got, want)
+	}
+}
+
 // Ensure that length of Client.rateLimits is the same as number of fields in RateLimits struct.
 func TestClient_rateLimits(t *testing.T) {
 	if got, want := len(Client{}.rateLimits), reflect.TypeOf(RateLimits{}).NumField(); got != want {