Skip to content

Commit

Permalink
Have the Hub UT framework send what it received (#206) (#218)
Browse files Browse the repository at this point in the history
Before this commit the UT framework would send the registered operation.
This was not useful for unit tests as it is those same tests that
registered the operation, so they are already aware of what it is.  What the
tests need is for the framework to send the actual received GraphQL
request so that it can be verified. This commit does this.

Signed-off-by: Marc Khouzam <[email protected]>
Co-authored-by: Marc Khouzam <[email protected]>
  • Loading branch information
anujc25 and marckhouzam authored Nov 5, 2024
1 parent e0abd1a commit 0fe7714
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 18 deletions.
134 changes: 128 additions & 6 deletions client/hub/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -77,6 +86,7 @@ func TestQueryWithTanzuHubClient(t *testing.T) {
mockResponses: []hubtesting.Operation{
{
Identifier: "QueryAllProjects",
Variables: defaultVariables,
Response: hub.Response{
Data: QueryAllProjectsResponse{
ApplicationEngineQuery: QueryAllProjectsApplicationEngineQuery{
Expand All @@ -95,6 +105,7 @@ func TestQueryWithTanzuHubClient(t *testing.T) {
mockResponses: []hubtesting.Operation{
{
Identifier: "QueryAllProjects",
Variables: defaultVariables,
Response: hub.Response{
Data: QueryAllProjectsResponse{
ApplicationEngineQuery: QueryAllProjectsApplicationEngineQuery{
Expand All @@ -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"}},
},
Expand All @@ -135,21 +198,80 @@ 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",
mockResponses: []hubtesting.Operation{},
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 {
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 9 additions & 9 deletions client/hub/testing/operations.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,23 @@ 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.
Response hub.Response

// 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.
Expand Down Expand Up @@ -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)
6 changes: 3 additions & 3 deletions client/hub/testing/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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)
}
Expand All @@ -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)
Expand Down

0 comments on commit 0fe7714

Please sign in to comment.