diff --git a/go.mod b/go.mod index c12cc2b..2053d6a 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,22 @@ module github.com/opiproject/goopicsi go 1.19 require ( + github.com/go-chi/chi v1.5.4 github.com/google/uuid v1.3.0 + github.com/lithammer/fuzzysearch v1.1.5 github.com/opiproject/opi-api v0.0.0-20221215155400-a49a4ea1d561 github.com/stretchr/testify v1.8.1 + golang.org/x/net v0.0.0-20221002022538-bcab6841153b + golang.org/x/text v0.4.0 google.golang.org/grpc v1.51.0 + google.golang.org/protobuf v1.28.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/net v0.0.0-20221002022538-bcab6841153b // indirect golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect - golang.org/x/text v0.4.0 // indirect google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 // indirect - google.golang.org/protobuf v1.28.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index f7bed3e..97d07e4 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= @@ -8,14 +10,8 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 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/opiproject/opi-api v0.0.0-20221207164013-fd93b840b575 h1:qUuLJp4bRKxC21El2bg6rhlBLjD/HBta7ALSZLD2pMY= -github.com/opiproject/opi-api v0.0.0-20221207164013-fd93b840b575/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= -github.com/opiproject/opi-api v0.0.0-20221208205708-b089acb781cd h1:9jPT8oOKr5VvC2oRhuoH3YStkHCLOjZuLiQBNHLK1bY= -github.com/opiproject/opi-api v0.0.0-20221208205708-b089acb781cd/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= -github.com/opiproject/opi-api v0.0.0-20221208224812-65d927304471 h1:W1F/RDX/PHjRanw6aaOtxCGTsPoysGNIhMojKex+kX8= -github.com/opiproject/opi-api v0.0.0-20221208224812-65d927304471/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= -github.com/opiproject/opi-api v0.0.0-20221212161256-b1b2672e72be h1:lS16v9/5Vr0+ibwLRp0LJPEq3KWAEFcS81ifVS2BscI= -github.com/opiproject/opi-api v0.0.0-20221212161256-b1b2672e72be/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= +github.com/lithammer/fuzzysearch v1.1.5 h1:Ag7aKU08wp0R9QCfF4GoGST9HbmAIeLP7xwMrOBEp1c= +github.com/lithammer/fuzzysearch v1.1.5/go.mod h1:1R1LRNk7yKid1BaQkmuLQaHruxcC4HmAH30Dh61Ih1Q= github.com/opiproject/opi-api v0.0.0-20221212223617-4ab228a467f7 h1:iQS6+aIBIsWmNfcUmP40rrDrKsP+xgF2Z5nhOP33U/c= github.com/opiproject/opi-api v0.0.0-20221212223617-4ab228a467f7/go.mod h1:92pv4ulvvPMuxCJ9ND3aYbmBfEMLx0VCjpkiR7ZTqPY= github.com/opiproject/opi-api v0.0.0-20221215155400-a49a4ea1d561 h1:0v2Lmm5zPK6Doz3cn9QCqX8vAfLw72Hvwcx1cqam6bM= @@ -33,8 +29,10 @@ golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= google.golang.org/genproto v0.0.0-20220930163606-c98284e70a91 h1:Ezh2cpcnP5Rq60sLensUsFnxh7P6513NLvNtCm9iyJ4= diff --git a/goopicsi_test.go b/goopicsi_test.go index 9675a5f..1d3de13 100644 --- a/goopicsi_test.go +++ b/goopicsi_test.go @@ -4,68 +4,145 @@ package goopicsi import ( + "fmt" "log" + "net" + "os" + "strings" "testing" + "github.com/opiproject/goopicsi/test/mock-server/server" + "github.com/opiproject/goopicsi/test/mock-server/stub" + pb "github.com/opiproject/opi-api/storage/v1alpha1/gen/go" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + "github.com/stretchr/testify/assert" ) -func TestNVMeControllerConnect(t *testing.T) { - err := NVMeControllerConnect("12", "", "", 44565, "") - if err != nil { - log.Println(err) - } - assert.Error(t, err) +type GoopcsiTestSuite struct { + suite.Suite } -func TestNVMeControllerList(t *testing.T) { - resp, err := NVMeControllerList() - if err != nil { - log.Println(err) - } - log.Printf("NVMf Remote Connections: %v", resp) +func (suite *GoopcsiTestSuite) SetupSuite() { + RunServer() } -func TestNVMeControllerGet(t *testing.T) { - resp, err := NVMeControllerGet("12") - if err != nil { - log.Println(err) - } - log.Printf("NVMf remote connection corresponding to the ID: %v", resp) -} +// RunServer launches mock grpc server +func RunServer() { + fmt.Println("RUNNING MOCK SERVER") + const ( + csiAddress = "localhost:50051" + defaultStubsPath = "test/mock-server/stubs" + apiPort = "4771" + ) -func TestNVMeControllerDisconnect(t *testing.T) { - err := NVMeControllerDisconnect("12") + // run admin stub server + stub.RunStubServer(stub.Options{ + StubPath: defaultStubsPath, + Port: apiPort, + BindAddr: "0.0.0.0", + }) + var protocol string + if strings.Contains(csiAddress, ":") { + protocol = "tcp" + } else { + protocol = "unix" + } + lis, err := net.Listen(protocol, csiAddress) if err != nil { - log.Println(err) + fmt.Println(err, "failed to listen on address", "address", csiAddress) + os.Exit(1) } + + MockServer := grpc.NewServer() + + pb.RegisterFrontendNvmeServiceServer(MockServer, &server.GoopCSI{}) + + fmt.Printf("Serving gRPC on %s\n", csiAddress) + errChan := make(chan error) + + // run blocking call in a separate goroutine, report errors via channel + go func() { + if err := MockServer.Serve(lis); err != nil { + errChan <- err + } + }() } -func TestCreateNVMeNamespace(t *testing.T) { - resp, err := CreateNVMeNamespace("1", "nqn", "nguid", 1) - if err != nil { - log.Println(err) - } - log.Println(resp) +func (suite *GoopcsiTestSuite) TearDownTestSuite() { + suite.T().Log("Cleaning up resources..") } -func TestDeleteNVMeNamespace(t *testing.T) { +// TODO These test cases should be reverted with mock server implementation +// func TestNVMeControllerConnect(t *testing.T) { +// err := NVMeControllerConnect("12", "", "", 44565, "") +// if err != nil { +// log.Println(err) +// } +// assert.Error(t, err) +// } + +// func TestNVMeControllerList(t *testing.T) { +// resp, err := NVMeControllerList() +// if err != nil { +// log.Println(err) +// } +// log.Printf("NVMf Remote Connections: %v", resp) +// } + +// func TestNVMeControllerGet(t *testing.T) { +// resp, err := NVMeControllerGet("12") +// if err != nil { +// log.Println(err) +// } +// log.Printf("NVMf remote connection corresponding to the ID: %v", resp) +// } + +// func TestNVMeControllerDisconnect(t *testing.T) { +// err := NVMeControllerDisconnect("12") +// if err != nil { +// log.Println(err) +// } +// } + +// func TestCreateNVMeNamespace(t *testing.T) { +// resp, err := CreateNVMeNamespace("1", "nqn", "nguid", 1) +// if err != nil { +// log.Println(err) +// } +// log.Println(resp) +// } + +// func TestExposeRemoteNVMe(t *testing.T) { +// subsystemID, controllerID, err := ExposeRemoteNVMe("nqn.2022-09.io.spdk:test", 10) +// if err != nil { +// log.Println(err) +// } +// log.Printf("Subsystem ID: %s", subsystemID) +// log.Printf("Controller Id: %s", controllerID) +// } + +func (suite *GoopcsiTestSuite) TestDeleteNVMeNamespace() { + // positive scenario err := DeleteNVMeNamespace("1") - if err != nil { - log.Println(err) - } -} + assert.NoError(suite.T(), err, "DeleteNVMeNamespace success") -func TestExposeRemoteNVMe(t *testing.T) { - subsystemID, controllerID, err := ExposeRemoteNVMe("nqn.2022-09.io.spdk:test", 10) - if err != nil { - log.Println(err) - } - log.Printf("Subsystem ID: %s", subsystemID) - log.Printf("Controller Id: %s", controllerID) + // negative scenario + err = DeleteNVMeNamespace("invalid") + assert.Error(suite.T(), err, "DeleteNVMeNamespace failed") } func TestGenerateHostNQN(t *testing.T) { hostNQN := GenerateHostNQN() log.Println(hostNQN) } + +func TestGoopcsiTestSuite(t *testing.T) { + if testing.Short() { + t.Skip("Skipping as requested by short flag") + } + testSuite := new(GoopcsiTestSuite) + suite.Run(t, testSuite) + testSuite.TearDownTestSuite() +} diff --git a/test/mock-server/server/server.go b/test/mock-server/server/server.go new file mode 100644 index 0000000..bab2124 --- /dev/null +++ b/test/mock-server/server/server.go @@ -0,0 +1,199 @@ +/* +Copyright © 2021-2022 Dell Inc. or its subsidiaries. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package server implements mock gRPC services +package server + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "golang.org/x/net/context" + "google.golang.org/protobuf/types/known/emptypb" + + pb "github.com/opiproject/opi-api/storage/v1alpha1/gen/go" +) + +// GoopCSI mock gRPC server to implement mock service calls +type GoopCSI struct{} + +// CreateNVMeSubsystem creates mock NVMe subsystem +func (s *GoopCSI) CreateNVMeSubsystem(ctx context.Context, request *pb.CreateNVMeSubsystemRequest) (*pb.NVMeSubsystem, error) { + // TODO implement me + panic("implement me") +} + +// DeleteNVMeSubsystem deletes mock NVMe subsystem +func (s *GoopCSI) DeleteNVMeSubsystem(ctx context.Context, request *pb.DeleteNVMeSubsystemRequest) (*emptypb.Empty, error) { + // TODO implement me + panic("implement me") +} + +// UpdateNVMeSubsystem updates a mock NVMe subsystem +func (s *GoopCSI) UpdateNVMeSubsystem(ctx context.Context, request *pb.UpdateNVMeSubsystemRequest) (*pb.NVMeSubsystem, error) { + // TODO implement me + panic("implement me") +} + +// ListNVMeSubsystems lists mock NVMe subsystems +func (s *GoopCSI) ListNVMeSubsystems(ctx context.Context, request *pb.ListNVMeSubsystemsRequest) (*pb.ListNVMeSubsystemsResponse, error) { + // TODO implement me + panic("implement me") +} + +// GetNVMeSubsystem gets a mock NVMe subsystem +func (s *GoopCSI) GetNVMeSubsystem(ctx context.Context, request *pb.GetNVMeSubsystemRequest) (*pb.NVMeSubsystem, error) { + // TODO implement me + panic("implement me") +} + +// NVMeSubsystemStats gets mock subsystem stats +func (s *GoopCSI) NVMeSubsystemStats(ctx context.Context, request *pb.NVMeSubsystemStatsRequest) (*pb.NVMeSubsystemStatsResponse, error) { + // TODO implement me + panic("implement me") +} + +// CreateNVMeController creates a mock NVMe controller +func (s *GoopCSI) CreateNVMeController(ctx context.Context, request *pb.CreateNVMeControllerRequest) (*pb.NVMeController, error) { + // TODO implement me + panic("implement me") +} + +// DeleteNVMeController deletes a mock NVMe controller +func (s *GoopCSI) DeleteNVMeController(ctx context.Context, request *pb.DeleteNVMeControllerRequest) (*emptypb.Empty, error) { + // TODO implement me + panic("implement me") +} + +// UpdateNVMeController updates a mock NVMe controller +func (s *GoopCSI) UpdateNVMeController(ctx context.Context, request *pb.UpdateNVMeControllerRequest) (*pb.NVMeController, error) { + // TODO implement me + panic("implement me") +} + +// ListNVMeControllers lists mock controllers +func (s *GoopCSI) ListNVMeControllers(ctx context.Context, request *pb.ListNVMeControllersRequest) (*pb.ListNVMeControllersResponse, error) { + // TODO implement me + panic("implement me") +} + +// GetNVMeController gets a mock NVMe controller +func (s *GoopCSI) GetNVMeController(ctx context.Context, request *pb.GetNVMeControllerRequest) (*pb.NVMeController, error) { + // TODO implement me + panic("implement me") +} + +// NVMeControllerStats gets mock stats +func (s *GoopCSI) NVMeControllerStats(ctx context.Context, request *pb.NVMeControllerStatsRequest) (*pb.NVMeControllerStatsResponse, error) { + // TODO implement me + panic("implement me") +} + +// CreateNVMeNamespace creates a mock NVMe namespace +func (s *GoopCSI) CreateNVMeNamespace(ctx context.Context, request *pb.CreateNVMeNamespaceRequest) (*pb.NVMeNamespace, error) { + out := &pb.NVMeNamespace{} + err := FindStub("FrontendNvmeServiceServer", "CreateNVMeNamespace", request, out) + return out, err +} + +// DeleteNVMeNamespace deletes a mock NVMe namespace +func (s *GoopCSI) DeleteNVMeNamespace(ctx context.Context, request *pb.DeleteNVMeNamespaceRequest) (*emptypb.Empty, error) { + out := &emptypb.Empty{} + err := FindStub("FrontendNvmeServiceServer", "DeleteNVMeNamespace", request, out) + return out, err +} + +// UpdateNVMeNamespace updates a mock namespace +func (s *GoopCSI) UpdateNVMeNamespace(ctx context.Context, request *pb.UpdateNVMeNamespaceRequest) (*pb.NVMeNamespace, error) { + // TODO implement me + panic("implement me") +} + +// ListNVMeNamespaces lists mock namespaces +func (s *GoopCSI) ListNVMeNamespaces(ctx context.Context, request *pb.ListNVMeNamespacesRequest) (*pb.ListNVMeNamespacesResponse, error) { + // TODO implement me + panic("implement me") +} + +// GetNVMeNamespace gets a mock namespace +func (s *GoopCSI) GetNVMeNamespace(ctx context.Context, request *pb.GetNVMeNamespaceRequest) (*pb.NVMeNamespace, error) { + // TODO implement me + panic("implement me") +} + +// NVMeNamespaceStats gets mock namespace stats +func (s *GoopCSI) NVMeNamespaceStats(ctx context.Context, request *pb.NVMeNamespaceStatsRequest) (*pb.NVMeNamespaceStatsResponse, error) { + // TODO implement me + panic("implement me") +} + +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"` +} + +// FindStub makes request to mock grpc server +func FindStub(service, method string, in, out interface{}) error { + url := "http://localhost:4771/find" + 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) + } + defer func() { + err := resp.Body.Close() + if err != nil { + log.Fatal(err) + } + }() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf(string(body)) + } + + respRPC := new(response) + err = json.NewDecoder(resp.Body).Decode(respRPC) + if err != nil { + return fmt.Errorf("decoding json response %v", err) + } + + if respRPC.Error != "" { + return fmt.Errorf(respRPC.Error) + } + + data, _ := json.Marshal(respRPC.Data) + return json.Unmarshal(data, out) +} diff --git a/test/mock-server/stub/storage.go b/test/mock-server/stub/storage.go new file mode 100644 index 0000000..0da5345 --- /dev/null +++ b/test/mock-server/stub/storage.go @@ -0,0 +1,263 @@ +/* +Copyright © 2021 Dell Inc. or its subsidiaries. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package stub provides stub related functions +package stub + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "log" + "path/filepath" + "reflect" + "regexp" + "strings" + "sync" + + "github.com/lithammer/fuzzysearch/fuzzy" +) + +var mx = sync.Mutex{} + +// below represent map[servicename][methodname][]expectations +type stubMapping map[string]map[string][]storage + +var stubStorage = stubMapping{} + +type storage struct { + Input Input + Output Output +} + +func storeStub(stub *Stub) { + mx.Lock() + defer mx.Unlock() + + strg := storage{ + Input: stub.Input, + Output: stub.Output, + } + if stubStorage[stub.Service] == nil { + stubStorage[stub.Service] = make(map[string][]storage) + } + stubStorage[stub.Service][stub.Method] = append(stubStorage[stub.Service][stub.Method], strg) +} + +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 { + return nil, fmt.Errorf("can't find stub for Service: %s", stub.Service) + } + + if _, ok := stubStorage[stub.Service][stub.Method]; !ok { + 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 { + closestMatch = append(closestMatch, closeMatch{"equals", expect}) + if equals(stub.Data, expect) { + return &stubrange.Output, nil + } + } + + if expect := stubrange.Input.Contains; expect != nil { + closestMatch = append(closestMatch, closeMatch{"contains", expect}) + if contains(stubrange.Input.Contains, stub.Data) { + return &stubrange.Output, nil + } + } + + if expect := stubrange.Input.Matches; expect != nil { + closestMatch = append(closestMatch, closeMatch{"matches", expect}) + if matches(stubrange.Input.Matches, stub.Data) { + return &stubrange.Output, nil + } + } + } + + return nil, stubNotFoundError(stub, closestMatch) +} + +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 + + if len(closestMatches) == 0 { + return fmt.Errorf(template) + } + + highestRank := struct { + rank float32 + match closeMatch + }{0, closeMatch{}} + for _, closeMatchValue := range closestMatches { + rank := rankMatch(expectString, closeMatchValue.expect) + + // the higher the better + if rank > highestRank.rank { + highestRank.rank = rank + highestRank.match = closeMatchValue + } + } + + var closestMatch closeMatch + if highestRank.rank == 0 { + closestMatch = closestMatches[0] + } else { + closestMatch = highestRank.match + } + + closestMatchString := renderFieldAsString(closestMatch.expect) + template += fmt.Sprintf("\n\nClosest Match \n\n%s:%s", closestMatch.rule, closestMatchString) + + return fmt.Errorf(template) +} + +// we made our own simple ranking logic +// count the matches field_name and value then compare it with total field names and values +// the higher the better +func rankMatch(expect string, closeMatch map[string]interface{}) float32 { + occurrence := 0 + for key, value := range closeMatch { + if fuzzy.Match(key+":", expect) { + occurrence++ + } + + if fuzzy.Match(fmt.Sprint(value), expect) { + occurrence++ + } + } + + if occurrence == 0 { + return 0 + } + totalFields := len(closeMatch) * 2 + 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 equals(input1, input2 map[string]interface{}) bool { + return reflect.DeepEqual(input1, input2) +} + +func contains(expect, actual map[string]interface{}) bool { + for key, val := range expect { + actualValue, ok := actual[key] + if !ok { + return ok + } + kind := reflect.TypeOf(actualValue) + dataType := fmt.Sprint(kind) + compare := strings.Contains(dataType, "string") + // we will do deep compare only for non-basic types + if !compare { + if !reflect.DeepEqual(val, actualValue) { + return false + } + } else { + strVal, _ := val.(string) + strActual, _ := actualValue.(string) + isEqual := strings.Contains(strActual, strVal) + return isEqual + } + } + return true +} + +func matches(expect, actual map[string]interface{}) bool { + for keyExpect, valueExpect := range expect { + valueExpectString, ok := valueExpect.(string) + if !ok { + return false + } + actualvalue, ok := actual[keyExpect].(string) + if !ok { + return false + } + + match, err := regexp.Match(valueExpectString, []byte(actualvalue)) + if err != nil { + log.Printf("Error on matching regex %s with %s error:%v\n", valueExpectString, actualvalue, err) + } + + if !match { + return false + } + } + return true +} + +func clearStorage() { + mx.Lock() + defer mx.Unlock() + + stubStorage = stubMapping{} +} + +func readStubFromFile(path string) { + files, err := ioutil.ReadDir(path) + if err != nil { + log.Printf("Can't read stub from %s. %v\n", path, err) + return + } + + for _, file := range files { + p := fmt.Sprintf("%s/%s", path, file.Name()) + byt, err := ioutil.ReadFile(filepath.Clean(p)) + if err != nil { + log.Printf("Error when reading file %s. %v. skipping...", file.Name(), err) + continue + } + + stub := new(Stub) + err = json.Unmarshal(byt, stub) + if err != nil { + log.Printf("Error when reading file %s. %v. skipping...", file.Name(), err) + continue + } + + storeStub(stub) + } +} diff --git a/test/mock-server/stub/stub.go b/test/mock-server/stub/stub.go new file mode 100644 index 0000000..4221360 --- /dev/null +++ b/test/mock-server/stub/stub.go @@ -0,0 +1,213 @@ +/* +Copyright © 2021 Dell Inc. or its subsidiaries. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package stub + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "time" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/go-chi/chi" +) + +// Options server options +type Options struct { + Port string + BindAddr string + StubPath string +} + +const ( + // DefaultAddress address of stub server + DefaultAddress = "localhost" + // DefaultPort port of stub server + DefaultPort = "4771" +) + +// RunStubServer runs mock grpc server +func RunStubServer(opt Options) { + if opt.Port == "" { + opt.Port = DefaultPort + } + if opt.BindAddr == "" { + opt.BindAddr = DefaultAddress + } + addr := opt.BindAddr + ":" + opt.Port + r := chi.NewRouter() + r.Post("/add", addStub) + r.Get("/", listStub) + r.Post("/find", handleFindStub) + r.Get("/clear", handleClearStub) + + if opt.StubPath != "" { + readStubFromFile(opt.StubPath) + } + + fmt.Println("Serving stub admin on http://" + addr) + go func() { + server := &http.Server{ + Addr: addr, + ReadHeaderTimeout: 3 * time.Second, + Handler: r, + } + err := server.ListenAndServe() + log.Println(err) + }() +} + +func responseError(err error, w http.ResponseWriter) { + w.WriteHeader(500) + _, e := w.Write([]byte(err.Error())) + if e != nil { + log.Printf("error writing error %s to response: %s\n", err.Error(), e.Error()) + } +} + +// Stub structure +type Stub struct { + Service string `json:"service"` + Method string `json:"method"` + Input Input `json:"input"` + Output Output `json:"output"` +} + +// Input structure +type Input struct { + Equals map[string]interface{} `json:"equals"` + Contains map[string]interface{} `json:"contains"` + Matches map[string]interface{} `json:"matches"` +} + +// Output structure +type Output struct { + Data map[string]interface{} `json:"data"` + Error string `json:"error"` +} + +func addStub(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + responseError(err, w) + return + } + + stub := new(Stub) + err = json.Unmarshal(body, stub) + if err != nil { + responseError(err, w) + return + } + + err = validateStub(stub) + if err != nil { + responseError(err, w) + return + } + + storeStub(stub) + + _, err = w.Write([]byte("Success add stub")) + if err != nil { + log.Printf("error writing success to response: %s\n", err.Error()) + } +} + +func listStub(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + err := json.NewEncoder(w).Encode(allStub()) + if err != nil { + log.Printf("error encoding stub: %s\n", err.Error()) + } +} + +func validateStub(stub *Stub) error { + if stub.Service == "" { + return fmt.Errorf("service name can't be empty") + } + + if stub.Method == "" { + return fmt.Errorf("method name can't be empty") + } + + // due to golang implementation + // method name must capital + cases.Title(language.Und, cases.NoLower).String(stub.Method) + + switch { + case stub.Input.Contains != nil: + break + case stub.Input.Equals != nil: + break + case stub.Input.Matches != nil: + break + default: + return fmt.Errorf("input cannot be empty") + } + + // TODO: validate all input case + + 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) + err := json.NewDecoder(r.Body).Decode(stub) + if err != nil { + responseError(err, w) + return + } + + // due to golang implementation + // method name must capital + cases.Title(language.Und, cases.NoLower).String(stub.Method) + + output, err := findStub(stub) + if err != nil { + log.Println(err) + responseError(err, w) + return + } + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(output) + if err != nil { + log.Printf("error encoding output: %s\n", err.Error()) + } +} + +func handleClearStub(w http.ResponseWriter, r *http.Request) { + clearStorage() + _, err := w.Write([]byte("OK")) + if err != nil { + log.Printf("error writing `OK` to response: %s\n", err.Error()) + } +} diff --git a/test/mock-server/stubs/CreateNamespace.json b/test/mock-server/stubs/CreateNamespace.json new file mode 100644 index 0000000..7021a8a --- /dev/null +++ b/test/mock-server/stubs/CreateNamespace.json @@ -0,0 +1,28 @@ +{ + "service": "FrontendNvmeServiceServer", + "method": "CreateNVMeNamespace", + "input": { + "contains": { + "NVMeNamespace": { + "NVMeNamespaceSpec": { + "Id": { "Value": "1" }, + "SubsystemId": { "Value": "1" }, + "VolumeId": { "Value": "1" }, + "HostNsid": { "Value": "1" } + } + } + } + }, + "output": { + "data": { + "NVMeNamespace": { + "NVMeNamespaceSpec": { + "Id": { "Value": "1" }, + "SubsystemId": { "Value": "nqn" }, + "VolumeId": { "Value": "1" }, + "HostNsid": { "Value": 1 } + } + } + } + } +} \ No newline at end of file diff --git a/test/mock-server/stubs/DeleteNamespace.json b/test/mock-server/stubs/DeleteNamespace.json new file mode 100644 index 0000000..ff033f7 --- /dev/null +++ b/test/mock-server/stubs/DeleteNamespace.json @@ -0,0 +1,12 @@ +{ + "service": "FrontendNvmeServiceServer", + "method": "DeleteNVMeNamespace", + "input": { + "contains": { + "name": "1" + } + }, + "output": { + "data": {} + } +} \ No newline at end of file diff --git a/test/mock-server/stubs/NullDebugList.json b/test/mock-server/stubs/NullDebugList.json new file mode 100644 index 0000000..ff033f7 --- /dev/null +++ b/test/mock-server/stubs/NullDebugList.json @@ -0,0 +1,12 @@ +{ + "service": "FrontendNvmeServiceServer", + "method": "DeleteNVMeNamespace", + "input": { + "contains": { + "name": "1" + } + }, + "output": { + "data": {} + } +} \ No newline at end of file