diff --git a/tests/README.md b/tests/README.md index 3622de9..e2cb051 100644 --- a/tests/README.md +++ b/tests/README.md @@ -31,6 +31,16 @@ 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 0000000..cc3a329 --- /dev/null +++ b/tests/integration/authorizations_test.go @@ -0,0 +1,303 @@ +// Copyright 2016 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 msgEnvMissing = "Skipping test because the required environment variable (%v) is not present." +const envKeyGitHubUsername = "GITHUB_USERNAME" +const envKeyGitHubPassword = "GITHUB_PASSWORD" +const envKeyClientID = "GITHUB_CLIENT_ID" +const envKeyClientSecret = "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(nil) + 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(nil) + 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.AuthorizationUpdateRequest) + authUpdate.Note = github.String("Updated note: " + randString()) + + updatedAuth, resp, err := client.Authorizations.Edit(*createdAuth.ID, 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(nil) + 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(envKeyClientID) + clientSecret := os.Getenv(envKeyClientSecret) + + authRequest := generateAppAuthTokenRequest(clientID, clientSecret) + + createdAuth, resp, err := userAuthenticatedClient.Authorizations.GetOrCreateForApp(clientID, 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(clientID, 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.Check(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.Check(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.Reset(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.Reset(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.Check(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.Check(clientID, *resetAuth.Token) + failOnError(t, err) + failIfNotStatusCode(t, resp, 200) + + // Let's revoke the token + resp, err = appAuthenticatedClient.Authorizations.Revoke(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.Check(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, ok := os.LookupEnv(envKeyGitHubUsername) + if !ok { + t.Skipf(msgEnvMissing, envKeyGitHubUsername) + } + + password, ok := os.LookupEnv(envKeyGitHubPassword) + if !ok { + t.Skipf(msgEnvMissing, envKeyGitHubPassword) + } + + 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, ok := os.LookupEnv(envKeyClientID) + if !ok { + t.Skipf(msgEnvMissing, envKeyClientID) + } + + password, ok := os.LookupEnv(envKeyClientSecret) + if !ok { + t.Skipf(msgEnvMissing, envKeyClientSecret) + } + + 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 9e83de9..16e383f 100644 --- a/tests/integration/github_test.go +++ b/tests/integration/github_test.go @@ -39,6 +39,17 @@ func init() { client = github.NewClient(tc) auth = true } + + // Environment variables required for Authorization integration tests + vars := []string{envKeyGitHubUsername, envKeyGitHubPassword, envKeyClientID, envKeyClientSecret} + + for _, v := range vars { + value := os.Getenv(v) + if value == "" { + print("!!! " + fmt.Sprintf(msgEnvMissing, v) + " !!!\n\n") + } + } + } func checkAuth(name string) bool {