diff --git a/.gitignore b/.gitignore index aa6934c2..22ec995b 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ gripmock .DS_Store protogen/* !protogen/go.mod +!protogen/empty.go !protogen/example/ temp \ No newline at end of file diff --git a/example/simple/client/main.go b/example/simple/client/main.go index 60016cda..776fa3af 100644 --- a/example/simple/client/main.go +++ b/example/simple/client/main.go @@ -33,6 +33,9 @@ func main() { if err != nil { log.Fatalf("error from grpc: %v", err) } + if r.ReturnCode != 1 { + log.Fatalf("grpc server returned code: %d, expected code: %d", r.ReturnCode, 1) + } log.Printf("Greeting: %s (return code %d)", r.Message, r.ReturnCode) name = "world" @@ -40,5 +43,28 @@ func main() { if err != nil { log.Fatalf("error from grpc: %v", err) } + if r.ReturnCode != 1 { + log.Fatalf("grpc server returned code: %d, expected code: %d", r.ReturnCode, 1) + } + log.Printf("Greeting: %s (return code %d)", r.Message, r.ReturnCode) + + name = "simple2" + r, err = c.SayHello(context.Background(), &pb.Request{Name: name}) + if err != nil { + log.Fatalf("error from grpc: %v", err) + } + if r.ReturnCode != 2 { + log.Fatalf("grpc server returned code: %d, expected code: %d", r.ReturnCode, 2) + } + log.Printf("Greeting: %s (return code %d)", r.Message, r.ReturnCode) + + name = "simple3" + r, err = c.SayHello(context.Background(), &pb.Request{Name: name}) + if err != nil { + log.Fatalf("error from grpc: %v", err) + } + if r.ReturnCode != 3 { + log.Fatalf("grpc server returned code: %d, expected code: %d", r.ReturnCode, 3) + } log.Printf("Greeting: %s (return code %d)", r.Message, r.ReturnCode) } diff --git a/example/simple/stub/simple2.yml b/example/simple/stub/simple2.yml new file mode 100644 index 00000000..0a1526ad --- /dev/null +++ b/example/simple/stub/simple2.yml @@ -0,0 +1,9 @@ +- service: Gripmock + method: SayHello + input: + equals: + name: simple2 + output: + data: + message: Hello Simple2 + return_code: 2 diff --git a/example/simple/stub/simple3.yaml b/example/simple/stub/simple3.yaml new file mode 100644 index 00000000..df2356d8 --- /dev/null +++ b/example/simple/stub/simple3.yaml @@ -0,0 +1,9 @@ +- service: Gripmock + method: SayHello + input: + equals: + name: simple3 + output: + data: + message: Hello Simple3 + return_code: 3 diff --git a/go.mod b/go.mod index 10ddc9aa..e5cdad5f 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/tokopedia/gripmock go 1.21 require ( - github.com/go-chi/chi v4.1.2+incompatible + github.com/goccy/go-yaml v1.11.0 + github.com/google/uuid v1.3.0 + github.com/gorilla/mux v1.8.0 github.com/lithammer/fuzzysearch v1.1.8 - github.com/stretchr/testify v1.8.4 + github.com/tokopedia/gripmock/protogen v0.0.0 github.com/tokopedia/gripmock/protogen/example v0.0.0 golang.org/x/text v0.12.0 google.golang.org/grpc v1.57.0 @@ -13,16 +15,14 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.10.0 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/kr/pretty v0.2.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/tokopedia/gripmock/protogen v0.0.0 // indirect + github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect golang.org/x/net v0.14.0 // indirect golang.org/x/sys v0.11.0 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) // this is for generated server to be able to run diff --git a/go.sum b/go.sum index 49185126..2bdb28a0 100644 --- a/go.sum +++ b/go.sum @@ -1,27 +1,36 @@ -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.11.0 h1:n7Z+zx8S9f9KgzG6KtQKf+kwqXZlLNR2F6018Dgau54= +github.com/goccy/go-yaml v1.11.0/go.mod h1:H+mJrWtjPTJAHvRbV09MCK9xYwODM+wRTVFFTWckfng= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4= github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -34,6 +43,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -57,6 +68,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d h1:uvYuEyMHKNt+lT4K3bN6fGswmK8qSvcreM3BwjDh+y4= google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw= @@ -65,8 +78,3 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gripmock.go b/gripmock.go index d78f0521..289407cd 100644 --- a/gripmock.go +++ b/gripmock.go @@ -12,6 +12,7 @@ import ( "strings" "syscall" + _ "github.com/tokopedia/gripmock/protogen" "github.com/tokopedia/gripmock/stub" ) diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go new file mode 100644 index 00000000..c699b9c1 --- /dev/null +++ b/pkg/sdk/client.go @@ -0,0 +1,63 @@ +package sdk + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +type StubApiClient struct { + url string + httpClient *http.Client +} + +func NewStubApiClient(url string, client *http.Client) *StubApiClient { + return &StubApiClient{url: url, httpClient: client} +} + +type Payload struct { + Service string `json:"service"` + Method string `json:"method"` + Data interface{} `json:"data"` +} + +type Response struct { + Data interface{} `json:"data"` + Error string `json:"error"` +} + +func (c *StubApiClient) Search(payload Payload) (any, error) { + postBody, err := json.Marshal(payload) + if err != nil { + return nil, err + } + + resp, err := c.httpClient.Post(c.url+"/api/stubs/search", "application/json", bytes.NewReader(postBody)) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + + return nil, fmt.Errorf(string(body)) + } + + result := new(Response) + decoder := json.NewDecoder(resp.Body) + decoder.UseNumber() + + if err := decoder.Decode(result); err != nil { + return nil, err + } + + if result.Error != "" { + return nil, fmt.Errorf(result.Error) + } + + return result.Data, nil +} diff --git a/pkg/storage/stubs.go b/pkg/storage/stubs.go new file mode 100644 index 00000000..d9e011b9 --- /dev/null +++ b/pkg/storage/stubs.go @@ -0,0 +1,131 @@ +package storage + +import ( + "errors" + "github.com/google/uuid" + "sync" +) + +var ErrServiceNotFound = errors.New("service not found") +var ErrMethodNotFound = errors.New("method not found") + +type Stub struct { + ID *uuid.UUID `json:"id,omitempty"` + Service string `json:"service"` + Method string `json:"method"` + Input Input `json:"input"` + Output Output `json:"output"` +} + +type Input struct { + Equals map[string]interface{} `json:"equals"` + Contains map[string]interface{} `json:"contains"` + Matches map[string]interface{} `json:"matches"` +} + +type Output struct { + Data map[string]interface{} `json:"data"` + Error string `json:"error"` +} + +type storage struct { + ID uuid.UUID + Input Input + Output Output +} + +type StubStorage struct { + mu sync.RWMutex + items map[string]map[string][]storage + total uint64 +} + +func New() *StubStorage { + return &StubStorage{ + items: make(map[string]map[string][]storage), + } +} + +func (r *StubStorage) Add(stubs ...*Stub) []uuid.UUID { + r.mu.Lock() + defer r.mu.Unlock() + + result := make([]uuid.UUID, 0, len(stubs)) + + for _, stub := range stubs { + if _, ok := r.items[stub.Service]; !ok { + r.items[stub.Service] = make(map[string][]storage, 1) + } + + r.items[stub.Service][stub.Method] = append(r.items[stub.Service][stub.Method], storage{ + ID: stub.GetID(), + Input: stub.Input, + Output: stub.Output, + }) + + result = append(result, stub.GetID()) + + r.total++ + } + + return result +} + +func (r *StubStorage) Delete(_ ...uuid.UUID) { + r.total-- // fixme +} + +func (r *StubStorage) Purge() { + r.mu.Lock() + defer r.mu.Unlock() + + r.items = map[string]map[string][]storage{} + r.total = 0 +} + +func (r *StubStorage) ItemsBy(service, method string) ([]storage, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + if _, ok := r.items[service]; !ok { + return nil, ErrServiceNotFound + } + + if _, ok := r.items[service][method]; !ok { + return nil, ErrMethodNotFound + } + + return r.items[service][method], nil +} + +func (r *StubStorage) Stubs() []Stub { + r.mu.RLock() + defer r.mu.RUnlock() + + results := make([]Stub, 0, r.total) + + for service, methods := range r.items { + for method, storages := range methods { + for _, datum := range storages { + results = append(results, Stub{ + ID: &datum.ID, + Service: service, + Method: method, + Input: datum.Input, + Output: datum.Output, + }) + } + } + } + + return results +} + +func (s *Stub) GetID() uuid.UUID { + if s.ID == nil { + id := uuid.New() + s.ID = &id + } + + return *s.ID +} diff --git a/pkg/template/engine.go b/pkg/template/engine.go new file mode 100644 index 00000000..c334664a --- /dev/null +++ b/pkg/template/engine.go @@ -0,0 +1,61 @@ +package template + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "text/template" + + "github.com/google/uuid" +) + +type Engine struct{} + +func New() *Engine { + return &Engine{} +} + +func (e *Engine) Execute(name string, data []byte) ([]byte, error) { + var buffer bytes.Buffer + + parse, err := template.New(name).Funcs(e.funcMap()).Parse(string(data)) + if err != nil { + return nil, err + } + + if err := parse.Execute(&buffer, nil); err != nil { + return nil, err + } + + return buffer.Bytes(), nil +} + +func (e *Engine) funcMap() template.FuncMap { + return template.FuncMap{ + "base64StdEncoding": func(str string) string { + return base64.StdEncoding.EncodeToString([]byte(str)) + }, + "uuidToHighLowLittleEndian": func(guid string) string { + v := uuid.MustParse(guid) + + high := int64(v[0]) | int64(v[1])<<8 | int64(v[2])<<16 | int64(v[3])<<24 | + int64(v[4])<<32 | int64(v[5])<<40 | int64(v[6])<<48 | int64(v[7])<<56 + + low := int64(v[8]) | int64(v[9])<<8 | int64(v[10])<<16 | int64(v[11])<<24 | + int64(v[12])<<32 | int64(v[13])<<40 | int64(v[14])<<48 | int64(v[15])<<56 + + var buffer bytes.Buffer + + err := json.NewEncoder(&buffer).Encode(map[string]int64{ + "high": high, + "low": low, + }) + + if err != nil { + return guid + } + + return buffer.String() + }, + } +} diff --git a/pkg/yaml2json/template.go b/pkg/yaml2json/template.go new file mode 100644 index 00000000..3e4dc41f --- /dev/null +++ b/pkg/yaml2json/template.go @@ -0,0 +1,23 @@ +package yaml2json + +import ( + "github.com/goccy/go-yaml" + "github.com/tokopedia/gripmock/pkg/template" +) + +type Convertor struct { + engine *template.Engine +} + +func New() *Convertor { + return &Convertor{engine: template.New()} +} + +func (t *Convertor) Execute(name string, data []byte) ([]byte, error) { + bytes, err := t.engine.Execute(name, data) + if err != nil { + return nil, err + } + + return yaml.YAMLToJSON(bytes) +} diff --git a/protoc-gen-gripmock/generator.go b/protoc-gen-gripmock/generator.go index bc906c06..1ef59c57 100644 --- a/protoc-gen-gripmock/generator.go +++ b/protoc-gen-gripmock/generator.go @@ -15,7 +15,7 @@ import ( "golang.org/x/tools/imports" "google.golang.org/protobuf/compiler/protogen" "google.golang.org/protobuf/proto" - descriptor "google.golang.org/protobuf/types/descriptorpb" + "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/pluginpb" ) @@ -40,7 +40,7 @@ func main() { plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) - protos := make([]*descriptor.FileDescriptorProto, len(plugin.Files)) + protos := make([]*descriptorpb.FileDescriptorProto, len(plugin.Files)) for index, file := range plugin.Files { protos[index] = file.Proto } @@ -129,7 +129,7 @@ func init() { ServerTemplate = string(data) } -func generateServer(protos []*descriptor.FileDescriptorProto, opt *Options) error { +func generateServer(protos []*descriptorpb.FileDescriptorProto, opt *Options) error { services := extractServices(protos) deps := resolveDependencies(protos) @@ -171,7 +171,7 @@ func generateServer(protos []*descriptor.FileDescriptorProto, opt *Options) erro return err } -func resolveDependencies(protos []*descriptor.FileDescriptorProto) map[string]string { +func resolveDependencies(protos []*descriptorpb.FileDescriptorProto) map[string]string { deps := map[string]string{} for _, proto := range protos { @@ -196,7 +196,7 @@ var aliases = map[string]bool{} var aliasNum = 1 var packages = map[string]string{} -func getGoPackage(proto *descriptor.FileDescriptorProto) (alias string, goPackage string) { +func getGoPackage(proto *descriptorpb.FileDescriptorProto) (alias string, goPackage string) { goPackage = proto.GetOptions().GetGoPackage() if goPackage == "" { return @@ -239,8 +239,8 @@ func getGoPackage(proto *descriptor.FileDescriptorProto) (alias string, goPackag } // change the structure also translate method type -func extractServices(protos []*descriptor.FileDescriptorProto) []Service { - svcTmp := []Service{} +func extractServices(protos []*descriptorpb.FileDescriptorProto) []Service { + var svcTmp []Service title := cases.Title(language.English, cases.NoLower) for _, proto := range protos { for _, svc := range proto.GetService() { @@ -277,7 +277,7 @@ func extractServices(protos []*descriptor.FileDescriptorProto) []Service { return svcTmp } -func getMessageType(protos []*descriptor.FileDescriptorProto, tipe string) string { +func getMessageType(protos []*descriptorpb.FileDescriptorProto, tipe string) string { split := strings.Split(tipe, ".")[1:] targetPackage := strings.Join(split[:len(split)-1], ".") targetType := split[len(split)-1] diff --git a/protoc-gen-gripmock/server.tmpl b/protoc-gen-gripmock/server.tmpl index 1eebb96f..6267cc77 100644 --- a/protoc-gen-gripmock/server.tmpl +++ b/protoc-gen-gripmock/server.tmpl @@ -16,6 +16,8 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/reflection" "google.golang.org/protobuf/reflect/protoreflect" + + "github.com/tokopedia/gripmock/pkg/sdk" ) {{ range $package, $alias := .Dependencies }} import {{$alias}} "{{$package}}" @@ -149,40 +151,21 @@ type response struct { } func findStub(service, method string, in, out protoreflect.ProtoMessage) error { - url := fmt.Sprintf("http://localhost%s/find", HTTP_PORT) - pyl := payload{ - Service: service, - Method: method, - Data: in, - } - byt, err := json.Marshal(pyl) - if err != nil { - return err - } - reader := bytes.NewReader(byt) - resp, err := http.DefaultClient.Post(url, "application/json", reader) - if err != nil { - return fmt.Errorf("Error request to stub server %v",err) - } - - if resp.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(resp.Body) - return fmt.Errorf(string(body)) - } - - respRPC := new(response) - decoder := json.NewDecoder(resp.Body) - decoder.UseNumber() - err = decoder.Decode(respRPC) + api := sdk.NewStubApiClient(fmt.Sprintf("http://localhost%s", HTTP_PORT), http.DefaultClient) + resp, err := api.Search(sdk.Payload{ + Service: service, + Method: method, + Data: in, + }) + if err != nil { + return err + } + + data, err := json.Marshal(resp) if err != nil { - return fmt.Errorf("decoding json response %v",err) - } - - if respRPC.Error != "" { - return fmt.Errorf(respRPC.Error) + return err } - data, _ := json.Marshal(respRPC.Data) return jsonpb.Unmarshal(data, out) } {{ end }} \ No newline at end of file diff --git a/protogen/empty.go b/protogen/empty.go new file mode 100644 index 00000000..356b3890 --- /dev/null +++ b/protogen/empty.go @@ -0,0 +1 @@ +package protogen diff --git a/protogen/example/empty.go b/protogen/example/empty.go new file mode 100644 index 00000000..975dd7b4 --- /dev/null +++ b/protogen/example/empty.go @@ -0,0 +1,11 @@ +package example + +import ( + _ "google.golang.org/grpc" + _ "google.golang.org/grpc/codes" + _ "google.golang.org/grpc/status" + _ "google.golang.org/protobuf/reflect/protoreflect" + _ "google.golang.org/protobuf/runtime/protoimpl" + _ "google.golang.org/protobuf/types/known/apipb" + _ "google.golang.org/protobuf/types/known/emptypb" +) diff --git a/stub/actions.go b/stub/actions.go new file mode 100644 index 00000000..8571d576 --- /dev/null +++ b/stub/actions.go @@ -0,0 +1,150 @@ +package stub + +import ( + "bytes" + "encoding/json" + "log" + "net/http" + "os" + "strings" + + "github.com/tokopedia/gripmock/pkg/storage" + "github.com/tokopedia/gripmock/pkg/yaml2json" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type Handler struct { + stubs *storage.StubStorage + convertor *yaml2json.Convertor +} + +type findStubPayload struct { + Service string `json:"service"` + Method string `json:"method"` + Data map[string]interface{} `json:"data"` +} + +func NewHandler() *Handler { + return &Handler{stubs: storage.New(), convertor: yaml2json.New()} +} + +func (h *Handler) searchHandle(w http.ResponseWriter, r *http.Request) { + stub := new(findStubPayload) + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + + if err := decoder.Decode(stub); err != nil { + h.responseError(err, w) + return + } + + defer r.Body.Close() + + // due to golang implementation + // method name must capital + title := cases.Title(language.English, cases.NoLower) + stub.Method = title.String(stub.Method) + + output, err := findStub(h.stubs, stub) + if err != nil { + log.Println(err) + h.responseError(err, w) + return + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(output) +} + +func (h *Handler) purgeHandle(w http.ResponseWriter, _ *http.Request) { + h.stubs.Purge() + w.WriteHeader(204) +} + +func (h *Handler) listHandle(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(h.stubs.Stubs()) + if err != nil { + h.responseError(err, w) + return + } +} + +func (h *Handler) addHandle(w http.ResponseWriter, r *http.Request) { + // todo: add supported input array + stub := new(storage.Stub) + decoder := json.NewDecoder(r.Body) + decoder.UseNumber() + + if err := decoder.Decode(stub); err != nil { + h.responseError(err, w) + return + } + + defer r.Body.Close() + + if err := validateStub(stub); err != nil { + h.responseError(err, w) + return + } + + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(h.stubs.Add(stub)) + if err != nil { + h.responseError(err, w) + return + } +} + +func (h *Handler) responseError(err error, w http.ResponseWriter) { + w.WriteHeader(500) + + _, _ = w.Write([]byte(err.Error())) +} + +func (h *Handler) readStubs(path string) { + files, err := os.ReadDir(path) + if err != nil { + log.Printf("Can't read stub from %s. %v\n", path, err) + return + } + + for _, file := range files { + if file.IsDir() { + h.readStubs(path + "/" + file.Name()) + continue + } + + byt, err := os.ReadFile(path + "/" + file.Name()) + if err != nil { + log.Printf("Error when reading file %s. %v. skipping...", file.Name(), err) + continue + } + + byt = bytes.TrimSpace(byt) + + if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") { + byt, err = h.convertor.Execute(file.Name(), byt) + if err != nil { + log.Printf("Error when unmarshalling file %s. %v. skipping...", file.Name(), err) + continue + } + } + + if byt[0] == '{' && byt[len(byt)-1] == '}' { + byt = []byte("[" + string(byt) + "]") + } + + var stubs []*storage.Stub + decoder := json.NewDecoder(bytes.NewReader(byt)) + decoder.UseNumber() + + if err = decoder.Decode(&stubs); err != nil { + log.Printf("Error when unmarshalling file %s. %v %v. skipping...", file.Name(), string(byt), err) + continue + } + + h.stubs.Add(stubs...) + } +} diff --git a/stub/storage.go b/stub/storage.go index f2dff4dd..33a35e6b 100644 --- a/stub/storage.go +++ b/stub/storage.go @@ -1,97 +1,58 @@ package stub import ( - "bytes" "encoding/json" + "errors" "fmt" "log" - "os" "reflect" "regexp" - "sync" "github.com/lithammer/fuzzysearch/fuzzy" + "github.com/tokopedia/gripmock/pkg/storage" ) -var mx = sync.Mutex{} - -// below represent map[servicename][methodname][]expectations -type stubMapping map[string]map[string][]storage - type matchFunc func(interface{}, interface{}) bool -var stubStorage = stubMapping{} - -type storage struct { - Input Input - Output Output -} - -func storeStub(stub *Stub) error { - return stubStorage.storeStub(stub) -} - -func (sm *stubMapping) storeStub(stub *Stub) error { - mx.Lock() - defer mx.Unlock() - - if (*sm)[stub.Service] == nil { - (*sm)[stub.Service] = make(map[string][]storage) - } - (*sm)[stub.Service][stub.Method] = append((*sm)[stub.Service][stub.Method], storage{ - Input: stub.Input, - Output: stub.Output, - }) - return nil -} - -func allStub() stubMapping { - mx.Lock() - defer mx.Unlock() - return stubStorage -} - type closeMatch struct { rule string expect map[string]interface{} } -func findStub(stub *findStubPayload) (*Output, error) { - mx.Lock() - defer mx.Unlock() - if _, ok := stubStorage[stub.Service]; !ok { +func findStub(stubStorage *storage.StubStorage, stub *findStubPayload) (*storage.Output, error) { + stubs, err := stubStorage.ItemsBy(stub.Service, stub.Method) + if errors.Is(err, storage.ErrServiceNotFound) { return nil, fmt.Errorf("can't find stub for Service: %s", stub.Service) } - if _, ok := stubStorage[stub.Service][stub.Method]; !ok { + if errors.Is(err, storage.ErrMethodNotFound) { return nil, fmt.Errorf("can't find stub for Service:%s and Method:%s", stub.Service, stub.Method) } - stubs := stubStorage[stub.Service][stub.Method] if len(stubs) == 0 { return nil, fmt.Errorf("stub for Service:%s and Method:%s is empty", stub.Service, stub.Method) } var closestMatch []closeMatch - for _, stubrange := range stubs { - if expect := stubrange.Input.Equals; expect != nil { + for _, strange := range stubs { + if expect := strange.Input.Equals; expect != nil { closestMatch = append(closestMatch, closeMatch{"equals", expect}) if equals(stub.Data, expect) { - return &stubrange.Output, nil + return &strange.Output, nil } } - if expect := stubrange.Input.Contains; expect != nil { + if expect := strange.Input.Contains; expect != nil { closestMatch = append(closestMatch, closeMatch{"contains", expect}) - if contains(stubrange.Input.Contains, stub.Data) { - return &stubrange.Output, nil + if contains(strange.Input.Contains, stub.Data) { + return &strange.Output, nil } } - if expect := stubrange.Input.Matches; expect != nil { + if expect := strange.Input.Matches; expect != nil { closestMatch = append(closestMatch, closeMatch{"matches", expect}) - if matches(stubrange.Input.Matches, stub.Data) { - return &stubrange.Output, nil + if matches(strange.Input.Matches, stub.Data) { + return &strange.Output, nil } } } @@ -101,8 +62,12 @@ func findStub(stub *findStubPayload) (*Output, error) { func stubNotFoundError(stub *findStubPayload, closestMatches []closeMatch) error { template := fmt.Sprintf("Can't find stub \n\nService: %s \n\nMethod: %s \n\nInput\n\n", stub.Service, stub.Method) - expectString := renderFieldAsString(stub.Data) - template += expectString + expectString, err := json.MarshalIndent(stub.Data, "", "\t") + if err != nil { + return err + } + + template += string(expectString) if len(closestMatches) == 0 { return fmt.Errorf(template) @@ -113,7 +78,7 @@ func stubNotFoundError(stub *findStubPayload, closestMatches []closeMatch) error match closeMatch }{0, closeMatch{}} for _, closeMatchValue := range closestMatches { - rank := rankMatch(expectString, closeMatchValue.expect) + rank := rankMatch(string(expectString), closeMatchValue.expect) // the higher the better if rank > highestRank.rank { @@ -129,7 +94,11 @@ func stubNotFoundError(stub *findStubPayload, closestMatches []closeMatch) error closestMatch = highestRank.match } - closestMatchString := renderFieldAsString(closestMatch.expect) + closestMatchString, err := json.MarshalIndent(closestMatch.expect, "", "\t") + if err != nil { + return err + } + template += fmt.Sprintf("\n\nClosest Match \n\n%s:%s", closestMatch.rule, closestMatchString) return fmt.Errorf(template) @@ -157,19 +126,6 @@ func rankMatch(expect string, closeMatch map[string]interface{}) float32 { return float32(occurrence) / float32(totalFields) } -func renderFieldAsString(fields map[string]interface{}) string { - template := "{\n" - for key, val := range fields { - template += fmt.Sprintf("\t%s: %v\n", key, val) - } - template += "}" - return template -} - -func deepEqual(expect, actual interface{}) bool { - return reflect.DeepEqual(expect, actual) -} - func regexMatch(expect, actual interface{}) bool { var expectedStr, expectedStringOk = expect.(string) var actualStr, actualStringOk = actual.(string) @@ -182,15 +138,15 @@ func regexMatch(expect, actual interface{}) bool { return match } - return deepEqual(expect, actual) + return reflect.DeepEqual(expect, actual) } func equals(expect, actual map[string]interface{}) bool { - return find(expect, actual, true, true, deepEqual) + return find(expect, actual, true, true, reflect.DeepEqual) } func contains(expect, actual map[string]interface{}) bool { - return find(expect, actual, true, false, deepEqual) + return find(expect, actual, true, false, reflect.DeepEqual) } func matches(expect, actual map[string]interface{}) bool { @@ -264,61 +220,3 @@ func find(expect, actual interface{}, acc, exactMatch bool, f matchFunc) bool { return f(expect, actual) } - -func clearStorage() { - mx.Lock() - defer mx.Unlock() - - stubStorage = stubMapping{} -} - -func readStubFromFile(path string) { - stubStorage.readStubFromFile(path) -} - -func (sm *stubMapping) readStubFromFile(path string) { - files, err := os.ReadDir(path) - if err != nil { - log.Printf("Can't read stub from %s. %v\n", path, err) - return - } - - for _, file := range files { - if file.IsDir() { - readStubFromFile(path + "/" + file.Name()) - continue - } - - byt, err := os.ReadFile(path + "/" + file.Name()) - if err != nil { - log.Printf("Error when reading file %s. %v. skipping...", file.Name(), err) - continue - } - - if byt[0] == '[' && byt[len(byt)-1] == ']' { - var stubs []*Stub - decoder := json.NewDecoder(bytes.NewReader(byt)) - decoder.UseNumber() - - if err = decoder.Decode(&stubs); err != nil { - log.Printf("Error when unmarshalling file %s. %v. skipping...", file.Name(), err) - continue - } - for _, s := range stubs { - sm.storeStub(s) - } - continue - } - - stub := new(Stub) - decoder := json.NewDecoder(bytes.NewReader(byt)) - decoder.UseNumber() - - if err = decoder.Decode(stub); err != nil { - log.Printf("Error when unmarshalling file %s. %v. skipping...", file.Name(), err) - continue - } - - sm.storeStub(stub) - } -} diff --git a/stub/storage_test.go b/stub/storage_test.go deleted file mode 100644 index 1c45a972..00000000 --- a/stub/storage_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package stub - -import ( - "encoding/json" - "os" - "testing" - - "github.com/stretchr/testify/require" -) - -func Test_readStubFromFile(t *testing.T) { - tests := []struct { - name string - mock func(service, method string, data []storage) (path string) - service string - method string - data []storage - }{ - { - name: "single file, single stub", - mock: func(service, method string, data []storage) (path string) { - dir, err := os.MkdirTemp("", "") - require.NoError(t, err) - tempF, err := os.CreateTemp(dir, "") - require.NoError(t, err) - defer tempF.Close() - - var stubs []Stub - for _, d := range data { - stubs = append(stubs, Stub{ - Service: service, - Method: method, - Input: d.Input, - Output: d.Output, - }) - } - byt, err := json.Marshal(stubs) - require.NoError(t, err) - _, err = tempF.Write(byt) - require.NoError(t, err) - - return dir - }, - service: "user", - method: "getname", - data: []storage{ - { - Input: Input{Equals: map[string]interface{}{"id": json.Number("1")}}, - Output: Output{Data: map[string]interface{}{"name": "user1"}}, - }, - }, - }, - { - name: "single file, multiple stub", - mock: func(service, method string, data []storage) (path string) { - dir, err := os.MkdirTemp("", "") - require.NoError(t, err) - tempF, err := os.CreateTemp(dir, "") - require.NoError(t, err) - defer tempF.Close() - - var stubs []Stub - for _, d := range data { - stubs = append(stubs, Stub{ - Service: service, - Method: method, - Input: d.Input, - Output: d.Output, - }) - } - byt, err := json.Marshal(stubs) - require.NoError(t, err) - _, err = tempF.Write(byt) - require.NoError(t, err) - - return dir - }, - service: "user", - method: "getname", - data: []storage{ - { - Input: Input{Equals: map[string]interface{}{"id": json.Number("1")}}, - Output: Output{Data: map[string]interface{}{"name": "user1"}}, - }, - { - Input: Input{Equals: map[string]interface{}{"id": json.Number("2")}}, - Output: Output{Data: map[string]interface{}{"name": "user2"}}, - }, - }, - }, - { - name: "multiple file, single stub", - mock: func(service, method string, data []storage) (path string) { - dir, err := os.MkdirTemp("", "") - require.NoError(t, err) - - for _, d := range data { - tempF, err := os.CreateTemp(dir, "") - require.NoError(t, err) - defer tempF.Close() - - stub := Stub{ - Service: service, - Method: method, - Input: d.Input, - Output: d.Output, - } - byt, err := json.Marshal(stub) - require.NoError(t, err) - _, err = tempF.Write(byt) - require.NoError(t, err) - } - - return dir - }, - service: "user", - method: "getname", - data: []storage{ - { - Input: Input{Equals: map[string]interface{}{"id": json.Number("1")}}, - Output: Output{Data: map[string]interface{}{"name": "user1"}}, - }, - { - Input: Input{Equals: map[string]interface{}{"id": json.Number("2")}}, - Output: Output{Data: map[string]interface{}{"name": "user2"}}, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - sm := stubMapping{} - sm.readStubFromFile(tt.mock(tt.service, tt.method, tt.data)) - require.ElementsMatch(t, tt.data, sm[tt.service][tt.method]) - }) - } -} diff --git a/stub/stub.go b/stub/stub.go index 31632857..05799410 100644 --- a/stub/stub.go +++ b/stub/stub.go @@ -1,9 +1,9 @@ package stub import ( - "encoding/json" "fmt" - "github.com/go-chi/chi" + "github.com/gorilla/mux" + "github.com/tokopedia/gripmock/pkg/storage" "golang.org/x/text/cases" "golang.org/x/text/language" "log" @@ -23,81 +23,29 @@ func RunStubServer(opt Options) { opt.Port = DefaultPort } addr := opt.BindAddr + ":" + opt.Port - r := chi.NewRouter() - r.Post("/add", addStub) - r.Get("/", listStub) - r.Post("/find", handleFindStub) - r.Get("/clear", handleClearStub) + api := NewHandler() if opt.StubPath != "" { - readStubFromFile(opt.StubPath) + api.readStubs(opt.StubPath) } + router := mux.NewRouter() + + apiRouter := router.PathPrefix("/api").Subrouter() + apiRouter.HandleFunc("/stubs/search", api.searchHandle).Methods("POST") + apiRouter.HandleFunc("/stubs", api.listHandle).Methods("GET") + apiRouter.HandleFunc("/stubs", api.addHandle).Methods("POST") + apiRouter.HandleFunc("/stubs", api.purgeHandle).Methods("DELETE") + fmt.Println("Serving stub admin on http://" + addr) go func() { - err := http.ListenAndServe(addr, r) + http.Handle("/", router) + err := http.ListenAndServe(addr, nil) log.Fatal(err) }() } -func responseError(err error, w http.ResponseWriter) { - w.WriteHeader(500) - w.Write([]byte(err.Error())) -} - -type Stub struct { - Service string `json:"service"` - Method string `json:"method"` - Input Input `json:"input"` - Output Output `json:"output"` -} - -type Input struct { - Equals map[string]interface{} `json:"equals"` - Contains map[string]interface{} `json:"contains"` - Matches map[string]interface{} `json:"matches"` -} - -type Output struct { - Data map[string]interface{} `json:"data"` - Error string `json:"error"` -} - -func addStub(w http.ResponseWriter, r *http.Request) { - stub := new(Stub) - decoder := json.NewDecoder(r.Body) - decoder.UseNumber() - - if err := decoder.Decode(stub); err != nil { - responseError(err, w) - return - } - - defer r.Body.Close() - - if err := validateStub(stub); err != nil { - responseError(err, w) - return - } - - if err := storeStub(stub); err != nil { - responseError(err, w) - return - } - - _, err := w.Write([]byte("Success add stub")) - if err != nil { - responseError(err, w) - return - } -} - -func listStub(w http.ResponseWriter, _ *http.Request) { - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(allStub()) -} - -func validateStub(stub *Stub) error { +func validateStub(stub *storage.Stub) error { if stub.Service == "" { return fmt.Errorf("service name can't be empty") } @@ -127,44 +75,6 @@ func validateStub(stub *Stub) error { if stub.Output.Error == "" && stub.Output.Data == nil { return fmt.Errorf("output can't be empty") } - return nil -} - -type findStubPayload struct { - Service string `json:"service"` - Method string `json:"method"` - Data map[string]interface{} `json:"data"` -} - -func handleFindStub(w http.ResponseWriter, r *http.Request) { - stub := new(findStubPayload) - decoder := json.NewDecoder(r.Body) - decoder.UseNumber() - - if err := decoder.Decode(stub); err != nil { - responseError(err, w) - return - } - - defer r.Body.Close() - - // due to golang implementation - // method name must capital - title := cases.Title(language.English, cases.NoLower) - stub.Method = title.String(stub.Method) - - output, err := findStub(stub) - if err != nil { - log.Println(err) - responseError(err, w) - return - } - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(output) -} - -func handleClearStub(w http.ResponseWriter, r *http.Request) { - clearStorage() - w.Write([]byte("OK")) + return nil } diff --git a/stub/stub_test.go b/stub/stub_test.go deleted file mode 100644 index 637af7a7..00000000 --- a/stub/stub_test.go +++ /dev/null @@ -1,317 +0,0 @@ -package stub - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestStub(t *testing.T) { - type test struct { - name string - mock func() *http.Request - handler http.HandlerFunc - expect string - } - - cases := []test{ - { - name: "add simple stub", - mock: func() *http.Request { - payload := `{ - "service": "Testing", - "method":"TestMethod", - "input":{ - "equals":{ - "Hola":"Mundo" - } - }, - "output":{ - "data":{ - "Hello":"World" - } - } - }` - read := bytes.NewReader([]byte(payload)) - return httptest.NewRequest("POST", "/add", read) - }, - handler: addStub, - expect: `Success add stub`, - }, - { - name: "list stub", - mock: func() *http.Request { - return httptest.NewRequest("GET", "/", nil) - }, - handler: listStub, - expect: "{\"Testing\":{\"TestMethod\":[{\"Input\":{\"equals\":{\"Hola\":\"Mundo\"},\"contains\":null,\"matches\":null},\"Output\":{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}}]}}\n", - }, - { - name: "find stub equals", - mock: func() *http.Request { - payload := `{"service":"Testing","method":"TestMethod","data":{"Hola":"Mundo"}}` - return httptest.NewRequest("POST", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}\n", - }, - { - name: "add nested stub equals", - mock: func() *http.Request { - payload := `{ - "service": "NestedTesting", - "method":"TestMethod", - "input":{ - "equals":{ - "name": "Afra Gokce", - "age": 1, - "girl": true, - "null": null, - "greetings": { - "hola": "mundo", - "merhaba": "dunya" - }, - "cities": ["Istanbul", "Jakarta"] - } - }, - "output":{ - "data":{ - "Hello":"World" - } - } - }` - read := bytes.NewReader([]byte(payload)) - return httptest.NewRequest("POST", "/add", read) - }, - handler: addStub, - expect: `Success add stub`, - }, - { - name: "find nested stub equals", - mock: func() *http.Request { - payload := `{"service":"NestedTesting","method":"TestMethod","data":{"name":"Afra Gokce","age":1,"girl":true,"null":null,"greetings":{"hola":"mundo","merhaba":"dunya"},"cities":["Istanbul","Jakarta"]}}` - return httptest.NewRequest("POST", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"Hello\":\"World\"},\"error\":\"\"}\n", - }, - { - name: "add stub contains", - mock: func() *http.Request { - payload := `{ - "service": "Testing", - "method":"TestMethod", - "input":{ - "contains":{ - "field1":"hello field1", - "field3":"hello field3" - } - }, - "output":{ - "data":{ - "hello":"world" - } - } - }` - return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload))) - }, - handler: addStub, - expect: `Success add stub`, - }, - { - name: "find stub contains", - mock: func() *http.Request { - payload := `{ - "service":"Testing", - "method":"TestMethod", - "data":{ - "field1":"hello field1", - "field2":"hello field2", - "field3":"hello field3" - } - }` - return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n", - }, - { - name: "add nested stub contains", - mock: func() *http.Request { - payload := `{ - "service": "NestedTesting", - "method":"TestMethod", - "input":{ - "contains":{ - "key": "value", - "greetings": { - "hola": "mundo", - "merhaba": "dunya" - }, - "cities": ["Istanbul", "Jakarta"] - } - }, - "output":{ - "data":{ - "hello":"world" - } - } - }` - return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload))) - }, - handler: addStub, - expect: `Success add stub`, - }, - { - name: "find nested stub contains", - mock: func() *http.Request { - payload := `{ - "service":"NestedTesting", - "method":"TestMethod", - "data":{ - "key": "value", - "anotherKey": "anotherValue", - "greetings": { - "hola": "mundo", - "merhaba": "dunya", - "hello": "world" - }, - "cities": ["Istanbul", "Jakarta", "Winterfell"] - } - }` - return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"hello\":\"world\"},\"error\":\"\"}\n", - }, - { - name: "add stub matches regex", - mock: func() *http.Request { - payload := `{ - "service":"Testing2", - "method":"TestMethod", - "input":{ - "matches":{ - "field1":".*ello$" - } - }, - "output":{ - "data":{ - "reply":"OK" - } - } - }` - return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload))) - }, - handler: addStub, - expect: "Success add stub", - }, - { - name: "find stub matches regex", - mock: func() *http.Request { - payload := `{ - "service":"Testing2", - "method":"TestMethod", - "data":{ - "field1":"hello" - } - }` - return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"reply\":\"OK\"},\"error\":\"\"}\n", - }, - { - name: "add nested stub matches regex", - mock: func() *http.Request { - payload := `{ - "service":"NestedTesting2", - "method":"TestMethod", - "input":{ - "matches":{ - "key": "[a-z]{3}ue", - "greetings": { - "hola": 1, - "merhaba": true, - "hello": "^he[l]{2,}o$" - }, - "cities": ["Istanbul", "Jakarta", ".*"], - "mixed": [5.5, false, ".*"] - } - }, - "output":{ - "data":{ - "reply":"OK" - } - } - }` - return httptest.NewRequest("POST", "/add", bytes.NewReader([]byte(payload))) - }, - handler: addStub, - expect: "Success add stub", - }, - { - name: "find nested stub matches regex", - mock: func() *http.Request { - payload := `{ - "service":"NestedTesting2", - "method":"TestMethod", - "data":{ - "key": "value", - "greetings": { - "hola": 1, - "merhaba": true, - "hello": "helllllo" - }, - "cities": ["Istanbul", "Jakarta", "Gotham"], - "mixed": [5.5, false, "Gotham"] - } - } - }` - return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "{\"data\":{\"reply\":\"OK\"},\"error\":\"\"}\n", - }, - { - name: "error find stub contains", - mock: func() *http.Request { - payload := `{ - "service":"Testing", - "method":"TestMethod", - "data":{ - "field1":"hello field1" - } - }` - return httptest.NewRequest("GET", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "Can't find stub \n\nService: Testing \n\nMethod: TestMethod \n\nInput\n\n{\n\tfield1: hello field1\n}\n\nClosest Match \n\ncontains:{\n\tfield1: hello field1\n\tfield3: hello field3\n}", - }, - { - name: "error find stub equals", - mock: func() *http.Request { - payload := `{"service":"Testing","method":"TestMethod","data":{"Hola":"Dunia"}}` - return httptest.NewRequest("POST", "/find", bytes.NewReader([]byte(payload))) - }, - handler: handleFindStub, - expect: "Can't find stub \n\nService: Testing \n\nMethod: TestMethod \n\nInput\n\n{\n\tHola: Dunia\n}\n\nClosest Match \n\nequals:{\n\tHola: Mundo\n}", - }, - } - - for _, v := range cases { - t.Run(v.name, func(t *testing.T) { - wrt := httptest.NewRecorder() - req := v.mock() - v.handler(wrt, req) - res, err := io.ReadAll(wrt.Result().Body) - - assert.NoError(t, err) - assert.Equal(t, v.expect, string(res)) - }) - } -}