diff --git a/README.md b/README.md index 1a472920730..333b4de85e1 100644 --- a/README.md +++ b/README.md @@ -90,21 +90,7 @@ For complete usage of go-github, see the full [package docs][]. ### Integration Tests ### -You can run the integration tests from from the `tests` directory with: - -```bash -GITHUB_AUTH_TOKEN= go test ./... -``` - -You can create a token here: https://github.com/settings/tokens - -These scopes are needed: - -* repo -* delete_repo -* user -* admin:public_key - +You can run integration tests from the `tests` directory. See the integration tests [README](tests/README.md). ## Roadmap ## This library is being initially developed for an internal application at diff --git a/github/authorizations.go b/github/authorizations.go new file mode 100644 index 00000000000..c5a2abca92c --- /dev/null +++ b/github/authorizations.go @@ -0,0 +1,312 @@ +// Copyright 2014 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 ( + "errors" + "fmt" +) + +// Scope models a GitHub authorization scope. +// +// GitHub API docs:https://developer.github.com/v3/oauth/#scopes +type Scope string + +// This is the set of scopes for GitHub API V3 +const ( + ScopeNone Scope = "(no scope)" // REVISIT: is this actually returned, or just a documentation artifact? + ScopeUser Scope = "user" + ScopeUserEmail Scope = "user:email" + ScopeUserFollow Scope = "user:follow" + ScopePublicRepo Scope = "public_repo" + ScopeRepo Scope = "repo" + ScopeRepoDeployment Scope = "repo_deployment" + ScopeRepoStatus Scope = "repo:status" + ScopeDeleteRepo Scope = "delete_repo" + ScopeNotifications Scope = "notifications" + ScopeGist Scope = "gist" + ScopeReadRepoHook Scope = "read:repo_hook" + ScopeWriteRepoHook Scope = "write:repo_hook" + ScopeAdminRepoHook Scope = "admin:repo_hook" + ScopeAdminOrgHook Scope = "admin:org_hook" + ScopeReadOrg Scope = "read:org" + ScopeWriteOrg Scope = "write:org" + ScopeAdminOrg Scope = "admin:org" + ScopeReadPublicKey Scope = "read:public_key" + ScopeWritePublicKey Scope = "write:public_key" + ScopeAdminPublicKey Scope = "admin:public_key" +) + +// AuthorizationsService handles communication with the authorization related +// methods of the GitHub API. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/ +type AuthorizationsService struct { + client *Client +} + +// Authorization models an individual GitHub authorization. +// Note that the User field is only present for the CheckAppAuthorization +// and ResetAppAuthorization operations. +type Authorization struct { + ID *int `json:"id,omitempty"` + URL *string `json:"url,omitempty"` + App *App `json:"app,omitempty"` + Token *string `json:"token,omitempty"` + HashedToken *string `json:"hashed_token,omitempty"` + TokenLastEight *string `json:"token_last_eight,omitempty"` + Note *string `json:"note,omitempty"` + NoteURL *string `json:"note_url,omitempty"` + CreatedAt *string `json:"created_at,omitempty"` + UpdatedAt *string `json:"updated_at,omitempty"` + Scopes []Scope `json:"scopes,omitempty"` + Fingerprint *string `json:"fingerprint,omitempty"` + User *User `json:"user,omitempty"` +} + +func (a Authorization) String() string { + return Stringify(a) +} + +// App models an individual GitHub app (in the context of authorization). +type App struct { + Name *string `json:"name,omitempty"` + URL *string `json:"url,omitempty"` + ClientID *string `json:"client_id,omitempty"` +} + +func (a App) String() string { + return Stringify(a) +} + +// AuthorizationRequest is used to create a new GitHub authorization. Note that not all fields +// are used for any one operation. +type AuthorizationRequest struct { + ClientID *string `json:"client_id,omitempty"` + ClientSecret *string `json:"client_secret,omitempty"` + Scopes []Scope `json:"scopes,omitempty"` + Note *string `json:"note,omitempty"` + NoteURL *string `json:"note_url,omitempty"` + Fingerprint *string `json:"fingerprint,omitempty"` +} + +func (a AuthorizationRequest) String() string { + return Stringify(a) +} + +// AuthorizationUpdate is used to update an existing GitHub authorization. +// Note that for any one update, you must only provide one of the "scopes" fields. +// That is, you may provide only one of "Scopes", or "AddScopes", or "RemoveScopes". +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#update-an-existing-authorization +type AuthorizationUpdate struct { + // ID is not serialized as the update operations take the ID in the path + ID *int `json:"-"` + Scopes []Scope `json:"scopes,omitempty"` + AddScopes []Scope `json:"add_scopes,omitempty"` + RemoveScopes []Scope `json:"remove_scopes,omitempty"` + Note *string `json:"note,omitempty"` + NoteURL *string `json:"note_url,omitempty"` + Fingerprint *string `json:"fingerprint,omitempty"` +} + +func (a AuthorizationUpdate) String() string { + return Stringify(a) +} + +// List lists your GitHub authorizations. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#list-your-authorizations +func (s *AuthorizationsService) List() ([]Authorization, *Response, error) { + + // GET /authorizations + req, err := s.client.NewRequest("GET", "authorizations", nil) + if err != nil { + return nil, nil, err + } + + auths := new([]Authorization) + resp, err := s.client.Do(req, auths) + + return *auths, resp, err +} + +// Get retrieves a single GitHub authorization. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#get-a-single-authorization +func (s *AuthorizationsService) Get(id int) (*Authorization, *Response, error) { + + if id == 0 { + return nil, nil, errors.New("You must provide an id parameter.") + } + + // GET /authorizations/:id + u := fmt.Sprintf("authorizations/%v", id) + + req, err := s.client.NewRequest("GET", u, nil) + if err != nil { + return nil, nil, err + } + + auth := new(Authorization) + resp, err := s.client.Do(req, auth) + + return auth, resp, err +} + +// Create creates a new GitHub authorization. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#create-a-new-authorization +func (s *AuthorizationsService) Create(auth *AuthorizationRequest) (*Authorization, *Response, error) { + + // POST /authorizations + req, err := s.client.NewRequest("POST", "authorizations", auth) + if err != nil { + return nil, nil, err + } + + authResponse := new(Authorization) + resp, err := s.client.Do(req, authResponse) + + return authResponse, resp, err +} + +// GetOrCreateForApp will create a new GitHub authorization for the specified OAuth application, +// (or optionally app/fingerprint combination) only if an authorization for that app/fingerprint doesn't already exist for that user. If a new token is +// created, the HTTP status code will be "201 Created", and the returned Authorization.Token field +// will be populated. If an existing token is returned, the status code will be "200 OK" and the +// Authorization.Token field will be empty. +// +// GitHub API docs: +// - https://developer.github.com/v3/oauth_authorizations/#get-or-create-an-authorization-for-a-specific-app +// - https://developer.github.com/v3/oauth_authorizations/#get-or-create-an-authorization-for-a-specific-app-and-fingerprint +func (s *AuthorizationsService) GetOrCreateForApp(auth *AuthorizationRequest) (*Authorization, *Response, error) { + + if *auth.ClientID == "" || *auth.ClientSecret == "" { + return nil, nil, errors.New("You must provide the ClientID and ClientSecret parameters (as part of AuthorizationRequest)") + } + + // PUT /authorizations/clients/:client_id + u := fmt.Sprintf("authorizations/clients/%v", *auth.ClientID) + + // We want to set the ClientID to nil as this operation does not expect it in the body + // But we also don't want to mess up the struct we received, so we make a copy + authCopy := *auth + authCopy.ClientID = nil + + req, err := s.client.NewRequest("PUT", u, authCopy) + if err != nil { + return nil, nil, err + } + + authResponse := new(Authorization) + resp, err := s.client.Do(req, authResponse) + + return authResponse, resp, err +} + +// Edit updates an existing GitHub authorization. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#update-an-existing-authorization +func (s *AuthorizationsService) Edit(auth *AuthorizationUpdate) (*Authorization, *Response, error) { + + // PATCH /authorizations/:id + u := fmt.Sprintf("authorizations/%v", *auth.ID) + req, err := s.client.NewRequest("PATCH", u, auth) + if err != nil { + return nil, nil, err + } + + authResponse := new(Authorization) + resp, err := s.client.Do(req, authResponse) + + return authResponse, resp, err +} + +// Delete deletes a GitHub authorization. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#delete-an-authorization +func (s *AuthorizationsService) Delete(id int) (*Response, error) { + + if id == 0 { + return nil, errors.New("You must provide an id parameter.") + } + // DELETE /authorizations/:id + u := fmt.Sprintf("authorizations/%v", id) + req, err := s.client.NewRequest("DELETE", u, nil) + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} + +// CheckAppAuthorization checks if an OAuth token is valid for a specific app. +// Note that this operation requires the use of BasicAuth, but where the username +// is the OAuth application clientID, and the password is its clientSecret. Invalid +// tokens will return a 404 Not Found. +// +// Note that for this operation, the returned Authorization.User field will be populated. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#check-an-authorization +func (s *AuthorizationsService) CheckAppAuthorization(clientID string, token string) (*Authorization, *Response, error) { + + // GET /applications/:client_id/tokens/:access_token + u := fmt.Sprintf("applications/%v/tokens/%v", clientID, token) + req, err := s.client.NewRequest("GET", u, nil) + + if err != nil { + return nil, nil, err + } + + auth := new(Authorization) + resp, err := s.client.Do(req, auth) + + return auth, resp, err +} + +// ResetAppAuthorization is used to reset a valid OAuth token without end user involvement. +// Note that this operation requires the use of BasicAuth, but where the username +// is the OAuth application clientID, and the password is its clientSecret. Invalid +// tokens will return a 404 Not Found. +// +// Note that for this operation, the returned Authorization.User field will be populated. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#reset-an-authorization +func (s *AuthorizationsService) ResetAppAuthorization(clientID string, token string) (*Authorization, *Response, error) { + + // POST /applications/:client_id/tokens/:access_token + u := fmt.Sprintf("applications/%v/tokens/%v", clientID, token) + req, err := s.client.NewRequest("POST", u, nil) + + if err != nil { + return nil, nil, err + } + + auth := new(Authorization) + resp, err := s.client.Do(req, auth) + + return auth, resp, err +} + +// RevokeAppAuthorization is used to revoke a single token for an OAuth application. +// Note that this operation requires the use of BasicAuth, but where the username +// is the OAuth application clientID, and the password is its clientSecret. Invalid +// tokens will return a 404 Not Found. +// +// GitHub API docs: https://developer.github.com/v3/oauth_authorizations/#revoke-an-authorization-for-an-application +func (s *AuthorizationsService) RevokeAppAuthorization(clientID string, token string) (*Response, error) { + + // DELETE /applications/:client_id/tokens/:access_token + u := fmt.Sprintf("applications/%v/tokens/%v", clientID, token) + req, err := s.client.NewRequest("DELETE", u, nil) + + if err != nil { + return nil, err + } + + return s.client.Do(req, nil) +} diff --git a/github/authorizations_test.go b/github/authorizations_test.go new file mode 100644 index 00000000000..8a2ff79722a --- /dev/null +++ b/github/authorizations_test.go @@ -0,0 +1,287 @@ +// Copyright 2013 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 ( + "encoding/json" + "fmt" + "net/http" + "reflect" + "testing" +) + +func TestAuthorizationsService_List(t *testing.T) { + + setup() + defer teardown() + + want := []Authorization{{ID: Int(1)}, {ID: Int(2)}} + + mux.HandleFunc("/authorizations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + + j, _ := json.Marshal(want) + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.List() + + if err != nil { + t.Errorf("Authorizations.List returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.List returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_Get(t *testing.T) { + + setup() + defer teardown() + + id := 1 + want := &Authorization{ID: Int(id)} + path := fmt.Sprintf("/authorizations/%v", id) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + + j, _ := json.Marshal(want) + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.Get(id) + + if err != nil { + t.Errorf("Authorizations.Get returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.Get returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_Create(t *testing.T) { + + setup() + defer teardown() + + input := &AuthorizationRequest{Note: String("12345")} + want := &Authorization{ID: Int(1), Note: input.Note} + + mux.HandleFunc("/authorizations", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + v := new(AuthorizationRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, input) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + j, _ := json.Marshal(want) + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.Create(input) + + if err != nil { + t.Errorf("Authorizations.Create returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.Create returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_GetOrCreateForApp(t *testing.T) { + + setup() + defer teardown() + + input := &AuthorizationRequest{ClientID: String("abcde"), ClientSecret: String("clientSecret"), Note: String("12345")} + expectedOnServer := &AuthorizationRequest{ClientSecret: input.ClientSecret, Note: input.Note} + want := &Authorization{ID: Int(1), Note: input.Note} + path := fmt.Sprintf("/authorizations/clients/%v", *input.ClientID) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + v := new(AuthorizationRequest) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, expectedOnServer) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + j, _ := json.Marshal(want) + + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.GetOrCreateForApp(input) + + if err != nil { + t.Errorf("Authorizations.GetOrCreateForApp returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.GetOrCreateForApp returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_Edit(t *testing.T) { + + setup() + defer teardown() + + id := 1 + path := fmt.Sprintf("/authorizations/%v", id) + input := &AuthorizationUpdate{ID: Int(id), Note: String("12345")} + + // The "ID" field does not get serialized into the body, it goes in the path, so we omit it here + expectedOnServer := &AuthorizationUpdate{Note: input.Note} + want := &Authorization{ID: input.ID, Note: input.Note} + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PATCH") + + v := new(AuthorizationUpdate) + json.NewDecoder(r.Body).Decode(v) + + if !reflect.DeepEqual(v, expectedOnServer) { + t.Errorf("Request body = %+v, want %+v", v, input) + } + + j, _ := json.Marshal(want) + + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.Edit(input) + + if err != nil { + t.Errorf("Authorizations.Edit returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.Edit returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_Delete(t *testing.T) { + + setup() + defer teardown() + + id := 1 + path := fmt.Sprintf("/authorizations/%v", id) + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBodyIsEmpty(t, r) + w.WriteHeader(http.StatusNoContent) + }) + + resp, err := client.Authorizations.Delete(id) + + if err != nil { + t.Errorf("Authorizations.Delete returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Authorizations.Delete should have returned status code %v, but returned %v", http.StatusNoContent, resp.StatusCode) + } +} + +func TestAuthorizationsService_CheckAppAuthorization(t *testing.T) { + + setup() + defer teardown() + + clientID := "abcde" + token := "12345" + path := "/applications/" + clientID + "/tokens/" + token + + app := &App{ClientID: &clientID} + + want := &Authorization{ID: Int(1), App: app} + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testBodyIsEmpty(t, r) + + j, _ := json.Marshal(want) + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.CheckAppAuthorization(clientID, token) + + if err != nil { + t.Errorf("Authorizations.CheckAppAuthorization returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.CheckAppAuthorization returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_ResetAppAuthorization(t *testing.T) { + + setup() + defer teardown() + + clientID := "abcde" + token := "12345" + path := "/applications/" + clientID + "/tokens/" + token + + app := &App{ClientID: &clientID} + + want := &Authorization{ID: Int(1), App: app} + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testBodyIsEmpty(t, r) + + j, _ := json.Marshal(want) + fmt.Fprint(w, string(j)) + }) + + output, _, err := client.Authorizations.ResetAppAuthorization(clientID, token) + + if err != nil { + t.Errorf("Authorizations.ResetAppAuthorization returned error: %v", err) + } + + if !reflect.DeepEqual(output, want) { + t.Errorf("Authorizations.ResetAppAuthorization returned %+v, want %+v", output, want) + } +} + +func TestAuthorizationsService_RevokeAppAuthorization(t *testing.T) { + + setup() + defer teardown() + + clientID := "abcde" + token := "12345" + path := "/applications/" + clientID + "/tokens/" + token + + mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + testBodyIsEmpty(t, r) + w.WriteHeader(http.StatusNoContent) + }) + + resp, err := client.Authorizations.RevokeAppAuthorization(clientID, token) + + if err != nil { + t.Errorf("Authorizations.RevokeAppAuthorization returned error: %v", err) + } + + if resp.StatusCode != http.StatusNoContent { + t.Errorf("Authorizations.RevokeAppAuthorization should have returned status code %v, but returned %v", http.StatusNoContent, resp.StatusCode) + } +} diff --git a/github/github.go b/github/github.go index 0c2a8db8cde..aa082d226d6 100644 --- a/github/github.go +++ b/github/github.go @@ -89,18 +89,19 @@ type Client struct { rate Rate // Rate limit for the client as determined by the most recent API call. // Services used for talking to different parts of the GitHub API. - Activity *ActivityService - Gists *GistsService - Git *GitService - Gitignores *GitignoresService - Issues *IssuesService - Organizations *OrganizationsService - PullRequests *PullRequestsService - Repositories *RepositoriesService - Search *SearchService - Users *UsersService - Licenses *LicensesService - Migrations *MigrationService + Activity *ActivityService + Gists *GistsService + Git *GitService + Gitignores *GitignoresService + Issues *IssuesService + Organizations *OrganizationsService + PullRequests *PullRequestsService + Repositories *RepositoriesService + Search *SearchService + Users *UsersService + Licenses *LicensesService + Migrations *MigrationService + Authorizations *AuthorizationsService } // ListOptions specifies the optional parameters to various List methods that @@ -164,6 +165,7 @@ func NewClient(httpClient *http.Client) *Client { c.Users = &UsersService{client: c} c.Licenses = &LicensesService{client: c} c.Migrations = &MigrationService{client: c} + c.Authorizations = &AuthorizationsService{client: c} return c } diff --git a/github/github_test.go b/github/github_test.go index 00b83150191..16b2aa87110 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -19,6 +19,7 @@ import ( "strings" "testing" "time" + "io" ) var ( @@ -97,6 +98,8 @@ func testFormValues(t *testing.T, r *http.Request, values values) { if got := r.Form; !reflect.DeepEqual(got, want) { t.Errorf("Request parameters: %v, want %v", got, want) } + + fmt.Println("got: ", r.Form) } func testHeader(t *testing.T, r *http.Request, header string, want string) { @@ -124,6 +127,21 @@ func testBody(t *testing.T, r *http.Request, want string) { } } +// testBodyIsEmpty verifies that r.Body is empty. +func testBodyIsEmpty(t *testing.T, r *http.Request) { + bytes := make([]byte, 256) + + length, err := r.Body.Read(bytes) + + if length > 0 { + t.Errorf("Body length should be zero, but is %v", length) + } + + if err != io.EOF { + t.Errorf("Expected EOF, but got %v", err) + } +} + // Helper function to test that a value is marshalled to JSON as expected. func testJSONMarshal(t *testing.T, v interface{}, want string) { j, err := json.Marshal(v) diff --git a/tests/README.md b/tests/README.md index 3622de935e1..6d26bd8c304 100644 --- a/tests/README.md +++ b/tests/README.md @@ -31,6 +31,11 @@ Run tests using: GITHUB_AUTH_TOKEN=XXX go test -v -tags=integration ./integration +Additionally there are a set of integration tests for the Authorizations API. These tests require a GitHub user (username and password), and also that a [GitHub Application](https://github.com/settings/applications/new) (with attendant Client ID and Client Secret) be available. Then, to execute just the Authorization tests: + + GITHUB_USERNAME='' GITHUB_PASSWORD='' GITHUB_CLIENT_ID='' GITHUB_CLIENT_SECRET='' go test -v -tags=integration --run=Authorizations ./integration + +If some or all of these environment variables are not available, certain of the Authorization integration tests will be skipped. fields ------ diff --git a/tests/integration/authorizations_test.go b/tests/integration/authorizations_test.go new file mode 100644 index 00000000000..7a10f1d0b94 --- /dev/null +++ b/tests/integration/authorizations_test.go @@ -0,0 +1,304 @@ +// Copyright 2014 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 tests + +import ( + "math/rand" + "os" + "strconv" + "strings" + "testing" + "time" + + "github.com/google/go-github/github" +) + +const MsgEnvarMissing = "Skipping test because the required environment variable (%v) is not present." +const EnvarKeyGitHubUsername = "GITHUB_USERNAME" +const EnvarKeyGitHubPassword = "GITHUB_PASSWORD" +const EnvarKeyClientID = "GITHUB_CLIENT_ID" +const EnvarKeyClientSecret = "GITHUB_CLIENT_SECRET" +const InvalidTokenValue = "iamnotacroken" + +// TestAuthorizationsBasicOperations tests the basic CRUD operations of the API (mostly for +// the Personal Access Token scenario). +func TestAuthorizationsBasicOperations(t *testing.T) { + + client := getUserPassClient(t) + + auths, resp, err := client.Authorizations.List() + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + initialAuthCount := len(auths) + + authReq := generatePersonalAuthTokenRequest() + + createdAuth, resp, err := client.Authorizations.Create(authReq) + failOnError(t, err) + failIfNotStatusCode(t, resp, 201) + + if *authReq.Note != *createdAuth.Note { + t.Fatal("Returned Authorization does not match the requested Authorization.") + } + + auths, resp, err = client.Authorizations.List() + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + if len(auths) != initialAuthCount+1 { + t.Fatalf("The number of Authorizations should have increased. Expected [%v], was [%v]", (initialAuthCount + 1), len(auths)) + } + + // Test updating the authorization + authUpdate := new(github.AuthorizationUpdate) + authUpdate.ID = createdAuth.ID + authUpdate.Note = github.String("Updated note: " + randString()) + + updatedAuth, resp, err := client.Authorizations.Edit(authUpdate) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + if *updatedAuth.Note != *authUpdate.Note { + t.Fatal("The returned Authorization does not match the requested updated value.") + } + + // Verify that the Get operation also reflects the update + retrievedAuth, resp, err := client.Authorizations.Get(*createdAuth.ID) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + if *retrievedAuth.Note != *updatedAuth.Note { + t.Fatal("The retrieved Authorization does not match the expected (updated) value.") + } + + // Now, let's delete... + resp, err = client.Authorizations.Delete(*createdAuth.ID) + failOnError(t, err) + failIfNotStatusCode(t, resp, 204) + + // Verify that we can no longer retrieve the auth + retrievedAuth, resp, err = client.Authorizations.Get(*createdAuth.ID) + if err == nil { + t.Fatal("Should have failed due to 404") + } + failIfNotStatusCode(t, resp, 404) + + // Verify that our count reset back to the initial value + auths, resp, err = client.Authorizations.List() + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + if len(auths) != initialAuthCount { + t.Fatal("The number of Authorizations should match the initial count Expected [%v], got [%v]", (initialAuthCount), len(auths)) + } + +} + +// TestAuthorizationsAppOperations tests the application/token related operations, such +// as creating, testing, resetting and revoking application OAuth tokens. +func TestAuthorizationsAppOperations(t *testing.T) { + + userAuthenticatedClient := getUserPassClient(t) + + appAuthenticatedClient := getOAuthAppClient(t) + + // We know these vars are set because getOAuthAppClient would have + // skipped the test by now + clientID := os.Getenv(EnvarKeyClientID) + clientSecret := os.Getenv(EnvarKeyClientSecret) + + authRequest := generateAppAuthTokenRequest(clientID, clientSecret) + + createdAuth, resp, err := userAuthenticatedClient.Authorizations.GetOrCreateForApp(authRequest) + failOnError(t, err) + failIfNotStatusCode(t, resp, 201) + + // Quick sanity check: + if *createdAuth.Note != *authRequest.Note { + t.Fatal("The returned auth does not match expected value.") + } + + // Let's try the same request again, this time it should return the same + // auth instead of creating a new one + secondAuth, resp, err := userAuthenticatedClient.Authorizations.GetOrCreateForApp(authRequest) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + // Verify that the IDs are the same + if *createdAuth.ID != *secondAuth.ID { + t.Fatalf("The ID of the second returned auth should be the same as the first. Expected [%v], got [%v]", createdAuth.ID, secondAuth.ID) + } + + // Verify the token + appAuth, resp, err := appAuthenticatedClient.Authorizations.CheckAppAuthorization(clientID, *createdAuth.Token) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + // Quick sanity check + if *appAuth.ID != *createdAuth.ID || *appAuth.Token != *createdAuth.Token { + t.Fatal("The returned auth/token does not match.") + } + + // Let's verify that we get a 404 for a non-existent token + _, resp, err = appAuthenticatedClient.Authorizations.CheckAppAuthorization(clientID, InvalidTokenValue) + if err == nil { + t.Fatal("An error should have been returned because of the invalid token.") + } + failIfNotStatusCode(t, resp, 404) + + // Let's reset the token + resetAuth, resp, err := appAuthenticatedClient.Authorizations.ResetAppAuthorization(clientID, *createdAuth.Token) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + // Let's verify that we get a 404 for a non-existent token + _, resp, err = appAuthenticatedClient.Authorizations.ResetAppAuthorization(clientID, InvalidTokenValue) + if err == nil { + t.Fatal("An error should have been returned because of the invalid token.") + } + failIfNotStatusCode(t, resp, 404) + + // Verify that the token has changed + if resetAuth.Token == createdAuth.Token { + t.Fatal("The reset token should be different from the original.") + } + + // Verify that we do have a token value + if *resetAuth.Token == "" { + t.Fatal("A token value should have been returned.") + } + + // Verify that the original token is now invalid + _, resp, err = appAuthenticatedClient.Authorizations.CheckAppAuthorization(clientID, *createdAuth.Token) + if err == nil { + t.Fatal("The original token should be invalid.") + } + failIfNotStatusCode(t, resp, 404) + + // Check that the reset token is valid + _, resp, err = appAuthenticatedClient.Authorizations.CheckAppAuthorization(clientID, *resetAuth.Token) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + // Let's revoke the token + resp, err = appAuthenticatedClient.Authorizations.RevokeAppAuthorization(clientID, *resetAuth.Token) + failOnError(t, err) + failIfNotStatusCode(t, resp, 204) + + // Sleep for two seconds... I've seen cases where the revocation appears not + // to have take place immediately. + time.Sleep(time.Second * 2) + + // Now, the reset token should also be invalid + _, resp, err = appAuthenticatedClient.Authorizations.CheckAppAuthorization(clientID, *resetAuth.Token) + if err == nil { + t.Fatal("The reset token should be invalid.") + } + failIfNotStatusCode(t, resp, 404) +} + +// generatePersonalAuthTokenRequest is a helper function that generates an +// AuthorizationRequest for a Personal Access Token (no client id). +func generatePersonalAuthTokenRequest() *github.AuthorizationRequest { + + rand := randString() + auth := github.AuthorizationRequest{ + Note: github.String("Personal token: Note generated by test: " + rand), + Scopes: []github.Scope{github.ScopePublicRepo}, + Fingerprint: github.String("Personal token: Fingerprint generated by test: " + rand), + } + + return &auth +} + +// generatePersonalAuthTokenRequest is a helper function that generates an +// AuthorizationRequest for an OAuth application Token (uses client id). +func generateAppAuthTokenRequest(clientID string, clientSecret string) *github.AuthorizationRequest { + + rand := randString() + auth := github.AuthorizationRequest{ + Note: github.String("App token: Note generated by test: " + rand), + Scopes: []github.Scope{github.ScopePublicRepo}, + Fingerprint: github.String("App token: Fingerprint generated by test: " + rand), + ClientID: github.String(clientID), + ClientSecret: github.String(clientSecret), + } + + return &auth +} + +// randString returns a (kinda) random string for uniqueness purposes. +func randString() string { + return strconv.FormatInt(rand.NewSource(time.Now().UnixNano()).Int63(), 10) +} + +// failOnError invokes t.Fatal() if err is present. +func failOnError(t *testing.T, err error) { + + if err != nil { + t.Fatal(err) + } +} + +// failIfNotStatusCode invokes t.Fatal() if the response's status code doesn't match the expected code. +func failIfNotStatusCode(t *testing.T, resp *github.Response, expectedCode int) { + + if resp.StatusCode != expectedCode { + t.Fatalf("Expected HTTP status code [%v] but received [%v]", expectedCode, resp.StatusCode) + } + +} + +// getUserPassClient returns a GitHub client for authorization testing. The client +// uses BasicAuth via GH username and password passed in environment variables +// (and will skip the calling test if those vars are not present). +func getUserPassClient(t *testing.T) *github.Client { + username, isPresent := os.LookupEnv(EnvarKeyGitHubUsername) + if !isPresent { + t.Skipf(MsgEnvarMissing, EnvarKeyGitHubUsername) + } + + password, isPresent := os.LookupEnv(EnvarKeyGitHubPassword) + if !isPresent { + t.Skipf(MsgEnvarMissing, EnvarKeyGitHubPassword) + } + + tp := github.BasicAuthTransport{ + Username: strings.TrimSpace(username), + Password: strings.TrimSpace(password), + } + + return github.NewClient(tp.Client()) +} + +// getOAuthAppClient returns a GitHub client for authorization testing. The client +// uses BasicAuth, but instead of username and password, it uses the client id +// and client secret passed in via environment variables +// (and will skip the calling test if those vars are not present). Certain API operations (check +// an authorization; reset an authorization; revoke an authorization for an app) +// require this authentication mechanism. +// +// See GitHub API docs: https://developer.com/v3/oauth_authorizations/#check-an-authorization +func getOAuthAppClient(t *testing.T) *github.Client { + + username, isPresent := os.LookupEnv(EnvarKeyClientID) + if !isPresent { + t.Skipf(MsgEnvarMissing, EnvarKeyClientID) + } + + password, isPresent := os.LookupEnv(EnvarKeyClientSecret) + if !isPresent { + t.Skipf(MsgEnvarMissing, EnvarKeyClientSecret) + } + + tp := github.BasicAuthTransport{ + Username: strings.TrimSpace(username), + Password: strings.TrimSpace(password), + } + + return github.NewClient(tp.Client()) +} diff --git a/tests/integration/github_test.go b/tests/integration/github_test.go index 9e83de98b92..937cf7e0c11 100644 --- a/tests/integration/github_test.go +++ b/tests/integration/github_test.go @@ -39,6 +39,18 @@ func init() { client = github.NewClient(tc) auth = true } + + // Environment variables required for Authorization integration tests + envars := []string{EnvarKeyGitHubUsername, EnvarKeyGitHubPassword, EnvarKeyClientID, EnvarKeyClientSecret} + + for _, envar := range envars { + value := os.Getenv(envar) + if value == "" { + print("!!! " + fmt.Sprintf(MsgEnvarMissing, envar) + " !!!\n\n") + } + } + + } func checkAuth(name string) bool {