Skip to content

Commit

Permalink
Add FirstMatch planner (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
nhatthm authored Dec 7, 2021
1 parent c14e8fd commit dbacb4e
Show file tree
Hide file tree
Showing 21 changed files with 1,147 additions and 90 deletions.
53 changes: 52 additions & 1 deletion SERVER.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
- [Return an error](#return-an-error-3)
- [Return with a custom handler](#return-with-a-custom-handler-3)
- [Execution Plan](#execution-plan)
- [First Match](#first-match)
- [Examples](#examples)

## Usage
Expand Down Expand Up @@ -1467,13 +1468,63 @@ type Planner interface {
}
```

Then use it with `Server.WithPlanner(newPlanner)` (see the [`ExampleServer_WithPlanner`](server_example_test.go#L24))
Then use it with `Server.WithPlanner(newPlanner)` (see the [`ExampleServer_WithPlanner`](server_example_test.go#L27))

When the `Server.Expect[METHOD]()` is called, the mocked server will prepare a request and sends it to the planner. If there is an incoming request, the server
will call `Planner.PLan()` to find the expectation that matches the request and executes it.

[<sub><sup>[table of contents]</sup></sub>](#table-of-contents)

### First Match

`planner.FirstMatch` creates a new `planner.Planner` that finds the first expectation that matches the incoming request.

For example, there are 3 expectations in order:

```
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 40})
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
Return(`{"id": 41, "name": "Item #41 - 1"}`)
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
Return(`{"id": 41, "name": "Item #41 - 2"}`)
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 42})
```

When the server receives a request with payload `{"id": 41}`, the `planner.FirstMatch` looks up and finds the second expectation which is the first
expectation that matches all the criteria. After that, there are only 3 expectations left:

```
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 40})
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
Return(`{"id": 41, "name": "Item #41 - 2"}`)
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 42})
```

When the server receives another request with payload `{"id": 40}`, the `planner.FirstMatch` does the same thing and there are only 2 expectations left:

```
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
Return(`{"id": 41, "name": "Item #41 - 2"}`)
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 42})
```

When the server receives another request with payload `{"id": 100}`, the `planner.FirstMatch` can not match it with any expectations and the server returns
a `FailedPrecondition` result with error message `unexpected request received`.

Due to the nature of the matcher, pay extra attention when you use repeatability. For example, given these expectations:

```
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
UnlimitedTimes().
Return(`{"id": 41, "name": "Item #41 - 1"}`)
Server.ExpectUnary("grpctest.Service/GetItem").WithPayload(&Item{Id: 41}).
Return(`{"id": 41, "name": "Item #41 - 2"}`)
```

The 2nd expectation is never taken in account because with the same criteria, the planner always picks the first match, which is the first expectation.

[<sub><sup>[table of contents]</sup></sub>](#table-of-contents)

## Examples

See:
Expand Down
20 changes: 20 additions & 0 deletions errors/error.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package errors

import (
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

const (
// ErrUnsupportedDataType represents that the data type is not supported.
ErrUnsupportedDataType err = "unsupported data type"
Expand All @@ -24,3 +29,18 @@ type err string
func (e err) Error() string {
return string(e)
}

// StatusError converts error to status.Error if applicable.
func StatusError(err error) error {
if err == nil {
return nil
}

code := status.Code(err)

if code == codes.Unknown {
return status.Error(codes.Internal, err.Error())
}

return err
}
40 changes: 40 additions & 0 deletions errors/error_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package errors

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func TestErr_String(t *testing.T) {
Expand All @@ -15,3 +18,40 @@ func TestErr_String(t *testing.T) {

assert.EqualError(t, actual, expected)
}

func TestStatusError(t *testing.T) {
t.Parallel()

testCases := []struct {
scenario string
err error
expected error
}{
{
scenario: "no error",
},
{
scenario: "generic error",
err: errors.New("error"),
expected: status.Error(codes.Internal, "error"),
},
{
scenario: "status ok",
err: status.Error(codes.OK, ""),
},
{
scenario: "status not ok",
err: status.Error(codes.Internal, "internal"),
expected: status.Error(codes.Internal, "internal"),
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.scenario, func(t *testing.T) {
t.Parallel()

assert.Equal(t, tc.expected, StatusError(tc.err))
})
}
}
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ require (
github.com/stretchr/objx v0.3.0 // indirect
github.com/yudai/gojsondiff v1.0.0 // indirect
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 // indirect
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 // indirect
golang.org/x/net v0.0.0-20211203184738-4852103109b8 // indirect
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
golang.org/x/text v0.3.7 // indirect
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 // indirect
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
)
12 changes: 6 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4 h1:DZshvxDdVoeKIbudAdFEKi+f70l51luSy/7b76ibTY0=
golang.org/x/net v0.0.0-20211118161319-6a13c67c3ce4/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211203184738-4852103109b8 h1:PFkPt/jI9Del3hmFplBtRp8tDhSRpFu7CyRs7VmEC0M=
golang.org/x/net v0.0.0-20211203184738-4852103109b8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand All @@ -191,8 +191,8 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
Expand Down Expand Up @@ -221,8 +221,8 @@ google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1 h1:b9mVrqYfq3P4bCdaLg1qtBnPzUYgglsIdjZkL/fQVOE=
google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9 h1:fU3FNfL/oBU2D5DvGqiuyVqqn40DdxvaTFHq7aivA3k=
google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
Expand Down
16 changes: 16 additions & 0 deletions planner/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"io"
"strings"

"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"

"github.com/nhatthm/grpcmock/format"
"github.com/nhatthm/grpcmock/request"
Expand Down Expand Up @@ -86,3 +88,17 @@ func NewError(ctx context.Context, expected request.Request, req service.Method,
messageArgs: messageArgs,
}
}

// UnexpectedRequestError returns an error because of the unexpected request.
func UnexpectedRequestError(m service.Method, in interface{}) error {
payload, err := value.Marshal(in)
if err != nil {
return status.Errorf(codes.FailedPrecondition, "unexpected request received: %q, unable to decode payload: %s", m.FullName(), err.Error())
}

if len(payload) > 0 {
return status.Errorf(codes.FailedPrecondition, "unexpected request received: %q, payload: %s", m.FullName(), payload)
}

return status.Errorf(codes.FailedPrecondition, "unexpected request received: %q", m.FullName())
}
69 changes: 69 additions & 0 deletions planner/error_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package planner_test

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/nhatthm/grpcmock/internal/grpctest"
"github.com/nhatthm/grpcmock/internal/test"
"github.com/nhatthm/grpcmock/planner"
)

func TestUnexpectedRequestError_Unary(t *testing.T) {
t.Parallel()

svc := test.GetItemsSvc()
in := test.DefaultItem()

expected := `rpc error: code = FailedPrecondition desc = unexpected request received: "/grpctest.Service/GetItem", payload: {"id":41,"locale":"en-US","name":"Item #41"}`

assert.EqualError(t, planner.UnexpectedRequestError(svc, in), expected)
}

func TestUnexpectedRequestError_ClientStream(t *testing.T) {
t.Parallel()

svc := test.CreateItemsSvc()
in := test.MockCreateItemsStreamer(
test.MockStreamRecvItemSuccess(test.DefaultItem()),
test.MockStreamRecvItemEOF(),
)(t)

expected := `rpc error: code = FailedPrecondition desc = unexpected request received: "/grpctest.Service/CreateItems", payload: [{"id":41,"locale":"en-US","name":"Item #41"}]`

assert.EqualError(t, planner.UnexpectedRequestError(svc, in), expected)
}

func TestUnexpectedRequestError_ServerStream(t *testing.T) {
t.Parallel()

svc := test.ListItemsSvc()
in := &grpctest.ListItemsRequest{}

expected := `rpc error: code = FailedPrecondition desc = unexpected request received: "/grpctest.Service/ListItems", payload: {}`

assert.EqualError(t, planner.UnexpectedRequestError(svc, in), expected)
}

func TestUnexpectedRequestError_BidirectionalStream(t *testing.T) {
t.Parallel()

svc := test.TransformItemsSvc()
in := test.NoMockBidirectionalStreamer(t)

expected := `rpc error: code = FailedPrecondition desc = unexpected request received: "/grpctest.Service/TransformItems"`

assert.EqualError(t, planner.UnexpectedRequestError(svc, in), expected)
}

func TestUnexpectedRequestError_Error(t *testing.T) {
t.Parallel()

svc := test.GetItemsSvc()
in := make(chan error, 1)

expected := `rpc error: code = FailedPrecondition desc = unexpected request received: "/grpctest.Service/GetItem", unable to decode payload: json: unsupported type: chan error`

assert.EqualError(t, planner.UnexpectedRequestError(svc, in), expected)
}
Loading

0 comments on commit dbacb4e

Please sign in to comment.