From 8e3173eacd2380f354fa1a6b7cd3c2ea0f7f4634 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 20 Sep 2022 17:06:14 +0400 Subject: [PATCH] [#270] client/netmap: Cover `NetMapSnapshot` with unit tests There is a need to test each `Client` operation. In previous implementation `Client` was based on real socket connection. This didn't allow to test the `Client` without OS resources. In order to write convenient and useful unit tests we need to slightly refactor the code. Introduce `neoFSAPIServer` interface as a provider of `Client` type's abstraction from the exact NeoFS API server. Add `netMapSnapshot` method for initial implementation. Define core interface provider used in real code. Set `coreServer` as an underlying `neoFSAPIServer` in `Client.Dial`. Cover `Client.NetMapSnapshot` method with unit tests using the opportunity to override the server. From now client library can be tested not only with real physical listeners but with imitations. Signed-off-by: Leonard Lyubich --- client/api.go | 35 +++++++++++ client/client.go | 12 ++++ client/client_test.go | 39 ++++++++++++ client/netmap.go | 4 +- client/netmap_test.go | 137 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 client/api.go create mode 100644 client/client_test.go create mode 100644 client/netmap_test.go diff --git a/client/api.go b/client/api.go new file mode 100644 index 00000000..b16f7575 --- /dev/null +++ b/client/api.go @@ -0,0 +1,35 @@ +package client + +import ( + "context" + "fmt" + + v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" + "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" +) + +// interface of NeoFS API server. Exists for test purposes only. +type neoFSAPIServer interface { + netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) +} + +// wrapper over real client connection which communicates over NeoFS API protocol. +// Provides neoFSAPIServer for Client instances used in real applications. +type coreServer client.Client + +// unifies errors of all RPC. +func rpcErr(e error) error { + return fmt.Errorf("rpc failure: %w", e) +} + +// executes NetmapService.NetmapSnapshot RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { + resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + + return resp, nil +} diff --git a/client/client.go b/client/client.go index 98fad3de..4e24a22a 100644 --- a/client/client.go +++ b/client/client.go @@ -45,6 +45,8 @@ type Client struct { prm PrmInit c client.Client + + server neoFSAPIServer } // Init brings the Client instance to its initial state. @@ -95,12 +97,22 @@ func (c *Client) Dial(prm PrmDial) error { client.WithRWTimeout(prm.streamTimeout), )...) + c.setNeoFSAPIServer((*coreServer)(&c.c)) + // TODO: (neofs-api-go#382) perform generic dial stage of the client.Client _, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest)) return nil } +// sets underlying provider of neoFSAPIServer. The method is used for testing as an approach +// to skip Dial stage and override NeoFS API server. MUST NOT be used outside test code. +// In real applications wrapper over github.com/nspcc-dev/neofs-api-go/v2/rpc/client +// is statically used. +func (c *Client) setNeoFSAPIServer(server neoFSAPIServer) { + c.server = server +} + // Close closes underlying connection to the NeoFS server. Implements io.Closer. // MUST NOT be called before successful Dial. Can be called concurrently // with server operations processing on running goroutines: in this case diff --git a/client/client_test.go b/client/client_test.go new file mode 100644 index 00000000..e515f809 --- /dev/null +++ b/client/client_test.go @@ -0,0 +1,39 @@ +package client + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "testing" + + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + "github.com/stretchr/testify/require" +) + +/* +File contains common functionality used for client package testing. +*/ + +var key, _ = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + +var statusErr apistatus.ServerInternal + +func init() { + statusErr.SetMessage("test status error") +} + +func assertStatusErr(tb testing.TB, res interface{ Status() apistatus.Status }) { + require.IsType(tb, &statusErr, res.Status()) + require.Equal(tb, statusErr.Message(), res.Status().(*apistatus.ServerInternal).Message()) +} + +func newClient(server neoFSAPIServer) *Client { + var prm PrmInit + prm.SetDefaultPrivateKey(*key) + + var c Client + c.Init(prm) + c.setNeoFSAPIServer(server) + + return &c +} diff --git a/client/netmap.go b/client/netmap.go index bacba89b..e8cd4048 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -253,9 +253,9 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResN return nil, fmt.Errorf("sign request: %w", err) } - resp, err := rpcapi.NetMapSnapshot(&c.c, &req, client.WithContext(ctx)) + resp, err := c.server.netMapSnapshot(ctx, req) if err != nil { - return nil, fmt.Errorf("rpc failure: %w", err) + return nil, err } var res ResNetMapSnapshot diff --git a/client/netmap_test.go b/client/netmap_test.go new file mode 100644 index 00000000..8bfaaac6 --- /dev/null +++ b/client/netmap_test.go @@ -0,0 +1,137 @@ +package client + +import ( + "context" + "errors" + "fmt" + "testing" + + v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/session" + "github.com/nspcc-dev/neofs-api-go/v2/signature" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + "github.com/nspcc-dev/neofs-sdk-go/netmap" + "github.com/stretchr/testify/require" +) + +type serverNetMap struct { + errTransport error + + signResponse bool + + statusOK bool + + setNetMap bool + netMap v2netmap.NetMap +} + +func (x *serverNetMap) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { + err := signature.VerifyServiceMessage(&req) + if err != nil { + return nil, err + } + + if x.errTransport != nil { + return nil, x.errTransport + } + + var body v2netmap.SnapshotResponseBody + + if x.setNetMap { + body.SetNetMap(&x.netMap) + } + + var meta session.ResponseMetaHeader + + if !x.statusOK { + meta.SetStatus(statusErr.ToStatusV2()) + } + + var resp v2netmap.SnapshotResponse + resp.SetBody(&body) + resp.SetMetaHeader(&meta) + + if x.signResponse { + err = signature.SignServiceMessage(key, &resp) + if err != nil { + panic(fmt.Sprintf("sign response: %v", err)) + } + } + + return &resp, nil +} + +func TestClient_NetMapSnapshot(t *testing.T) { + var err error + var prm PrmNetMapSnapshot + var res *ResNetMapSnapshot + var srv serverNetMap + c := newClient(&srv) + ctx := context.Background() + + // missing context + require.PanicsWithValue(t, panicMsgMissingContext, func() { + //nolint:staticcheck + _, _ = c.NetMapSnapshot(nil, prm) + }) + + // request signature + srv.errTransport = errors.New("any error") + + _, err = c.NetMapSnapshot(ctx, prm) + require.ErrorIs(t, err, srv.errTransport) + + srv.errTransport = nil + + // unsigned response + _, err = c.NetMapSnapshot(ctx, prm) + require.Error(t, err) + + srv.signResponse = true + + // status failure + res, err = c.NetMapSnapshot(ctx, prm) + require.NoError(t, err) + assertStatusErr(t, res) + + srv.statusOK = true + + // missing netmap field + _, err = c.NetMapSnapshot(ctx, prm) + require.Error(t, err) + + srv.setNetMap = true + + // invalid network map + var netMap netmap.NetMap + + var node netmap.NodeInfo + // TODO: #260 use instance corrupter + + var nodeV2 v2netmap.NodeInfo + + node.WriteToV2(&nodeV2) + require.Error(t, new(netmap.NodeInfo).ReadFromV2(nodeV2)) + + netMap.SetNodes([]netmap.NodeInfo{node}) + netMap.WriteToV2(&srv.netMap) + + _, err = c.NetMapSnapshot(ctx, prm) + require.Error(t, err) + + // correct network map + // TODO: #260 use instance normalizer + node.SetPublicKey([]byte{1, 2, 3}) + node.SetNetworkEndpoints("1", "2", "3") + + node.WriteToV2(&nodeV2) + require.NoError(t, new(netmap.NodeInfo).ReadFromV2(nodeV2)) + + netMap.SetNodes([]netmap.NodeInfo{node}) + netMap.WriteToV2(&srv.netMap) + + res, err = c.NetMapSnapshot(ctx, prm) + require.NoError(t, err) + require.True(t, apistatus.IsSuccessful(res.Status())) + require.Equal(t, netMap, res.NetMap()) +}