diff --git a/client/hub/client_test.go b/client/hub/client_test.go index 4d0352d6..1cdce999 100644 --- a/client/hub/client_test.go +++ b/client/hub/client_test.go @@ -31,12 +31,21 @@ import ( // } // } +// A set of variables that are sent with the query. +// This allows us to test that the mock server returns these +// variables in the response. +var defaultVariables = map[string]interface{}{ + "name": "john", + "lines": 100, +} + // getProjects is a wrapper of an `QueryAllProjects“ API call to fetch project names // Note: This is only for the testing the MockServer with HubClient func getProjects(hc hub.Client) ([]string, error) { req := &hub.Request{ - OpName: "QueryAllProjects", - Query: QueryAllProjects_Operation, + OpName: "QueryAllProjects", + Query: QueryAllProjects_Operation, + Variables: defaultVariables, } var responseData QueryAllProjectsResponse err := hc.Request(context.Background(), req, &responseData) @@ -77,6 +86,7 @@ func TestQueryWithTanzuHubClient(t *testing.T) { mockResponses: []hubtesting.Operation{ { Identifier: "QueryAllProjects", + Variables: defaultVariables, Response: hub.Response{ Data: QueryAllProjectsResponse{ ApplicationEngineQuery: QueryAllProjectsApplicationEngineQuery{ @@ -95,6 +105,7 @@ func TestQueryWithTanzuHubClient(t *testing.T) { mockResponses: []hubtesting.Operation{ { Identifier: "QueryAllProjects", + Variables: defaultVariables, Response: hub.Response{ Data: QueryAllProjectsResponse{ ApplicationEngineQuery: QueryAllProjectsApplicationEngineQuery{ @@ -118,11 +129,63 @@ func TestQueryWithTanzuHubClient(t *testing.T) { }, expectedOutput: []string{"project1", "project2", "project3"}, }, + { + name: "when projects found and query returns response - use responder implementation", + mockResponses: []hubtesting.Operation{ + { + Identifier: "QueryAllProjects", + // Change the value of the default variables + // to make sure the mock server returns the variables that were + // received in the query and NOT the ones that are registered here. + // Note that the variable keys must match the ones in the query, but + // the values can be different. + Variables: map[string]interface{}{ + "name": "notjohn", + "lines": 0, + }, + Responder: func(_ context.Context, receivedReq hubtesting.Request) hub.Response { + return hub.Response{ + Data: QueryAllProjectsResponse{ + ApplicationEngineQuery: QueryAllProjectsApplicationEngineQuery{ + QueryProjects: QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnection{ + Projects: []QueryAllProjectsApplicationEngineQueryQueryProjectsKubernetesKindProjectConnectionProjectsKubernetesKindProject{ + { + Name: "project1", + }, + { + Name: "project2", + }, + { + // Check that the mock server returns the received query to the Responder + // by putting the received query as a project name. + Name: fmt.Sprintf("%v", receivedReq), + }, + }, + }, + }, + }, + } + }, + }, + }, + expectedOutput: []string{"project1", "project2", `{ +query QueryAllProjects { + applicationEngineQuery { + queryProjects(first: 1000) { + projects { + name + } + } + } +} + map[lines:100 name:john]}`}, + }, { name: "when query returns error response", mockResponses: []hubtesting.Operation{ { Identifier: "QueryAllProjects", + Variables: defaultVariables, Response: hub.Response{ Errors: gqlerror.List{{Message: "fake-error-message"}}, }, @@ -135,14 +198,34 @@ func TestQueryWithTanzuHubClient(t *testing.T) { mockResponses: []hubtesting.Operation{ { Identifier: "QueryAllProjects", - Responder: func(ctx context.Context, op hubtesting.Operation) hub.Response { + // Change the value of the default variables + // to make sure the mock server returns the variables that were + // received in the query and NOT the ones that are registered here. + // Note that the variable keys must match the ones in the query, but + // the values can be different. + Variables: map[string]interface{}{ + "name": "notjohn", + "lines": 0, + }, + Responder: func(_ context.Context, receivedReq hubtesting.Request) hub.Response { return hub.Response{ - Errors: gqlerror.List{{Message: fmt.Sprintf("operation %s failed with error %s", op.Identifier, "fake-error-message")}}, + Errors: gqlerror.List{{Message: fmt.Sprintf("operation failed with error and received %v", receivedReq)}}, } }, }, }, - expectedErrString: "operation QueryAllProjects failed with error fake-error-message", + expectedErrString: `operation failed with error and received { +query QueryAllProjects { + applicationEngineQuery { + queryProjects(first: 1000) { + projects { + name + } + } + } +} + map[lines:100 name:john]} +`, }, { name: "when the query is not registered with the server or incorrect query is used", @@ -150,6 +233,45 @@ func TestQueryWithTanzuHubClient(t *testing.T) { expectedOutput: []string{}, expectedErrString: "operation not found", }, + { + name: "when the query is registered but has variables that use different keys", + mockResponses: []hubtesting.Operation{ + { + Identifier: "QueryAllProjects", + Variables: map[string]interface{}{ + "differentkey": "anothervalue", + "name": "john", + "lines": 100, + }, + Response: hub.Response{ + Errors: gqlerror.List{{Message: "fake-error-message"}}, + }, + }, + }, + expectedOutput: []string{}, + // The error should not be the error returned by the registered query + // because we are testing that the registered query does not match + // because it uses different variable keys that the real query. + expectedErrString: "operation not found", + }, + { + name: "when the query is registered but has variables that use different values", + mockResponses: []hubtesting.Operation{ + { + Identifier: "QueryAllProjects", + // Change the value of the default variables but use the same keys + // This should still match the registered query and return the error + Variables: map[string]interface{}{ + "name": "notjohn", + "lines": 0, + }, + Response: hub.Response{ + Errors: gqlerror.List{{Message: "fake-error-message"}}, + }, + }, + }, + expectedErrString: "fake-error-message", + }, } for _, tt := range tests { @@ -280,7 +402,7 @@ log 4 } } -func mockAppLogGenerator(ctx context.Context, _ hubtesting.Operation, eventData chan<- hubtesting.Response) { +func mockAppLogGenerator(_ context.Context, _ hubtesting.Request, eventData chan<- hubtesting.Response) { i := 0 for i < 5 { time.Sleep(1 * time.Second) diff --git a/client/hub/testing/operations.go b/client/hub/testing/operations.go index 44986ac1..2c22bdd4 100644 --- a/client/hub/testing/operations.go +++ b/client/hub/testing/operations.go @@ -59,7 +59,7 @@ type Operation struct { Variables map[string]interface{} // Response represents the response that should be returned whenever the server makes - // a match on Operation.opType, Operation.Identifier, and Operation.Variables. + // a match on Operation.opType, Operation.Identifier, and Operation.Variables keys. // Response is to be used for Query and Mutation operations only. // Note: User can define either `Response` or implement `Responder` function but should // not be defining both. @@ -67,15 +67,15 @@ type Operation struct { // Responder implements the function that based on some operation parameters should respond // differently. - // Tests that do not need flexibility in returning different responses based on the Operation - // should just configure the `Response` field instead. + // Tests that do not need flexibility in returning different responses based on the received + // request should just configure the `Response` field instead. // Responder is to be used for Query and Mutation operations only. // Note: User can define either `Response` or implement `Responder` function but should // not be defining both. Responder Responder // EventGenerator should implement a eventData generator for testing and - // send mock event response to the `eventData` channel. To suggest end of + // send mock event response to the `eventData` channel. To suggest the end of // the event responses from server side, you can close the eventData channel // Note: This is only to be used for the Subscription where you will need to // mock the generation of the events. This should not be used with Query or Mutation. @@ -115,10 +115,10 @@ type OperationError struct { // Responder implements the function that based on some operation parameters should respond // differently. This type of Responder implementation is more useful when you want -// to implement a generic function that returns data based on the operation -type Responder func(ctx context.Context, op Operation) hub.Response +// to implement a generic function that returns data based on the received Request +type Responder func(ctx context.Context, receivedRequest Request) hub.Response // EventGenerator should implement a eventData generator for testing and -// send mock event response to the `eventData` channel. To suggest end of -// the event responses from server side, you can close the eventData channel -type EventGenerator func(ctx context.Context, op Operation, eventData chan<- Response) +// send mock event response to the `eventData` channel. To suggest the end of +// the event responses from the server side, you can close the eventData channel +type EventGenerator func(ctx context.Context, receivedRequest Request, eventData chan<- Response) diff --git a/client/hub/testing/server.go b/client/hub/testing/server.go index 29b5a50f..aa1b2b4a 100644 --- a/client/hub/testing/server.go +++ b/client/hub/testing/server.go @@ -134,7 +134,7 @@ func NewServer(t *testing.T, opts ...ServerOptions) *Server { //nolint:gocyclo if strings.Contains(reqBody.Query, s.mutations[i].Identifier) { if s.equalVariables(s.mutations[i].Variables, reqBody.Variables) { if s.mutations[i].Responder != nil { - s.respond(w, http.StatusOK, s.mutations[i].Responder(r.Context(), s.mutations[i])) + s.respond(w, http.StatusOK, s.mutations[i].Responder(r.Context(), reqBody)) } else { s.respond(w, http.StatusOK, s.mutations[i].Response) } @@ -147,7 +147,7 @@ func NewServer(t *testing.T, opts ...ServerOptions) *Server { //nolint:gocyclo if strings.Contains(reqBody.Query, s.queries[i].Identifier) { if s.equalVariables(s.queries[i].Variables, reqBody.Variables) { if s.queries[i].Responder != nil { - s.respond(w, http.StatusOK, s.queries[i].Responder(r.Context(), s.queries[i])) + s.respond(w, http.StatusOK, s.queries[i].Responder(r.Context(), reqBody)) } else { s.respond(w, http.StatusOK, s.queries[i].Response) } @@ -169,7 +169,7 @@ func NewServer(t *testing.T, opts ...ServerOptions) *Server { //nolint:gocyclo respChan := make(chan Response) - go s.subscriptions[i].EventGenerator(r.Context(), s.subscriptions[i], respChan) + go s.subscriptions[i].EventGenerator(r.Context(), reqBody, respChan) for eventResp := range respChan { event, err := formatServerSentEvent("update", eventResp)