From 38538704ceef34b8319f0d93fdd89c0c39cfd135 Mon Sep 17 00:00:00 2001 From: Toan Nguyen Date: Thu, 21 Mar 2024 09:58:50 +0700 Subject: [PATCH] improve tests --- README.md | 17 +++++ go.mod | 2 +- go.sum | 2 + rest/client.go | 4 +- rest/connector_test.go | 136 +++++++++++++++++++++++++++++++++ rest/metadata.go | 9 ++- rest/schema.go | 2 +- rest/testdata/auth/schema.yaml | 15 ++-- 8 files changed, 171 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index ed67f5c..d872e42 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,17 @@ files: spec: openapi2 - path: openapi.yaml spec: openapi3 + trimPrefix: /v1 + envPrefix: PET_STORE + methodAlias: + post: create + put: update - path: schema.json spec: ndc ``` +`trimPrefix`, `envPrefix` and `methodAlias` options are used to convert OpenAPI by [ndc-rest-schema](https://github.com/hasura/ndc-rest-schema#openapi). + **Supported specs** - `openapi3`: OpenAPI 3.0/3.1. @@ -45,3 +52,13 @@ ndc-rest convert -f ./rest/testdata/jsonplaceholder/swagger.json -o ./rest/testd ``` > The `convert` command is imported from the [NDC REST Schema](https://github.com/hasura/ndc-rest-schema#quick-start) CLI tool. + +### Environment variable template + +The connector can replaces `{{xxx}}` templates with environment variables. The converter automatically renders variables for API keys and tokens when converting OpenAPI documents. However, you can add your custom variables as well. + +### Authentication + +The current version supports API key and Auth token authentication schemes. The configuration is inspired from `securitySchemes` [with env variables](https://github.com/hasura/ndc-rest-schema#authentication) + +See [this example](rest/testdata/auth/schema.yaml) for more context. diff --git a/go.mod b/go.mod index 71cd06d..512f6d7 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.22.0 require ( - github.com/hasura/ndc-rest-schema v0.0.0-20240320034216-4c2976a29855 + github.com/hasura/ndc-rest-schema v0.0.0-20240321015327-71425bf24217 github.com/hasura/ndc-sdk-go v0.1.1-0.20240317172640-9c7a7adc1cd3 github.com/lmittmann/tint v1.0.4 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index dc69b89..d04e8de 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0Q github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/hasura/ndc-rest-schema v0.0.0-20240320034216-4c2976a29855 h1:d3bq7jjsK/aPcFih6B+h1uM3CkNY24F8Eo5JXx8QBo8= github.com/hasura/ndc-rest-schema v0.0.0-20240320034216-4c2976a29855/go.mod h1:R1xvYOx/TqgHEiP5Nm8qTRwFfJmxmV1y1De4b1i1CFg= +github.com/hasura/ndc-rest-schema v0.0.0-20240321015327-71425bf24217 h1:HhfRPEHzEpBO2j55oHV7r+uBQcg2KMgWlmEtp3TvWO4= +github.com/hasura/ndc-rest-schema v0.0.0-20240321015327-71425bf24217/go.mod h1:R1xvYOx/TqgHEiP5Nm8qTRwFfJmxmV1y1De4b1i1CFg= github.com/hasura/ndc-sdk-go v0.1.1-0.20240317172640-9c7a7adc1cd3 h1:A1N3ilX1EIxjTA2qaHPXFnIECpYjKhIFmtva8qJrpHk= github.com/hasura/ndc-sdk-go v0.1.1-0.20240317172640-9c7a7adc1cd3/go.mod h1:EeM3hKbhCfBjDDva8mP4D2KeptTqAaxNqNw8rFQAnMs= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= diff --git a/rest/client.go b/rest/client.go index 532cd07..f867e1f 100644 --- a/rest/client.go +++ b/rest/client.go @@ -155,7 +155,9 @@ func evalHTTPResponse(resp *http.Response, selection schema.NestedField) (any, e return utils.EvalNestedColumnFields(selection, result) default: - return nil, fmt.Errorf("unsupported content type %s", contentType) + return nil, schema.NewConnectorError(http.StatusInternalServerError, "failed to evaluate response", map[string]any{ + "cause": fmt.Sprintf("unsupported content type %s", contentType), + }) } } diff --git a/rest/connector_test.go b/rest/connector_test.go index 9b40069..ed3f785 100644 --- a/rest/connector_test.go +++ b/rest/connector_test.go @@ -6,7 +6,9 @@ import ( "encoding/json" "fmt" "io" + "log/slog" "net/http" + "net/http/httptest" "os" "path" "reflect" @@ -86,7 +88,141 @@ func TestRESTConnector_configurationFailure(t *testing.T) { } func TestRESTConnector_authentication(t *testing.T) { + apiKey := "random_api_key" + bearerToken := "random_bearer_token" + slog.SetLogLoggerLevel(slog.LevelDebug) + server := createMockServer(t, apiKey, bearerToken) + defer server.Close() + t.Setenv("PET_STORE_URL", server.URL) + t.Setenv("PET_STORE_API_KEY", apiKey) + t.Setenv("PET_STORE_BEARER_TOKEN", bearerToken) + connServer, err := connector.NewServer(NewRESTConnector(), &connector.ServerOptions{ + Configuration: "testdata/auth", + }, connector.WithoutRecovery()) + assertNoError(t, err) + testServer := connServer.BuildTestServer() + defer testServer.Close() + + t.Run("auth_default", func(t *testing.T) { + reqBody := []byte(`{ + "collection": "findPets", + "query": { + "fields": { + "__value": { + "type": "column", + "column": "__value" + } + } + }, + "arguments": {}, + "collection_relationships": {} + }`) + + res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(reqBody)) + assertNoError(t, err) + assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ + { + Rows: []map[string]any{ + {"__value": map[string]any{}}, + }, + }, + }) + }) + + t.Run("auth_api_key", func(t *testing.T) { + reqBody := []byte(`{ + "operation": "addPet", + "query": { + "fields": { + "__value": { + "type": "column", + "column": "__value" + } + } + }, + "arguments": {}, + "collection_relationships": {} + }`) + + res, err := http.Post(fmt.Sprintf("%s/mutation", testServer.URL), "application/json", bytes.NewBuffer(reqBody)) + assertNoError(t, err) + assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ + { + Rows: []map[string]any{ + {"__value": map[string]any{}}, + }, + }, + }) + }) + + t.Run("auth_bearer", func(t *testing.T) { + reqBody := []byte(`{ + "collection": "findPetsByStatus", + "query": { + "fields": { + "__value": { + "type": "column", + "column": "__value" + } + } + }, + "arguments": {}, + "collection_relationships": {} + }`) + + res, err := http.Post(fmt.Sprintf("%s/query", testServer.URL), "application/json", bytes.NewBuffer(reqBody)) + assertNoError(t, err) + assertHTTPResponse(t, res, http.StatusOK, schema.QueryResponse{ + { + Rows: []map[string]any{ + {"__value": map[string]any{}}, + }, + }, + }) + }) +} + +func createMockServer(t *testing.T, apiKey string, bearerToken string) *httptest.Server { + mux := http.NewServeMux() + + writeResponse := func(w http.ResponseWriter) { + w.Header().Add("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{}`)) + + } + mux.HandleFunc("/pet", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet, http.MethodPost: + if r.Header.Get("api_key") != apiKey { + t.Errorf("invalid api key, expected %s, got %s", apiKey, r.Header.Get("api_key")) + t.FailNow() + return + } + writeResponse(w) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + }) + + mux.HandleFunc("/pet/findByStatus", func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + if r.Header.Get("Authorization") != fmt.Sprintf("Bearer %s", bearerToken) { + t.Errorf("invalid bearer token, expected %s, got %s", bearerToken, r.Header.Get("Authorization")) + t.FailNow() + return + } + writeResponse(w) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + }) + + return httptest.NewServer(mux) } func assertNdcOperations(t *testing.T, dir string, targetURL string) { diff --git a/rest/metadata.go b/rest/metadata.go index a460e9c..4e6ecc3 100644 --- a/rest/metadata.go +++ b/rest/metadata.go @@ -2,6 +2,7 @@ package rest import ( "fmt" + "log" "net/url" "strings" @@ -99,6 +100,7 @@ func (rm RESTMetadata) buildURL(endpoint string) string { } func (rm RESTMetadata) applySecurity(req *rest.Request) (*rest.Request, error) { + log.Println("security", req.URL, req.Security) if req.Security.IsEmpty() { req.Security = rm.settings.Security } @@ -115,15 +117,16 @@ func (rm RESTMetadata) applySecurity(req *rest.Request) (*rest.Request, error) { if req.Headers == nil { req.Headers = make(map[string]string) } + switch securityScheme.Type { case rest.HTTPAuthScheme: headerName := securityScheme.Header if headerName == "" { headerName = "Authorization" } - scheme := securityScheme.Name - if securityScheme.Name == "bearer" || securityScheme.Name == "basic" { - scheme = utils.ToPascalCase(securityScheme.Name) + scheme := securityScheme.Scheme + if scheme == "bearer" || scheme == "basic" { + scheme = utils.ToPascalCase(securityScheme.Scheme) } req.Headers[headerName] = fmt.Sprintf("%s %s", scheme, securityScheme.Value) case rest.APIKeyScheme: diff --git a/rest/schema.go b/rest/schema.go index f4edc0e..e1b0e7a 100644 --- a/rest/schema.go +++ b/rest/schema.go @@ -86,7 +86,7 @@ func buildSchemaFile(configDir string, conf *SchemaFile, envVars map[string]stri switch conf.Spec { case rest.NDCSpec: var result rest.NDCRestSchema - fileFormat, err := rest.ParseSchemaFileFormat(conf.Path) + fileFormat, err := rest.ParseSchemaFileFormat(strings.Trim(path.Ext(conf.Path), ".")) if err != nil { return nil, err } diff --git a/rest/testdata/auth/schema.yaml b/rest/testdata/auth/schema.yaml index 7fa60da..1b85650 100644 --- a/rest/testdata/auth/schema.yaml +++ b/rest/testdata/auth/schema.yaml @@ -21,18 +21,15 @@ settings: read:pets: read your pets write:pets: modify pets in your account security: - - api_key + - api_key: [] version: 1.0.18 collections: [] functions: - request: url: "/pet" method: get - parameters: {} - security: - - petstore_auth: - - write:pets - - read:pets + parameters: [] + security: [] arguments: {} description: Finds Pets name: findPets @@ -55,7 +52,7 @@ functions: - pending - sold security: - - bearer + - bearer: [] arguments: status: description: Status values that need to be considered for filter @@ -78,9 +75,7 @@ procedures: headers: Content-Type: application/json security: - - petstore_auth: - - write:pets - - read:pets + - api_key: [] arguments: body: description: Request body of /pet