[#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 <leonard@nspcc.ru>
This commit is contained in:
Leonard Lyubich 2022-09-20 17:06:14 +04:00 committed by LeL
parent 89124d442d
commit 8e3173eacd
5 changed files with 225 additions and 2 deletions

35
client/api.go Normal file
View file

@ -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
}

View file

@ -45,6 +45,8 @@ type Client struct {
prm PrmInit prm PrmInit
c client.Client c client.Client
server neoFSAPIServer
} }
// Init brings the Client instance to its initial state. // Init brings the Client instance to its initial state.
@ -95,12 +97,22 @@ func (c *Client) Dial(prm PrmDial) error {
client.WithRWTimeout(prm.streamTimeout), client.WithRWTimeout(prm.streamTimeout),
)...) )...)
c.setNeoFSAPIServer((*coreServer)(&c.c))
// TODO: (neofs-api-go#382) perform generic dial stage of the client.Client // TODO: (neofs-api-go#382) perform generic dial stage of the client.Client
_, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest)) _, _ = rpc.Balance(&c.c, new(v2accounting.BalanceRequest))
return nil 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. // Close closes underlying connection to the NeoFS server. Implements io.Closer.
// MUST NOT be called before successful Dial. Can be called concurrently // MUST NOT be called before successful Dial. Can be called concurrently
// with server operations processing on running goroutines: in this case // with server operations processing on running goroutines: in this case

39
client/client_test.go Normal file
View file

@ -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
}

View file

@ -253,9 +253,9 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (*ResN
return nil, fmt.Errorf("sign request: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("rpc failure: %w", err) return nil, err
} }
var res ResNetMapSnapshot var res ResNetMapSnapshot

137
client/netmap_test.go Normal file
View file

@ -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())
}