forked from TrueCloudLab/frostfs-sdk-go
[#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:
parent
89124d442d
commit
8e3173eacd
5 changed files with 225 additions and 2 deletions
35
client/api.go
Normal file
35
client/api.go
Normal 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
|
||||||
|
}
|
|
@ -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
39
client/client_test.go
Normal 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
|
||||||
|
}
|
|
@ -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
137
client/netmap_test.go
Normal 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())
|
||||||
|
}
|
Loading…
Reference in a new issue