diff --git a/Makefile b/Makefile index fe207a3..bf2d001 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ webtest: go tool cover -html=testcoverage.out apitest: - ./apitest --stop-on-fail -d test/ + ./apitest -c apitest.test.yml --stop-on-fail -d test/ gox: deps go get github.com/mitchellh/gox diff --git a/README.md b/README.md index 66cd022..cca312f 100644 --- a/README.md +++ b/README.md @@ -1895,6 +1895,31 @@ will return this response: } ``` +### `bounce-query` + +The endpoint `bounce-query` returns the a response that includes in its `body` the request `query string` as it is. +This is useful in endpoints where a body cannot be configured, like oAuth urls, so we can simulate responses in the request for testing. + +```yaml +{ + "request": { + "endpoint": "bounce-query?here=is&all=stuff", + "method": "POST", + "body": {} + } +} +``` + +will return this response: + +```yaml +{ + "response": { + "body": "here=is&all=stuff" + } +} +``` + ## HTTP Server Proxy The proxy different stores can be used to both store and read their stored requests diff --git a/api_testsuite.go b/api_testsuite.go index 236c3e7..0ec0de4 100644 --- a/api_testsuite.go +++ b/api_testsuite.go @@ -205,6 +205,7 @@ func (ats *Suite) parseAndRunTest(v interface{}, manifestDir, testFilePath strin return false } loader.ServerURL = serverURL + loader.OAuthClient = ats.Config.OAuthClient isParallelPathSpec := false switch t := v.(type) { @@ -354,6 +355,7 @@ func (ats *Suite) loadManifest() ([]byte, error) { return nil, fmt.Errorf("can not load server url into manifest (%s): %s", ats.manifestPath, err) } loader.ServerURL = serverURL + loader.OAuthClient = ats.Config.OAuthClient manifestFile, err := filesystem.Fs.Open(ats.manifestPath) if err != nil { return res, fmt.Errorf("error opening manifestPath (%s): %s", ats.manifestPath, err) diff --git a/apitest.test.yml b/apitest.test.yml new file mode 100644 index 0000000..275b7a9 --- /dev/null +++ b/apitest.test.yml @@ -0,0 +1,6 @@ +apitest: + oauth_client: + my_client: + endpoint: + token_url: "http://localhost:9999/bounce-query?access_token=mytoken" + secret: "foobar" diff --git a/config.go b/config.go index e1b79bf..770f08b 100644 --- a/config.go +++ b/config.go @@ -8,6 +8,7 @@ import ( "time" "github.com/programmfabrik/apitest/pkg/lib/filesystem" + "github.com/programmfabrik/apitest/pkg/lib/util" "github.com/sirupsen/logrus" "github.com/spf13/afero" @@ -26,6 +27,7 @@ type ConfigStruct struct { File string `mapstructure:"file"` Format string `mapstructure:"format"` } `mapstructure:"report"` + OAuthClient util.OAuthClientsConfig `mapstructure:"oauth_client"` } } @@ -56,6 +58,7 @@ type TestToolConfig struct { TestDirectories []string LogNetwork bool LogVerbose bool + OAuthClient util.OAuthClientsConfig } // NewTestToolConfig is mostly used for testing purpose. We can setup our config with this function @@ -65,6 +68,7 @@ func NewTestToolConfig(serverURL string, rootDirectory []string, logNetwork bool rootDirectorys: rootDirectory, LogNetwork: logNetwork, LogVerbose: logVerbose, + OAuthClient: Config.Apitest.OAuthClient, } err = config.extractTestDirectories() return config, err diff --git a/go.mod b/go.mod index 0f55eb7..3c82332 100644 --- a/go.mod +++ b/go.mod @@ -4,19 +4,16 @@ go 1.13 require ( github.com/clbanning/mxj v1.8.4 - github.com/gabriel-vasile/mimetype v0.3.22 - github.com/gorilla/mux v1.7.4 - github.com/k0kubun/pp v3.0.1+incompatible - github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-sqlite3 v1.14.4 - github.com/mitchellh/gox v1.0.1 // indirect github.com/moul/http2curl v1.0.0 github.com/pkg/errors v0.8.1 github.com/programmfabrik/go-test-utils v0.0.0-20191114143449-b8e16b04adb1 github.com/sergi/go-diff v1.0.0 github.com/sirupsen/logrus v1.4.2 + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/afero v1.2.2 github.com/spf13/cobra v0.0.5 github.com/spf13/viper v1.5.0 github.com/tidwall/gjson v1.3.4 + golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be ) diff --git a/http_server.go b/http_server.go index a8d61e1..e57a670 100644 --- a/http_server.go +++ b/http_server.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "context" "encoding/json" "io" @@ -37,6 +38,9 @@ func (ats *Suite) StartHttpServer() { // bounce binary response with information in headers mux.Handle("/bounce", logH(http.HandlerFunc(bounceBinary))) + // bounce query response with query in response body, as it is + mux.Handle("/bounce-query", logH(http.HandlerFunc(bounceQuery))) + // Start listening into proxy ats.httpServerProxy = httpproxy.New(ats.HttpServer.Proxy) ats.httpServerProxy.RegisterRoutes(mux, "/") @@ -195,6 +199,13 @@ func bounceBinary(w http.ResponseWriter, r *http.Request) { io.Copy(w, r.Body) } +// bounceQuery returns the request query in response body +// for those cases where a body cannt be provided +func bounceQuery(w http.ResponseWriter, r *http.Request) { + rBody := bytes.NewBufferString(r.URL.RawQuery) + io.Copy(w, rBody) +} + func cookiesMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ckHeader := r.Header.Values("X-Test-Set-Cookies") diff --git a/pkg/lib/template/template_loader.go b/pkg/lib/template/template_loader.go index da0bb6b..8175c12 100644 --- a/pkg/lib/template/template_loader.go +++ b/pkg/lib/template/template_loader.go @@ -13,6 +13,7 @@ import ( "strings" "text/template" + "github.com/pkg/errors" "github.com/programmfabrik/apitest/pkg/lib/datastore" "github.com/programmfabrik/apitest/pkg/lib/cjson" @@ -73,6 +74,7 @@ type Loader struct { datastore *datastore.Datastore HTTPServerHost string ServerURL *url.URL + OAuthClient util.OAuthClientsConfig } func NewLoader(datastore *datastore.Datastore) Loader { @@ -358,6 +360,14 @@ func (loader *Loader) Render( } return reflect.ValueOf(v).IsZero() }, + "oauth2_token": func(client string, login string, password string) (interface{}, error) { + oAuthClient, ok := loader.OAuthClient[client] + if !ok { + return nil, errors.Errorf("OAuth client %s not configured", client) + } + oAuthClient.Key = client + return oAuthClient.GetAuthToken(login, password) + }, } tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(string(tmplBytes)) if err != nil { diff --git a/pkg/lib/util/oauth.go b/pkg/lib/util/oauth.go new file mode 100644 index 0000000..6b5faf5 --- /dev/null +++ b/pkg/lib/util/oauth.go @@ -0,0 +1,39 @@ +package util + +import ( + "context" + "net/http" + "time" + + "golang.org/x/oauth2" +) + +// OAuthClientsConfig is our config for multiple oAuth clients +type OAuthClientsConfig map[string]OAuthClientConfig + +// OAuthClientConfig is our config for a single oAuth client +type OAuthClientConfig struct { + Key string + Endpoint OAuthEndpointConfig `mapstructure:"endpoint"` + Secret string `mapstructure:"secret"` +} + +// OAuthEndpointConfig is our config for an oAuth endpoint +type OAuthEndpointConfig struct { + TokenURL string `mapstructure:"token_url"` +} + +// GetAuthToken sends request to oAuth token endpoint +// to get a token on behalf of a user +func (c OAuthClientConfig) GetAuthToken(username string, password string) (*oauth2.Token, error) { + cfg := oauth2.Config{ + ClientID: c.Key, + ClientSecret: c.Secret, + Endpoint: oauth2.Endpoint{ + TokenURL: c.Endpoint.TokenURL, + }, + } + httpClient := &http.Client{Timeout: 5 * time.Second} + ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient) + return cfg.PasswordCredentialsToken(ctx, username, password) +} diff --git a/pkg/lib/util/oauth_test.go b/pkg/lib/util/oauth_test.go new file mode 100644 index 0000000..e7f3e46 --- /dev/null +++ b/pkg/lib/util/oauth_test.go @@ -0,0 +1,68 @@ +package util + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestGetToken(t *testing.T) { + theToken := "thetoken" + theClient := "my_client" + theSecret := "foobar" + basicAuth := theClient + ":" + theSecret + basicAuthHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(basicAuth)) + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + authHeader := r.Header.Get("Authorization") + if authHeader != basicAuthHeader { + w.WriteHeader(403) + return + } + fmt.Fprintf(w, `access_token=%s`, theToken) + })) + defer ts.Close() + + cfg := OAuthClientConfig{ + Key: "nobody", + Endpoint: OAuthEndpointConfig{ + TokenURL: ts.URL, + }, + Secret: "whatever", + } + _, err := cfg.GetAuthToken("hey", "yo") + if err == nil { + t.Fatal("Expected error") + } + + cfg = OAuthClientConfig{ + Key: theClient, + Endpoint: OAuthEndpointConfig{ + TokenURL: ts.URL, + }, + Secret: "whatever", + } + _, err = cfg.GetAuthToken("hey", "yo") + if err == nil { + t.Fatal("Expected error") + } + + cfg = OAuthClientConfig{ + Key: theClient, + Endpoint: OAuthEndpointConfig{ + TokenURL: ts.URL, + }, + Secret: theSecret, + } + token, err := cfg.GetAuthToken("hey", "yo") + if err != nil { + t.Fatal(err) + } + if token == nil { + t.Fatal("No token nor error returned") + } + if token.AccessToken != theToken { + t.Fatalf("Received token: %s , expected: %s", token.AccessToken, theToken) + } +} diff --git a/test/oauth2/bounce_token.json b/test/oauth2/bounce_token.json new file mode 100644 index 0000000..3f8df53 --- /dev/null +++ b/test/oauth2/bounce_token.json @@ -0,0 +1,19 @@ +{ + "name": "Bounce token", + "request": { + "server_url": "http://localhost:9999", + "endpoint": "bounce-json", + "method": "POST", + "body": { + "token": {{ datastore "access_token" | marshal }} + } + }, + "response": { + "statuscode": 200, + "body": { + "body": { + "token": "mytoken" + } + } + } +} \ No newline at end of file diff --git a/test/oauth2/manifest.json b/test/oauth2/manifest.json new file mode 100644 index 0000000..ef81051 --- /dev/null +++ b/test/oauth2/manifest.json @@ -0,0 +1,17 @@ +{ + "http_server": { + "addr": ":9999", + "dir": "./", + "testmode": false, + "proxy": { + "oauth2": { + "mode": "passthru" + } + } + }, + "name": "oauth2", + "tests": [ + "@store_token.json", + "@bounce_token.json" + ] +} \ No newline at end of file diff --git a/test/oauth2/store_token.json b/test/oauth2/store_token.json new file mode 100644 index 0000000..44d5c4a --- /dev/null +++ b/test/oauth2/store_token.json @@ -0,0 +1,6 @@ +{ + "name": "Store oauth2 access token", + "store": { + "access_token": {{ oauth2_token "my_client" "user" "password" | marshal | qjson "access_token" }} + } +} \ No newline at end of file