Skip to content

Add missing apps methods #733

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 30, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 130 additions & 1 deletion github/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,69 @@

package github

import "context"
import (
"context"
"fmt"
"time"
)

// AppsService provides access to the installation related functions
// in the GitHub API.
//
// 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"`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually more of a question for @shurcooL or @willnorris...

In the light of #597, do you think we should start using int64 for numeric IDs for new APIs in order to make the implementation of #597 easier (by providing an example and reducing the number of changes needed)?

Or do you think that we should remain internally consistent and just change everything all at once when #597 is addressed?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO it's unclear what the state of #597 is right now. The issue has been opened a while ago, but I haven't seen any reports of issues from production use. I'm not convinced that we should act right away, or how we should act. I think that we need to collect more data in that issue (or, wait until there's a concrete need to act).

Because of that, I think we should not try to incorporate that issue into this one. So, I suggest we stay consistent, and deal with int -> int64 changes (or another solution), if at all, later and separately from this PR.

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
Expand All @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shurcooL - what do you think about making this a new type ... struct and include the TotalCount field, then returning a pointer to the struct in the return value of this method?

I'm asking because it seems more consistent with the rest of the package and also allows GitHub to add/tweak their response and we could more flexibly support it however they decide to tweak it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I think we should have this method return a pointer to a struct that corresponds to the JSON response in the documentation.

Returning []*Installation would've made sense if the GitHub response was a JSON array of installation objects, but it's a JSON object containing installations field with the array.

The only question is what to name it, if we make it exported (I think we have to, don't we?). Perhaps ListUserInstallationsResponse?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then again, it's unfortunate it's quite different from the existing ListInstallations method (https://developer.github.com/v3/apps/#find-installations) which does return a JSON array of installations...

I can't help but think GitHub might've made the API inconsistent unintentionally and might want to change it while it's still in preview, so it'd be worth shooting them an email about it.

Copy link
Contributor Author

@mandrean mandrean Oct 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gmlewis @shurcooL I did this in my original contribution, but later removed it because I didn't see this pattern being used in other places in go-github where the same type of JSON structure was being handled.

I tried to stay "idiomatic" to your library. But personally I would have mimicked their JSON response as closely as possible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't see this pattern being used in other places in go-github where the same type of JSON structure was being handled.

Mind sharing examples of that? It would be helpful.

Copy link
Contributor Author

@mandrean mandrean Oct 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@shurcooL: This is currently in master: https://github.com/google/go-github/blob/master/github/apps_installation.go#L40

But as you can see in the GitHub API docs, the response actually has the total_count field: https://developer.github.com/v3/apps/installations/#list-repositories

So I just reused that same pattern for my ListUserRepos and ListUserInstallations contribution.

Copy link
Member

@dmitshur dmitshur Oct 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@gmlewis What do you think about this finding?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, looks like we do this elsewhere... I suppose that if the GitHub API starts expanding these return values to more than just total_count that we can then revisit making a named structure.
So I'm fine with proceeding as written.
Thanks, @mandrean and @shurcooL!

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
}
27 changes: 27 additions & 0 deletions github/apps_installation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 25 additions & 0 deletions github/apps_installation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
110 changes: 110 additions & 0 deletions github/apps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
}
}
Loading