From 56b1f1519ef5d28d12607779f4524ceb52e731a7 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 18 Nov 2024 13:49:39 +0300 Subject: [PATCH 01/16] client: Increase coverage for `Dial` method One of the added tests fails: it is planned to be corrected in the future. Signed-off-by: Leonard Lyubich --- client/client_test.go | 103 ++++++++++++++++++++++++++++++++---------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 0f6e66e3..b36374ea 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -29,35 +29,90 @@ func newClient(t *testing.T, server neoFSAPIServer) *Client { return c } -func TestClient_DialContext(t *testing.T) { +func TestClient_Dial(t *testing.T) { var prmInit PrmInit c, err := New(prmInit) require.NoError(t, err) - // try to connect to any host - var prm PrmDial - prm.SetServerURI("localhost:8080") - - assert := func(ctx context.Context, errExpected error) { - // use the particular context - prm.SetContext(ctx) - - // expect particular context error according to Dial docs - require.ErrorIs(t, c.Dial(prm), errExpected) - } - - // create pre-abandoned context - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - assert(ctx, context.Canceled) - - // create "pre-deadlined" context - ctx, cancel = context.WithTimeout(context.Background(), 0) - defer cancel() - - assert(ctx, context.DeadlineExceeded) + t.Run("failure", func(t *testing.T) { + t.Run("endpoint", func(t *testing.T) { + for _, tc := range []struct { + name string + s string + assert func(t testing.TB, err error) + }{ + {name: "missing", s: "", assert: func(t testing.TB, err error) { + require.ErrorIs(t, c.Dial(PrmDial{}), ErrMissingServer) + }}, + {name: "contains control char", s: "grpc://st1.storage.fs.neo.org:8080" + string(rune(0x7f)), assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, "net/url: invalid control character in URL") + }}, + {name: "missing port", s: "grpc://st1.storage.fs.neo.org", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, "dial tcp: address st1.storage.fs.neo.org: missing port in address") + }}, + {name: "invalid port", s: "grpc://st1.storage.fs.neo.org:foo", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, `invalid port ":foo" after host`) + }}, + {name: "unsupported scheme", s: "unknown://st1.storage.fs.neo.org:8080", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, "unsupported scheme: unknown") + }}, + {name: "multiaddr", s: "/ip4/st1.storage.fs.neo.org/tcp/8080", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, "invalid endpoint options") + }}, + {name: "host only", s: "st1.storage.fs.neo.org", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, "dial tcp: address st1.storage.fs.neo.org: missing port in address") + }}, + {name: "invalid port without scheme", s: "st1.storage.fs.neo.org:foo", assert: func(t testing.TB, err error) { + require.ErrorContains(t, err, `invalid port ":foo" after host`) + }}, + } { + t.Run(tc.name, func(t *testing.T) { + var p PrmDial + p.SetServerURI(tc.s) + tc.assert(t, c.Dial(p)) + }) + } + }) + t.Run("dial timeout", func(t *testing.T) { + var p PrmDial + p.SetServerURI("grpc://localhost:8080") + p.SetTimeout(0) + require.ErrorIs(t, c.Dial(p), ErrNonPositiveTimeout) + p.SetTimeout(-1) + require.ErrorIs(t, c.Dial(p), ErrNonPositiveTimeout) + }) + t.Run("stream timeout", func(t *testing.T) { + var p PrmDial + p.SetServerURI("grpc://localhost:8080") + p.SetStreamTimeout(0) + require.ErrorIs(t, c.Dial(p), ErrNonPositiveTimeout) + p.SetStreamTimeout(-1) + require.ErrorIs(t, c.Dial(p), ErrNonPositiveTimeout) + }) + t.Run("context", func(t *testing.T) { + var anyValidPrm PrmDial + anyValidPrm.SetServerURI("localhost:8080") + t.Run("cancelled", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + p := anyValidPrm + p.SetContext(ctx) + err := c.Dial(p) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("deadline", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 0) + cancel() + + p := anyValidPrm + p.SetContext(ctx) + err := c.Dial(p) + require.ErrorIs(t, err, context.DeadlineExceeded) + }) + }) + }) } type nopPublicKey struct{} From 1a3a6e56c542dffa2beefa0b7169197e720899db Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 18 Nov 2024 15:05:18 +0300 Subject: [PATCH 02/16] client: Do not lose some URI parsing failure reasons on dial Previously, `Client.Dial` lost causing error of network endpoint parsing. Although the method returned "invalid endpoint options" error, the original reason could not be obtained. This made debugging difficult. Now causes are kept and returned. For this, function `WithNetworkURIAddress` is inlined and the `ParseURI` error is not ignored. Fixes one of the corresponding tests. Signed-off-by: Leonard Lyubich --- client/client.go | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/client/client.go b/client/client.go index f3f76bda..c45eb777 100644 --- a/client/client.go +++ b/client/client.go @@ -133,11 +133,24 @@ func (c *Client) Dial(prm PrmDial) error { prm.streamTimeout = 10 * time.Second } - c.c = *client.New(append( - client.WithNetworkURIAddress(prm.endpoint, prm.tlsConfig), + addr, withTLS, err := client.ParseURI(prm.endpoint) + if err != nil { + return fmt.Errorf("invalid server URI: %w", err) + } + + var tlsCfg *tls.Config + if withTLS { + if tlsCfg = prm.tlsConfig; tlsCfg == nil { + tlsCfg = new(tls.Config) + } + } + + c.c = *client.New( + client.WithNetworkAddress(addr), + client.WithTLSCfg(tlsCfg), client.WithDialTimeout(prm.timeoutDial), client.WithRWTimeout(prm.streamTimeout), - )...) + ) c.setNeoFSAPIServer((*coreServer)(&c.c)) From e3cc3b8f88b582ca7610e2b379aa36d2f75078ef Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 18 Nov 2024 15:58:12 +0300 Subject: [PATCH 03/16] client: Place URI parsing function from neofs-api-go module internally The function is placed inside the lib to facilitate its support and as a preliminary preparation for the obsolescence of `github.com/nspcc-dev/neofs-api-go/v2` module. Refs #381. Signed-off-by: Leonard Lyubich --- client/client.go | 3 +- client/client_test.go | 3 ++ internal/uriutil/uri.go | 41 +++++++++++++++++++++++++++ internal/uriutil/uri_test.go | 54 ++++++++++++++++++++++++++++++++++++ pool/pool.go | 4 +-- 5 files changed, 102 insertions(+), 3 deletions(-) create mode 100644 internal/uriutil/uri.go create mode 100644 internal/uriutil/uri_test.go diff --git a/client/client.go b/client/client.go index c45eb777..cd932380 100644 --- a/client/client.go +++ b/client/client.go @@ -11,6 +11,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" + "github.com/nspcc-dev/neofs-sdk-go/internal/uriutil" "github.com/nspcc-dev/neofs-sdk-go/stat" ) @@ -133,7 +134,7 @@ func (c *Client) Dial(prm PrmDial) error { prm.streamTimeout = 10 * time.Second } - addr, withTLS, err := client.ParseURI(prm.endpoint) + addr, withTLS, err := uriutil.Parse(prm.endpoint) if err != nil { return fmt.Errorf("invalid server URI: %w", err) } diff --git a/client/client_test.go b/client/client_test.go index b36374ea..829fe615 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -47,15 +47,18 @@ func TestClient_Dial(t *testing.T) { }}, {name: "contains control char", s: "grpc://st1.storage.fs.neo.org:8080" + string(rune(0x7f)), assert: func(t testing.TB, err error) { require.ErrorContains(t, err, "net/url: invalid control character in URL") + require.ErrorContains(t, err, "invalid server URI") }}, {name: "missing port", s: "grpc://st1.storage.fs.neo.org", assert: func(t testing.TB, err error) { require.ErrorContains(t, err, "dial tcp: address st1.storage.fs.neo.org: missing port in address") }}, {name: "invalid port", s: "grpc://st1.storage.fs.neo.org:foo", assert: func(t testing.TB, err error) { require.ErrorContains(t, err, `invalid port ":foo" after host`) + require.ErrorContains(t, err, "invalid server URI") }}, {name: "unsupported scheme", s: "unknown://st1.storage.fs.neo.org:8080", assert: func(t testing.TB, err error) { require.ErrorContains(t, err, "unsupported scheme: unknown") + require.ErrorContains(t, err, "invalid server URI") }}, {name: "multiaddr", s: "/ip4/st1.storage.fs.neo.org/tcp/8080", assert: func(t testing.TB, err error) { require.ErrorContains(t, err, "invalid endpoint options") diff --git a/internal/uriutil/uri.go b/internal/uriutil/uri.go new file mode 100644 index 00000000..5c78311c --- /dev/null +++ b/internal/uriutil/uri.go @@ -0,0 +1,41 @@ +package uriutil + +import ( + "fmt" + "net" + "net/url" +) + +// Parse parses URI and returns a host and a flag indicating that TLS is +// enabled. If multi-address is provided the argument is returned unchanged. +func Parse(s string) (string, bool, error) { + uri, err := url.ParseRequestURI(s) + if err != nil { + return s, false, nil + } + + const ( + grpcScheme = "grpc" + grpcTLSScheme = "grpcs" + ) + + // check if passed string was parsed correctly + // URIs that do not start with a slash after the scheme are interpreted as: + // `scheme:opaque` => if `opaque` is not empty, then it is supposed that URI + // is in `host:port` format + if uri.Host == "" { + uri.Host = uri.Scheme + uri.Scheme = grpcScheme // assume GRPC by default + if uri.Opaque != "" { + uri.Host = net.JoinHostPort(uri.Host, uri.Opaque) + } + } + + switch uri.Scheme { + case grpcTLSScheme, grpcScheme: + default: + return "", false, fmt.Errorf("unsupported scheme: %s", uri.Scheme) + } + + return uri.Host, uri.Scheme == grpcTLSScheme, nil +} diff --git a/internal/uriutil/uri_test.go b/internal/uriutil/uri_test.go new file mode 100644 index 00000000..00a12a25 --- /dev/null +++ b/internal/uriutil/uri_test.go @@ -0,0 +1,54 @@ +package uriutil_test + +import ( + "testing" + + "github.com/nspcc-dev/neofs-sdk-go/internal/uriutil" + "github.com/stretchr/testify/require" +) + +func TestParseURI(t *testing.T) { + for _, tc := range []struct { + s string + host string + withTLS bool + }{ + {s: "not a URI", host: "not a URI", withTLS: false}, + {s: "8080", host: "8080", withTLS: false}, + {s: "127.0.0.1", host: "127.0.0.1", withTLS: false}, + {s: "st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: false}, + // multiaddr + {s: "/ip4/127.0.0.1/tcp/8080", host: "", withTLS: false}, + // no scheme (TCP) + {s: "127.0.0.1:8080", host: "127.0.0.1:8080", withTLS: false}, + {s: "st1.storage.fs.neo.org:8080", host: "st1.storage.fs.neo.org:8080", withTLS: false}, + // with scheme, no port + {s: "grpc://127.0.0.1", host: "127.0.0.1", withTLS: false}, + {s: "grpc://st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: false}, + {s: "grpcs://127.0.0.1", host: "127.0.0.1", withTLS: true}, + {s: "grpcs://st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: true}, + // with scheme and port + {s: "grpc://127.0.0.1:8080", host: "127.0.0.1:8080", withTLS: false}, + {s: "grpc://st1.storage.fs.neo.org:8080", host: "st1.storage.fs.neo.org:8080", withTLS: false}, + {s: "grpcs://127.0.0.1:8082", host: "127.0.0.1:8082", withTLS: true}, + {s: "grpcs://st1.storage.fs.neo.org:8082", host: "st1.storage.fs.neo.org:8082", withTLS: true}, + } { + host, withTLS, err := uriutil.Parse(tc.s) + require.NoError(t, err, tc.s) + require.Equal(t, tc.host, host, tc.s) + require.Equal(t, tc.withTLS, withTLS, tc.s) + } + + t.Run("invalid", func(t *testing.T) { + for _, tc := range []struct { + name, s, err string + }{ + {name: "unsupported scheme", s: "unknown://st1.storage.fs.neo.org:8082", err: "unsupported scheme: unknown"}, + } { + t.Run(tc.name, func(t *testing.T) { + _, _, err := uriutil.Parse(tc.s) + require.EqualError(t, err, tc.err) + }) + } + }) +} diff --git a/pool/pool.go b/pool/pool.go index f4dc3d16..7b91fb99 100644 --- a/pool/pool.go +++ b/pool/pool.go @@ -10,7 +10,6 @@ import ( "sync/atomic" "time" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/accounting" sdkClient "github.com/nspcc-dev/neofs-sdk-go/client" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" @@ -18,6 +17,7 @@ import ( cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/internal/uriutil" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -837,7 +837,7 @@ func fillDefaultInitParams(params *InitParameters, cache *sessionCache, statisti } func isNodeValid(node NodeParam) error { - _, _, err := client.ParseURI(node.address) + _, _, err := uriutil.Parse(node.address) return err } From ce6872caf702f2e8a15adf0550e59c0f1232e381 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 18 Nov 2024 16:28:06 +0300 Subject: [PATCH 04/16] internal/util: Drop wrong statement from `ParseURI` func docs The function return zeros on multiaddr input. Signed-off-by: Leonard Lyubich --- internal/uriutil/uri.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/uriutil/uri.go b/internal/uriutil/uri.go index 5c78311c..efd4accc 100644 --- a/internal/uriutil/uri.go +++ b/internal/uriutil/uri.go @@ -7,7 +7,7 @@ import ( ) // Parse parses URI and returns a host and a flag indicating that TLS is -// enabled. If multi-address is provided the argument is returned unchanged. +// enabled. func Parse(s string) (string, bool, error) { uri, err := url.ParseRequestURI(s) if err != nil { From 92b9745c8b048a804dcc17fcf70854f93cdc6a77 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 18 Nov 2024 18:37:37 +0300 Subject: [PATCH 05/16] client: Do not try to dial an invalid server endpoint Previously, `Client.Dial` method sometimes tried to connect to obviously invalid addresses (e.g. with missing port). although a preliminary endpoint check is performed, some errors were ignored, and they still popped up during the subsequent dial. Thus, in general the client's behavior was correct and the expected error was still caught. Thus, in general, the client's behavior was correct and the error was still caught, however, unnecessary obviously failed actions were made, which could be cut off at the pre-check stage. Now the `util.ParseURI` function has been improved and catches most cases. Test endpoints are corrected for `pool.Pool`. Signed-off-by: Leonard Lyubich --- client/client_test.go | 12 ++++++++---- internal/uriutil/uri.go | 12 +++++++++++- internal/uriutil/uri_test.go | 18 +++++++----------- pool/pool_test.go | 37 +++++++++++++++++++----------------- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/client/client_test.go b/client/client_test.go index 829fe615..d6809614 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -50,7 +50,8 @@ func TestClient_Dial(t *testing.T) { require.ErrorContains(t, err, "invalid server URI") }}, {name: "missing port", s: "grpc://st1.storage.fs.neo.org", assert: func(t testing.TB, err error) { - require.ErrorContains(t, err, "dial tcp: address st1.storage.fs.neo.org: missing port in address") + require.ErrorContains(t, err, "missing port in address") + require.ErrorContains(t, err, "invalid server URI") }}, {name: "invalid port", s: "grpc://st1.storage.fs.neo.org:foo", assert: func(t testing.TB, err error) { require.ErrorContains(t, err, `invalid port ":foo" after host`) @@ -61,13 +62,16 @@ func TestClient_Dial(t *testing.T) { require.ErrorContains(t, err, "invalid server URI") }}, {name: "multiaddr", s: "/ip4/st1.storage.fs.neo.org/tcp/8080", assert: func(t testing.TB, err error) { - require.ErrorContains(t, err, "invalid endpoint options") + require.ErrorContains(t, err, "missing port in address") + require.ErrorContains(t, err, "invalid server URI") }}, {name: "host only", s: "st1.storage.fs.neo.org", assert: func(t testing.TB, err error) { - require.ErrorContains(t, err, "dial tcp: address st1.storage.fs.neo.org: missing port in address") + require.ErrorContains(t, err, "missing port in address") + require.ErrorContains(t, err, "invalid server URI") }}, {name: "invalid port without scheme", s: "st1.storage.fs.neo.org:foo", assert: func(t testing.TB, err error) { - require.ErrorContains(t, err, `invalid port ":foo" after host`) + require.ErrorContains(t, err, "missing port in address") + require.ErrorContains(t, err, "invalid server URI") }}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/internal/uriutil/uri.go b/internal/uriutil/uri.go index efd4accc..f9430b73 100644 --- a/internal/uriutil/uri.go +++ b/internal/uriutil/uri.go @@ -1,9 +1,11 @@ package uriutil import ( + "errors" "fmt" "net" "net/url" + "strings" ) // Parse parses URI and returns a host and a flag indicating that TLS is @@ -11,7 +13,11 @@ import ( func Parse(s string) (string, bool, error) { uri, err := url.ParseRequestURI(s) if err != nil { - return s, false, nil + if !strings.Contains(s, "/") { + _, _, err := net.SplitHostPort(s) + return s, false, err + } + return s, false, err } const ( @@ -37,5 +43,9 @@ func Parse(s string) (string, bool, error) { return "", false, fmt.Errorf("unsupported scheme: %s", uri.Scheme) } + if uri.Port() == "" { + return "", false, errors.New("missing port in address") + } + return uri.Host, uri.Scheme == grpcTLSScheme, nil } diff --git a/internal/uriutil/uri_test.go b/internal/uriutil/uri_test.go index 00a12a25..f1eae700 100644 --- a/internal/uriutil/uri_test.go +++ b/internal/uriutil/uri_test.go @@ -13,20 +13,9 @@ func TestParseURI(t *testing.T) { host string withTLS bool }{ - {s: "not a URI", host: "not a URI", withTLS: false}, - {s: "8080", host: "8080", withTLS: false}, - {s: "127.0.0.1", host: "127.0.0.1", withTLS: false}, - {s: "st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: false}, - // multiaddr - {s: "/ip4/127.0.0.1/tcp/8080", host: "", withTLS: false}, // no scheme (TCP) {s: "127.0.0.1:8080", host: "127.0.0.1:8080", withTLS: false}, {s: "st1.storage.fs.neo.org:8080", host: "st1.storage.fs.neo.org:8080", withTLS: false}, - // with scheme, no port - {s: "grpc://127.0.0.1", host: "127.0.0.1", withTLS: false}, - {s: "grpc://st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: false}, - {s: "grpcs://127.0.0.1", host: "127.0.0.1", withTLS: true}, - {s: "grpcs://st1.storage.fs.neo.org", host: "st1.storage.fs.neo.org", withTLS: true}, // with scheme and port {s: "grpc://127.0.0.1:8080", host: "127.0.0.1:8080", withTLS: false}, {s: "grpc://st1.storage.fs.neo.org:8080", host: "st1.storage.fs.neo.org:8080", withTLS: false}, @@ -44,6 +33,13 @@ func TestParseURI(t *testing.T) { name, s, err string }{ {name: "unsupported scheme", s: "unknown://st1.storage.fs.neo.org:8082", err: "unsupported scheme: unknown"}, + {name: "garbage URI", s: "not a URI", err: "address not a URI: missing port in address"}, + {name: "port only", s: "8080", err: "address 8080: missing port in address"}, + {name: "ip only", s: "127.0.0.1", err: "address 127.0.0.1: missing port in address"}, + {name: "host only", s: "st1.storage.fs.neo.org", err: "address st1.storage.fs.neo.org: missing port in address"}, + {name: "multiaddr", s: "/ip4/127.0.0.1/tcp/8080", err: "missing port in address"}, + {name: "ip with scheme without port", s: "grpc://127.0.0.1", err: "missing port in address"}, + {name: "host with scheme without port", s: "grpc://st1.storage.fs.neo.org", err: "missing port in address"}, } { t.Run(tc.name, func(t *testing.T) { _, _, err := uriutil.Parse(tc.s) diff --git a/pool/pool_test.go b/pool/pool_test.go index 8bffac3b..60a20f10 100644 --- a/pool/pool_test.go +++ b/pool/pool_test.go @@ -3,6 +3,7 @@ package pool import ( "context" "errors" + "fmt" "strconv" "testing" "time" @@ -20,6 +21,8 @@ import ( "go.uber.org/zap" ) +func anyValidPeerAddress(ind uint) string { return fmt.Sprintf("peer%d:8080", ind) } + func TestBuildPoolClientFailed(t *testing.T) { mockClientBuilder1 := func(_ string) (internalClient, error) { return nil, errors.New("oops") @@ -37,7 +40,7 @@ func TestBuildPoolClientFailed(t *testing.T) { t.Run(name, func(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, - nodeParams: []NodeParam{{1, "peer0", 1}}, + nodeParams: []NodeParam{{1, anyValidPeerAddress(0), 1}}, } opts.setClientBuilder(b) @@ -51,8 +54,8 @@ func TestBuildPoolClientFailed(t *testing.T) { func TestBuildPoolOneNodeFailed(t *testing.T) { nodes := []NodeParam{ - {1, "peer0", 1}, - {2, "peer1", 1}, + {1, anyValidPeerAddress(0), 1}, + {2, anyValidPeerAddress(1), 1}, } mockClientBuilder := func(addr string) (internalClient, error) { @@ -123,7 +126,7 @@ func TestOneNode(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, - nodeParams: []NodeParam{{1, "peer0", 1}}, + nodeParams: []NodeParam{{1, anyValidPeerAddress(0), 1}}, } opts.setClientBuilder(mockClientBuilder) @@ -146,8 +149,8 @@ func TestTwoNodes(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, nodeParams: []NodeParam{ - {1, "peer0", 1}, - {1, "peer1", 1}, + {1, anyValidPeerAddress(0), 1}, + {1, anyValidPeerAddress(1), 1}, }, } opts.setClientBuilder(mockClientBuilder) @@ -174,8 +177,8 @@ func assertAuthKeyForAny(addr string, nodes []NodeParam) bool { func TestOneOfTwoFailed(t *testing.T) { nodes := []NodeParam{ - {1, "peer0", 1}, - {9, "peer1", 1}, + {1, anyValidPeerAddress(0), 1}, + {9, anyValidPeerAddress(1), 1}, } mockClientBuilder := func(addr string) (internalClient, error) { @@ -224,8 +227,8 @@ func TestTwoFailed(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, nodeParams: []NodeParam{ - {1, "peer0", 1}, - {1, "peer1", 1}, + {1, anyValidPeerAddress(0), 1}, + {1, anyValidPeerAddress(1), 1}, }, clientRebalanceInterval: 200 * time.Millisecond, } @@ -257,7 +260,7 @@ func TestSessionCache(t *testing.T) { opts := InitParameters{ signer: usr.RFC6979, nodeParams: []NodeParam{ - {1, "peer0", 1}, + {1, anyValidPeerAddress(0), 1}, }, clientRebalanceInterval: 30 * time.Second, } @@ -335,8 +338,8 @@ func TestSessionCache(t *testing.T) { func TestPriority(t *testing.T) { nodes := []NodeParam{ - {1, "peer0", 1}, - {2, "peer1", 100}, + {1, anyValidPeerAddress(0), 1}, + {2, anyValidPeerAddress(1), 100}, } mockClientBuilder := func(addr string) (internalClient, error) { @@ -392,7 +395,7 @@ func TestSessionCacheWithKey(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, nodeParams: []NodeParam{ - {1, "peer0", 1}, + {1, anyValidPeerAddress(0), 1}, }, clientRebalanceInterval: 30 * time.Second, } @@ -427,7 +430,7 @@ func TestSessionTokenOwner(t *testing.T) { opts := InitParameters{ signer: usertest.User().RFC6979, nodeParams: []NodeParam{ - {1, "peer0", 1}, + {1, anyValidPeerAddress(0), 1}, }, } opts.setClientBuilder(mockClientBuilder) @@ -552,8 +555,8 @@ func TestHandleError(t *testing.T) { func TestSwitchAfterErrorThreshold(t *testing.T) { nodes := []NodeParam{ - {1, "peer0", 1}, - {2, "peer1", 100}, + {1, anyValidPeerAddress(0), 1}, + {2, anyValidPeerAddress(1), 100}, } errorThreshold := 5 From ae384edbb5e85fb8d11a6998115c87ea8cb45a80 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 19 Nov 2024 17:27:45 +0300 Subject: [PATCH 06/16] client: Unify homogeneous internal call mechanics There is no any point to have them different. Signed-off-by: Leonard Lyubich --- client/api.go | 8 +++++--- client/netmap_test.go | 3 +-- client/session.go | 3 +-- client/session_test.go | 3 +-- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/client/api.go b/client/api.go index 88f3d313..2106a359 100644 --- a/client/api.go +++ b/client/api.go @@ -18,7 +18,7 @@ var ( // interface of NeoFS API server. Exists for test purposes only. type neoFSAPIServer interface { - createSession(cli *client.Client, req *session.CreateRequest, opts ...client.CallOption) (*session.CreateResponse, error) + createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) } @@ -43,8 +43,10 @@ func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRe return resp, nil } -func (x *coreServer) createSession(cli *client.Client, req *session.CreateRequest, opts ...client.CallOption) (*session.CreateResponse, error) { - resp, err := rpcAPICreateSession(cli, req, opts...) +// executes SessionService.Create RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) createSession(ctx context.Context, req session.CreateRequest) (*session.CreateResponse, error) { + resp, err := rpcAPICreateSession((*client.Client)(x), &req, client.WithContext(ctx)) if err != nil { return nil, rpcErr(err) } diff --git a/client/netmap_test.go b/client/netmap_test.go index a1a218e5..35360444 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -7,7 +7,6 @@ import ( "testing" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -29,7 +28,7 @@ type serverNetMap struct { signer neofscrypto.Signer } -func (x *serverNetMap) createSession(*client.Client, *session.CreateRequest, ...client.CallOption) (*session.CreateResponse, error) { +func (x *serverNetMap) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { return nil, nil } diff --git a/client/session.go b/client/session.go index d13f3691..9070daa5 100644 --- a/client/session.go +++ b/client/session.go @@ -4,7 +4,6 @@ import ( "context" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" @@ -113,7 +112,7 @@ func (c *Client) SessionCreate(ctx context.Context, signer user.Signer, prm PrmS cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.createSession(&c.c, &req, client.WithContext(ctx)) + return c.server.createSession(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2session.CreateResponse) diff --git a/client/session_test.go b/client/session_test.go index c94994dc..a21350ac 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -5,7 +5,6 @@ import ( "testing" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-api-go/v2/session" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" @@ -21,7 +20,7 @@ func (m sessionAPIServer) netMapSnapshot(context.Context, v2netmap.SnapshotReque return nil, nil } -func (m sessionAPIServer) createSession(*client.Client, *session.CreateRequest, ...client.CallOption) (*session.CreateResponse, error) { +func (m sessionAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { var body session.CreateResponseBody m.setBody(&body) From d109f2d20a3fe60df85a364c753f3d8485bdc0b1 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 19 Nov 2024 17:33:05 +0300 Subject: [PATCH 07/16] client: Return error from unimplemented test methods Nil response without an error can lead to the undesired behavior. Since other methods are not expected to be called, unimplemented error fits the best. Signed-off-by: Leonard Lyubich --- client/netmap_test.go | 2 +- client/session_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/netmap_test.go b/client/netmap_test.go index 35360444..285eb951 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -29,7 +29,7 @@ type serverNetMap struct { } func (x *serverNetMap) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { - return nil, nil + return nil, errors.New("unimplemented") } func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { diff --git a/client/session_test.go b/client/session_test.go index a21350ac..925490dd 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "errors" "testing" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" @@ -17,7 +18,7 @@ type sessionAPIServer struct { } func (m sessionAPIServer) netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { - return nil, nil + return nil, errors.New("unimplemented") } func (m sessionAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { From ee40390833df556e8f6899a3400825f88827beaf Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 19 Nov 2024 18:02:35 +0300 Subject: [PATCH 08/16] client: Drop overriding of session and netmap calls via global vars There is already an interface for this. Also, changing global variables is always weird. Signed-off-by: Leonard Lyubich --- client/api.go | 10 ++---- client/container_statistic_test.go | 52 +++++++----------------------- client/netmap_test.go | 6 ++-- client/session_test.go | 22 +++++-------- client/util_test.go | 18 +++++++++++ 5 files changed, 41 insertions(+), 67 deletions(-) create mode 100644 client/util_test.go diff --git a/client/api.go b/client/api.go index 2106a359..a60d0c81 100644 --- a/client/api.go +++ b/client/api.go @@ -10,12 +10,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/session" ) -var ( - // special variables for test purposes only, to overwrite real RPC calls. - rpcAPINetMapSnapshot = rpcapi.NetMapSnapshot - rpcAPICreateSession = rpcapi.CreateSession -) - // interface of NeoFS API server. Exists for test purposes only. type neoFSAPIServer interface { createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) @@ -35,7 +29,7 @@ func rpcErr(e error) error { // 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 := rpcAPINetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx)) + resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx)) if err != nil { return nil, rpcErr(err) } @@ -46,7 +40,7 @@ func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRe // executes SessionService.Create RPC declared in NeoFS API protocol // using underlying client.Client. func (x *coreServer) createSession(ctx context.Context, req session.CreateRequest) (*session.CreateResponse, error) { - resp, err := rpcAPICreateSession((*client.Client)(x), &req, client.WithContext(ctx)) + resp, err := rpcapi.CreateSession((*client.Client)(x), &req, client.WithContext(ctx)) if err != nil { return nil, rpcErr(err) } diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index d9e56def..53b51833 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -480,29 +480,15 @@ func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPINetMapSnapshot = func(_ *client.Client, _ *netmapv2.SnapshotRequest, _ ...client.CallOption) (*netmapv2.SnapshotResponse, error) { - var resp netmapv2.SnapshotResponse - var meta session.ResponseMetaHeader - var netMap netmapv2.NetMap - - body := netmapv2.SnapshotResponseBody{} - body.SetNetMap(&netMap) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil + srv := serverNetMap{ + signResponse: true, + statusOK: true, + setNetMap: true, + signer: usr, } - + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect - c.setNeoFSAPIServer((*coreServer)(&c.c)) _, err := c.NetMapSnapshot(ctx, PrmNetMapSnapshot{}) require.NoError(t, err) @@ -513,30 +499,14 @@ func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { func TestClientStatistic_CreateSession(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPICreateSession = func(_ *client.Client, _ *session.CreateRequest, _ ...client.CallOption) (*session.CreateResponse, error) { - var resp session.CreateResponse - var meta session.ResponseMetaHeader - - body := session.CreateResponseBody{} - body.SetID(randBytes(10)) - - body.SetSessionKey(neofscrypto.PublicKeyBytes(usr.Public())) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil + srv := sessionAPIServer{ + signer: usr, + id: randBytes(10), + key: neofscrypto.PublicKeyBytes(usr.Public()), } - + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect - c.setNeoFSAPIServer((*coreServer)(&c.c)) var prm PrmSessionCreate diff --git a/client/netmap_test.go b/client/netmap_test.go index 285eb951..2eccf91b 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -16,6 +16,8 @@ import ( ) type serverNetMap struct { + unimplementedNeoFSAPIServer + errTransport error signResponse bool @@ -28,10 +30,6 @@ type serverNetMap struct { signer neofscrypto.Signer } -func (x *serverNetMap) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { - return nil, errors.New("unimplemented") -} - func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { err := verifyServiceMessage(&req) if err != nil { diff --git a/client/session_test.go b/client/session_test.go index 925490dd..052596a1 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -2,10 +2,8 @@ package client import ( "context" - "errors" "testing" - v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" "github.com/nspcc-dev/neofs-api-go/v2/session" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" @@ -13,17 +11,17 @@ import ( ) type sessionAPIServer struct { - signer neofscrypto.Signer - setBody func(body *session.CreateResponseBody) -} + unimplementedNeoFSAPIServer + signer neofscrypto.Signer -func (m sessionAPIServer) netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { - return nil, errors.New("unimplemented") + id []byte + key []byte } func (m sessionAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { var body session.CreateResponseBody - m.setBody(&body) + body.SetID(m.id) + body.SetSessionKey(m.key) var resp session.CreateResponse resp.SetBody(&body) @@ -45,9 +43,7 @@ func TestClient_SessionCreate(t *testing.T) { prmSessionCreate.SetExp(1) t.Run("missing session id", func(t *testing.T) { - c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, setBody: func(body *session.CreateResponseBody) { - body.SetSessionKey([]byte{1}) - }}) + c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, key: []byte{1}}) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) @@ -56,9 +52,7 @@ func TestClient_SessionCreate(t *testing.T) { }) t.Run("missing session key", func(t *testing.T) { - c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, setBody: func(body *session.CreateResponseBody) { - body.SetID([]byte{1}) - }}) + c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, id: []byte{1}}) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) diff --git a/client/util_test.go b/client/util_test.go new file mode 100644 index 00000000..cdd2614c --- /dev/null +++ b/client/util_test.go @@ -0,0 +1,18 @@ +package client + +import ( + "context" + "errors" + + "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/session" +) + +type unimplementedNeoFSAPIServer struct{} + +func (unimplementedNeoFSAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) netMapSnapshot(context.Context, netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { + return nil, errors.New("unimplemented") +} From 93b61f8759fee254c27f77624dcd4244094f1079 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 19 Nov 2024 18:15:01 +0300 Subject: [PATCH 09/16] client: Unify the override of all operations For two operations it has already been properly done, and for the rest for some reason through global variables. This also returns skipped tests to action: in approach with interfaces any method can be overridden. Signed-off-by: Leonard Lyubich --- client/accounting.go | 9 +- client/accounting_test.go | 20 ++ client/api.go | 263 +++++++++++++++- client/container.go | 27 +- client/container_statistic_test.go | 462 +++-------------------------- client/container_test.go | 120 ++++++++ client/netmap.go | 12 +- client/netmap_test.go | 81 ++++- client/object_delete.go | 7 +- client/object_delete_test.go | 27 ++ client/object_get.go | 23 +- client/object_get_test.go | 89 ++++++ client/object_hash.go | 9 +- client/object_hash_test.go | 19 ++ client/object_put.go | 26 +- client/object_put_test.go | 48 +-- client/object_search.go | 15 +- client/object_search_test.go | 120 ++++---- client/reputation.go | 12 +- client/reputation_test.go | 37 +++ client/session_test.go | 27 +- client/util_test.go | 61 ++++ 22 files changed, 889 insertions(+), 625 deletions(-) create mode 100644 client/reputation_test.go diff --git a/client/accounting.go b/client/accounting.go index 503d3236..ce5375e8 100644 --- a/client/accounting.go +++ b/client/accounting.go @@ -5,18 +5,11 @@ import ( v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/accounting" "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" ) -var ( - // special variable for test purposes, to overwrite real RPC calls. - rpcAPIBalance = rpcapi.Balance -) - // PrmBalanceGet groups parameters of BalanceGet operation. type PrmBalanceGet struct { prmCommonMeta @@ -74,7 +67,7 @@ func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (accounting. cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIBalance(&c.c, &req, client.WithContext(ctx)) + return c.server.getBalance(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2accounting.BalanceResponse) diff --git a/client/accounting_test.go b/client/accounting_test.go index 377aa1ce..2946f972 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -2,11 +2,31 @@ package client import ( "context" + "fmt" "testing" + apiaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/stretchr/testify/require" ) +type testGetBalanceServer struct { + unimplementedNeoFSAPIServer +} + +func (x testGetBalanceServer) getBalance(context.Context, apiaccounting.BalanceRequest) (*apiaccounting.BalanceResponse, error) { + var body apiaccounting.BalanceResponseBody + body.SetBalance(new(apiaccounting.Decimal)) + var resp apiaccounting.BalanceResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_BalanceGet(t *testing.T) { c := newClient(t, nil) ctx := context.Background() diff --git a/client/api.go b/client/api.go index a60d0c81..eb7daee5 100644 --- a/client/api.go +++ b/client/api.go @@ -4,17 +4,52 @@ import ( "context" "fmt" - v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/accounting" + "github.com/nspcc-dev/neofs-api-go/v2/container" + "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-api-go/v2/reputation" rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" + "github.com/nspcc-dev/neofs-api-go/v2/rpc/common" "github.com/nspcc-dev/neofs-api-go/v2/session" ) +type getObjectResponseStream interface { + Read(*object.GetResponse) error +} + +type getObjectPayloadRangeResponseStream interface { + Read(*object.GetRangeResponse) error +} + +type searchObjectsResponseStream interface { + Read(*object.SearchResponse) error +} + // interface of NeoFS API server. Exists for test purposes only. type neoFSAPIServer interface { createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) - - netMapSnapshot(context.Context, v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) + getBalance(context.Context, accounting.BalanceRequest) (*accounting.BalanceResponse, error) + netMapSnapshot(context.Context, netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) + getNetworkInfo(context.Context, netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) + getNodeInfo(context.Context, netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) + putContainer(context.Context, container.PutRequest) (*container.PutResponse, error) + getContainer(context.Context, container.GetRequest) (*container.GetResponse, error) + deleteContainer(context.Context, container.DeleteRequest) (*container.DeleteResponse, error) + listContainers(context.Context, container.ListRequest) (*container.ListResponse, error) + getEACL(context.Context, container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) + setEACL(context.Context, container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) + announceContainerSpace(context.Context, container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) + announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) + announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) + putObject(context.Context) (objectWriter, error) + deleteObject(context.Context, object.DeleteRequest) (*object.DeleteResponse, error) + hashObjectPayloadRanges(context.Context, object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) + headObject(context.Context, object.HeadRequest) (*object.HeadResponse, error) + getObject(context.Context, object.GetRequest) (getObjectResponseStream, error) + getObjectPayloadRange(context.Context, object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) + searchObjects(context.Context, object.SearchRequest) (searchObjectsResponseStream, error) } // wrapper over real client connection which communicates over NeoFS API protocol. @@ -28,7 +63,7 @@ func rpcErr(e error) error { // 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) { +func (x *coreServer) netMapSnapshot(ctx context.Context, req netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx)) if err != nil { return nil, rpcErr(err) @@ -37,6 +72,36 @@ func (x *coreServer) netMapSnapshot(ctx context.Context, req v2netmap.SnapshotRe return resp, nil } +// executes NetmapService.NetworkInfo RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getNetworkInfo(ctx context.Context, req netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { + resp, err := rpcapi.NetworkInfo((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes NetmapService.LocalNodeInfo RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getNodeInfo(ctx context.Context, req netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { + resp, err := rpcapi.LocalNodeInfo((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes AccountingService.Balance RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getBalance(ctx context.Context, req accounting.BalanceRequest) (*accounting.BalanceResponse, error) { + resp, err := rpcapi.Balance((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + // executes SessionService.Create RPC declared in NeoFS API protocol // using underlying client.Client. func (x *coreServer) createSession(ctx context.Context, req session.CreateRequest) (*session.CreateResponse, error) { @@ -47,3 +112,193 @@ func (x *coreServer) createSession(ctx context.Context, req session.CreateReques return resp, nil } + +// executes ContainerService.Put RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) putContainer(ctx context.Context, req container.PutRequest) (*container.PutResponse, error) { + resp, err := rpcapi.PutContainer((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ContainerService.Get RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getContainer(ctx context.Context, req container.GetRequest) (*container.GetResponse, error) { + resp, err := rpcapi.GetContainer((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ContainerService.Delete RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) deleteContainer(ctx context.Context, req container.DeleteRequest) (*container.DeleteResponse, error) { + // rpcapi.DeleteContainer returns wrong response type + resp := new(container.DeleteResponse) + err := client.SendUnary((*client.Client)(x), + common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "Delete"), + &req, resp, client.WithContext(ctx)) + if err != nil { + return nil, err + } + return resp, nil +} + +// executes ContainerService.List RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) listContainers(ctx context.Context, req container.ListRequest) (*container.ListResponse, error) { + resp, err := rpcapi.ListContainers((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ContainerService.GetExtendedACL RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getEACL(ctx context.Context, req container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { + resp, err := rpcapi.GetEACL((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ContainerService.SetExtendedACL RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) setEACL(ctx context.Context, req container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { + // rpcapi.SetEACL returns wrong response type + resp := new(container.SetExtendedACLResponse) + err := client.SendUnary((*client.Client)(x), + common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "SetExtendedACL"), + &req, resp, client.WithContext(ctx)) + if err != nil { + return nil, err + } + return resp, nil +} + +// executes ContainerService.AnnounceUsedSpace RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) announceContainerSpace(ctx context.Context, req container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { + // rpcapi.AnnounceUsedSpace returns wrong response type + resp := new(container.AnnounceUsedSpaceResponse) + err := client.SendUnary((*client.Client)(x), + common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "AnnounceUsedSpace"), + &req, resp, client.WithContext(ctx)) + if err != nil { + return nil, err + } + return resp, nil +} + +// executes ReputationService.AnnounceIntermediateResult RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) announceIntermediateReputation(ctx context.Context, req reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { + resp, err := rpcapi.AnnounceIntermediateResult((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ReputationService.AnnounceLocalTrust RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) announceLocalTrust(ctx context.Context, req reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { + resp, err := rpcapi.AnnounceLocalTrust((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +type corePutObjectStream struct { + reqWriter *rpcapi.PutRequestWriter + resp object.PutResponse +} + +func (x *corePutObjectStream) Write(req *object.PutRequest) error { + return x.reqWriter.Write(req) +} + +func (x *corePutObjectStream) Close() (*object.PutResponse, error) { + if err := x.reqWriter.Close(); err != nil { + return nil, err + } + return &x.resp, nil +} + +// executes ObjectService.Put RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) putObject(ctx context.Context) (objectWriter, error) { + var err error + var stream corePutObjectStream + stream.reqWriter, err = rpcapi.PutObject((*client.Client)(x), &stream.resp, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return &stream, nil +} + +// executes ObjectService.Delete RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) deleteObject(ctx context.Context, req object.DeleteRequest) (*object.DeleteResponse, error) { + resp, err := rpcapi.DeleteObject((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ObjectService.GetRangeHash RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) hashObjectPayloadRanges(ctx context.Context, req object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { + resp, err := rpcapi.HashObjectRange((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ObjectService.Head RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) headObject(ctx context.Context, req object.HeadRequest) (*object.HeadResponse, error) { + resp, err := rpcapi.HeadObject((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return resp, nil +} + +// executes ObjectService.Get RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getObject(ctx context.Context, req object.GetRequest) (getObjectResponseStream, error) { + stream, err := rpcapi.GetObject((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return stream, nil +} + +// executes ObjectService.GetRange RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) getObjectPayloadRange(ctx context.Context, req object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { + stream, err := rpcapi.GetObjectRange((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return stream, nil +} + +// executes ObjectService.Search RPC declared in NeoFS API protocol +// using underlying client.Client. +func (x *coreServer) searchObjects(ctx context.Context, req object.SearchRequest) (searchObjectsResponseStream, error) { + stream, err := rpcapi.SearchObjects((*client.Client)(x), &req, client.WithContext(ctx)) + if err != nil { + return nil, rpcErr(err) + } + return stream, nil +} diff --git a/client/container.go b/client/container.go index 6a627b49..b594b086 100644 --- a/client/container.go +++ b/client/container.go @@ -7,8 +7,6 @@ import ( v2container "github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -19,17 +17,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) -var ( - // special variables for test purposes, to overwrite real RPC calls. - rpcAPIPutContainer = rpcapi.PutContainer - rpcAPIGetContainer = rpcapi.GetContainer - rpcAPIListContainers = rpcapi.ListContainers - rpcAPIDeleteContainer = rpcapi.DeleteContainer - rpcAPIGetEACL = rpcapi.GetEACL - rpcAPISetEACL = rpcapi.SetEACL - rpcAPIAnnounceUsedSpace = rpcapi.AnnounceUsedSpace -) - // PrmContainerPut groups optional parameters of ContainerPut operation. type PrmContainerPut struct { prmCommonMeta @@ -137,7 +124,7 @@ func (c *Client) ContainerPut(ctx context.Context, cont container.Container, sig c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIPutContainer(&c.c, &req, client.WithContext(ctx)) + return c.server.putContainer(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2container.PutResponse) @@ -205,7 +192,7 @@ func (c *Client) ContainerGet(ctx context.Context, id cid.ID, prm PrmContainerGe cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIGetContainer(&c.c, &req, client.WithContext(ctx)) + return c.server.getContainer(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2container.GetResponse) @@ -271,7 +258,7 @@ func (c *Client) ContainerList(ctx context.Context, ownerID user.ID, prm PrmCont cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIListContainers(&c.c, &req, client.WithContext(ctx)) + return c.server.listContainers(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2container.ListResponse) @@ -405,7 +392,7 @@ func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscry c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIDeleteContainer(&c.c, &req, client.WithContext(ctx)) + return c.server.deleteContainer(ctx, req) } // process call @@ -457,7 +444,7 @@ func (c *Client) ContainerEACL(ctx context.Context, id cid.ID, prm PrmContainerE cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIGetEACL(&c.c, &req, client.WithContext(ctx)) + return c.server.getEACL(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2container.GetExtendedACLResponse) @@ -594,7 +581,7 @@ func (c *Client) ContainerSetEACL(ctx context.Context, table eacl.Table, signer c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPISetEACL(&c.c, &req, client.WithContext(ctx)) + return c.server.setEACL(ctx, req) } // process call @@ -663,7 +650,7 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, announcements [ cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIAnnounceUsedSpace(&c.c, &req, client.WithContext(ctx)) + return c.server.announceContainerSpace(ctx, req) } // process call diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index 53b51833..7b5d7b17 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -3,28 +3,17 @@ package client import ( "context" "crypto/rand" - "fmt" + "io" mathRand "math/rand/v2" "strconv" "testing" "time" "github.com/google/uuid" - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - v2acl "github.com/nspcc-dev/neofs-api-go/v2/acl" - v2container "github.com/nspcc-dev/neofs-api-go/v2/container" - netmapv2 "github.com/nspcc-dev/neofs-api-go/v2/netmap" - v2object "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" - "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/container" "github.com/nspcc-dev/neofs-sdk-go/container/acl" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" - neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -82,12 +71,6 @@ func randBytes(l int) []byte { return r } -func randRefsContainerID() *refs.ContainerID { - var id refs.ContainerID - cidtest.ID().WriteToV2(&id) - return &id -} - func prepareContainer(accountID user.ID) container.Container { cont := container.Container{} cont.Init() @@ -121,27 +104,8 @@ func testEaclTable(containerID cid.ID) eacl.Table { func TestClientStatistic_AccountBalance(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIBalance = func(_ *client.Client, _ *accounting.BalanceRequest, _ ...client.CallOption) (*accounting.BalanceResponse, error) { - var resp accounting.BalanceResponse - var meta session.ResponseMetaHeader - var balance accounting.Decimal - var body accounting.BalanceResponseBody - - body.SetBalance(&balance) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - err := signServiceMessage(usr, &resp, nil) - if err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testGetBalanceServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -156,26 +120,8 @@ func TestClientStatistic_AccountBalance(t *testing.T) { func TestClientStatistic_ContainerPut(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIPutContainer = func(_ *client.Client, _ *v2container.PutRequest, _ ...client.CallOption) (*v2container.PutResponse, error) { - var resp v2container.PutResponse - var meta session.ResponseMetaHeader - var body v2container.PutResponseBody - - body.SetContainerID(randRefsContainerID()) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - err := signServiceMessage(usr.RFC6979, &resp, nil) - if err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testPutContainerServer + c := newClient(t, &srv) cont := prepareContainer(usr.ID) collector := newCollector() @@ -189,46 +135,8 @@ func TestClientStatistic_ContainerPut(t *testing.T) { } func TestClientStatistic_ContainerGet(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIGetContainer = func(_ *client.Client, _ *v2container.GetRequest, _ ...client.CallOption) (*v2container.GetResponse, error) { - var cont v2container.Container - var ver refs.Version - var placementPolicyV2 netmapv2.PlacementPolicy - var replicas []netmapv2.Replica - var resp v2container.GetResponse - var meta session.ResponseMetaHeader - var owner refs.OwnerID - - usr.ID.WriteToV2(&owner) - cont.SetOwnerID(&owner) - cont.SetVersion(&ver) - - nonce, err := uuid.New().MarshalBinary() - require.NoError(t, err) - cont.SetNonce(nonce) - - replica := netmapv2.Replica{} - replica.SetCount(1) - replicas = append(replicas, replica) - placementPolicyV2.SetReplicas(replicas) - cont.SetPlacementPolicy(&placementPolicyV2) - - body := v2container.GetResponseBody{} - body.SetContainer(&cont) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err = signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + c := newClient(t, new(testGetContainerServer)) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -242,23 +150,8 @@ func TestClientStatistic_ContainerGet(t *testing.T) { func TestClientStatistic_ContainerList(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIListContainers = func(_ *client.Client, _ *v2container.ListRequest, _ ...client.CallOption) (*v2container.ListResponse, error) { - var resp v2container.ListResponse - var meta session.ResponseMetaHeader - var body v2container.ListResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testListContainersServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -272,23 +165,8 @@ func TestClientStatistic_ContainerList(t *testing.T) { func TestClientStatistic_ContainerDelete(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIDeleteContainer = func(_ *client.Client, _ *v2container.DeleteRequest, _ ...client.CallOption) (*v2container.PutResponse, error) { - var resp v2container.PutResponse - var meta session.ResponseMetaHeader - var body v2container.PutResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testDeleteContainerServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -300,28 +178,9 @@ func TestClientStatistic_ContainerDelete(t *testing.T) { } func TestClientStatistic_ContainerEacl(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIGetEACL = func(_ *client.Client, _ *v2container.GetExtendedACLRequest, _ ...client.CallOption) (*v2container.GetExtendedACLResponse, error) { - var resp v2container.GetExtendedACLResponse - var meta session.ResponseMetaHeader - var aclTable v2acl.Table - var body v2container.GetExtendedACLResponseBody - - body.SetEACL(&aclTable) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testGetEACLServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -335,23 +194,8 @@ func TestClientStatistic_ContainerEacl(t *testing.T) { func TestClientStatistic_ContainerSetEacl(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPISetEACL = func(_ *client.Client, _ *v2container.SetExtendedACLRequest, _ ...client.CallOption) (*v2container.PutResponse, error) { - var resp v2container.PutResponse - var meta session.ResponseMetaHeader - var body v2container.PutResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testSetEACLServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -364,25 +208,9 @@ func TestClientStatistic_ContainerSetEacl(t *testing.T) { } func TestClientStatistic_ContainerAnnounceUsedSpace(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIAnnounceUsedSpace = func(_ *client.Client, _ *v2container.AnnounceUsedSpaceRequest, _ ...client.CallOption) (*v2container.PutResponse, error) { - var resp v2container.PutResponse - var meta session.ResponseMetaHeader - var body v2container.PutResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testAnnounceContainerSpaceServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -401,34 +229,8 @@ func TestClientStatistic_ContainerAnnounceUsedSpace(t *testing.T) { func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPINetworkInfo = func(_ *client.Client, _ *netmapv2.NetworkInfoRequest, _ ...client.CallOption) (*netmapv2.NetworkInfoResponse, error) { - var resp netmapv2.NetworkInfoResponse - var meta session.ResponseMetaHeader - var netInfo netmapv2.NetworkInfo - var netConfig netmapv2.NetworkConfig - var p1 netmapv2.NetworkParameter - - p1.SetKey(randBytes(10)) - p1.SetValue(randBytes(10)) - - netConfig.SetParameters(p1) - netInfo.SetNetworkConfig(&netConfig) - - body := netmapv2.NetworkInfoResponseBody{} - body.SetNetworkInfo(&netInfo) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testGetNetworkInfoServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -441,33 +243,9 @@ func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { } func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPILocalNodeInfo = func(_ *client.Client, _ *netmapv2.LocalNodeInfoRequest, _ ...client.CallOption) (*netmapv2.LocalNodeInfoResponse, error) { - var resp netmapv2.LocalNodeInfoResponse - var meta session.ResponseMetaHeader - var ver refs.Version - var nodeInfo netmapv2.NodeInfo - - nodeInfo.SetPublicKey(neofscrypto.PublicKeyBytes(usr.Public())) - nodeInfo.SetAddresses("https://some-endpont.com") - - body := netmapv2.LocalNodeInfoResponseBody{} - body.SetVersion(&ver) - body.SetNodeInfo(&nodeInfo) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testGetNodeInfoServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -478,14 +256,8 @@ func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { } func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { - usr := usertest.User() ctx := context.Background() - srv := serverNetMap{ - signResponse: true, - statusOK: true, - setNetMap: true, - signer: usr, - } + var srv testNetmapSnapshotServer c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -499,11 +271,7 @@ func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { func TestClientStatistic_CreateSession(t *testing.T) { usr := usertest.User() ctx := context.Background() - srv := sessionAPIServer{ - signer: usr, - id: randBytes(10), - key: neofscrypto.PublicKeyBytes(usr.Public()), - } + var srv testCreateSessionServer c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -517,23 +285,14 @@ func TestClientStatistic_CreateSession(t *testing.T) { } func TestClientStatistic_ObjectPut(t *testing.T) { - t.Skip("need changes to api-go, to set `wc client.MessageWriterCloser` in rpcapi.PutRequestWriter") - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIPutObject = func(_ *client.Client, _ *v2object.PutResponse, _ ...client.CallOption) (objectWriter, error) { - var resp rpcapi.PutRequestWriter - - return &resp, nil - } - + var srv testPutObjectServer + c := newClient(t, &srv) containerID := cidtest.ID() collector := newCollector() c.prm.statisticCallback = collector.Collect - c.setNeoFSAPIServer((*coreServer)(&c.c)) var tokenSession session2.Object tokenSession.SetID(uuid.New()) @@ -562,39 +321,15 @@ func TestClientStatistic_ObjectPut(t *testing.T) { err = writer.Close() require.NoError(t, err) - require.Equal(t, 2, collector.methods[stat.MethodObjectPut].requests) + require.Equal(t, 1, collector.methods[stat.MethodObjectPut].requests) + require.Equal(t, 1, collector.methods[stat.MethodObjectPutStream].requests) } func TestClientStatistic_ObjectDelete(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIDeleteObject = func(_ *client.Client, _ *v2object.DeleteRequest, _ ...client.CallOption) (*v2object.DeleteResponse, error) { - var resp v2object.DeleteResponse - var meta session.ResponseMetaHeader - var body v2object.DeleteResponseBody - var addr refs.Address - var objID refs.ObjectID - var contID = randRefsContainerID() - - objID.SetValue(randBytes(32)) - - addr.SetContainerID(contID) - addr.SetObjectID(&objID) - - body.SetTombstone(&addr) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testDeleteObjectServer + c := newClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -610,20 +345,10 @@ func TestClientStatistic_ObjectDelete(t *testing.T) { } func TestClientStatistic_ObjectGet(t *testing.T) { - t.Skip("need changes to api-go, to set `r client.MessageReader` in rpcapi.GetResponseReader") - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIGetObject = func(_ *client.Client, _ *v2object.GetRequest, _ ...client.CallOption) (*rpcapi.GetResponseReader, error) { - var resp rpcapi.GetResponseReader - - // todo: fill - - return &resp, nil - } - + var srv testGetObjectServer + c := newClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -634,37 +359,17 @@ func TestClientStatistic_ObjectGet(t *testing.T) { _, reader, err := c.ObjectGetInit(ctx, containerID, objectID, usr, prm) require.NoError(t, err) - - buff := make([]byte, 32) - _, err = reader.Read(buff) + _, err = io.Copy(io.Discard, reader) require.NoError(t, err) - require.Equal(t, 2, collector.methods[stat.MethodObjectGet].requests) + require.Equal(t, 1, collector.methods[stat.MethodObjectGet].requests) } func TestClientStatistic_ObjectHead(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIHeadObject = func(_ *client.Client, _ *v2object.HeadRequest, _ ...client.CallOption) (*v2object.HeadResponse, error) { - var resp v2object.HeadResponse - var meta session.ResponseMetaHeader - var body v2object.HeadResponseBody - var headerPart v2object.HeaderWithSignature - - body.SetHeaderPart(&headerPart) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testHeadObjectServer + c := newClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -680,20 +385,10 @@ func TestClientStatistic_ObjectHead(t *testing.T) { } func TestClientStatistic_ObjectRange(t *testing.T) { - t.Skip("need changes to api-go, to set `r client.MessageReader` in rpcapi.ObjectRangeResponseReader") - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIGetObjectRange = func(_ *client.Client, _ *v2object.GetRangeRequest, _ ...client.CallOption) (*rpcapi.ObjectRangeResponseReader, error) { - var resp rpcapi.ObjectRangeResponseReader - - // todo: fill - - return &resp, nil - } - + var srv testGetObjectPayloadRangeServer + c := newClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -704,38 +399,17 @@ func TestClientStatistic_ObjectRange(t *testing.T) { reader, err := c.ObjectRangeInit(ctx, containerID, objectID, 0, 1, usr, prm) require.NoError(t, err) - - buff := make([]byte, 32) - _, err = reader.Read(buff) + _, err = io.Copy(io.Discard, reader) require.NoError(t, err) - require.Equal(t, 2, collector.methods[stat.MethodObjectRange].requests) + require.Equal(t, 1, collector.methods[stat.MethodObjectRange].requests) } func TestClientStatistic_ObjectHash(t *testing.T) { usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIHashObjectRange = func(_ *client.Client, _ *v2object.GetRangeHashRequest, _ ...client.CallOption) (*v2object.GetRangeHashResponse, error) { - var resp v2object.GetRangeHashResponse - var meta session.ResponseMetaHeader - var body v2object.GetRangeHashResponseBody - - body.SetHashList([][]byte{ - randBytes(4), - }) - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testHashObjectPayloadRangesServer + c := newClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -752,20 +426,10 @@ func TestClientStatistic_ObjectHash(t *testing.T) { } func TestClientStatistic_ObjectSearch(t *testing.T) { - t.Skip("need changes to api-go, to set `r client.MessageReader` in rpcapi.SearchResponseReader") - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPISearchObjects = func(_ *client.Client, _ *v2object.SearchRequest, _ ...client.CallOption) (*rpcapi.SearchResponseReader, error) { - var resp rpcapi.SearchResponseReader - - // todo: fill - - return &resp, nil - } - + var srv testSearchObjectsServer + c := newClient(t, &srv) containerID := cidtest.ID() collector := newCollector() @@ -783,29 +447,13 @@ func TestClientStatistic_ObjectSearch(t *testing.T) { err = reader.Iterate(iterator) require.NoError(t, err) - require.Equal(t, 2, collector.methods[stat.MethodObjectSearch].requests) + require.Equal(t, 1, collector.methods[stat.MethodObjectSearch].requests) } func TestClientStatistic_AnnounceIntermediateTrust(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIAnnounceIntermediateResult = func(_ *client.Client, _ *reputation.AnnounceIntermediateResultRequest, _ ...client.CallOption) (*reputation.AnnounceIntermediateResultResponse, error) { - var resp reputation.AnnounceIntermediateResultResponse - var meta session.ResponseMetaHeader - var body reputation.AnnounceIntermediateResultResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testAnnounceIntermediateReputationServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -819,25 +467,9 @@ func TestClientStatistic_AnnounceIntermediateTrust(t *testing.T) { } func TestClientStatistic_MethodAnnounceLocalTrust(t *testing.T) { - usr := usertest.User() ctx := context.Background() - c := newClient(t, nil) - - rpcAPIAnnounceLocalTrust = func(_ *client.Client, _ *reputation.AnnounceLocalTrustRequest, _ ...client.CallOption) (*reputation.AnnounceLocalTrustResponse, error) { - var resp reputation.AnnounceLocalTrustResponse - var meta session.ResponseMetaHeader - var body reputation.AnnounceLocalTrustResponseBody - - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - - if err := signServiceMessage(usr, &resp, nil); err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } - - return &resp, nil - } - + var srv testAnnounceLocalTrustServer + c := newClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect diff --git a/client/container_test.go b/client/container_test.go index 4f370673..998e4ab7 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -2,14 +2,134 @@ package client import ( "context" + "fmt" "testing" + apiacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" + "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" "github.com/stretchr/testify/require" ) +type testPutContainerServer struct { + unimplementedNeoFSAPIServer +} + +func (x testPutContainerServer) putContainer(context.Context, apicontainer.PutRequest) (*apicontainer.PutResponse, error) { + id := cidtest.ID() + var idV2 refs.ContainerID + id.WriteToV2(&idV2) + var body apicontainer.PutResponseBody + body.SetContainerID(&idV2) + var resp apicontainer.PutResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testGetContainerServer struct { + unimplementedNeoFSAPIServer +} + +func (x testGetContainerServer) getContainer(context.Context, apicontainer.GetRequest) (*apicontainer.GetResponse, error) { + cnr := containertest.Container() + var cnrV2 apicontainer.Container + cnr.WriteToV2(&cnrV2) + var body apicontainer.GetResponseBody + body.SetContainer(&cnrV2) + var resp apicontainer.GetResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testListContainersServer struct { + unimplementedNeoFSAPIServer +} + +func (x testListContainersServer) listContainers(context.Context, apicontainer.ListRequest) (*apicontainer.ListResponse, error) { + var resp apicontainer.ListResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testDeleteContainerServer struct { + unimplementedNeoFSAPIServer +} + +func (x testDeleteContainerServer) deleteContainer(context.Context, apicontainer.DeleteRequest) (*apicontainer.DeleteResponse, error) { + var resp apicontainer.DeleteResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testGetEACLServer struct { + unimplementedNeoFSAPIServer +} + +func (x testGetEACLServer) getEACL(context.Context, apicontainer.GetExtendedACLRequest) (*apicontainer.GetExtendedACLResponse, error) { + var body apicontainer.GetExtendedACLResponseBody + body.SetEACL(new(apiacl.Table)) + var resp apicontainer.GetExtendedACLResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testSetEACLServer struct { + unimplementedNeoFSAPIServer +} + +func (x testSetEACLServer) setEACL(context.Context, apicontainer.SetExtendedACLRequest) (*apicontainer.SetExtendedACLResponse, error) { + var resp apicontainer.SetExtendedACLResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testAnnounceContainerSpaceServer struct { + unimplementedNeoFSAPIServer +} + +func (x testAnnounceContainerSpaceServer) announceContainerSpace(context.Context, apicontainer.AnnounceUsedSpaceRequest) (*apicontainer.AnnounceUsedSpaceResponse, error) { + var resp apicontainer.AnnounceUsedSpaceResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_Container(t *testing.T) { c := newClient(t, nil) ctx := context.Background() diff --git a/client/netmap.go b/client/netmap.go index 672dc86c..04ff1d8f 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -5,20 +5,12 @@ import ( "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" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/version" ) -var ( - // special variables for test purposes only, to overwrite real RPC calls. - rpcAPINetworkInfo = rpcapi.NetworkInfo - rpcAPILocalNodeInfo = rpcapi.LocalNodeInfo -) - // NetworkInfoExecutor describes methods to get network information. type NetworkInfoExecutor interface { NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (netmap.NetworkInfo, error) @@ -85,7 +77,7 @@ func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEnd cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPILocalNodeInfo(&c.c, &req, client.WithContext(ctx)) + return c.server.getNodeInfo(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2netmap.LocalNodeInfoResponse) @@ -163,7 +155,7 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (netmap.Ne cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPINetworkInfo(&c.c, &req, client.WithContext(ctx)) + return c.server.getNetworkInfo(ctx, req) } cc.result = func(r responseV2) { resp := r.(*v2netmap.NetworkInfoResponse) diff --git a/client/netmap_test.go b/client/netmap_test.go index 2eccf91b..a16045d5 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -7,6 +7,7 @@ import ( "testing" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -15,22 +16,22 @@ import ( "github.com/stretchr/testify/require" ) -type serverNetMap struct { +type testNetmapSnapshotServer struct { unimplementedNeoFSAPIServer errTransport error - signResponse bool + unsignedResponse bool - statusOK bool + statusFail bool - setNetMap bool - netMap v2netmap.NetMap + unsetNetMap bool + netMap v2netmap.NetMap signer neofscrypto.Signer } -func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { +func (x *testNetmapSnapshotServer) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { err := verifyServiceMessage(&req) if err != nil { return nil, err @@ -42,13 +43,13 @@ func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRe var body v2netmap.SnapshotResponseBody - if x.setNetMap { + if !x.unsetNetMap { body.SetNetMap(&x.netMap) } var meta session.ResponseMetaHeader - if !x.statusOK { + if x.statusFail { meta.SetStatus(statusErr.ErrorToV2()) } @@ -56,8 +57,12 @@ func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRe resp.SetBody(&body) resp.SetMetaHeader(&meta) - if x.signResponse { - err = signServiceMessage(x.signer, &resp, nil) + signer := x.signer + if signer == nil { + signer = neofscryptotest.Signer() + } + if !x.unsignedResponse { + err = signServiceMessage(signer, &resp, nil) if err != nil { panic(fmt.Sprintf("sign response: %v", err)) } @@ -66,11 +71,55 @@ func (x *serverNetMap) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRe return &resp, nil } +type testGetNetworkInfoServer struct { + unimplementedNeoFSAPIServer +} + +func (x testGetNetworkInfoServer) getNetworkInfo(context.Context, v2netmap.NetworkInfoRequest) (*v2netmap.NetworkInfoResponse, error) { + var netPrm v2netmap.NetworkParameter + netPrm.SetValue([]byte("any")) + var netCfg v2netmap.NetworkConfig + netCfg.SetParameters(netPrm) + var netInfo v2netmap.NetworkInfo + netInfo.SetNetworkConfig(&netCfg) + var body v2netmap.NetworkInfoResponseBody + body.SetNetworkInfo(&netInfo) + var resp v2netmap.NetworkInfoResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testGetNodeInfoServer struct { + unimplementedNeoFSAPIServer +} + +func (x testGetNodeInfoServer) getNodeInfo(context.Context, v2netmap.LocalNodeInfoRequest) (*v2netmap.LocalNodeInfoResponse, error) { + var nodeInfo v2netmap.NodeInfo + nodeInfo.SetPublicKey([]byte("any")) + nodeInfo.SetAddresses("any") + var body v2netmap.LocalNodeInfoResponseBody + body.SetVersion(new(refs.Version)) + body.SetNodeInfo(&nodeInfo) + var resp v2netmap.LocalNodeInfoResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_NetMapSnapshot(t *testing.T) { var err error var prm PrmNetMapSnapshot var res netmap.NetMap - var srv serverNetMap + var srv testNetmapSnapshotServer signer := neofscryptotest.Signer() @@ -88,23 +137,25 @@ func TestClient_NetMapSnapshot(t *testing.T) { srv.errTransport = nil // unsigned response + srv.unsignedResponse = true _, err = c.NetMapSnapshot(ctx, prm) require.Error(t, err) - - srv.signResponse = true + srv.unsignedResponse = false // failure error + srv.statusFail = true _, err = c.NetMapSnapshot(ctx, prm) require.Error(t, err) require.ErrorIs(t, err, apistatus.ErrServerInternal) - srv.statusOK = true + srv.statusFail = false // missing netmap field + srv.unsetNetMap = true _, err = c.NetMapSnapshot(ctx, prm) require.Error(t, err) - srv.setNetMap = true + srv.unsetNetMap = false // invalid network map var netMap netmap.NetMap diff --git a/client/object_delete.go b/client/object_delete.go index 51490a96..db5d45bf 100644 --- a/client/object_delete.go +++ b/client/object_delete.go @@ -8,8 +8,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -18,9 +16,6 @@ import ( ) var ( - // special variable for test purposes only, to overwrite real RPC calls. - rpcAPIDeleteObject = rpcapi.DeleteObject - // ErrNoSession indicates that session wasn't set in some Prm* structure. ErrNoSession = errors.New("session is not set") ) @@ -108,7 +103,7 @@ func (c *Client) ObjectDelete(ctx context.Context, containerID cid.ID, objectID return oid.ID{}, err } - resp, err := rpcAPIDeleteObject(&c.c, &req, client.WithContext(ctx)) + resp, err := c.server.deleteObject(ctx, req) if err != nil { return oid.ID{}, err } diff --git a/client/object_delete_test.go b/client/object_delete_test.go index cde6a380..507d6db9 100644 --- a/client/object_delete_test.go +++ b/client/object_delete_test.go @@ -2,13 +2,40 @@ package client import ( "context" + "fmt" "testing" + apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-api-go/v2/refs" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" "github.com/stretchr/testify/require" ) +type testDeleteObjectServer struct { + unimplementedNeoFSAPIServer +} + +func (x testDeleteObjectServer) deleteObject(context.Context, apiobject.DeleteRequest) (*apiobject.DeleteResponse, error) { + id := oidtest.ID() + var idV2 refs.ObjectID + id.WriteToV2(&idV2) + var addr refs.Address + addr.SetObjectID(&idV2) + var body apiobject.DeleteResponseBody + body.SetTombstone(&addr) + var resp apiobject.DeleteResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_ObjectDelete(t *testing.T) { t.Run("missing signer", func(t *testing.T) { c := newClient(t, nil) diff --git a/client/object_get.go b/client/object_get.go index 7efbd0c0..1f3fabf8 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -9,8 +9,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -19,13 +17,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) -var ( - // special variables for test purposes only, to overwrite real RPC calls. - rpcAPIGetObject = rpcapi.GetObject - rpcAPIHeadObject = rpcapi.HeadObject - rpcAPIGetObjectRange = rpcapi.GetObjectRange -) - // shared parameters of GET/HEAD/RANGE. type prmObjectRead struct { sessionContainer @@ -76,9 +67,7 @@ type PayloadReader struct { cancelCtxStream context.CancelFunc client *Client - stream interface { - Read(resp *v2object.GetResponse) error - } + stream getObjectResponseStream err error @@ -291,7 +280,7 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID ctx, cancel := context.WithCancel(ctx) - stream, err := rpcAPIGetObject(&c.c, &req, client.WithContext(ctx)) + stream, err := c.server.getObject(ctx, req) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) @@ -377,7 +366,7 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi return nil, err } - resp, err := rpcAPIHeadObject(&c.c, &req, client.WithContext(ctx)) + resp, err := c.server.headObject(ctx, req) if err != nil { err = fmt.Errorf("write request: %w", err) return nil, err @@ -428,9 +417,7 @@ type ObjectRangeReader struct { err error - stream interface { - Read(resp *v2object.GetRangeResponse) error - } + stream getObjectPayloadRangeResponseStream tailPayload []byte @@ -628,7 +615,7 @@ func (c *Client) ObjectRangeInit(ctx context.Context, containerID cid.ID, object ctx, cancel := context.WithCancel(ctx) - stream, err := rpcAPIGetObjectRange(&c.c, &req, client.WithContext(ctx)) + stream, err := c.server.getObjectPayloadRange(ctx, req) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) diff --git a/client/object_get_test.go b/client/object_get_test.go index 57606323..0e598659 100644 --- a/client/object_get_test.go +++ b/client/object_get_test.go @@ -2,14 +2,103 @@ package client import ( "context" + "fmt" + "io" "testing" + apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/stretchr/testify/require" ) +type testGetObjectResponseStream struct { + sent bool +} + +func (x *testGetObjectResponseStream) Read(resp *apiobject.GetResponse) error { + if x.sent { + return io.EOF + } + + var body apiobject.GetResponseBody + body.SetObjectPart(new(apiobject.GetObjectPartInit)) + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), resp, nil); err != nil { + return fmt.Errorf("sign response message: %w", err) + } + + x.sent = true + return nil +} + +type testGetObjectServer struct { + unimplementedNeoFSAPIServer + + stream testGetObjectResponseStream +} + +func (x *testGetObjectServer) getObject(context.Context, apiobject.GetRequest) (getObjectResponseStream, error) { + x.stream.sent = false + return &x.stream, nil +} + +type testGetObjectPayloadRangeServer struct { + unimplementedNeoFSAPIServer + + stream testGetObjectPayloadRangeResponseStream +} + +func (x *testGetObjectPayloadRangeServer) getObjectPayloadRange(_ context.Context, req apiobject.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { + x.stream.sent = false + x.stream.ln = req.GetBody().GetRange().GetLength() + return &x.stream, nil +} + +type testGetObjectPayloadRangeResponseStream struct { + ln uint64 + sent bool +} + +func (x *testGetObjectPayloadRangeResponseStream) Read(resp *apiobject.GetRangeResponse) error { + if x.sent { + return io.EOF + } + + var rngPart apiobject.GetRangePartChunk + rngPart.SetChunk(make([]byte, x.ln)) + var body apiobject.GetRangeResponseBody + body.SetRangePart(&rngPart) + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), resp, nil); err != nil { + return fmt.Errorf("sign response message: %w", err) + } + + x.sent = true + return nil +} + +type testHeadObjectServer struct { + unimplementedNeoFSAPIServer +} + +func (x *testHeadObjectServer) headObject(context.Context, apiobject.HeadRequest) (*apiobject.HeadResponse, error) { + var body apiobject.HeadResponseBody + body.SetHeaderPart(new(apiobject.HeaderWithSignature)) + var resp apiobject.HeadResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_Get(t *testing.T) { t.Run("missing signer", func(t *testing.T) { c := newClient(t, nil) diff --git a/client/object_hash.go b/client/object_hash.go index cc521534..a0ddf566 100644 --- a/client/object_hash.go +++ b/client/object_hash.go @@ -7,8 +7,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -16,11 +14,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) -var ( - // special variable for test purposes only, to overwrite real RPC calls. - rpcAPIHashObjectRange = rpcapi.HashObjectRange -) - // PrmObjectHash groups parameters of ObjectHash operation. type PrmObjectHash struct { sessionContainer @@ -153,7 +146,7 @@ func (c *Client) ObjectHash(ctx context.Context, containerID cid.ID, objectID oi return nil, err } - resp, err := rpcAPIHashObjectRange(&c.c, &req, client.WithContext(ctx)) + resp, err := c.server.hashObjectPayloadRanges(ctx, req) if err != nil { err = fmt.Errorf("write request: %w", err) return nil, err diff --git a/client/object_hash_test.go b/client/object_hash_test.go index d2ebd704..4c2a1218 100644 --- a/client/object_hash_test.go +++ b/client/object_hash_test.go @@ -2,14 +2,33 @@ package client import ( "context" + "fmt" "testing" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" "github.com/stretchr/testify/require" ) +type testHashObjectPayloadRangesServer struct { + unimplementedNeoFSAPIServer +} + +func (x *testHashObjectPayloadRangesServer) hashObjectPayloadRanges(context.Context, v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) { + var body v2object.GetRangeHashResponseBody + body.SetHashList([][]byte{{1}}) + var resp v2object.GetRangeHashResponse + resp.SetBody(&body) + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + func TestClient_ObjectHash(t *testing.T) { c := newClient(t, nil) diff --git a/client/object_put.go b/client/object_put.go index 1b50bdfd..6279a3e3 100644 --- a/client/object_put.go +++ b/client/object_put.go @@ -8,8 +8,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/bearer" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -23,16 +21,9 @@ var ( ErrNoSessionExplicitly = errors.New("session was removed explicitly") ) -var ( - // special variable for test purposes only, to overwrite real RPC calls. - rpcAPIPutObject = func(cli *client.Client, r *v2object.PutResponse, o ...client.CallOption) (objectWriter, error) { - return rpcapi.PutObject(cli, r, o...) - } -) - type objectWriter interface { Write(*v2object.PutRequest) error - Close() error + Close() (*v2object.PutResponse, error) } // shortStatisticCallback is a shorter version of [stat.OperationCallback] which is calling from [client.Client]. @@ -86,7 +77,6 @@ type DefaultObjectWriter struct { chunkCalled bool - respV2 v2object.PutResponse req v2object.PutRequest partInit v2object.PutObjectPartInit partChunk v2object.PutObjectPartChunk @@ -183,8 +173,8 @@ func (x *DefaultObjectWriter) Write(chunk []byte) (n int, err error) { x.err = x.stream.Write(&x.req) if x.err != nil { if errors.Is(x.err, io.EOF) { - _ = x.stream.Close() - x.err = x.client.processResponse(&x.respV2) + resp, _ := x.stream.Close() + x.err = x.client.processResponse(resp) x.streamClosed = true x.cancelCtxStream() } @@ -239,17 +229,19 @@ func (x *DefaultObjectWriter) Close() error { return x.err } - if x.err = x.stream.Close(); x.err != nil { + var resp *v2object.PutResponse + resp, x.err = x.stream.Close() + if x.err != nil { return x.err } - if x.err = x.client.processResponse(&x.respV2); x.err != nil { + if x.err = x.client.processResponse(resp); x.err != nil { return x.err } const fieldID = "ID" - idV2 := x.respV2.GetBody().GetObjectID() + idV2 := resp.GetBody().GetObjectID() if idV2 == nil { x.err = newErrMissingResponseField(fieldID) return x.err @@ -297,7 +289,7 @@ func (c *Client) ObjectPutInit(ctx context.Context, hdr object.Object, signer us } ctx, cancel := context.WithCancel(ctx) - stream, err := rpcAPIPutObject(&c.c, &w.respV2, client.WithContext(ctx)) + stream, err := c.server.putObject(ctx) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) diff --git a/client/object_put_test.go b/client/object_put_test.go index ad6d9c76..431a6483 100644 --- a/client/object_put_test.go +++ b/client/object_put_test.go @@ -3,28 +3,26 @@ package client import ( "context" "errors" + "fmt" "io" "testing" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/object" - "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/nspcc-dev/neofs-sdk-go/version" "github.com/stretchr/testify/require" ) -type testPutStreamAccessDenied struct { - resp *v2object.PutResponse - signer user.Signer - t *testing.T +type testPutObjectStream struct { + denyAccess bool } -func (t *testPutStreamAccessDenied) Write(req *v2object.PutRequest) error { +func (t *testPutObjectStream) Write(req *v2object.PutRequest) error { switch req.GetBody().GetObjectPart().(type) { case *v2object.PutObjectPartInit: return nil @@ -35,29 +33,45 @@ func (t *testPutStreamAccessDenied) Write(req *v2object.PutRequest) error { } } -func (t *testPutStreamAccessDenied) Close() error { +func (t *testPutObjectStream) Close() (*v2object.PutResponse, error) { m := new(v2session.ResponseMetaHeader) var v refs.Version version.Current().WriteToV2(&v) m.SetVersion(&v) - m.SetStatus(apistatus.ErrObjectAccessDenied.ErrorToV2()) + if t.denyAccess { + m.SetStatus(apistatus.ErrObjectAccessDenied.ErrorToV2()) + } + + var resp v2object.PutResponse + resp.SetMetaHeader(m) + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} - t.resp.SetMetaHeader(m) - require.NoError(t.t, signServiceMessage(t.signer, t.resp, nil)) +type testPutObjectServer struct { + unimplementedNeoFSAPIServer - return nil + stream testPutObjectStream +} + +func (x *testPutObjectServer) putObject(context.Context) (objectWriter, error) { + return &x.stream, nil } func TestClient_ObjectPutInit(t *testing.T) { t.Run("EOF-on-status-return", func(t *testing.T) { - c := newClient(t, nil) - usr := usertest.User() - - rpcAPIPutObject = func(_ *client.Client, r *v2object.PutResponse, _ ...client.CallOption) (objectWriter, error) { - return &testPutStreamAccessDenied{resp: r, signer: usr, t: t}, nil + srv := testPutObjectServer{ + stream: testPutObjectStream{ + denyAccess: true, + }, } + c := newClient(t, &srv) + usr := usertest.User() w, err := c.ObjectPutInit(context.Background(), object.Object{}, usr, PrmObjectPutInit{}) require.NoError(t, err) diff --git a/client/object_search.go b/client/object_search.go index a3641bc6..5be1fd41 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -9,8 +9,6 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -19,11 +17,6 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) -var ( - // special variable for test purposes only, to overwrite real RPC calls. - rpcAPISearchObjects = rpcapi.SearchObjects -) - // PrmObjectSearch groups optional parameters of ObjectSearch operation. type PrmObjectSearch struct { sessionContainer @@ -68,10 +61,8 @@ type ObjectListReader struct { client *Client cancelCtxStream context.CancelFunc err error - stream interface { - Read(resp *v2object.SearchResponse) error - } - tail []v2refs.ObjectID + stream searchObjectsResponseStream + tail []v2refs.ObjectID statisticCallback shortStatisticCallback } @@ -228,7 +219,7 @@ func (c *Client) ObjectSearchInit(ctx context.Context, containerID cid.ID, signe var r ObjectListReader ctx, r.cancelCtxStream = context.WithCancel(ctx) - r.stream, err = rpcAPISearchObjects(&c.c, &req, client.WithContext(ctx)) + r.stream, err = c.server.searchObjects(ctx, req) if err != nil { err = fmt.Errorf("open stream: %w", err) return nil, err diff --git a/client/object_search_test.go b/client/object_search_test.go index 60f867aa..66f38c9d 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -3,27 +3,29 @@ package client import ( "context" "errors" + "fmt" "io" "testing" + apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" "github.com/nspcc-dev/neofs-api-go/v2/refs" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) func TestObjectSearch(t *testing.T) { ids := oidtest.IDs(20) - p, resp := testListReaderResponse() - buf := make([]oid.ID, 2) - checkRead := func(t *testing.T, expected []oid.ID, expectedErr error) { - n, err := resp.Read(buf) + checkRead := func(t *testing.T, r *ObjectListReader, expected []oid.ID, expectedErr error) { + n, err := r.Read(buf) if expectedErr == nil { require.NoError(t, err) require.True(t, len(expected) == len(buf), "expected the same length") @@ -36,90 +38,79 @@ func TestObjectSearch(t *testing.T) { require.Equal(t, expected, buf[:len(expected)]) } - // nil panic - require.Panics(t, func() { - _, _ = resp.Read(nil) - }) - // no data - resp.stream = newSearchStream(p, io.EOF, []oid.ID{}) - checkRead(t, []oid.ID{}, io.EOF) + stream := newTestSearchObjectsStream(t, []oid.ID{}) + checkRead(t, stream, []oid.ID{}, io.EOF) + + stream = newTestSearchObjectsStream(t, ids[:3], ids[3:6], ids[6:7], nil, ids[7:8]) // both ID fetched - resp.stream = newSearchStream(p, nil, ids[:3]) - checkRead(t, ids[:2], nil) + checkRead(t, stream, ids[:2], nil) // one ID cached, second fetched - resp.stream = newSearchStream(p, nil, ids[3:6]) - checkRead(t, ids[2:4], nil) + checkRead(t, stream, ids[2:4], nil) // both ID cached - resp.stream = nil // shouldn't be called, panic if so - checkRead(t, ids[4:6], nil) + streamCp := stream.stream + stream.stream = nil // shouldn't be called, panic if so + checkRead(t, stream, ids[4:6], nil) + stream.stream = streamCp // both ID fetched in 2 requests, with empty one in the middle - resp.stream = newSearchStream(p, nil, ids[6:7], nil, ids[7:8]) - checkRead(t, ids[6:8], nil) + checkRead(t, stream, ids[6:8], nil) // read from tail multiple times - resp.stream = newSearchStream(p, nil, ids[8:11]) + stream = newTestSearchObjectsStream(t, ids[8:11]) buf = buf[:1] - checkRead(t, ids[8:9], nil) - checkRead(t, ids[9:10], nil) - checkRead(t, ids[10:11], nil) + checkRead(t, stream, ids[8:9], nil) + checkRead(t, stream, ids[9:10], nil) + checkRead(t, stream, ids[10:11], nil) // handle EOF buf = buf[:2] - resp.stream = newSearchStream(p, io.EOF, ids[11:12]) - checkRead(t, ids[11:12], io.EOF) + stream = newTestSearchObjectsStream(t, ids[11:12]) + checkRead(t, stream, ids[11:12], io.EOF) } func TestObjectIterate(t *testing.T) { ids := oidtest.IDs(3) t.Run("no objects", func(t *testing.T) { - p, resp := testListReaderResponse() - - resp.stream = newSearchStream(p, io.EOF, []oid.ID{}) + stream := newTestSearchObjectsStream(t) var actual []oid.ID - require.NoError(t, resp.Iterate(func(id oid.ID) bool { + require.NoError(t, stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) return false })) require.Len(t, actual, 0) }) t.Run("iterate all sequence", func(t *testing.T) { - p, resp := testListReaderResponse() - - resp.stream = newSearchStream(p, io.EOF, ids[0:2], nil, ids[2:3]) + stream := newTestSearchObjectsStream(t, ids[0:2], nil, ids[2:3]) var actual []oid.ID - require.NoError(t, resp.Iterate(func(id oid.ID) bool { + require.NoError(t, stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) return false })) require.Equal(t, ids[:3], actual) }) t.Run("stop by return value", func(t *testing.T) { - p, resp := testListReaderResponse() - + stream := newTestSearchObjectsStream(t, ids) var actual []oid.ID - resp.stream = &singleStreamResponder{signer: p, idList: [][]oid.ID{ids}} - require.NoError(t, resp.Iterate(func(id oid.ID) bool { + require.NoError(t, stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) return len(actual) == 2 })) require.Equal(t, ids[:2], actual) }) t.Run("stop after error", func(t *testing.T) { - p, resp := testListReaderResponse() expectedErr := errors.New("test error") - resp.stream = newSearchStream(p, expectedErr, ids[:2]) + stream := newTestSearchObjectsStreamWithEndErr(t, expectedErr, ids[:2]) var actual []oid.ID - err := resp.Iterate(func(id oid.ID) bool { + err := stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) return false }) @@ -137,35 +128,37 @@ func TestClient_ObjectSearch(t *testing.T) { }) } -func testListReaderResponse() (neofscrypto.Signer, *ObjectListReader) { - return neofscryptotest.Signer(), &ObjectListReader{ - cancelCtxStream: func() {}, - client: &Client{}, - tail: nil, +func newTestSearchObjectsStreamWithEndErr(t *testing.T, endError error, idList ...[]oid.ID) *ObjectListReader { + usr := usertest.User() + srv := testSearchObjectsServer{ + stream: testSearchObjectsResponseStream{ + signer: usr, + endError: endError, + idList: idList, + }, } + stream, err := newClient(t, &srv).ObjectSearchInit(context.Background(), cidtest.ID(), usr, PrmObjectSearch{}) + require.NoError(t, err) + return stream } -func newSearchStream(signer neofscrypto.Signer, endError error, idList ...[]oid.ID) *singleStreamResponder { - return &singleStreamResponder{ - signer: signer, - endError: endError, - idList: idList, - } +func newTestSearchObjectsStream(t *testing.T, idList ...[]oid.ID) *ObjectListReader { + return newTestSearchObjectsStreamWithEndErr(t, nil, idList...) } -type singleStreamResponder struct { +type testSearchObjectsResponseStream struct { signer neofscrypto.Signer n int endError error idList [][]oid.ID } -func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error { - if s.n >= len(s.idList) { +func (s *testSearchObjectsResponseStream) Read(resp *v2object.SearchResponse) error { + if len(s.idList) == 0 || s.n == len(s.idList) { if s.endError != nil { return s.endError } - return ErrUnexpectedReadCall + return io.EOF } var body v2object.SearchResponseBody @@ -179,11 +172,26 @@ func (s *singleStreamResponder) Read(resp *v2object.SearchResponse) error { } resp.SetBody(&body) + signer := s.signer + if signer == nil { + signer = neofscryptotest.Signer() + } err := signServiceMessage(s.signer, resp, nil) if err != nil { - return err + return fmt.Errorf("sign response message: %w", err) } s.n++ return nil } + +type testSearchObjectsServer struct { + unimplementedNeoFSAPIServer + + stream testSearchObjectsResponseStream +} + +func (x *testSearchObjectsServer) searchObjects(context.Context, apiobject.SearchRequest) (searchObjectsResponseStream, error) { + x.stream.n = 0 + return &x.stream, nil +} diff --git a/client/reputation.go b/client/reputation.go index a0e70e3b..b86d26ce 100644 --- a/client/reputation.go +++ b/client/reputation.go @@ -4,18 +4,10 @@ import ( "context" v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" "github.com/nspcc-dev/neofs-sdk-go/reputation" "github.com/nspcc-dev/neofs-sdk-go/stat" ) -var ( - // special variables for test purposes only, to overwrite real RPC calls. - rpcAPIAnnounceIntermediateResult = rpcapi.AnnounceIntermediateResult - rpcAPIAnnounceLocalTrust = rpcapi.AnnounceLocalTrust -) - // PrmAnnounceLocalTrust groups optional parameters of AnnounceLocalTrust operation. type PrmAnnounceLocalTrust struct { prmCommonMeta @@ -77,7 +69,7 @@ func (c *Client) AnnounceLocalTrust(ctx context.Context, epoch uint64, trusts [] cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIAnnounceLocalTrust(&c.c, &req, client.WithContext(ctx)) + return c.server.announceLocalTrust(ctx, req) } // process call @@ -149,7 +141,7 @@ func (c *Client) AnnounceIntermediateTrust(ctx context.Context, epoch uint64, tr cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return rpcAPIAnnounceIntermediateResult(&c.c, &req, client.WithContext(ctx)) + return c.server.announceIntermediateReputation(ctx, req) } // process call diff --git a/client/reputation_test.go b/client/reputation_test.go new file mode 100644 index 00000000..cbe7c716 --- /dev/null +++ b/client/reputation_test.go @@ -0,0 +1,37 @@ +package client + +import ( + "context" + "fmt" + + "github.com/nspcc-dev/neofs-api-go/v2/reputation" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" +) + +type testAnnounceIntermediateReputationServer struct { + unimplementedNeoFSAPIServer +} + +func (x testAnnounceIntermediateReputationServer) announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { + var resp reputation.AnnounceIntermediateResultResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} + +type testAnnounceLocalTrustServer struct { + unimplementedNeoFSAPIServer +} + +func (x testAnnounceLocalTrustServer) announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { + var resp reputation.AnnounceLocalTrustResponse + + if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + return nil, fmt.Errorf("sign response message: %w", err) + } + + return &resp, nil +} diff --git a/client/session_test.go b/client/session_test.go index 052596a1..ecae5557 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -6,27 +6,36 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/session" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) -type sessionAPIServer struct { +type testCreateSessionServer struct { unimplementedNeoFSAPIServer signer neofscrypto.Signer - id []byte - key []byte + unsetID bool + unsetKey bool } -func (m sessionAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { +func (m testCreateSessionServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { var body session.CreateResponseBody - body.SetID(m.id) - body.SetSessionKey(m.key) + if !m.unsetID { + body.SetID([]byte{1}) + } + if !m.unsetKey { + body.SetSessionKey([]byte{2}) + } var resp session.CreateResponse resp.SetBody(&body) - if err := signServiceMessage(m.signer, &resp, nil); err != nil { + signer := m.signer + if signer == nil { + signer = neofscryptotest.Signer() + } + if err := signServiceMessage(signer, &resp, nil); err != nil { return nil, err } @@ -43,7 +52,7 @@ func TestClient_SessionCreate(t *testing.T) { prmSessionCreate.SetExp(1) t.Run("missing session id", func(t *testing.T) { - c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, key: []byte{1}}) + c.setNeoFSAPIServer(&testCreateSessionServer{signer: usr, unsetID: true}) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) @@ -52,7 +61,7 @@ func TestClient_SessionCreate(t *testing.T) { }) t.Run("missing session key", func(t *testing.T) { - c.setNeoFSAPIServer(&sessionAPIServer{signer: usr, id: []byte{1}}) + c.setNeoFSAPIServer(&testCreateSessionServer{signer: usr, unsetKey: true}) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) diff --git a/client/util_test.go b/client/util_test.go index cdd2614c..ada23b23 100644 --- a/client/util_test.go +++ b/client/util_test.go @@ -4,7 +4,11 @@ import ( "context" "errors" + "github.com/nspcc-dev/neofs-api-go/v2/accounting" + "github.com/nspcc-dev/neofs-api-go/v2/container" "github.com/nspcc-dev/neofs-api-go/v2/netmap" + "github.com/nspcc-dev/neofs-api-go/v2/object" + "github.com/nspcc-dev/neofs-api-go/v2/reputation" "github.com/nspcc-dev/neofs-api-go/v2/session" ) @@ -13,6 +17,63 @@ type unimplementedNeoFSAPIServer struct{} func (unimplementedNeoFSAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { return nil, errors.New("unimplemented") } +func (unimplementedNeoFSAPIServer) getBalance(context.Context, accounting.BalanceRequest) (*accounting.BalanceResponse, error) { + return nil, errors.New("unimplemented") +} func (unimplementedNeoFSAPIServer) netMapSnapshot(context.Context, netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { return nil, errors.New("unimplemented") } +func (unimplementedNeoFSAPIServer) getNetworkInfo(context.Context, netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) getNodeInfo(context.Context, netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) putContainer(context.Context, container.PutRequest) (*container.PutResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) getContainer(context.Context, container.GetRequest) (*container.GetResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) deleteContainer(context.Context, container.DeleteRequest) (*container.DeleteResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) listContainers(context.Context, container.ListRequest) (*container.ListResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) getEACL(context.Context, container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) setEACL(context.Context, container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) announceContainerSpace(context.Context, container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) putObject(context.Context) (objectWriter, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) deleteObject(context.Context, object.DeleteRequest) (*object.DeleteResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) hashObjectPayloadRanges(context.Context, object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) headObject(context.Context, object.HeadRequest) (*object.HeadResponse, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) getObject(context.Context, object.GetRequest) (getObjectResponseStream, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) getObjectPayloadRange(context.Context, object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { + return nil, errors.New("unimplemented") +} +func (unimplementedNeoFSAPIServer) searchObjects(context.Context, object.SearchRequest) (searchObjectsResponseStream, error) { + return nil, errors.New("unimplemented") +} From b75db12121973c8ed9b516294bd6bc6bd0f29e46 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Wed, 20 Nov 2024 21:32:52 +0300 Subject: [PATCH 10/16] client: Inline gRPC mechanics from neofs-api-go lib One step closer to github.com/nspcc-dev/neofs-api-go/v2 module deprecation. Previously, for the `Client`, network connecting, sending a request and receiving a response was almost completely hidden in the lib. Now the code is built into the client itself. The test framework used to be too abstract and far from the production version. Although the gRPC lib is vendored, it covers most of the `Client` mechanics. In fact, proper testing requires emulating the behavior of the client-server communication as realistically as possible. This reworks the tests by abstracting only the network transport via in-memory buffer implementation supplied by gRPC lib itself. From now unit tests should provide protocol-declared RPC handlers exactly like NeoFS storage nodes does. This automatically includes gRPC layer to testing, making the client implementation more resilient to corresponding module updates. Signed-off-by: Leonard Lyubich --- client/accounting.go | 11 +- client/accounting_test.go | 31 ++- client/api.go | 294 ----------------------------- client/client.go | 94 ++++++--- client/client_test.go | 38 +++- client/common.go | 51 ++++- client/container.go | 71 ++++++- client/container_statistic_test.go | 43 ++--- client/container_test.go | 135 ++++++++----- client/netmap.go | 33 +++- client/netmap_test.go | 173 ++++++++++------- client/object_delete.go | 11 +- client/object_delete_test.go | 34 ++-- client/object_get.go | 93 ++++++--- client/object_get_test.go | 108 +++++------ client/object_hash.go | 10 +- client/object_hash_test.go | 24 ++- client/object_put.go | 66 +++++-- client/object_put_test.go | 82 ++++---- client/object_replicate.go | 108 +++-------- client/object_replicate_test.go | 58 +++--- client/object_search.go | 41 +++- client/object_search_test.go | 102 +++++----- client/object_test.go | 12 ++ client/reputation.go | 21 ++- client/reputation_test.go | 37 +++- client/session.go | 11 +- client/session_test.go | 38 ++-- client/util_test.go | 79 -------- 29 files changed, 976 insertions(+), 933 deletions(-) create mode 100644 client/object_test.go delete mode 100644 client/util_test.go diff --git a/client/accounting.go b/client/accounting.go index ce5375e8..8a1f3e2e 100644 --- a/client/accounting.go +++ b/client/accounting.go @@ -4,6 +4,7 @@ import ( "context" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" + protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/accounting" "github.com/nspcc-dev/neofs-sdk-go/stat" @@ -67,7 +68,15 @@ func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (accounting. cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.getBalance(ctx, req) + resp, err := c.accounting.Balance(ctx, req.ToGRPCMessage().(*protoaccounting.BalanceRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2accounting.BalanceResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2accounting.BalanceResponse) diff --git a/client/accounting_test.go b/client/accounting_test.go index 2946f972..0970e4b2 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -5,30 +5,41 @@ import ( "fmt" "testing" - apiaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" + v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" + protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/stretchr/testify/require" ) +// returns Client of Accounting service provided by given server. +func newTestAccountingClient(t testing.TB, srv protoaccounting.AccountingServiceServer) *Client { + return newClient(t, testService{desc: &protoaccounting.AccountingService_ServiceDesc, impl: srv}) +} + type testGetBalanceServer struct { - unimplementedNeoFSAPIServer + protoaccounting.UnimplementedAccountingServiceServer } -func (x testGetBalanceServer) getBalance(context.Context, apiaccounting.BalanceRequest) (*apiaccounting.BalanceResponse, error) { - var body apiaccounting.BalanceResponseBody - body.SetBalance(new(apiaccounting.Decimal)) - var resp apiaccounting.BalanceResponse - resp.SetBody(&body) +func (x *testGetBalanceServer) Balance(context.Context, *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { + resp := protoaccounting.BalanceResponse{ + Body: &protoaccounting.BalanceResponse_Body{ + Balance: new(protoaccounting.Decimal), + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 v2accounting.BalanceResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoaccounting.BalanceResponse), nil } func TestClient_BalanceGet(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) ctx := context.Background() t.Run("missing", func(t *testing.T) { diff --git a/client/api.go b/client/api.go index eb7daee5..0ba5e0b7 100644 --- a/client/api.go +++ b/client/api.go @@ -1,304 +1,10 @@ package client import ( - "context" "fmt" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - rpcapi "github.com/nspcc-dev/neofs-api-go/v2/rpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/common" - "github.com/nspcc-dev/neofs-api-go/v2/session" ) -type getObjectResponseStream interface { - Read(*object.GetResponse) error -} - -type getObjectPayloadRangeResponseStream interface { - Read(*object.GetRangeResponse) error -} - -type searchObjectsResponseStream interface { - Read(*object.SearchResponse) error -} - -// interface of NeoFS API server. Exists for test purposes only. -type neoFSAPIServer interface { - createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) - getBalance(context.Context, accounting.BalanceRequest) (*accounting.BalanceResponse, error) - netMapSnapshot(context.Context, netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) - getNetworkInfo(context.Context, netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) - getNodeInfo(context.Context, netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) - putContainer(context.Context, container.PutRequest) (*container.PutResponse, error) - getContainer(context.Context, container.GetRequest) (*container.GetResponse, error) - deleteContainer(context.Context, container.DeleteRequest) (*container.DeleteResponse, error) - listContainers(context.Context, container.ListRequest) (*container.ListResponse, error) - getEACL(context.Context, container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) - setEACL(context.Context, container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) - announceContainerSpace(context.Context, container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) - announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) - announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) - putObject(context.Context) (objectWriter, error) - deleteObject(context.Context, object.DeleteRequest) (*object.DeleteResponse, error) - hashObjectPayloadRanges(context.Context, object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) - headObject(context.Context, object.HeadRequest) (*object.HeadResponse, error) - getObject(context.Context, object.GetRequest) (getObjectResponseStream, error) - getObjectPayloadRange(context.Context, object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) - searchObjects(context.Context, object.SearchRequest) (searchObjectsResponseStream, 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 netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { - resp, err := rpcapi.NetMapSnapshot((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - - return resp, nil -} - -// executes NetmapService.NetworkInfo RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getNetworkInfo(ctx context.Context, req netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { - resp, err := rpcapi.NetworkInfo((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes NetmapService.LocalNodeInfo RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getNodeInfo(ctx context.Context, req netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { - resp, err := rpcapi.LocalNodeInfo((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes AccountingService.Balance RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getBalance(ctx context.Context, req accounting.BalanceRequest) (*accounting.BalanceResponse, error) { - resp, err := rpcapi.Balance((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes SessionService.Create RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) createSession(ctx context.Context, req session.CreateRequest) (*session.CreateResponse, error) { - resp, err := rpcapi.CreateSession((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - - return resp, nil -} - -// executes ContainerService.Put RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) putContainer(ctx context.Context, req container.PutRequest) (*container.PutResponse, error) { - resp, err := rpcapi.PutContainer((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ContainerService.Get RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getContainer(ctx context.Context, req container.GetRequest) (*container.GetResponse, error) { - resp, err := rpcapi.GetContainer((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ContainerService.Delete RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) deleteContainer(ctx context.Context, req container.DeleteRequest) (*container.DeleteResponse, error) { - // rpcapi.DeleteContainer returns wrong response type - resp := new(container.DeleteResponse) - err := client.SendUnary((*client.Client)(x), - common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "Delete"), - &req, resp, client.WithContext(ctx)) - if err != nil { - return nil, err - } - return resp, nil -} - -// executes ContainerService.List RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) listContainers(ctx context.Context, req container.ListRequest) (*container.ListResponse, error) { - resp, err := rpcapi.ListContainers((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ContainerService.GetExtendedACL RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getEACL(ctx context.Context, req container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { - resp, err := rpcapi.GetEACL((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ContainerService.SetExtendedACL RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) setEACL(ctx context.Context, req container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { - // rpcapi.SetEACL returns wrong response type - resp := new(container.SetExtendedACLResponse) - err := client.SendUnary((*client.Client)(x), - common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "SetExtendedACL"), - &req, resp, client.WithContext(ctx)) - if err != nil { - return nil, err - } - return resp, nil -} - -// executes ContainerService.AnnounceUsedSpace RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) announceContainerSpace(ctx context.Context, req container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { - // rpcapi.AnnounceUsedSpace returns wrong response type - resp := new(container.AnnounceUsedSpaceResponse) - err := client.SendUnary((*client.Client)(x), - common.CallMethodInfoUnary("neo.fs.v2.container.ContainerService", "AnnounceUsedSpace"), - &req, resp, client.WithContext(ctx)) - if err != nil { - return nil, err - } - return resp, nil -} - -// executes ReputationService.AnnounceIntermediateResult RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) announceIntermediateReputation(ctx context.Context, req reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { - resp, err := rpcapi.AnnounceIntermediateResult((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ReputationService.AnnounceLocalTrust RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) announceLocalTrust(ctx context.Context, req reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { - resp, err := rpcapi.AnnounceLocalTrust((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -type corePutObjectStream struct { - reqWriter *rpcapi.PutRequestWriter - resp object.PutResponse -} - -func (x *corePutObjectStream) Write(req *object.PutRequest) error { - return x.reqWriter.Write(req) -} - -func (x *corePutObjectStream) Close() (*object.PutResponse, error) { - if err := x.reqWriter.Close(); err != nil { - return nil, err - } - return &x.resp, nil -} - -// executes ObjectService.Put RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) putObject(ctx context.Context) (objectWriter, error) { - var err error - var stream corePutObjectStream - stream.reqWriter, err = rpcapi.PutObject((*client.Client)(x), &stream.resp, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return &stream, nil -} - -// executes ObjectService.Delete RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) deleteObject(ctx context.Context, req object.DeleteRequest) (*object.DeleteResponse, error) { - resp, err := rpcapi.DeleteObject((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ObjectService.GetRangeHash RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) hashObjectPayloadRanges(ctx context.Context, req object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { - resp, err := rpcapi.HashObjectRange((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ObjectService.Head RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) headObject(ctx context.Context, req object.HeadRequest) (*object.HeadResponse, error) { - resp, err := rpcapi.HeadObject((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return resp, nil -} - -// executes ObjectService.Get RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getObject(ctx context.Context, req object.GetRequest) (getObjectResponseStream, error) { - stream, err := rpcapi.GetObject((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return stream, nil -} - -// executes ObjectService.GetRange RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) getObjectPayloadRange(ctx context.Context, req object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { - stream, err := rpcapi.GetObjectRange((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return stream, nil -} - -// executes ObjectService.Search RPC declared in NeoFS API protocol -// using underlying client.Client. -func (x *coreServer) searchObjects(ctx context.Context, req object.SearchRequest) (searchObjectsResponseStream, error) { - stream, err := rpcapi.SearchObjects((*client.Client)(x), &req, client.WithContext(ctx)) - if err != nil { - return nil, rpcErr(err) - } - return stream, nil -} diff --git a/client/client.go b/client/client.go index cd932380..337087cf 100644 --- a/client/client.go +++ b/client/client.go @@ -4,15 +4,24 @@ import ( "context" "crypto/tls" "fmt" + "net" "sync" "time" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" + protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" "github.com/nspcc-dev/neofs-sdk-go/internal/uriutil" "github.com/nspcc-dev/neofs-sdk-go/stat" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" ) const ( @@ -52,14 +61,21 @@ const ( type Client struct { prm PrmInit - c client.Client - - server neoFSAPIServer + conn *grpc.ClientConn + // based on conn + accounting protoaccounting.AccountingServiceClient + container protocontainer.ContainerServiceClient + netmap protonetmap.NetmapServiceClient + object protoobject.ObjectServiceClient + reputation protoreputation.ReputationServiceClient + session protosession.SessionServiceClient endpoint string nodeKey []byte buffers *sync.Pool + + streamTimeout time.Duration } // New creates an instance of Client initialized with the given parameters. @@ -130,8 +146,9 @@ func (c *Client) Dial(prm PrmDial) error { if prm.streamTimeout <= 0 { return ErrNonPositiveTimeout } + c.streamTimeout = prm.streamTimeout } else { - prm.streamTimeout = 10 * time.Second + c.streamTimeout = 10 * time.Second } addr, withTLS, err := uriutil.Parse(prm.endpoint) @@ -139,26 +156,40 @@ func (c *Client) Dial(prm PrmDial) error { return fmt.Errorf("invalid server URI: %w", err) } - var tlsCfg *tls.Config - if withTLS { - if tlsCfg = prm.tlsConfig; tlsCfg == nil { - tlsCfg = new(tls.Config) - } + if prm.parentCtx == nil { + prm.parentCtx = context.Background() } - c.c = *client.New( - client.WithNetworkAddress(addr), - client.WithTLSCfg(tlsCfg), - client.WithDialTimeout(prm.timeoutDial), - client.WithRWTimeout(prm.streamTimeout), - ) + ctx, cancel := context.WithTimeout(prm.parentCtx, prm.timeoutDial) + defer cancel() - c.setNeoFSAPIServer((*coreServer)(&c.c)) + var creds credentials.TransportCredentials + if withTLS { + creds = credentials.NewTLS(prm.tlsConfig) + } else { + creds = insecure.NewCredentials() + } - if prm.parentCtx == nil { - prm.parentCtx = context.Background() + // TODO: copy-pasted from neofs-api-go. Replace deprecated func with + // grpc.NewClient. This was not done because some options are no longer + // supported. Review carefully and make a proper transition. + //nolint:staticcheck + if c.conn, err = grpc.DialContext(ctx, addr, + grpc.WithTransportCredentials(creds), + grpc.WithReturnConnectionError(), + grpc.FailOnNonTempDialError(true), + grpc.WithContextDialer(prm.customConnFunc), + ); err != nil { + return fmt.Errorf("gRPC dial: %w", err) } + c.accounting = protoaccounting.NewAccountingServiceClient(c.conn) + c.container = protocontainer.NewContainerServiceClient(c.conn) + c.netmap = protonetmap.NewNetmapServiceClient(c.conn) + c.object = protoobject.NewObjectServiceClient(c.conn) + c.reputation = protoreputation.NewReputationServiceClient(c.conn) + c.session = protosession.NewSessionServiceClient(c.conn) + endpointInfo, err := c.EndpointInfo(prm.parentCtx, PrmEndpointInfo{}) if err != nil { return err @@ -169,13 +200,10 @@ func (c *Client) Dial(prm PrmDial) error { 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 -} +// Conn returns underlying gRPC connection to the configured endpoint. Must not +// be called before successful [Client.Dial]. Conn is not intended for normal +// use, but may be required to use services not supported by the Client. +func (c *Client) Conn() *grpc.ClientConn { return c.conn } // Close closes underlying connection to the NeoFS server. Implements io.Closer. // MUST NOT be called before successful Dial. Can be called concurrently @@ -187,7 +215,7 @@ func (c *Client) setNeoFSAPIServer(server neoFSAPIServer) { // // See also [Client.Dial]. func (c *Client) Close() error { - return c.c.Conn().Close() + return c.Conn().Close() } func (c *Client) sendStatistic(m stat.Method, err error) func() { @@ -239,6 +267,8 @@ func (x *PrmInit) SetStatisticCallback(statisticCallback stat.OperationCallback) x.statisticCallback = statisticCallback } +type connFunc = func(ctx context.Context, addr string) (net.Conn, error) + // PrmDial groups connection parameters for the Client. // // See also Dial. @@ -254,6 +284,8 @@ type PrmDial struct { streamTimeout time.Duration parentCtx context.Context + + customConnFunc connFunc } // SetServerURI sets server URI in the NeoFS network. @@ -302,3 +334,11 @@ func (x *PrmDial) SetStreamTimeout(timeout time.Duration) { func (x *PrmDial) SetContext(ctx context.Context) { x.parentCtx = ctx } + +// allows to override default gRPC dialer for testing. The func must not be nil. +func (x *PrmDial) setDialFunc(connFunc connFunc) { + if connFunc == nil { + panic("nil func does not override the default") + } + x.customConnFunc = connFunc +} diff --git a/client/client_test.go b/client/client_test.go index d6809614..6a54aec1 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,11 +2,16 @@ package client import ( "context" + "net" "testing" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/grpc/test/bufconn" ) /* @@ -19,12 +24,41 @@ func init() { statusErr.SetMessage("test status error") } -func newClient(t *testing.T, server neoFSAPIServer) *Client { +// pairs service spec and implementation to-be-registered in some [grpc.Server]. +type testService struct { + desc *grpc.ServiceDesc + impl any +} + +// returns ready-to-go [Client] of provided optional services. By default, any +// other service is unsupported. +// +// If caller registers stat callback (like [PrmInit.SetStatisticCallback] does) +// processing nodeKey, it must include NetmapService with implemented +// LocalNodeInfo method. +func newClient(t testing.TB, svcs ...testService) *Client { var prm PrmInit c, err := New(prm) require.NoError(t, err) - c.setNeoFSAPIServer(server) + + srv := grpc.NewServer() + for _, svc := range svcs { + srv.RegisterService(svc.desc, svc.impl) + } + + lis := bufconn.Listen(10 << 10) + go func() { _ = srv.Serve(lis) }() + + var dialPrm PrmDial + dialPrm.SetServerURI("grpc://localhost:8080") // any valid + dialPrm.setDialFunc(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }) + err = c.Dial(dialPrm) + if err != nil { + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unimplemented, st.Code()) + } return c } diff --git a/client/common.go b/client/common.go index 5134b340..c48cd846 100644 --- a/client/common.go +++ b/client/common.go @@ -1,7 +1,9 @@ package client import ( + "context" "fmt" + "time" "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" @@ -9,6 +11,8 @@ import ( apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/version" + "google.golang.org/grpc/encoding" + "google.golang.org/grpc/encoding/proto" ) // Various field numbers in from NeoFS API definitions. @@ -316,6 +320,51 @@ func (c *Client) initCallContext(ctx *contextCall) { // // See also Dial and Close. // See also github.com/nspcc-dev/neofs-api-go/v2/rpc/client package docs. +// Deprecated: use [Client.Conn] instead. func (c *Client) ExecRaw(f func(client *client.Client) error) error { - return f(&c.c) + return f(client.New(client.WithGRPCConn(c.conn), client.WithRWTimeout(c.streamTimeout))) +} + +type onlyBinarySendingCodec struct{} + +func (x onlyBinarySendingCodec) Name() string { + // may be any non-empty, conflicts are unlikely to arise + return "neofs_binary_sender" +} + +func (x onlyBinarySendingCodec) Marshal(msg any) ([]byte, error) { + bMsg, ok := msg.([]byte) + if ok { + return bMsg, nil + } + + return nil, fmt.Errorf("message is not of type %T", bMsg) +} + +func (x onlyBinarySendingCodec) Unmarshal(raw []byte, msg any) error { + return encoding.GetCodec(proto.Name).Unmarshal(raw, msg) +} + +// Tries to make an action within given timeout canceling the context at +// expiration. +// +// Copy-pasted from https://github.com/nspcc-dev/neofs-api-go/blob/4d4eaa29436e2b1ce9bcdddd6551133c388a1cdb/rpc/grpc/init.go#L53. +// TODO: https://github.com/nspcc-dev/neofs-sdk-go/issues/640. +func dowithTimeout(timeout time.Duration, cancel context.CancelFunc, action func() error) error { + ch := make(chan error, 1) + go func() { + ch <- action() + close(ch) + }() + + tt := time.NewTimer(timeout) + + select { + case err := <-ch: + tt.Stop() + return err + case <-tt.C: + cancel() + return context.DeadlineExceeded + } } diff --git a/client/container.go b/client/container.go index b594b086..58287ad3 100644 --- a/client/container.go +++ b/client/container.go @@ -6,6 +6,7 @@ import ( "fmt" v2container "github.com/nspcc-dev/neofs-api-go/v2/container" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/container" @@ -124,7 +125,15 @@ func (c *Client) ContainerPut(ctx context.Context, cont container.Container, sig c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return c.server.putContainer(ctx, req) + resp, err := c.container.Put(ctx, req.ToGRPCMessage().(*protocontainer.PutRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.PutResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2container.PutResponse) @@ -192,7 +201,15 @@ func (c *Client) ContainerGet(ctx context.Context, id cid.ID, prm PrmContainerGe cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.getContainer(ctx, req) + resp, err := c.container.Get(ctx, req.ToGRPCMessage().(*protocontainer.GetRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.GetResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2container.GetResponse) @@ -258,7 +275,15 @@ func (c *Client) ContainerList(ctx context.Context, ownerID user.ID, prm PrmCont cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.listContainers(ctx, req) + resp, err := c.container.List(ctx, req.ToGRPCMessage().(*protocontainer.ListRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.ListResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2container.ListResponse) @@ -392,7 +417,15 @@ func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscry c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return c.server.deleteContainer(ctx, req) + resp, err := c.container.Delete(ctx, req.ToGRPCMessage().(*protocontainer.DeleteRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.DeleteResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } // process call @@ -444,7 +477,15 @@ func (c *Client) ContainerEACL(ctx context.Context, id cid.ID, prm PrmContainerE cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.getEACL(ctx, req) + resp, err := c.container.GetExtendedACL(ctx, req.ToGRPCMessage().(*protocontainer.GetExtendedACLRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.GetExtendedACLResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2container.GetExtendedACLResponse) @@ -581,7 +622,15 @@ func (c *Client) ContainerSetEACL(ctx context.Context, table eacl.Table, signer c.initCallContext(&cc) cc.req = &req cc.call = func() (responseV2, error) { - return c.server.setEACL(ctx, req) + resp, err := c.container.SetExtendedACL(ctx, req.ToGRPCMessage().(*protocontainer.SetExtendedACLRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.SetExtendedACLResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } // process call @@ -650,7 +699,15 @@ func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, announcements [ cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.announceContainerSpace(ctx, req) + resp, err := c.container.AnnounceUsedSpace(ctx, req.ToGRPCMessage().(*protocontainer.AnnounceUsedSpaceRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2container.AnnounceUsedSpaceResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } // process call diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index 7b5d7b17..cf8c8af4 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -105,7 +105,7 @@ func TestClientStatistic_AccountBalance(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testGetBalanceServer - c := newClient(t, &srv) + c := newTestAccountingClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -121,7 +121,7 @@ func TestClientStatistic_ContainerPut(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testPutContainerServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) cont := prepareContainer(usr.ID) collector := newCollector() @@ -136,7 +136,8 @@ func TestClientStatistic_ContainerPut(t *testing.T) { func TestClientStatistic_ContainerGet(t *testing.T) { ctx := context.Background() - c := newClient(t, new(testGetContainerServer)) + var srv testGetContainerServer + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -151,7 +152,7 @@ func TestClientStatistic_ContainerList(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testListContainersServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -166,7 +167,7 @@ func TestClientStatistic_ContainerDelete(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testDeleteContainerServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -180,7 +181,7 @@ func TestClientStatistic_ContainerDelete(t *testing.T) { func TestClientStatistic_ContainerEacl(t *testing.T) { ctx := context.Background() var srv testGetEACLServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -195,7 +196,7 @@ func TestClientStatistic_ContainerSetEacl(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testSetEACLServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -210,7 +211,7 @@ func TestClientStatistic_ContainerSetEacl(t *testing.T) { func TestClientStatistic_ContainerAnnounceUsedSpace(t *testing.T) { ctx := context.Background() var srv testAnnounceContainerSpaceServer - c := newClient(t, &srv) + c := newTestContainerClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -230,7 +231,7 @@ func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testGetNetworkInfoServer - c := newClient(t, &srv) + c := newTestNetmapClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -245,7 +246,7 @@ func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { ctx := context.Background() var srv testGetNodeInfoServer - c := newClient(t, &srv) + c := newTestNetmapClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -258,7 +259,7 @@ func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { ctx := context.Background() var srv testNetmapSnapshotServer - c := newClient(t, &srv) + c := newTestNetmapClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -272,7 +273,7 @@ func TestClientStatistic_CreateSession(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testCreateSessionServer - c := newClient(t, &srv) + c := newTestSessionClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -288,7 +289,7 @@ func TestClientStatistic_ObjectPut(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testPutObjectServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() collector := newCollector() @@ -329,7 +330,7 @@ func TestClientStatistic_ObjectDelete(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testDeleteObjectServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -348,7 +349,7 @@ func TestClientStatistic_ObjectGet(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testGetObjectServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -369,7 +370,7 @@ func TestClientStatistic_ObjectHead(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testHeadObjectServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -388,7 +389,7 @@ func TestClientStatistic_ObjectRange(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testGetObjectPayloadRangeServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -409,7 +410,7 @@ func TestClientStatistic_ObjectHash(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testHashObjectPayloadRangesServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() objectID := oid.ID{} @@ -429,7 +430,7 @@ func TestClientStatistic_ObjectSearch(t *testing.T) { usr := usertest.User() ctx := context.Background() var srv testSearchObjectsServer - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) containerID := cidtest.ID() collector := newCollector() @@ -453,7 +454,7 @@ func TestClientStatistic_ObjectSearch(t *testing.T) { func TestClientStatistic_AnnounceIntermediateTrust(t *testing.T) { ctx := context.Background() var srv testAnnounceIntermediateReputationServer - c := newClient(t, &srv) + c := newTestReputationClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -469,7 +470,7 @@ func TestClientStatistic_AnnounceIntermediateTrust(t *testing.T) { func TestClientStatistic_MethodAnnounceLocalTrust(t *testing.T) { ctx := context.Background() var srv testAnnounceLocalTrustServer - c := newClient(t, &srv) + c := newTestReputationClient(t, &srv) collector := newCollector() c.prm.statisticCallback = collector.Collect diff --git a/client/container_test.go b/client/container_test.go index 998e4ab7..0392ea24 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -5,9 +5,10 @@ import ( "fmt" "testing" - apiacl "github.com/nspcc-dev/neofs-api-go/v2/acl" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl/grpc" apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/refs" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" "github.com/nspcc-dev/neofs-sdk-go/container" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" @@ -17,121 +18,155 @@ import ( "github.com/stretchr/testify/require" ) +// returns Client of Container service provided by given server. +func newTestContainerClient(t testing.TB, srv protocontainer.ContainerServiceServer) *Client { + return newClient(t, testService{desc: &protocontainer.ContainerService_ServiceDesc, impl: srv}) +} + type testPutContainerServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testPutContainerServer) putContainer(context.Context, apicontainer.PutRequest) (*apicontainer.PutResponse, error) { +func (x *testPutContainerServer) Put(context.Context, *protocontainer.PutRequest) (*protocontainer.PutResponse, error) { id := cidtest.ID() - var idV2 refs.ContainerID - id.WriteToV2(&idV2) - var body apicontainer.PutResponseBody - body.SetContainerID(&idV2) - var resp apicontainer.PutResponse - resp.SetBody(&body) - - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + resp := protocontainer.PutResponse{ + Body: &protocontainer.PutResponse_Body{ + ContainerId: &protorefs.ContainerID{Value: id[:]}, + }, + } + + var respV2 apicontainer.PutResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.PutResponse), nil } type testGetContainerServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testGetContainerServer) getContainer(context.Context, apicontainer.GetRequest) (*apicontainer.GetResponse, error) { +func (x *testGetContainerServer) Get(context.Context, *protocontainer.GetRequest) (*protocontainer.GetResponse, error) { cnr := containertest.Container() var cnrV2 apicontainer.Container cnr.WriteToV2(&cnrV2) - var body apicontainer.GetResponseBody - body.SetContainer(&cnrV2) - var resp apicontainer.GetResponse - resp.SetBody(&body) + resp := protocontainer.GetResponse{ + Body: &protocontainer.GetResponse_Body{ + Container: cnrV2.ToGRPCMessage().(*protocontainer.Container), + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.GetResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.GetResponse), nil } type testListContainersServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testListContainersServer) listContainers(context.Context, apicontainer.ListRequest) (*apicontainer.ListResponse, error) { - var resp apicontainer.ListResponse +func (x *testListContainersServer) List(context.Context, *protocontainer.ListRequest) (*protocontainer.ListResponse, error) { + var resp protocontainer.ListResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.ListResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.ListResponse), nil } type testDeleteContainerServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testDeleteContainerServer) deleteContainer(context.Context, apicontainer.DeleteRequest) (*apicontainer.DeleteResponse, error) { - var resp apicontainer.DeleteResponse +func (x *testDeleteContainerServer) Delete(context.Context, *protocontainer.DeleteRequest) (*protocontainer.DeleteResponse, error) { + var resp protocontainer.DeleteResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.DeleteResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.DeleteResponse), nil } type testGetEACLServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testGetEACLServer) getEACL(context.Context, apicontainer.GetExtendedACLRequest) (*apicontainer.GetExtendedACLResponse, error) { - var body apicontainer.GetExtendedACLResponseBody - body.SetEACL(new(apiacl.Table)) - var resp apicontainer.GetExtendedACLResponse - resp.SetBody(&body) +func (x *testGetEACLServer) GetExtendedACL(context.Context, *protocontainer.GetExtendedACLRequest) (*protocontainer.GetExtendedACLResponse, error) { + resp := protocontainer.GetExtendedACLResponse{ + Body: &protocontainer.GetExtendedACLResponse_Body{ + Eacl: new(protoacl.EACLTable), + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.GetExtendedACLResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.GetExtendedACLResponse), nil } type testSetEACLServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testSetEACLServer) setEACL(context.Context, apicontainer.SetExtendedACLRequest) (*apicontainer.SetExtendedACLResponse, error) { - var resp apicontainer.SetExtendedACLResponse +func (x *testSetEACLServer) SetExtendedACL(context.Context, *protocontainer.SetExtendedACLRequest) (*protocontainer.SetExtendedACLResponse, error) { + var resp protocontainer.SetExtendedACLResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.SetExtendedACLResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.SetExtendedACLResponse), nil } type testAnnounceContainerSpaceServer struct { - unimplementedNeoFSAPIServer + protocontainer.UnimplementedContainerServiceServer } -func (x testAnnounceContainerSpaceServer) announceContainerSpace(context.Context, apicontainer.AnnounceUsedSpaceRequest) (*apicontainer.AnnounceUsedSpaceResponse, error) { - var resp apicontainer.AnnounceUsedSpaceResponse +func (x *testAnnounceContainerSpaceServer) AnnounceUsedSpace(context.Context, *protocontainer.AnnounceUsedSpaceRequest) (*protocontainer.AnnounceUsedSpaceResponse, error) { + var resp protocontainer.AnnounceUsedSpaceResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apicontainer.AnnounceUsedSpaceResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protocontainer.AnnounceUsedSpaceResponse), nil } func TestClient_Container(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) ctx := context.Background() t.Run("missing signer", func(t *testing.T) { diff --git a/client/netmap.go b/client/netmap.go index 04ff1d8f..46861367 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -5,6 +5,7 @@ import ( "fmt" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/nspcc-dev/neofs-sdk-go/stat" @@ -77,7 +78,15 @@ func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEnd cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.getNodeInfo(ctx, req) + resp, err := c.netmap.LocalNodeInfo(ctx, req.ToGRPCMessage().(*protonetmap.LocalNodeInfoRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2netmap.LocalNodeInfoResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2netmap.LocalNodeInfoResponse) @@ -155,7 +164,15 @@ func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (netmap.Ne cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.getNetworkInfo(ctx, req) + resp, err := c.netmap.NetworkInfo(ctx, req.ToGRPCMessage().(*protonetmap.NetworkInfoRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2netmap.NetworkInfoResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2netmap.NetworkInfoResponse) @@ -221,21 +238,23 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (netma return netmap.NetMap{}, err } - var resp *v2netmap.SnapshotResponse - - resp, err = c.server.netMapSnapshot(ctx, req) + resp, err := c.netmap.NetmapSnapshot(ctx, req.ToGRPCMessage().(*protonetmap.NetmapSnapshotRequest)) if err != nil { + return netmap.NetMap{}, rpcErr(err) + } + var respV2 v2netmap.SnapshotResponse + if err = respV2.FromGRPCMessage(resp); err != nil { return netmap.NetMap{}, err } var res netmap.NetMap - if err = c.processResponse(resp); err != nil { + if err = c.processResponse(&respV2); err != nil { return netmap.NetMap{}, err } const fieldNetMap = "network map" - netMapV2 := resp.GetBody().NetMap() + netMapV2 := respV2.GetBody().NetMap() if netMapV2 == nil { err = newErrMissingResponseField(fieldNetMap) return netmap.NetMap{}, err diff --git a/client/netmap_test.go b/client/netmap_test.go index a16045d5..84989534 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -7,17 +7,26 @@ import ( "testing" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/session" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/netmap" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) +// returns Client of Netmap service provided by given server. +func newTestNetmapClient(t testing.TB, srv protonetmap.NetmapServiceServer) *Client { + return newClient(t, testService{desc: &protonetmap.NetmapService_ServiceDesc, impl: srv}) +} + type testNetmapSnapshotServer struct { - unimplementedNeoFSAPIServer + protonetmap.UnimplementedNetmapServiceServer errTransport error @@ -26,13 +35,18 @@ type testNetmapSnapshotServer struct { statusFail bool unsetNetMap bool - netMap v2netmap.NetMap + netMap *protonetmap.Netmap signer neofscrypto.Signer } -func (x *testNetmapSnapshotServer) netMapSnapshot(_ context.Context, req v2netmap.SnapshotRequest) (*v2netmap.SnapshotResponse, error) { - err := verifyServiceMessage(&req) +func (x *testNetmapSnapshotServer) NetmapSnapshot(_ context.Context, req *protonetmap.NetmapSnapshotRequest) (*protonetmap.NetmapSnapshotResponse, error) { + var reqV2 v2netmap.SnapshotRequest + if err := reqV2.FromGRPCMessage(req); err != nil { + panic(err) + } + + err := verifyServiceMessage(&reqV2) if err != nil { return nil, err } @@ -41,78 +55,95 @@ func (x *testNetmapSnapshotServer) netMapSnapshot(_ context.Context, req v2netma return nil, x.errTransport } - var body v2netmap.SnapshotResponseBody - + var nm *protonetmap.Netmap if !x.unsetNetMap { - body.SetNetMap(&x.netMap) + if x.netMap != nil { + nm = x.netMap + } else { + nm = new(protonetmap.Netmap) + } + } + resp := protonetmap.NetmapSnapshotResponse{ + Body: &protonetmap.NetmapSnapshotResponse_Body{ + Netmap: nm, + }, } - - var meta session.ResponseMetaHeader - if x.statusFail { - meta.SetStatus(statusErr.ErrorToV2()) + resp.MetaHeader = &protosession.ResponseMetaHeader{ + Status: statusErr.ErrorToV2().ToGRPCMessage().(*protostatus.Status), + } } - var resp v2netmap.SnapshotResponse - resp.SetBody(&body) - resp.SetMetaHeader(&meta) - + var respV2 v2netmap.SnapshotResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } signer := x.signer if signer == nil { signer = neofscryptotest.Signer() } if !x.unsignedResponse { - err = signServiceMessage(signer, &resp, nil) + err = signServiceMessage(signer, &respV2, nil) if err != nil { panic(fmt.Sprintf("sign response: %v", err)) } } - return &resp, nil + return respV2.ToGRPCMessage().(*protonetmap.NetmapSnapshotResponse), nil } type testGetNetworkInfoServer struct { - unimplementedNeoFSAPIServer + protonetmap.UnimplementedNetmapServiceServer } -func (x testGetNetworkInfoServer) getNetworkInfo(context.Context, v2netmap.NetworkInfoRequest) (*v2netmap.NetworkInfoResponse, error) { - var netPrm v2netmap.NetworkParameter - netPrm.SetValue([]byte("any")) - var netCfg v2netmap.NetworkConfig - netCfg.SetParameters(netPrm) - var netInfo v2netmap.NetworkInfo - netInfo.SetNetworkConfig(&netCfg) - var body v2netmap.NetworkInfoResponseBody - body.SetNetworkInfo(&netInfo) - var resp v2netmap.NetworkInfoResponse - resp.SetBody(&body) - - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { +func (x *testGetNetworkInfoServer) NetworkInfo(context.Context, *protonetmap.NetworkInfoRequest) (*protonetmap.NetworkInfoResponse, error) { + resp := protonetmap.NetworkInfoResponse{ + Body: &protonetmap.NetworkInfoResponse_Body{ + NetworkInfo: &protonetmap.NetworkInfo{ + NetworkConfig: &protonetmap.NetworkConfig{ + Parameters: []*protonetmap.NetworkConfig_Parameter{ + {Value: []byte("any")}, + }, + }, + }, + }, + } + + var respV2 v2netmap.NetworkInfoResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protonetmap.NetworkInfoResponse), nil } type testGetNodeInfoServer struct { - unimplementedNeoFSAPIServer + protonetmap.UnimplementedNetmapServiceServer } -func (x testGetNodeInfoServer) getNodeInfo(context.Context, v2netmap.LocalNodeInfoRequest) (*v2netmap.LocalNodeInfoResponse, error) { - var nodeInfo v2netmap.NodeInfo - nodeInfo.SetPublicKey([]byte("any")) - nodeInfo.SetAddresses("any") - var body v2netmap.LocalNodeInfoResponseBody - body.SetVersion(new(refs.Version)) - body.SetNodeInfo(&nodeInfo) - var resp v2netmap.LocalNodeInfoResponse - resp.SetBody(&body) - - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { +func (x *testGetNodeInfoServer) LocalNodeInfo(context.Context, *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { + resp := protonetmap.LocalNodeInfoResponse{ + Body: &protonetmap.LocalNodeInfoResponse_Body{ + Version: new(protorefs.Version), + NodeInfo: &protonetmap.NodeInfo{ + PublicKey: []byte("any"), + Addresses: []string{"any"}, + }, + }, + } + + var respV2 v2netmap.LocalNodeInfoResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protonetmap.LocalNodeInfoResponse), nil } func TestClient_NetMapSnapshot(t *testing.T) { @@ -125,14 +156,17 @@ func TestClient_NetMapSnapshot(t *testing.T) { srv.signer = signer - c := newClient(t, &srv) + c := newTestNetmapClient(t, &srv) ctx := context.Background() - // request signature + // transport error srv.errTransport = errors.New("any error") _, err = c.NetMapSnapshot(ctx, prm) - require.ErrorIs(t, err, srv.errTransport) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unknown, st.Code()) + require.Contains(t, st.Message(), srv.errTransport.Error()) srv.errTransport = nil @@ -158,34 +192,31 @@ func TestClient_NetMapSnapshot(t *testing.T) { srv.unsetNetMap = false // 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) + srv.netMap = &protonetmap.Netmap{ + Nodes: []*protonetmap.NodeInfo{new(protonetmap.NodeInfo)}, + } _, 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) + srv.netMap.Nodes[0].PublicKey = []byte{1, 2, 3} + srv.netMap.Nodes[0].Addresses = []string{"1", "2", "3"} res, err = c.NetMapSnapshot(ctx, prm) require.NoError(t, err) - require.Equal(t, netMap, res) + + require.Zero(t, res.Epoch()) + ns := res.Nodes() + require.Len(t, ns, 1) + node := ns[0] + require.False(t, node.IsOnline()) + require.False(t, node.IsOffline()) + require.False(t, node.IsMaintenance()) + require.Zero(t, node.NumberOfAttributes()) + require.Equal(t, srv.netMap.Nodes[0].PublicKey, node.PublicKey()) + var es []string + netmap.IterateNetworkEndpoints(node, func(e string) { es = append(es, e) }) + require.Equal(t, srv.netMap.Nodes[0].Addresses, es) } diff --git a/client/object_delete.go b/client/object_delete.go index db5d45bf..435e1709 100644 --- a/client/object_delete.go +++ b/client/object_delete.go @@ -7,6 +7,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -103,19 +104,23 @@ func (c *Client) ObjectDelete(ctx context.Context, containerID cid.ID, objectID return oid.ID{}, err } - resp, err := c.server.deleteObject(ctx, req) + resp, err := c.object.Delete(ctx, req.ToGRPCMessage().(*protoobject.DeleteRequest)) if err != nil { + return oid.ID{}, rpcErr(err) + } + var respV2 v2object.DeleteResponse + if err = respV2.FromGRPCMessage(resp); err != nil { return oid.ID{}, err } var res oid.ID - if err = c.processResponse(resp); err != nil { + if err = c.processResponse(&respV2); err != nil { return oid.ID{}, err } const fieldTombstone = "tombstone" - idTombV2 := resp.GetBody().GetTombstone().GetObjectID() + idTombV2 := respV2.GetBody().GetTombstone().GetObjectID() if idTombV2 == nil { err = newErrMissingResponseField(fieldTombstone) return oid.ID{}, err diff --git a/client/object_delete_test.go b/client/object_delete_test.go index 507d6db9..93bf875e 100644 --- a/client/object_delete_test.go +++ b/client/object_delete_test.go @@ -6,7 +6,8 @@ import ( "testing" apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -15,30 +16,33 @@ import ( ) type testDeleteObjectServer struct { - unimplementedNeoFSAPIServer + protoobject.UnimplementedObjectServiceServer } -func (x testDeleteObjectServer) deleteObject(context.Context, apiobject.DeleteRequest) (*apiobject.DeleteResponse, error) { +func (x *testDeleteObjectServer) Delete(context.Context, *protoobject.DeleteRequest) (*protoobject.DeleteResponse, error) { id := oidtest.ID() - var idV2 refs.ObjectID - id.WriteToV2(&idV2) - var addr refs.Address - addr.SetObjectID(&idV2) - var body apiobject.DeleteResponseBody - body.SetTombstone(&addr) - var resp apiobject.DeleteResponse - resp.SetBody(&body) - - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + resp := protoobject.DeleteResponse{ + Body: &protoobject.DeleteResponse_Body{ + Tombstone: &protorefs.Address{ + ObjectId: &protorefs.ObjectID{Value: id[:]}, + }, + }, + } + + var respV2 apiobject.DeleteResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoobject.DeleteResponse), nil } func TestClient_ObjectDelete(t *testing.T) { t.Run("missing signer", func(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) _, err := c.ObjectDelete(context.Background(), cid.ID{}, oid.ID{}, nil, PrmObjectDelete{}) require.ErrorIs(t, err, ErrMissingSigner) diff --git a/client/object_get.go b/client/object_get.go index 1f3fabf8..592d05c4 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "time" "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -58,6 +60,15 @@ type PrmObjectGet struct { prmObjectRead } +// used part of [protoobject.ObjectService_GetClient] simplifying test +// implementations. +type getObjectResponseStream interface { + // Recv reads next message with the object part from the stream. Recv returns + // [io.EOF] after the server sent the last message and gracefully finished the + // stream. Any other error means stream abort. + Recv() (*protoobject.GetResponse, error) +} + // PayloadReader is a data stream of the particular NeoFS object. Implements // [io.ReadCloser]. // @@ -66,8 +77,9 @@ type PrmObjectGet struct { type PayloadReader struct { cancelCtxStream context.CancelFunc - client *Client - stream getObjectResponseStream + client *Client + stream getObjectResponseStream + singleMsgTimeout time.Duration err error @@ -81,20 +93,28 @@ type PayloadReader struct { // readHeader reads header of the object. Result means success. // Failure reason can be received via Close. func (x *PayloadReader) readHeader(dst *object.Object) bool { - var resp v2object.GetResponse - x.err = x.stream.Read(&resp) + var resp *protoobject.GetResponse + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.Recv() + return err + }) if x.err != nil { return false } + var respV2 v2object.GetResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { + return false + } - x.err = x.client.processResponse(&resp) + x.err = x.client.processResponse(&respV2) if x.err != nil { return false } var partInit *v2object.GetObjectPartInit - switch v := resp.GetBody().GetObjectPart().(type) { + switch v := respV2.GetBody().GetObjectPart().(type) { default: x.err = fmt.Errorf("unexpected message instead of heading part: %T", v) return false @@ -133,18 +153,26 @@ func (x *PayloadReader) readChunk(buf []byte) (int, bool) { var lastRead int for { - var resp v2object.GetResponse - x.err = x.stream.Read(&resp) + var resp *protoobject.GetResponse + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.Recv() + return err + }) if x.err != nil { return read, false } + var respV2 v2object.GetResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { + return read, false + } - x.err = x.client.processResponse(&resp) + x.err = x.client.processResponse(&respV2) if x.err != nil { return read, false } - part := resp.GetBody().GetObjectPart() + part := respV2.GetBody().GetObjectPart() partChunk, ok := part.(*v2object.GetObjectPartChunk) if !ok { x.err = fmt.Errorf("unexpected message instead of chunk part: %T", part) @@ -280,7 +308,7 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID ctx, cancel := context.WithCancel(ctx) - stream, err := c.server.getObject(ctx, req) + stream, err := c.object.Get(ctx, req.ToGRPCMessage().(*protoobject.GetRequest)) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) @@ -290,6 +318,7 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID var r PayloadReader r.cancelCtxStream = cancel r.stream = stream + r.singleMsgTimeout = c.streamTimeout r.client = c r.statisticCallback = func(err error) { c.sendStatistic(stat.MethodObjectGetStream, err) @@ -366,17 +395,20 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi return nil, err } - resp, err := c.server.headObject(ctx, req) + resp, err := c.object.Head(ctx, req.ToGRPCMessage().(*protoobject.HeadRequest)) if err != nil { - err = fmt.Errorf("write request: %w", err) + return nil, rpcErr(err) + } + var respV2 v2object.HeadResponse + if err = respV2.FromGRPCMessage(resp); err != nil { return nil, err } - if err = c.processResponse(resp); err != nil { + if err = c.processResponse(&respV2); err != nil { return nil, err } - switch v := resp.GetBody().GetHeaderPart().(type) { + switch v := respV2.GetBody().GetHeaderPart().(type) { default: err = fmt.Errorf("unexpected header type %T", v) return nil, err @@ -405,6 +437,15 @@ type PrmObjectRange struct { prmObjectRead } +// used part of [protoobject.ObjectService_GetRangeClient] simplifying test +// implementations. +type getObjectPayloadRangeResponseStream interface { + // Recv reads next message with the object payload part from the stream. Recv + // returns [io.EOF] after the server sent the last message and gracefully + // finished the stream. Any other error means stream abort. + Recv() (*protoobject.GetRangeResponse, error) +} + // ObjectRangeReader is designed to read payload range of one object // from NeoFS system. Implements [io.ReadCloser]. // @@ -417,7 +458,8 @@ type ObjectRangeReader struct { err error - stream getObjectPayloadRangeResponseStream + stream getObjectPayloadRangeResponseStream + singleMsgTimeout time.Duration tailPayload []byte @@ -443,19 +485,27 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) { var lastRead int for { - var resp v2object.GetRangeResponse - x.err = x.stream.Read(&resp) + var resp *protoobject.GetRangeResponse + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.Recv() + return err + }) if x.err != nil { return read, false } + var respV2 v2object.GetRangeResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { + return read, false + } - x.err = x.client.processResponse(&resp) + x.err = x.client.processResponse(&respV2) if x.err != nil { return read, false } // get chunk message - switch v := resp.GetBody().GetRangePart().(type) { + switch v := respV2.GetBody().GetRangePart().(type) { default: x.err = fmt.Errorf("unexpected message received: %T", v) return read, false @@ -615,7 +665,7 @@ func (c *Client) ObjectRangeInit(ctx context.Context, containerID cid.ID, object ctx, cancel := context.WithCancel(ctx) - stream, err := c.server.getObjectPayloadRange(ctx, req) + stream, err := c.object.GetRange(ctx, req.ToGRPCMessage().(*protoobject.GetRangeRequest)) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) @@ -626,6 +676,7 @@ func (c *Client) ObjectRangeInit(ctx context.Context, containerID cid.ID, object r.remainingPayloadLen = int(length) r.cancelCtxStream = cancel r.stream = stream + r.singleMsgTimeout = c.streamTimeout r.client = c r.statisticCallback = func(err error) { c.sendStatistic(stat.MethodObjectRangeStream, err)() diff --git a/client/object_get_test.go b/client/object_get_test.go index 0e598659..f9479d0a 100644 --- a/client/object_get_test.go +++ b/client/object_get_test.go @@ -3,10 +3,10 @@ package client import ( "context" "fmt" - "io" "testing" apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" @@ -14,94 +14,86 @@ import ( "github.com/stretchr/testify/require" ) -type testGetObjectResponseStream struct { - sent bool +type testGetObjectServer struct { + protoobject.UnimplementedObjectServiceServer } -func (x *testGetObjectResponseStream) Read(resp *apiobject.GetResponse) error { - if x.sent { - return io.EOF +func (x *testGetObjectServer) Get(_ *protoobject.GetRequest, stream protoobject.ObjectService_GetServer) error { + resp := protoobject.GetResponse{ + Body: &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{ + Init: new(protoobject.GetResponse_Body_Init), + }, + }, } - var body apiobject.GetResponseBody - body.SetObjectPart(new(apiobject.GetObjectPartInit)) - resp.SetBody(&body) - - if err := signServiceMessage(neofscryptotest.Signer(), resp, nil); err != nil { + var respV2 apiobject.GetResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return fmt.Errorf("sign response message: %w", err) } - x.sent = true - return nil -} - -type testGetObjectServer struct { - unimplementedNeoFSAPIServer - - stream testGetObjectResponseStream -} - -func (x *testGetObjectServer) getObject(context.Context, apiobject.GetRequest) (getObjectResponseStream, error) { - x.stream.sent = false - return &x.stream, nil + return stream.SendMsg(respV2.ToGRPCMessage().(*protoobject.GetResponse)) } type testGetObjectPayloadRangeServer struct { - unimplementedNeoFSAPIServer - - stream testGetObjectPayloadRangeResponseStream + protoobject.UnimplementedObjectServiceServer } -func (x *testGetObjectPayloadRangeServer) getObjectPayloadRange(_ context.Context, req apiobject.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { - x.stream.sent = false - x.stream.ln = req.GetBody().GetRange().GetLength() - return &x.stream, nil -} - -type testGetObjectPayloadRangeResponseStream struct { - ln uint64 - sent bool -} - -func (x *testGetObjectPayloadRangeResponseStream) Read(resp *apiobject.GetRangeResponse) error { - if x.sent { - return io.EOF +func (x *testGetObjectPayloadRangeServer) GetRange(req *protoobject.GetRangeRequest, stream protoobject.ObjectService_GetRangeServer) error { + ln := req.GetBody().GetRange().GetLength() + if ln == 0 { + return nil } - var rngPart apiobject.GetRangePartChunk - rngPart.SetChunk(make([]byte, x.ln)) - var body apiobject.GetRangeResponseBody - body.SetRangePart(&rngPart) - resp.SetBody(&body) + resp := protoobject.GetRangeResponse{ + Body: &protoobject.GetRangeResponse_Body{ + RangePart: &protoobject.GetRangeResponse_Body_Chunk{ + Chunk: make([]byte, ln), + }, + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), resp, nil); err != nil { + var respV2 apiobject.GetRangeResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return fmt.Errorf("sign response message: %w", err) } - x.sent = true - return nil + return stream.SendMsg(respV2.ToGRPCMessage().(*protoobject.GetRangeResponse)) } type testHeadObjectServer struct { - unimplementedNeoFSAPIServer + protoobject.UnimplementedObjectServiceServer } -func (x *testHeadObjectServer) headObject(context.Context, apiobject.HeadRequest) (*apiobject.HeadResponse, error) { - var body apiobject.HeadResponseBody - body.SetHeaderPart(new(apiobject.HeaderWithSignature)) - var resp apiobject.HeadResponse - resp.SetBody(&body) +func (x *testHeadObjectServer) Head(context.Context, *protoobject.HeadRequest) (*protoobject.HeadResponse, error) { + resp := protoobject.HeadResponse{ + Body: &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_Header{ + Header: new(protoobject.HeaderWithSignature), + }, + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 apiobject.HeadResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoobject.HeadResponse), nil } func TestClient_Get(t *testing.T) { t.Run("missing signer", func(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) ctx := context.Background() var nonilAddr v2refs.Address diff --git a/client/object_hash.go b/client/object_hash.go index a0ddf566..a8b633c6 100644 --- a/client/object_hash.go +++ b/client/object_hash.go @@ -6,6 +6,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -146,14 +147,17 @@ func (c *Client) ObjectHash(ctx context.Context, containerID cid.ID, objectID oi return nil, err } - resp, err := c.server.hashObjectPayloadRanges(ctx, req) + resp, err := c.object.GetRangeHash(ctx, req.ToGRPCMessage().(*protoobject.GetRangeHashRequest)) if err != nil { - err = fmt.Errorf("write request: %w", err) + return nil, rpcErr(err) + } + var respV2 v2object.GetRangeHashResponse + if err = respV2.FromGRPCMessage(resp); err != nil { return nil, err } var res [][]byte - if err = c.processResponse(resp); err != nil { + if err = c.processResponse(&respV2); err != nil { return nil, err } diff --git a/client/object_hash_test.go b/client/object_hash_test.go index 4c2a1218..7b4d1188 100644 --- a/client/object_hash_test.go +++ b/client/object_hash_test.go @@ -6,6 +6,7 @@ import ( "testing" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" @@ -13,24 +14,29 @@ import ( ) type testHashObjectPayloadRangesServer struct { - unimplementedNeoFSAPIServer + protoobject.UnimplementedObjectServiceServer } -func (x *testHashObjectPayloadRangesServer) hashObjectPayloadRanges(context.Context, v2object.GetRangeHashRequest) (*v2object.GetRangeHashResponse, error) { - var body v2object.GetRangeHashResponseBody - body.SetHashList([][]byte{{1}}) - var resp v2object.GetRangeHashResponse - resp.SetBody(&body) +func (x *testHashObjectPayloadRangesServer) GetRangeHash(context.Context, *protoobject.GetRangeHashRequest) (*protoobject.GetRangeHashResponse, error) { + resp := protoobject.GetRangeHashResponse{ + Body: &protoobject.GetRangeHashResponse_Body{ + HashList: [][]byte{{1}}, + }, + } - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 v2object.GetRangeHashResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoobject.GetRangeHashResponse), nil } func TestClient_ObjectHash(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) t.Run("missing signer", func(t *testing.T) { var reqBody v2object.GetRangeHashRequestBody diff --git a/client/object_put.go b/client/object_put.go index 6279a3e3..4adf6619 100644 --- a/client/object_put.go +++ b/client/object_put.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "time" "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" "github.com/nspcc-dev/neofs-sdk-go/bearer" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" "github.com/nspcc-dev/neofs-sdk-go/object" @@ -21,9 +23,16 @@ var ( ErrNoSessionExplicitly = errors.New("session was removed explicitly") ) -type objectWriter interface { - Write(*v2object.PutRequest) error - Close() (*v2object.PutResponse, error) +// used part of [protoobject.ObjectService_PutClient] simplifying test +// implementations. +type putObjectStream interface { + // Send writes next message with the object part to the stream. No error does + // not guarantee delivery to the server. Send returns [io.EOF] after the server + // sent the response and gracefully finished the stream: the result can be + // accessed via CloseAndRecv. Any other error means stream abort. + Send(*protoobject.PutRequest) error + // CloseAndRecv finishes the stream and reads response from the server. + CloseAndRecv() (*protoobject.PutResponse, error) } // shortStatisticCallback is a shorter version of [stat.OperationCallback] which is calling from [client.Client]. @@ -67,9 +76,10 @@ type ObjectWriter interface { type DefaultObjectWriter struct { cancelCtxStream context.CancelFunc - client *Client - stream objectWriter - streamClosed bool + client *Client + stream putObjectStream + singleMsgTimeout time.Duration + streamClosed bool signer neofscrypto.Signer res ResObjectPut @@ -126,7 +136,9 @@ func (x *DefaultObjectWriter) writeHeader(hdr object.Object) error { return x.err } - x.err = x.stream.Write(&x.req) + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + return x.stream.Send(x.req.ToGRPCMessage().(*protoobject.PutRequest)) + }) return x.err } @@ -170,11 +182,25 @@ func (x *DefaultObjectWriter) Write(chunk []byte) (n int, err error) { return writtenBytes, x.err } - x.err = x.stream.Write(&x.req) + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + return x.stream.Send(x.req.ToGRPCMessage().(*protoobject.PutRequest)) + }) if x.err != nil { if errors.Is(x.err, io.EOF) { - resp, _ := x.stream.Close() - x.err = x.client.processResponse(resp) + var resp *protoobject.PutResponse + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.CloseAndRecv() + return err + }) + if x.err != nil { + return writtenBytes, x.err + } + var respV2 v2object.PutResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { + return writtenBytes, x.err + } + x.err = x.client.processResponse(&respV2) x.streamClosed = true x.cancelCtxStream() } @@ -229,19 +255,26 @@ func (x *DefaultObjectWriter) Close() error { return x.err } - var resp *v2object.PutResponse - resp, x.err = x.stream.Close() - if x.err != nil { + var resp *protoobject.PutResponse + if x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.CloseAndRecv() + return err + }); x.err != nil { + return x.err + } + var respV2 v2object.PutResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { return x.err } - if x.err = x.client.processResponse(resp); x.err != nil { + if x.err = x.client.processResponse(&respV2); x.err != nil { return x.err } const fieldID = "ID" - idV2 := resp.GetBody().GetObjectID() + idV2 := respV2.GetBody().GetObjectID() if idV2 == nil { x.err = newErrMissingResponseField(fieldID) return x.err @@ -289,7 +322,7 @@ func (c *Client) ObjectPutInit(ctx context.Context, hdr object.Object, signer us } ctx, cancel := context.WithCancel(ctx) - stream, err := c.server.putObject(ctx) + stream, err := c.object.Put(ctx) if err != nil { cancel() err = fmt.Errorf("open stream: %w", err) @@ -306,6 +339,7 @@ func (c *Client) ObjectPutInit(ctx context.Context, hdr object.Object, signer us w.cancelCtxStream = cancel w.client = c w.stream = stream + w.singleMsgTimeout = c.streamTimeout w.partInit.SetCopiesNumber(prm.copyNum) w.req.SetBody(new(v2object.PutRequestBody)) c.prepareRequest(&w.req, &prm.meta) diff --git a/client/object_put_test.go b/client/object_put_test.go index 431a6483..3159034d 100644 --- a/client/object_put_test.go +++ b/client/object_put_test.go @@ -8,79 +8,79 @@ import ( "testing" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" - v2session "github.com/nspcc-dev/neofs-api-go/v2/session" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/object" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/nspcc-dev/neofs-sdk-go/version" "github.com/stretchr/testify/require" ) -type testPutObjectStream struct { +type testPutObjectServer struct { + protoobject.UnimplementedObjectServiceServer + denyAccess bool } -func (t *testPutObjectStream) Write(req *v2object.PutRequest) error { - switch req.GetBody().GetObjectPart().(type) { - case *v2object.PutObjectPartInit: - return nil - case *v2object.PutObjectPartChunk: - return io.EOF - default: - return errors.New("excuse me?") +func (x *testPutObjectServer) Put(stream protoobject.ObjectService_PutServer) error { + for { + req, err := stream.Recv() + if errors.Is(err, io.EOF) { + break + } + switch req.GetBody().GetObjectPart().(type) { + case *protoobject.PutRequest_Body_Init_, + *protoobject.PutRequest_Body_Chunk: + default: + return errors.New("excuse me?") + } } -} - -func (t *testPutObjectStream) Close() (*v2object.PutResponse, error) { - m := new(v2session.ResponseMetaHeader) var v refs.Version version.Current().WriteToV2(&v) - - m.SetVersion(&v) - if t.denyAccess { - m.SetStatus(apistatus.ErrObjectAccessDenied.ErrorToV2()) + id := oidtest.ID() + resp := protoobject.PutResponse{ + Body: &protoobject.PutResponse_Body{ + ObjectId: &protorefs.ObjectID{Value: id[:]}, + }, + MetaHeader: &protosession.ResponseMetaHeader{ + Version: v.ToGRPCMessage().(*protorefs.Version), + }, } - var resp v2object.PutResponse - resp.SetMetaHeader(m) - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.denyAccess { + resp.MetaHeader.Status = apistatus.ErrObjectAccessDenied.ErrorToV2().ToGRPCMessage().(*protostatus.Status) } - return &resp, nil -} - -type testPutObjectServer struct { - unimplementedNeoFSAPIServer - - stream testPutObjectStream -} + var respV2 v2object.PutResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { + return fmt.Errorf("sign response message: %w", err) + } -func (x *testPutObjectServer) putObject(context.Context) (objectWriter, error) { - return &x.stream, nil + return stream.SendAndClose(respV2.ToGRPCMessage().(*protoobject.PutResponse)) } func TestClient_ObjectPutInit(t *testing.T) { t.Run("EOF-on-status-return", func(t *testing.T) { srv := testPutObjectServer{ - stream: testPutObjectStream{ - denyAccess: true, - }, + denyAccess: true, } - c := newClient(t, &srv) + c := newTestObjectClient(t, &srv) usr := usertest.User() w, err := c.ObjectPutInit(context.Background(), object.Object{}, usr, PrmObjectPutInit{}) require.NoError(t, err) - n, err := w.Write([]byte{1}) - require.Zero(t, n) - require.ErrorIs(t, err, new(apistatus.ObjectAccessDenied)) - err = w.Close() - require.NoError(t, err) + require.ErrorIs(t, err, apistatus.ErrObjectAccessDenied) }) } diff --git a/client/object_replicate.go b/client/object_replicate.go index 689813c4..b298bdfb 100644 --- a/client/object_replicate.go +++ b/client/object_replicate.go @@ -11,14 +11,11 @@ import ( objectgrpc "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/common" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/message" "github.com/nspcc-dev/neofs-api-go/v2/status" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protowire" ) @@ -54,37 +51,49 @@ import ( // - [apistatus.ErrContainerNotFound]: the container to which the replicated // object is associated was not found. func (c *Client) ReplicateObject(ctx context.Context, id oid.ID, src io.ReadSeeker, signer neofscrypto.Signer, signedReplication bool) (*neofscrypto.Signature, error) { - const svcName = "neo.fs.v2.object.ObjectService" - const opName = "Replicate" - stream, err := c.c.Init(common.CallMethodInfoUnary(svcName, opName), - client.WithContext(ctx), client.AllowBinarySendingOnly()) + msg, err := prepareReplicateMessage(id, src, signer, signedReplication) if err != nil { - return nil, fmt.Errorf("init service=%s/op=%s RPC: %w", svcName, opName, err) + return nil, err } - msg, err := prepareReplicateMessage(id, src, signer, signedReplication) + var resp objectgrpc.ReplicateResponse + err = c.conn.Invoke(ctx, objectgrpc.ObjectService_Replicate_FullMethodName, msg, &resp, grpc.ForceCodec(onlyBinarySendingCodec{})) if err != nil { + return nil, fmt.Errorf("send request over gRPC: %w", err) + } + + var st *status.Status + if mst := resp.GetStatus(); mst != nil { + st = new(status.Status) + err := st.FromGRPCMessage(mst) + if err != nil { + return nil, fmt.Errorf("decode response status: %w", err) + } + } + if err = apistatus.ErrorFromV2(st); err != nil { return nil, err } - err = stream.WriteMessage(client.BinaryMessage(msg)) - if err != nil && !errors.Is(err, io.EOF) { // io.EOF means the server closed the stream on its side - return nil, fmt.Errorf("send request: %w", err) + if !signedReplication { + return nil, nil } - resp := replicateResponse{_sigRequested: signedReplication} - err = stream.ReadMessage(&resp) - if err != nil { - if errors.Is(err, io.EOF) { - err = io.ErrUnexpectedEOF - } + sigBin := resp.GetObjectSignature() + if len(sigBin) == 0 { + return nil, errors.New("requested but missing signature") + } - return nil, fmt.Errorf("recv response: %w", err) + var sigV2 refs.Signature + if err := sigV2.Unmarshal(sigBin); err != nil { + return nil, fmt.Errorf("decoding signature from proto message: %w", err) } - _ = stream.Close() + var sig neofscrypto.Signature + if err = sig.ReadFromV2(sigV2); err != nil { + return nil, fmt.Errorf("invalid signature: %w", err) + } - return resp.objSig, resp.err + return &sig, nil } // DemuxReplicatedObject allows to share same argument between multiple @@ -210,58 +219,3 @@ func newReplicateMessage(id oid.ID, src io.ReadSeeker, signer neofscrypto.Signer return msg, nil } - -type replicateResponse struct { - _sigRequested bool - - objSig *neofscrypto.Signature - err error -} - -func (x replicateResponse) ToGRPCMessage() grpc.Message { - return new(objectgrpc.ReplicateResponse) -} - -func (x *replicateResponse) FromGRPCMessage(gm grpc.Message) error { - m, ok := gm.(*objectgrpc.ReplicateResponse) - if !ok { - return message.NewUnexpectedMessageType(gm, m) - } - - var st *status.Status - if mst := m.GetStatus(); mst != nil { - st = new(status.Status) - err := st.FromGRPCMessage(mst) - if err != nil { - return fmt.Errorf("decode response status: %w", err) - } - } - - x.err = apistatus.ErrorFromV2(st) - if x.err != nil { - return nil - } - - if !x._sigRequested { - return nil - } - - sig := m.GetObjectSignature() - if sig == nil { - return errors.New("requested but missing signature") - } - - sigV2 := new(refs.Signature) - err := sigV2.Unmarshal(sig) - if err != nil { - return fmt.Errorf("decoding signature from proto message: %w", err) - } - - x.objSig = new(neofscrypto.Signature) - err = x.objSig.ReadFromV2(*sigV2) - if err != nil { - return fmt.Errorf("invalid signature: %w", err) - } - - return nil -} diff --git a/client/object_replicate_test.go b/client/object_replicate_test.go index 49ade9fe..dc10a69d 100644 --- a/client/object_replicate_test.go +++ b/client/object_replicate_test.go @@ -5,13 +5,11 @@ import ( "context" "crypto/rand" "fmt" - "net" "sync" "testing" objectgrpc "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" "github.com/nspcc-dev/neofs-api-go/v2/refs" - "github.com/nspcc-dev/neofs-api-go/v2/rpc/client" status "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -20,9 +18,6 @@ import ( oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" objecttest "github.com/nspcc-dev/neofs-sdk-go/object/test" "github.com/stretchr/testify/require" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/test/bufconn" "google.golang.org/protobuf/proto" ) @@ -145,31 +140,6 @@ func (x *testReplicationServer) Replicate(_ context.Context, req *objectgrpc.Rep return &resp, nil } -func serveObjectReplication(tb testing.TB, clientSigner neofscrypto.Signer, clientObj object.Object) (*testReplicationServer, *Client) { - lis := bufconn.Listen(1 << 10) - - var replicationSrv testReplicationServer - - gSrv := grpc.NewServer() - objectgrpc.RegisterObjectServiceServer(gSrv, &replicationSrv) - - gConn, err := grpc.Dial("", grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) { //nolint:staticcheck //SA1019: grpc.Dial is deprecated: use NewClient instead. Will be supported throughout 1.x. - return lis.Dial() - }), grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(tb, err) - - tb.Cleanup(gSrv.Stop) - - go func() { _ = gSrv.Serve(lis) }() - - replicationSrv.clientObj = clientObj - replicationSrv.clientSigner = clientSigner - - return &replicationSrv, &Client{ - c: *client.New(client.WithGRPCConn(gConn)), - } -} - func TestClient_ReplicateObject(t *testing.T) { ctx := context.Background() signer := neofscryptotest.Signer() @@ -179,8 +149,11 @@ func TestClient_ReplicateObject(t *testing.T) { bObj := obj.Marshal() t.Run("OK", func(t *testing.T) { - srv, cli := serveObjectReplication(t, signer, obj) - srv.respStatusCode = 0 + srv := testReplicationServer{ + clientSigner: signer, + clientObj: obj, + } + cli := newTestObjectClient(t, &srv) _, err := cli.ReplicateObject(ctx, id, bytes.NewReader(bObj), signer, false) require.NoError(t, err) @@ -188,13 +161,18 @@ func TestClient_ReplicateObject(t *testing.T) { t.Run("invalid binary object", func(t *testing.T) { bObj := []byte("Hello, world!") // definitely incorrect binary object - _, cli := serveObjectReplication(t, signer, obj) + cli := newClient(t) _, err := cli.ReplicateObject(ctx, id, bytes.NewReader(bObj), signer, false) require.Error(t, err) }) t.Run("statuses", func(t *testing.T) { + srv := testReplicationServer{ + clientSigner: signer, + clientObj: obj, + } + cli := newTestObjectClient(t, &srv) for _, tc := range []struct { code uint32 expErr error @@ -204,7 +182,6 @@ func TestClient_ReplicateObject(t *testing.T) { {code: 2048, expErr: apistatus.ErrObjectAccessDenied, desc: "forbidden"}, {code: 3072, expErr: apistatus.ErrContainerNotFound, desc: "container not found"}, } { - srv, cli := serveObjectReplication(t, signer, obj) srv.respStatusCode = tc.code _, err := cli.ReplicateObject(ctx, id, bytes.NewReader(bObj), signer, false) @@ -213,8 +190,11 @@ func TestClient_ReplicateObject(t *testing.T) { }) t.Run("sign object data", func(t *testing.T) { - srv, cli := serveObjectReplication(t, signer, obj) - srv.respStatusCode = 0 + srv := testReplicationServer{ + clientSigner: signer, + clientObj: obj, + } + cli := newTestObjectClient(t, &srv) sig, err := cli.ReplicateObject(ctx, id, bytes.NewReader(bObj), signer, true) require.NoError(t, err) @@ -223,7 +203,11 @@ func TestClient_ReplicateObject(t *testing.T) { t.Run("demux", func(t *testing.T) { demuxObj := DemuxReplicatedObject(bytes.NewReader(bObj)) - _, cli := serveObjectReplication(t, signer, obj) + srv := testReplicationServer{ + clientSigner: signer, + clientObj: obj, + } + cli := newTestObjectClient(t, &srv) _, err := cli.ReplicateObject(ctx, id, demuxObj, signer, false) require.NoError(t, err) diff --git a/client/object_search.go b/client/object_search.go index 5be1fd41..52a60f2f 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "time" "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" v2refs "github.com/nspcc-dev/neofs-api-go/v2/refs" "github.com/nspcc-dev/neofs-sdk-go/bearer" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" @@ -54,15 +56,25 @@ func (x *PrmObjectSearch) SetFilters(filters object.SearchFilters) { x.filters = filters } +// used part of [protoobject.ObjectService_SearchClient] simplifying test +// implementations. +type searchObjectsResponseStream interface { + // Recv reads next message with found object IDs from the stream. Recv returns + // [io.EOF] after the server sent the last message and gracefully finished the + // stream. Any other error means stream abort. + Recv() (*protoobject.SearchResponse, error) +} + // ObjectListReader is designed to read list of object identifiers from NeoFS system. // // Must be initialized using Client.ObjectSearch, any other usage is unsafe. type ObjectListReader struct { - client *Client - cancelCtxStream context.CancelFunc - err error - stream searchObjectsResponseStream - tail []v2refs.ObjectID + client *Client + cancelCtxStream context.CancelFunc + err error + stream searchObjectsResponseStream + singleMsgTimeout time.Duration + tail []v2refs.ObjectID statisticCallback shortStatisticCallback } @@ -86,19 +98,27 @@ func (x *ObjectListReader) Read(buf []oid.ID) (int, error) { } for { - var resp v2object.SearchResponse - x.err = x.stream.Read(&resp) + var resp *protoobject.SearchResponse + x.err = dowithTimeout(x.singleMsgTimeout, x.cancelCtxStream, func() error { + var err error + resp, err = x.stream.Recv() + return err + }) if x.err != nil { return read, x.err } + var respV2 v2object.SearchResponse + if x.err = respV2.FromGRPCMessage(resp); x.err != nil { + return read, x.err + } - x.err = x.client.processResponse(&resp) + x.err = x.client.processResponse(&respV2) if x.err != nil { return read, x.err } // read new chunk of objects - ids := resp.GetBody().GetIDList() + ids := respV2.GetBody().GetIDList() if len(ids) == 0 { // just skip empty lists since they are not prohibited by protocol continue @@ -219,11 +239,12 @@ func (c *Client) ObjectSearchInit(ctx context.Context, containerID cid.ID, signe var r ObjectListReader ctx, r.cancelCtxStream = context.WithCancel(ctx) - r.stream, err = c.server.searchObjects(ctx, req) + r.stream, err = c.object.Search(ctx, req.ToGRPCMessage().(*protoobject.SearchRequest)) if err != nil { err = fmt.Errorf("open stream: %w", err) return nil, err } + r.singleMsgTimeout = c.streamTimeout r.client = c r.statisticCallback = func(err error) { c.sendStatistic(stat.MethodObjectSearchStream, err)() diff --git a/client/object_search_test.go b/client/object_search_test.go index 66f38c9d..33caa346 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -7,9 +7,12 @@ import ( "io" "testing" - apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/refs" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" cid "github.com/nspcc-dev/neofs-sdk-go/container/id" cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" @@ -114,13 +117,13 @@ func TestObjectIterate(t *testing.T) { actual = append(actual, id) return false }) - require.True(t, errors.Is(err, expectedErr), "got: %v", err) + require.ErrorIs(t, err, apistatus.ErrServerInternal) require.Equal(t, ids[:2], actual) }) } func TestClient_ObjectSearch(t *testing.T) { - c := newClient(t, nil) + c := newClient(t) t.Run("missing signer", func(t *testing.T) { _, err := c.ObjectSearchInit(context.Background(), cid.ID{}, nil, PrmObjectSearch{}) @@ -131,13 +134,11 @@ func TestClient_ObjectSearch(t *testing.T) { func newTestSearchObjectsStreamWithEndErr(t *testing.T, endError error, idList ...[]oid.ID) *ObjectListReader { usr := usertest.User() srv := testSearchObjectsServer{ - stream: testSearchObjectsResponseStream{ - signer: usr, - endError: endError, - idList: idList, - }, + signer: usr, + endStatus: apistatus.ErrorToV2(endError).ToGRPCMessage().(*protostatus.Status), + idList: idList, } - stream, err := newClient(t, &srv).ObjectSearchInit(context.Background(), cidtest.ID(), usr, PrmObjectSearch{}) + stream, err := newTestObjectClient(t, &srv).ObjectSearchInit(context.Background(), cidtest.ID(), usr, PrmObjectSearch{}) require.NoError(t, err) return stream } @@ -146,52 +147,61 @@ func newTestSearchObjectsStream(t *testing.T, idList ...[]oid.ID) *ObjectListRea return newTestSearchObjectsStreamWithEndErr(t, nil, idList...) } -type testSearchObjectsResponseStream struct { - signer neofscrypto.Signer - n int - endError error - idList [][]oid.ID +type testSearchObjectsServer struct { + protoobject.UnimplementedObjectServiceServer + + signer neofscrypto.Signer + endStatus *protostatus.Status + idList [][]oid.ID } -func (s *testSearchObjectsResponseStream) Read(resp *v2object.SearchResponse) error { - if len(s.idList) == 0 || s.n == len(s.idList) { - if s.endError != nil { - return s.endError +func (x *testSearchObjectsServer) Search(_ *protoobject.SearchRequest, stream protoobject.ObjectService_SearchServer) error { + signer := x.signer + if signer == nil { + signer = neofscryptotest.Signer() + } + for i := range x.idList { + resp := protoobject.SearchResponse{ + Body: &protoobject.SearchResponse_Body{ + IdList: make([]*protorefs.ObjectID, len(x.idList[i])), + }, + } + for j := range x.idList[i] { + resp.Body.IdList[j] = &protorefs.ObjectID{Value: x.idList[i][j][:]} + } + + var respV2 v2object.SearchResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(signer, &respV2, nil); err != nil { + return fmt.Errorf("sign response message: %w", err) + } + if err := stream.Send(respV2.ToGRPCMessage().(*protoobject.SearchResponse)); err != nil { + return err } - return io.EOF } - var body v2object.SearchResponseBody + if x.endStatus == nil { + return nil + } - if s.idList[s.n] != nil { - ids := make([]refs.ObjectID, len(s.idList[s.n])) - for i := range s.idList[s.n] { - s.idList[s.n][i].WriteToV2(&ids[i]) - } - body.SetIDList(ids) + resp := protoobject.SearchResponse{ + MetaHeader: &protosession.ResponseMetaHeader{ + Status: x.endStatus, + }, } - resp.SetBody(&body) - signer := s.signer - if signer == nil { - signer = neofscryptotest.Signer() + var respV2 v2object.SearchResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) } - err := signServiceMessage(s.signer, resp, nil) - if err != nil { + if err := signServiceMessage(signer, &respV2, nil); err != nil { return fmt.Errorf("sign response message: %w", err) } + if err := stream.Send(respV2.ToGRPCMessage().(*protoobject.SearchResponse)); err != nil { + return err + } - s.n++ - return nil -} - -type testSearchObjectsServer struct { - unimplementedNeoFSAPIServer - - stream testSearchObjectsResponseStream -} - -func (x *testSearchObjectsServer) searchObjects(context.Context, apiobject.SearchRequest) (searchObjectsResponseStream, error) { - x.stream.n = 0 - return &x.stream, nil + return stream.Send(respV2.ToGRPCMessage().(*protoobject.SearchResponse)) } diff --git a/client/object_test.go b/client/object_test.go new file mode 100644 index 00000000..86cd3650 --- /dev/null +++ b/client/object_test.go @@ -0,0 +1,12 @@ +package client + +import ( + "testing" + + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" +) + +// returns Client of Object service provided by given server. +func newTestObjectClient(t testing.TB, srv protoobject.ObjectServiceServer) *Client { + return newClient(t, testService{desc: &protoobject.ObjectService_ServiceDesc, impl: srv}) +} diff --git a/client/reputation.go b/client/reputation.go index b86d26ce..444c5012 100644 --- a/client/reputation.go +++ b/client/reputation.go @@ -4,6 +4,7 @@ import ( "context" v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation" + protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" "github.com/nspcc-dev/neofs-sdk-go/reputation" "github.com/nspcc-dev/neofs-sdk-go/stat" ) @@ -69,7 +70,15 @@ func (c *Client) AnnounceLocalTrust(ctx context.Context, epoch uint64, trusts [] cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.announceLocalTrust(ctx, req) + resp, err := c.reputation.AnnounceLocalTrust(ctx, req.ToGRPCMessage().(*protoreputation.AnnounceLocalTrustRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2reputation.AnnounceLocalTrustResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } // process call @@ -141,7 +150,15 @@ func (c *Client) AnnounceIntermediateTrust(ctx context.Context, epoch uint64, tr cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.announceIntermediateReputation(ctx, req) + resp, err := c.reputation.AnnounceIntermediateResult(ctx, req.ToGRPCMessage().(*protoreputation.AnnounceIntermediateResultRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2reputation.AnnounceIntermediateResultResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } // process call diff --git a/client/reputation_test.go b/client/reputation_test.go index cbe7c716..975af462 100644 --- a/client/reputation_test.go +++ b/client/reputation_test.go @@ -3,35 +3,52 @@ package client import ( "context" "fmt" + "testing" "github.com/nspcc-dev/neofs-api-go/v2/reputation" + protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" ) +// returns Client of Reputation service provided by given server. +func newTestReputationClient(t testing.TB, srv protoreputation.ReputationServiceServer) *Client { + return newClient(t, testService{desc: &protoreputation.ReputationService_ServiceDesc, impl: srv}) +} + type testAnnounceIntermediateReputationServer struct { - unimplementedNeoFSAPIServer + protoreputation.UnimplementedReputationServiceServer } -func (x testAnnounceIntermediateReputationServer) announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { - var resp reputation.AnnounceIntermediateResultResponse +func (x *testAnnounceIntermediateReputationServer) AnnounceIntermediateResult(context.Context, *protoreputation.AnnounceIntermediateResultRequest, +) (*protoreputation.AnnounceIntermediateResultResponse, error) { + var resp protoreputation.AnnounceIntermediateResultResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 reputation.AnnounceIntermediateResultResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoreputation.AnnounceIntermediateResultResponse), nil } type testAnnounceLocalTrustServer struct { - unimplementedNeoFSAPIServer + protoreputation.UnimplementedReputationServiceServer } -func (x testAnnounceLocalTrustServer) announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { - var resp reputation.AnnounceLocalTrustResponse +func (x *testAnnounceLocalTrustServer) AnnounceLocalTrust(context.Context, *protoreputation.AnnounceLocalTrustRequest, +) (*protoreputation.AnnounceLocalTrustResponse, error) { + var resp protoreputation.AnnounceLocalTrustResponse - if err := signServiceMessage(neofscryptotest.Signer(), &resp, nil); err != nil { + var respV2 reputation.AnnounceLocalTrustResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } + if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } - return &resp, nil + return respV2.ToGRPCMessage().(*protoreputation.AnnounceLocalTrustResponse), nil } diff --git a/client/session.go b/client/session.go index 9070daa5..99631ab8 100644 --- a/client/session.go +++ b/client/session.go @@ -5,6 +5,7 @@ import ( "github.com/nspcc-dev/neofs-api-go/v2/refs" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" ) @@ -112,7 +113,15 @@ func (c *Client) SessionCreate(ctx context.Context, signer user.Signer, prm PrmS cc.meta = prm.prmCommonMeta cc.req = &req cc.call = func() (responseV2, error) { - return c.server.createSession(ctx, req) + resp, err := c.session.Create(ctx, req.ToGRPCMessage().(*protosession.CreateRequest)) + if err != nil { + return nil, rpcErr(err) + } + var respV2 v2session.CreateResponse + if err = respV2.FromGRPCMessage(resp); err != nil { + return nil, err + } + return &respV2, nil } cc.result = func(r responseV2) { resp := r.(*v2session.CreateResponse) diff --git a/client/session_test.go b/client/session_test.go index ecae5557..dd9dcc0e 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -5,54 +5,63 @@ import ( "testing" "github.com/nspcc-dev/neofs-api-go/v2/session" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" ) +// returns Client of Session service provided by given server. +func newTestSessionClient(t testing.TB, srv protosession.SessionServiceServer) *Client { + return newClient(t, testService{desc: &protosession.SessionService_ServiceDesc, impl: srv}) +} + type testCreateSessionServer struct { - unimplementedNeoFSAPIServer + protosession.UnimplementedSessionServiceServer signer neofscrypto.Signer unsetID bool unsetKey bool } -func (m testCreateSessionServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { - var body session.CreateResponseBody +func (m *testCreateSessionServer) Create(context.Context, *protosession.CreateRequest) (*protosession.CreateResponse, error) { + resp := protosession.CreateResponse{ + Body: new(protosession.CreateResponse_Body), + } + if !m.unsetID { - body.SetID([]byte{1}) + resp.Body.Id = []byte{1} } if !m.unsetKey { - body.SetSessionKey([]byte{2}) + resp.Body.SessionKey = []byte{1} } - var resp session.CreateResponse - resp.SetBody(&body) - + var respV2 session.CreateResponse + if err := respV2.FromGRPCMessage(&resp); err != nil { + panic(err) + } signer := m.signer if signer == nil { signer = neofscryptotest.Signer() } - if err := signServiceMessage(signer, &resp, nil); err != nil { + if err := signServiceMessage(signer, &respV2, nil); err != nil { return nil, err } - return &resp, nil + return respV2.ToGRPCMessage().(*protosession.CreateResponse), nil } func TestClient_SessionCreate(t *testing.T) { ctx := context.Background() usr := usertest.User() - c := newClient(t, nil) - var prmSessionCreate PrmSessionCreate prmSessionCreate.SetExp(1) t.Run("missing session id", func(t *testing.T) { - c.setNeoFSAPIServer(&testCreateSessionServer{signer: usr, unsetID: true}) + srv := testCreateSessionServer{signer: usr, unsetID: true} + c := newTestSessionClient(t, &srv) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) @@ -61,7 +70,8 @@ func TestClient_SessionCreate(t *testing.T) { }) t.Run("missing session key", func(t *testing.T) { - c.setNeoFSAPIServer(&testCreateSessionServer{signer: usr, unsetKey: true}) + srv := testCreateSessionServer{signer: usr, unsetKey: true} + c := newTestSessionClient(t, &srv) result, err := c.SessionCreate(ctx, usr, prmSessionCreate) require.Nil(t, result) diff --git a/client/util_test.go b/client/util_test.go deleted file mode 100644 index ada23b23..00000000 --- a/client/util_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package client - -import ( - "context" - "errors" - - "github.com/nspcc-dev/neofs-api-go/v2/accounting" - "github.com/nspcc-dev/neofs-api-go/v2/container" - "github.com/nspcc-dev/neofs-api-go/v2/netmap" - "github.com/nspcc-dev/neofs-api-go/v2/object" - "github.com/nspcc-dev/neofs-api-go/v2/reputation" - "github.com/nspcc-dev/neofs-api-go/v2/session" -) - -type unimplementedNeoFSAPIServer struct{} - -func (unimplementedNeoFSAPIServer) createSession(context.Context, session.CreateRequest) (*session.CreateResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getBalance(context.Context, accounting.BalanceRequest) (*accounting.BalanceResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) netMapSnapshot(context.Context, netmap.SnapshotRequest) (*netmap.SnapshotResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getNetworkInfo(context.Context, netmap.NetworkInfoRequest) (*netmap.NetworkInfoResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getNodeInfo(context.Context, netmap.LocalNodeInfoRequest) (*netmap.LocalNodeInfoResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) putContainer(context.Context, container.PutRequest) (*container.PutResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getContainer(context.Context, container.GetRequest) (*container.GetResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) deleteContainer(context.Context, container.DeleteRequest) (*container.DeleteResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) listContainers(context.Context, container.ListRequest) (*container.ListResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getEACL(context.Context, container.GetExtendedACLRequest) (*container.GetExtendedACLResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) setEACL(context.Context, container.SetExtendedACLRequest) (*container.SetExtendedACLResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) announceContainerSpace(context.Context, container.AnnounceUsedSpaceRequest) (*container.AnnounceUsedSpaceResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) announceIntermediateReputation(context.Context, reputation.AnnounceIntermediateResultRequest) (*reputation.AnnounceIntermediateResultResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) announceLocalTrust(context.Context, reputation.AnnounceLocalTrustRequest) (*reputation.AnnounceLocalTrustResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) putObject(context.Context) (objectWriter, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) deleteObject(context.Context, object.DeleteRequest) (*object.DeleteResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) hashObjectPayloadRanges(context.Context, object.GetRangeHashRequest) (*object.GetRangeHashResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) headObject(context.Context, object.HeadRequest) (*object.HeadResponse, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getObject(context.Context, object.GetRequest) (getObjectResponseStream, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) getObjectPayloadRange(context.Context, object.GetRangeRequest) (getObjectPayloadRangeResponseStream, error) { - return nil, errors.New("unimplemented") -} -func (unimplementedNeoFSAPIServer) searchObjects(context.Context, object.SearchRequest) (searchObjectsResponseStream, error) { - return nil, errors.New("unimplemented") -} From e94cd49ea6d79aacdae69cd3cff18160448de9aa Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 21 Nov 2024 20:42:13 +0300 Subject: [PATCH 11/16] client: Drop debug printline Signed-off-by: Leonard Lyubich --- client/object_replicate_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/client/object_replicate_test.go b/client/object_replicate_test.go index dc10a69d..003a9869 100644 --- a/client/object_replicate_test.go +++ b/client/object_replicate_test.go @@ -222,7 +222,6 @@ func TestClient_ReplicateObject(t *testing.T) { defer wg.Done() _, err := cli.ReplicateObject(ctx, id, demuxObj, signer, false) - fmt.Println(err) require.NoError(t, err) }() } From 0de224ef363114a0268f2104f28d2f80d789db39 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Thu, 21 Nov 2024 21:36:57 +0300 Subject: [PATCH 12/16] client: Increase test coverage of `BalanceGet` method Mainly checks the correctness and integrity of messages sent over the network To be continued: other methods will be covered by similar tests in the future. Signed-off-by: Leonard Lyubich --- client/accounting_test.go | 437 ++++++++++++++++++++++++++++- client/client_test.go | 45 ++- client/container_statistic_test.go | 8 +- client/netmap_test.go | 33 ++- 4 files changed, 504 insertions(+), 19 deletions(-) diff --git a/client/accounting_test.go b/client/accounting_test.go index 0970e4b2..b76e6104 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -1,37 +1,226 @@ package client import ( + "bytes" "context" + "errors" "fmt" + "math/rand" "testing" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + protostatus "github.com/nspcc-dev/neofs-api-go/v2/status/grpc" + accountingtest "github.com/nspcc-dev/neofs-sdk-go/accounting/test" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/user" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" + "github.com/nspcc-dev/neofs-sdk-go/version" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" ) +func newDefaultAccountingService(srv protoaccounting.AccountingServiceServer) testService { + return testService{desc: &protoaccounting.AccountingService_ServiceDesc, impl: srv} +} + // returns Client of Accounting service provided by given server. func newTestAccountingClient(t testing.TB, srv protoaccounting.AccountingServiceServer) *Client { - return newClient(t, testService{desc: &protoaccounting.AccountingService_ServiceDesc, impl: srv}) + return newClient(t, newDefaultAccountingService(srv)) } type testGetBalanceServer struct { protoaccounting.UnimplementedAccountingServiceServer + + reqXHdrs []string + reqAcc []byte + + handlerErr error + + respUnsigned bool + respSigner neofscrypto.Signer + respMeta *protosession.ResponseMetaHeader + respBodyCons func() *protoaccounting.BalanceResponse_Body +} + +// returns [protoaccounting.AccountingServiceServer] supporting Balance method +// only. Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestGetBalanceServer() *testGetBalanceServer { return new(testGetBalanceServer) } + +// makes the server to assert that any request has given X-headers. By +// default, no headers are expected. +func (x *testGetBalanceServer) checkRequestXHeaders(xhdrs []string) { + if len(xhdrs)%2 != 0 { + panic("odd number of elements") + } + x.reqXHdrs = xhdrs +} + +// makes the server to assert that any request is for the given +// account. By default, any account is accepted. +func (x *testGetBalanceServer) checkRequestAccount(acc user.ID) { + x.reqAcc = acc[:] +} + +// makes the server to always respond with the unsigned message. By default, any +// response is signed. +// +// Overrides signResponsesBy. +func (x *testGetBalanceServer) respondWithoutSigning() { + x.respUnsigned = true +} + +// makes the server to always sign responses using given signer. By default, +// random signer is used. +// +// Has no effect with respondWithoutSigning. +func (x *testGetBalanceServer) signResponsesBy(signer neofscrypto.Signer) { + x.respSigner = signer +} + +// makes the server to always respond with the specifically constructed body. By +// default, any valid body is returned. +// +// Conflicts with respondWithBalance. +func (x *testGetBalanceServer) respondWithBody(newBody func() *protoaccounting.BalanceResponse_Body) { + x.respBodyCons = newBody +} + +// makes the server to always respond with the given balance. By default, any +// valid balance is returned. +// +// Conflicts with respondWithBody. +func (x *testGetBalanceServer) respondWithBalance(balance *protoaccounting.Decimal) { + x.respondWithBody(func() *protoaccounting.BalanceResponse_Body { + return &protoaccounting.BalanceResponse_Body{Balance: balance} + }) +} + +// makes the server to always respond with the given meta header. By default, +// empty header is returned. +// +// Conflicts with respondWithStatus. +func (x *testGetBalanceServer) respondWithMeta(meta *protosession.ResponseMetaHeader) { + x.respMeta = meta +} + +// makes the server to always respond with the given status. By default, status +// OK is returned. +// +// Conflicts with respondWithMeta. +func (x *testGetBalanceServer) respondWithStatus(st *protostatus.Status) { + x.respondWithMeta(&protosession.ResponseMetaHeader{Status: st}) +} + +// makes the server to return given error from the handler. By default, some +// response message is returned. +func (x *testGetBalanceServer) setHandlerError(err error) { + x.handlerErr = err +} + +func (x *testGetBalanceServer) verifyBalanceRequest(req *protoaccounting.BalanceRequest) error { + // signatures + var reqV2 v2accounting.BalanceRequest + if err := reqV2.FromGRPCMessage(req); err != nil { + panic(err) + } + if err := verifyServiceMessage(&reqV2); err != nil { + return newInvalidRequestVerificationHeaderErr(err) + } + // meta header + metaHdr := req.MetaHeader + curVersion := version.Current() + switch { + case metaHdr == nil: + return newInvalidRequestErr(errors.New("missing meta header")) + case metaHdr.Version == nil: + return newInvalidRequestMetaHeaderErr(errors.New("missing protocol version")) + case metaHdr.Version.Major != curVersion.Major() || metaHdr.Version.Minor != curVersion.Minor(): + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong protocol version v%d.%d, expected %s", + metaHdr.Version.Major, metaHdr.Version.Minor, curVersion)) + case metaHdr.Epoch != 0: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("non-zero epoch #%d", metaHdr.Epoch)) + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Epoch)) + case metaHdr.SessionToken != nil: + return newInvalidRequestMetaHeaderErr(errors.New("session token attached while should not be")) + case metaHdr.BearerToken != nil: + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + case metaHdr.MagicNumber != 0: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("non-zero network magic #%d", metaHdr.MagicNumber)) + case metaHdr.Origin != nil: + return newInvalidRequestMetaHeaderErr(errors.New("origin header is presented while should not be")) + case len(metaHdr.XHeaders) != len(x.reqXHdrs)/2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("number of x-headers %d differs parameterized %d", + len(metaHdr.XHeaders), len(x.reqXHdrs)/2)) + } + for i := range metaHdr.XHeaders { + if metaHdr.XHeaders[i].Key != x.reqXHdrs[2*i] { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("x-header #%d key %q does not equal parameterized %q", + i, metaHdr.XHeaders[i].Key, x.reqXHdrs[2*i])) + } + if metaHdr.XHeaders[i].Value != x.reqXHdrs[2*i+1] { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("x-header #%d value %q does not equal parameterized %q", + i, metaHdr.XHeaders[i].Value, x.reqXHdrs[2*i+1])) + } + } + // body + body := req.Body + switch { + case body == nil: + return newInvalidRequestBodyErr(errors.New("missing body")) + case body.OwnerId == nil: + return newErrMissingRequestBodyField("account") + } + if x.reqAcc != nil && !bytes.Equal(body.OwnerId.Value, x.reqAcc[:]) { + return newErrInvalidRequestField("account", fmt.Errorf("test input mismatch")) + } + return nil } -func (x *testGetBalanceServer) Balance(context.Context, *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { +func (x *testGetBalanceServer) Balance(_ context.Context, req *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { + if err := x.verifyBalanceRequest(req); err != nil { + return nil, err + } + + if x.handlerErr != nil { + return nil, x.handlerErr + } + resp := protoaccounting.BalanceResponse{ - Body: &protoaccounting.BalanceResponse_Body{ - Balance: new(protoaccounting.Decimal), - }, + MetaHeader: x.respMeta, + } + if x.respBodyCons != nil { + resp.Body = x.respBodyCons() + } else { + resp.Body = &protoaccounting.BalanceResponse_Body{ + Balance: &protoaccounting.Decimal{ + Value: rand.Int63(), + Precision: rand.Uint32(), + }, + } + } + + if x.respUnsigned { + return &resp, nil } var respV2 v2accounting.BalanceResponse if err := respV2.FromGRPCMessage(&resp); err != nil { panic(err) } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { + signer := x.respSigner + if signer == nil { + signer = neofscryptotest.Signer() + } + if err := signServiceMessage(signer, &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } @@ -41,6 +230,9 @@ func (x *testGetBalanceServer) Balance(context.Context, *protoaccounting.Balance func TestClient_BalanceGet(t *testing.T) { c := newClient(t) ctx := context.Background() + anyUsr := usertest.ID() + var anyValidPrm PrmBalanceGet + anyValidPrm.SetAccount(anyUsr) t.Run("missing", func(t *testing.T) { t.Run("account", func(t *testing.T) { @@ -48,4 +240,237 @@ func TestClient_BalanceGet(t *testing.T) { require.ErrorIs(t, err, ErrMissingAccount) }) }) + t.Run("exact in-out", func(t *testing.T) { + /* + This test is dedicated for cases when user input results in sending a certain + request to the server and receiving a specific response to it. For user input + errors, transport, client internals, etc. see/add other tests. + */ + + balance := accountingtest.Decimal() + acc := usertest.ID() + xhdrs := []string{ + "x-key1", "x-val1", + "x-key2", "x-val2", + } + + srv := newTestGetBalanceServer() + srv.checkRequestAccount(acc) + srv.checkRequestXHeaders(xhdrs) + srv.respondWithBalance(&protoaccounting.Decimal{ + Value: balance.Value(), + Precision: balance.Precision(), + }) + + c := newTestAccountingClient(t, srv) + + var prm PrmBalanceGet + prm.SetAccount(acc) + prm.WithXHeaders(xhdrs...) + res, err := c.BalanceGet(ctx, prm) + require.NoError(t, err) + require.Equal(t, balance, res) + + // statuses + type customStatusTestcase struct { + msg string + detail *protostatus.Status_Detail + assert func(testing.TB, error) + } + for _, tc := range []struct { + code uint32 + err error + constErr error + custom []customStatusTestcase + }{ + // TODO: use const codes after transition to current module's proto lib + {code: 1024, err: new(apistatus.ServerInternal), constErr: apistatus.ErrServerInternal, custom: []customStatusTestcase{ + {msg: "some server failure", assert: func(t testing.TB, err error) { + var e *apistatus.ServerInternal + require.ErrorAs(t, err, &e) + require.Equal(t, "some server failure", e.Message()) + }}, + }}, + {code: 1025, err: new(apistatus.WrongMagicNumber), constErr: apistatus.ErrWrongMagicNumber, custom: []customStatusTestcase{ + {assert: func(t testing.TB, err error) { + var e *apistatus.WrongMagicNumber + require.ErrorAs(t, err, &e) + _, ok := e.CorrectMagic() + require.Zero(t, ok) + }}, + { + detail: &protostatus.Status_Detail{Id: 0, Value: []byte{140, 15, 162, 245, 219, 236, 37, 191}}, + assert: func(t testing.TB, err error) { + var e *apistatus.WrongMagicNumber + require.ErrorAs(t, err, &e) + magic, ok := e.CorrectMagic() + require.EqualValues(t, 1, ok) + require.EqualValues(t, uint64(10092464466800944575), magic) + }, + }, + { + detail: &protostatus.Status_Detail{Id: 0, Value: []byte{1, 2, 3}}, + assert: func(t testing.TB, err error) { + var e *apistatus.WrongMagicNumber + require.ErrorAs(t, err, &e) + _, ok := e.CorrectMagic() + require.EqualValues(t, -1, ok) + }, + }, + }}, + {code: 1026, err: new(apistatus.SignatureVerification), constErr: apistatus.ErrSignatureVerification, custom: []customStatusTestcase{ + {msg: "invalid request signature", assert: func(t testing.TB, err error) { + var e *apistatus.SignatureVerification + require.ErrorAs(t, err, &e) + require.Equal(t, "invalid request signature", e.Message()) + }}, + }}, + {code: 1027, err: new(apistatus.NodeUnderMaintenance), constErr: apistatus.ErrNodeUnderMaintenance, custom: []customStatusTestcase{ + {msg: "node is under maintenance", assert: func(t testing.TB, err error) { + var e *apistatus.NodeUnderMaintenance + require.ErrorAs(t, err, &e) + require.Equal(t, "node is under maintenance", e.Message()) + }}, + }}, + } { + st := &protostatus.Status{Code: tc.code} + srv.respondWithStatus(st) + + res, err := c.BalanceGet(ctx, prm) + require.Zero(t, res) + require.ErrorAs(t, err, &tc.err) + require.ErrorIs(t, err, tc.constErr) + + for _, tcCustom := range tc.custom { + st.Message = tcCustom.msg + if tcCustom.detail != nil { + st.Details = []*protostatus.Status_Detail{tcCustom.detail} + } + srv.respondWithStatus(st) + + _, err := c.BalanceGet(ctx, prm) + require.ErrorAs(t, err, &tc.err) + tcCustom.assert(t, tc.err) + } + } + }) + t.Run("sign request failure", func(t *testing.T) { + c.prm.signer = neofscryptotest.FailSigner(neofscryptotest.Signer()) + _, err := c.BalanceGet(ctx, anyValidPrm) + require.ErrorContains(t, err, "sign request") + }) + t.Run("transport failure", func(t *testing.T) { + // note: errors returned from gRPC handlers are gRPC statuses, therefore, + // strictly speaking, they are not transport errors (like connection refusal for + // example). At the same time, according to the NeoFS protocol, all its statuses + // are transmitted in the message. So, returning an error from gRPC handler + // instead of a status field in the response is a protocol violation and can be + // equated to a transport error. + transportErr := errors.New("any transport failure") + srv := newTestGetBalanceServer() + srv.setHandlerError(transportErr) + c := newTestAccountingClient(t, srv) + + _, err := c.BalanceGet(ctx, anyValidPrm) + require.ErrorContains(t, err, "rpc failure") + require.ErrorContains(t, err, "write request") + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Unknown, st.Code()) + require.Equal(t, err.Error(), st.Message()) + }) + t.Run("response message decoding failure", func(t *testing.T) { + svc := testService{ + desc: &grpc.ServiceDesc{ServiceName: "neo.fs.v2.accounting.AccountingService", Methods: []grpc.MethodDesc{ + { + MethodName: "Balance", + Handler: func(srv any, ctx context.Context, dec func(any) error, interceptor grpc.UnaryServerInterceptor) (any, error) { + return timestamppb.Now(), nil // any completely different message + }, + }, + }}, + impl: nil, // disables interface assert + } + c := newClient(t, svc) + _, err := c.BalanceGet(ctx, anyValidPrm) + require.ErrorContains(t, err, "invalid response signature") + // TODO: Although the client will not accept such a response, current error + // does not make it clear what exactly the problem is. It is worth reacting to + // the incorrect structure if possible. + }) + t.Run("invalid response verification header", func(t *testing.T) { + srv := newTestGetBalanceServer() + srv.respondWithoutSigning() + // TODO: add cases with less radical corruption such as replacing one byte or + // dropping only one of the signatures + c := newTestAccountingClient(t, srv) + + _, err := c.BalanceGet(ctx, anyValidPrm) + require.ErrorContains(t, err, "invalid response signature") + }) + t.Run("invalid response body", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoaccounting.BalanceResponse_Body + assertErr func(testing.TB, error) + }{ + {name: "missing", body: nil, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing balance field in the response") + // TODO: worth clarifying that body is completely missing? + }}, + {name: "missing", body: new(protoaccounting.BalanceResponse_Body), assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing balance field in the response") + }}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetBalanceServer() + srv.respondWithBody(func() *protoaccounting.BalanceResponse_Body { return tc.body }) + c := newTestAccountingClient(t, srv) + + _, err := c.BalanceGet(ctx, anyValidPrm) + tc.assertErr(t, err) + }) + } + }) + t.Run("response callback", func(t *testing.T) { + // NetmapService.LocalNodeInfo is called on dial, so it should also be + // initialized. The handler is called for it too. + netmapSrvSigner := neofscryptotest.Signer() + netmapSrvEpoch := rand.Uint64() + netmapSrv := newTestGetNodeInfoServer() + netmapSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: netmapSrvEpoch}) + netmapSrv.signResponsesBy(netmapSrvSigner) + + accountingSrvSigner := neofscryptotest.Signer() + accountingSrvEpoch := netmapSrvEpoch + 1 + accountingSrv := newTestGetBalanceServer() + accountingSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: accountingSrvEpoch}) + accountingSrv.signResponsesBy(accountingSrvSigner) + + var collected []ResponseMetaInfo + var cbErr error + c := newClientWithResponseCallback(t, func(meta ResponseMetaInfo) error { + collected = append(collected, meta) + return cbErr + }, + newDefaultAccountingService(accountingSrv), + newDefaultNetmapServiceDesc(netmapSrv), + ) + + _, err := c.BalanceGet(ctx, anyValidPrm) + require.NoError(t, err) + require.Equal(t, []ResponseMetaInfo{ + {key: netmapSrvSigner.PublicKeyBytes, epoch: netmapSrvEpoch}, + {key: accountingSrvSigner.PublicKeyBytes, epoch: accountingSrvEpoch}, + }, collected) + + cbErr = errors.New("any response meta handler failure") + _, err = c.BalanceGet(ctx, anyValidPrm) + require.ErrorContains(t, err, "response callback error") + require.ErrorContains(t, err, err.Error()) + require.Len(t, collected, 3) + require.Equal(t, collected[2], collected[1]) + }) } diff --git a/client/client_test.go b/client/client_test.go index 6a54aec1..4fc4b05a 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "fmt" "net" "testing" @@ -24,20 +25,40 @@ func init() { statusErr.SetMessage("test status error") } +func newInvalidRequestErr(cause error) error { + return fmt.Errorf("invalid request: %w", cause) +} + +func newInvalidRequestMetaHeaderErr(cause error) error { + return newInvalidRequestErr(fmt.Errorf("invalid meta header: %w", cause)) +} + +func newInvalidRequestVerificationHeaderErr(cause error) error { + return newInvalidRequestErr(fmt.Errorf("invalid verification header: %w", cause)) +} + +func newInvalidRequestBodyErr(cause error) error { + return newInvalidRequestErr(fmt.Errorf("invalid body: %w", cause)) +} + +func newErrMissingRequestBodyField(name string) error { + return newInvalidRequestBodyErr(fmt.Errorf("missing %s field", name)) +} + +func newErrInvalidRequestField(name string, err error) error { + return newInvalidRequestBodyErr(fmt.Errorf("invalid %s field: %w", name, err)) +} + // pairs service spec and implementation to-be-registered in some [grpc.Server]. type testService struct { desc *grpc.ServiceDesc impl any } -// returns ready-to-go [Client] of provided optional services. By default, any -// other service is unsupported. -// -// If caller registers stat callback (like [PrmInit.SetStatisticCallback] does) -// processing nodeKey, it must include NetmapService with implemented -// LocalNodeInfo method. -func newClient(t testing.TB, svcs ...testService) *Client { +// extends newClient with response meta info callback. +func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error, svcs ...testService) *Client { var prm PrmInit + prm.SetResponseInfoCallback(cb) c, err := New(prm) require.NoError(t, err) @@ -63,6 +84,16 @@ func newClient(t testing.TB, svcs ...testService) *Client { return c } +// returns ready-to-go [Client] of provided optional services. By default, any +// other service is unsupported. +// +// If caller registers stat callback (like [PrmInit.SetStatisticCallback] does) +// processing nodeKey, it must include NetmapService with implemented +// LocalNodeInfo method. +func newClient(t testing.TB, svcs ...testService) *Client { + return newClientWithResponseCallback(t, nil, svcs...) +} + func TestClient_Dial(t *testing.T) { var prmInit PrmInit diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index cf8c8af4..62b64e8a 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -104,8 +104,8 @@ func testEaclTable(containerID cid.ID) eacl.Table { func TestClientStatistic_AccountBalance(t *testing.T) { usr := usertest.User() ctx := context.Background() - var srv testGetBalanceServer - c := newTestAccountingClient(t, &srv) + srv := newTestGetBalanceServer() + c := newTestAccountingClient(t, srv) collector := newCollector() c.prm.statisticCallback = collector.Collect @@ -245,8 +245,8 @@ func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { ctx := context.Background() - var srv testGetNodeInfoServer - c := newTestNetmapClient(t, &srv) + srv := newTestGetNodeInfoServer() + c := newTestNetmapClient(t, srv) collector := newCollector() c.prm.statisticCallback = collector.Collect diff --git a/client/netmap_test.go b/client/netmap_test.go index 84989534..a5ad2afd 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -20,9 +20,13 @@ import ( "google.golang.org/grpc/status" ) +func newDefaultNetmapServiceDesc(srv protonetmap.NetmapServiceServer) testService { + return testService{desc: &protonetmap.NetmapService_ServiceDesc, impl: srv} +} + // returns Client of Netmap service provided by given server. func newTestNetmapClient(t testing.TB, srv protonetmap.NetmapServiceServer) *Client { - return newClient(t, testService{desc: &protonetmap.NetmapService_ServiceDesc, impl: srv}) + return newClient(t, newDefaultNetmapServiceDesc(srv)) } type testNetmapSnapshotServer struct { @@ -122,6 +126,26 @@ func (x *testGetNetworkInfoServer) NetworkInfo(context.Context, *protonetmap.Net type testGetNodeInfoServer struct { protonetmap.UnimplementedNetmapServiceServer + + respSigner neofscrypto.Signer + respMeta *protosession.ResponseMetaHeader +} + +// returns [protonetmap.NetmapServiceServer] supporting LocalNodeInfo method +// only. Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestGetNodeInfoServer() *testGetNodeInfoServer { return new(testGetNodeInfoServer) } + +// makes the server to always sign responses using given signer. By default, +// random signer is used. +func (x *testGetNodeInfoServer) signResponsesBy(signer neofscrypto.Signer) { + x.respSigner = signer +} + +// makes the server to always respond with the given meta header. By default, +// empty header is returned. +func (x *testGetNodeInfoServer) respondWithMeta(meta *protosession.ResponseMetaHeader) { + x.respMeta = meta } func (x *testGetNodeInfoServer) LocalNodeInfo(context.Context, *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { @@ -133,13 +157,18 @@ func (x *testGetNodeInfoServer) LocalNodeInfo(context.Context, *protonetmap.Loca Addresses: []string{"any"}, }, }, + MetaHeader: x.respMeta, } var respV2 v2netmap.LocalNodeInfoResponse if err := respV2.FromGRPCMessage(&resp); err != nil { panic(err) } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { + signer := x.respSigner + if signer == nil { + signer = neofscryptotest.Signer() + } + if err := signServiceMessage(signer, &respV2, nil); err != nil { return nil, fmt.Errorf("sign response message: %w", err) } From 756cd3f551d6ecd4834110442bb08bf91abd019e Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 22 Nov 2024 12:16:13 +0300 Subject: [PATCH 13/16] client: Clarify `Client.BalanceGet` unit test names Previous name 'missing' could be misinterpreted: missing what? Signed-off-by: Leonard Lyubich --- client/accounting_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/accounting_test.go b/client/accounting_test.go index b76e6104..200edd37 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -234,8 +234,8 @@ func TestClient_BalanceGet(t *testing.T) { var anyValidPrm PrmBalanceGet anyValidPrm.SetAccount(anyUsr) - t.Run("missing", func(t *testing.T) { - t.Run("account", func(t *testing.T) { + t.Run("invalid user input", func(t *testing.T) { + t.Run("missing account", func(t *testing.T) { _, err := c.BalanceGet(ctx, PrmBalanceGet{}) require.ErrorIs(t, err, ErrMissingAccount) }) From 4269131e184c18a683c1213ed7d8934f683f7179 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 22 Nov 2024 16:40:21 +0300 Subject: [PATCH 14/16] client: Increase `Client.BalanceGet` stat test coverage There is no point in keeping this test separate from the others, so merged. Signed-off-by: Leonard Lyubich --- client/accounting_test.go | 111 +++++++++++++++++++++++------ client/client_test.go | 21 ++++-- client/container_statistic_test.go | 16 ----- client/netmap_test.go | 17 ++++- 4 files changed, 118 insertions(+), 47 deletions(-) diff --git a/client/accounting_test.go b/client/accounting_test.go index 200edd37..570d724a 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -7,6 +7,7 @@ import ( "fmt" "math/rand" "testing" + "time" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" @@ -16,6 +17,7 @@ import ( apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" "github.com/nspcc-dev/neofs-sdk-go/user" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/nspcc-dev/neofs-sdk-go/version" @@ -43,6 +45,7 @@ type testGetBalanceServer struct { handlerErr error + respSleep time.Duration respUnsigned bool respSigner neofscrypto.Signer respMeta *protosession.ResponseMetaHeader @@ -69,18 +72,18 @@ func (x *testGetBalanceServer) checkRequestAccount(acc user.ID) { x.reqAcc = acc[:] } -// makes the server to always respond with the unsigned message. By default, any +// tells the server whether to sign all the responses or not. By default, any // response is signed. // -// Overrides signResponsesBy. -func (x *testGetBalanceServer) respondWithoutSigning() { - x.respUnsigned = true +// Calling with false overrides signResponsesBy. +func (x *testGetBalanceServer) setEnabledResponseSigning(sign bool) { + x.respUnsigned = !sign } // makes the server to always sign responses using given signer. By default, // random signer is used. // -// Has no effect with respondWithoutSigning. +// Has no effect with signing is disabled using setEnabledResponseSigning. func (x *testGetBalanceServer) signResponsesBy(signer neofscrypto.Signer) { x.respSigner = signer } @@ -125,6 +128,12 @@ func (x *testGetBalanceServer) setHandlerError(err error) { x.handlerErr = err } +// makes the server to sleep specified time before any request processing. By +// default, and if dur is non-positive, request is handled instantly. +func (x *testGetBalanceServer) setSleepDuration(dur time.Duration) { + x.respSleep = dur +} + func (x *testGetBalanceServer) verifyBalanceRequest(req *protoaccounting.BalanceRequest) error { // signatures var reqV2 v2accounting.BalanceRequest @@ -186,6 +195,8 @@ func (x *testGetBalanceServer) verifyBalanceRequest(req *protoaccounting.Balance } func (x *testGetBalanceServer) Balance(_ context.Context, req *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { + time.Sleep(x.respSleep) + if err := x.verifyBalanceRequest(req); err != nil { return nil, err } @@ -236,7 +247,7 @@ func TestClient_BalanceGet(t *testing.T) { t.Run("invalid user input", func(t *testing.T) { t.Run("missing account", func(t *testing.T) { - _, err := c.BalanceGet(ctx, PrmBalanceGet{}) + _, err := c.BalanceGet(context.Background(), PrmBalanceGet{}) require.ErrorIs(t, err, ErrMissingAccount) }) }) @@ -377,7 +388,7 @@ func TestClient_BalanceGet(t *testing.T) { st, ok := status.FromError(err) require.True(t, ok) require.Equal(t, codes.Unknown, st.Code()) - require.Equal(t, err.Error(), st.Message()) + require.Contains(t, st.Message(), transportErr.Error()) }) t.Run("response message decoding failure", func(t *testing.T) { svc := testService{ @@ -400,7 +411,7 @@ func TestClient_BalanceGet(t *testing.T) { }) t.Run("invalid response verification header", func(t *testing.T) { srv := newTestGetBalanceServer() - srv.respondWithoutSigning() + srv.setEnabledResponseSigning(false) // TODO: add cases with less radical corruption such as replacing one byte or // dropping only one of the signatures c := newTestAccountingClient(t, srv) @@ -437,17 +448,17 @@ func TestClient_BalanceGet(t *testing.T) { t.Run("response callback", func(t *testing.T) { // NetmapService.LocalNodeInfo is called on dial, so it should also be // initialized. The handler is called for it too. - netmapSrvSigner := neofscryptotest.Signer() - netmapSrvEpoch := rand.Uint64() - netmapSrv := newTestGetNodeInfoServer() - netmapSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: netmapSrvEpoch}) - netmapSrv.signResponsesBy(netmapSrvSigner) - - accountingSrvSigner := neofscryptotest.Signer() - accountingSrvEpoch := netmapSrvEpoch + 1 - accountingSrv := newTestGetBalanceServer() - accountingSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: accountingSrvEpoch}) - accountingSrv.signResponsesBy(accountingSrvSigner) + nodeInfoSrvSigner := neofscryptotest.Signer() + nodeInfoSrvEpoch := rand.Uint64() + nodeInfoSrv := newTestGetNodeInfoServer() + nodeInfoSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: nodeInfoSrvEpoch}) + nodeInfoSrv.signResponsesBy(nodeInfoSrvSigner) + + balanceSrvSigner := neofscryptotest.Signer() + balanceSrvEpoch := nodeInfoSrvEpoch + 1 + balanceSrv := newTestGetBalanceServer() + balanceSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: balanceSrvEpoch}) + balanceSrv.signResponsesBy(balanceSrvSigner) var collected []ResponseMetaInfo var cbErr error @@ -455,15 +466,15 @@ func TestClient_BalanceGet(t *testing.T) { collected = append(collected, meta) return cbErr }, - newDefaultAccountingService(accountingSrv), - newDefaultNetmapServiceDesc(netmapSrv), + newDefaultNetmapServiceDesc(nodeInfoSrv), + newDefaultAccountingService(balanceSrv), ) _, err := c.BalanceGet(ctx, anyValidPrm) require.NoError(t, err) require.Equal(t, []ResponseMetaInfo{ - {key: netmapSrvSigner.PublicKeyBytes, epoch: netmapSrvEpoch}, - {key: accountingSrvSigner.PublicKeyBytes, epoch: accountingSrvEpoch}, + {key: nodeInfoSrvSigner.PublicKeyBytes, epoch: nodeInfoSrvEpoch}, + {key: balanceSrvSigner.PublicKeyBytes, epoch: balanceSrvEpoch}, }, collected) cbErr = errors.New("any response meta handler failure") @@ -473,4 +484,58 @@ func TestClient_BalanceGet(t *testing.T) { require.Len(t, collected, 3) require.Equal(t, collected[2], collected[1]) }) + t.Run("exec statistics", func(t *testing.T) { + // NetmapService.LocalNodeInfo is called on dial, so it should also be + // initialized. Statistics are tracked for it too. + nodeEndpoint := "grpc://localhost:8082" // any valid + nodePub := []byte("any public key") + + nodeInfoSrv := newTestGetNodeInfoServer() + nodeInfoSrv.respondWithNodePublicKey(nodePub) + + balanceSrv := newTestGetBalanceServer() + + type statItem struct { + mtd stat.Method + dur time.Duration + err error + } + var lastItem *statItem + cb := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + if lastItem == nil { + require.Nil(t, pub) + } else { + require.Equal(t, nodePub, pub) + } + require.Equal(t, nodeEndpoint, endpoint) + require.Positive(t, dur) + lastItem = &statItem{mtd, dur, err} + } + + c := newCustomClient(t, nodeEndpoint, func(prm *PrmInit) { prm.SetStatisticCallback(cb) }, + newDefaultNetmapServiceDesc(nodeInfoSrv), + newDefaultAccountingService(balanceSrv), + ) + // dial + require.NotNil(t, lastItem) + require.Equal(t, stat.MethodEndpointInfo, lastItem.mtd) + require.Positive(t, lastItem.dur) + require.NoError(t, lastItem.err) + + // failure + _, callErr := c.BalanceGet(ctx, PrmBalanceGet{}) + require.Error(t, callErr) + require.Equal(t, stat.MethodBalanceGet, lastItem.mtd) + require.Positive(t, lastItem.dur) + require.Equal(t, callErr, lastItem.err) + + // OK + sleepDur := 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + balanceSrv.setSleepDuration(sleepDur) + _, _ = c.BalanceGet(ctx, anyValidPrm) + require.Equal(t, stat.MethodBalanceGet, lastItem.mtd) + require.Greater(t, lastItem.dur, sleepDur) + require.NoError(t, lastItem.err) + }) } diff --git a/client/client_test.go b/client/client_test.go index 4fc4b05a..03004157 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -55,10 +55,13 @@ type testService struct { impl any } -// extends newClient with response meta info callback. -func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error, svcs ...testService) *Client { +// the most generic alternative of newClient. Both endpoint and parameter setter +// are optional. +func newCustomClient(t testing.TB, endpoint string, setPrm func(*PrmInit), svcs ...testService) *Client { var prm PrmInit - prm.SetResponseInfoCallback(cb) + if setPrm != nil { + setPrm(&prm) + } c, err := New(prm) require.NoError(t, err) @@ -72,7 +75,10 @@ func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error go func() { _ = srv.Serve(lis) }() var dialPrm PrmDial - dialPrm.SetServerURI("grpc://localhost:8080") // any valid + if endpoint == "" { + endpoint = "grpc://localhost:8080" + } + dialPrm.SetServerURI(endpoint) // any valid dialPrm.setDialFunc(func(ctx context.Context, _ string) (net.Conn, error) { return lis.DialContext(ctx) }) err = c.Dial(dialPrm) if err != nil { @@ -84,6 +90,11 @@ func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error return c } +// extends newClient with response meta info callback. +func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error, svcs ...testService) *Client { + return newCustomClient(t, "", func(prm *PrmInit) { prm.SetResponseInfoCallback(cb) }, svcs...) +} + // returns ready-to-go [Client] of provided optional services. By default, any // other service is unsupported. // @@ -91,7 +102,7 @@ func newClientWithResponseCallback(t testing.TB, cb func(ResponseMetaInfo) error // processing nodeKey, it must include NetmapService with implemented // LocalNodeInfo method. func newClient(t testing.TB, svcs ...testService) *Client { - return newClientWithResponseCallback(t, nil, svcs...) + return newCustomClient(t, "", nil, svcs...) } func TestClient_Dial(t *testing.T) { diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go index 62b64e8a..2412d494 100644 --- a/client/container_statistic_test.go +++ b/client/container_statistic_test.go @@ -101,22 +101,6 @@ func testEaclTable(containerID cid.ID) eacl.Table { return table } -func TestClientStatistic_AccountBalance(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - srv := newTestGetBalanceServer() - c := newTestAccountingClient(t, srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmBalanceGet - prm.SetAccount(usr.ID) - _, err := c.BalanceGet(ctx, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodBalanceGet].requests) -} - func TestClientStatistic_ContainerPut(t *testing.T) { usr := usertest.User() ctx := context.Background() diff --git a/client/netmap_test.go b/client/netmap_test.go index a5ad2afd..5719631e 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -127,8 +127,9 @@ func (x *testGetNetworkInfoServer) NetworkInfo(context.Context, *protonetmap.Net type testGetNodeInfoServer struct { protonetmap.UnimplementedNetmapServiceServer - respSigner neofscrypto.Signer - respMeta *protosession.ResponseMetaHeader + respSigner neofscrypto.Signer + respMeta *protosession.ResponseMetaHeader + respNodePub []byte } // returns [protonetmap.NetmapServiceServer] supporting LocalNodeInfo method @@ -148,17 +149,27 @@ func (x *testGetNodeInfoServer) respondWithMeta(meta *protosession.ResponseMetaH x.respMeta = meta } +// makes the server to always respond with the given node public key. By default, +// any key is returned. +func (x *testGetNodeInfoServer) respondWithNodePublicKey(pub []byte) { + x.respNodePub = pub +} + func (x *testGetNodeInfoServer) LocalNodeInfo(context.Context, *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { resp := protonetmap.LocalNodeInfoResponse{ Body: &protonetmap.LocalNodeInfoResponse_Body{ Version: new(protorefs.Version), NodeInfo: &protonetmap.NodeInfo{ - PublicKey: []byte("any"), Addresses: []string{"any"}, }, }, MetaHeader: x.respMeta, } + if x.respNodePub != nil { + resp.Body.NodeInfo.PublicKey = x.respNodePub + } else { + resp.Body.NodeInfo.PublicKey = []byte("any") + } var respV2 v2netmap.LocalNodeInfoResponse if err := respV2.FromGRPCMessage(&resp); err != nil { From 614a5a7eefc9a12df38e526d8bc1b4dab66402e5 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 22 Nov 2024 18:42:34 +0300 Subject: [PATCH 15/16] client: Disable contextcheck linter for `Dial` It speaks the truth, but it's not that easy to fix. Signed-off-by: Leonard Lyubich --- client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/client/client.go b/client/client.go index 337087cf..1dc766b6 100644 --- a/client/client.go +++ b/client/client.go @@ -128,6 +128,7 @@ func New(prm PrmInit) (*Client, error) { // - [ErrNonPositiveTimeout] // // See also [Client.Close]. +// nolint:contextcheck func (c *Client) Dial(prm PrmDial) error { if prm.endpoint == "" { return ErrMissingServer From 195a8f2bf8033f3f6ac35442eba425f9e3d539c6 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Fri, 22 Nov 2024 19:32:43 +0300 Subject: [PATCH 16/16] client: Fix duration in operation statistics Previously, an incorrect duration was passed to the statistics handler of all operations. Deferred function sent the time elapsed from the start point to the almost instant calculation of the closure arguments (nanoseconds). Now client correctly measures the execution time. Also, as a slight improvement, these actions are deferred conditionally now. Signed-off-by: Leonard Lyubich --- client/accounting.go | 10 +++++-- client/client.go | 11 ++----- client/container.go | 64 +++++++++++++++++++++++++++-------------- client/netmap.go | 28 ++++++++++++------ client/object_delete.go | 10 +++++-- client/object_get.go | 47 ++++++++++++++++++++---------- client/object_hash.go | 10 +++++-- client/object_put.go | 21 +++++++++----- client/object_search.go | 19 ++++++++---- client/reputation.go | 19 ++++++++---- client/session.go | 10 +++++-- 11 files changed, 164 insertions(+), 85 deletions(-) diff --git a/client/accounting.go b/client/accounting.go index 8a1f3e2e..8be2ec4e 100644 --- a/client/accounting.go +++ b/client/accounting.go @@ -2,6 +2,7 @@ package client import ( "context" + "time" v2accounting "github.com/nspcc-dev/neofs-api-go/v2/accounting" protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" @@ -35,9 +36,12 @@ func (x *PrmBalanceGet) SetAccount(id user.ID) { // - [ErrMissingAccount] func (c *Client) BalanceGet(ctx context.Context, prm PrmBalanceGet) (accounting.Decimal, error) { var err error - defer func() { - c.sendStatistic(stat.MethodBalanceGet, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodBalanceGet, time.Since(startTime), err) + }() + } switch { case prm.account.IsZero(): diff --git a/client/client.go b/client/client.go index 1dc766b6..6e6a6d42 100644 --- a/client/client.go +++ b/client/client.go @@ -219,15 +219,8 @@ func (c *Client) Close() error { return c.Conn().Close() } -func (c *Client) sendStatistic(m stat.Method, err error) func() { - if c.prm.statisticCallback == nil { - return func() {} - } - - ts := time.Now() - return func() { - c.prm.statisticCallback(c.nodeKey, c.endpoint, m, time.Since(ts), err) - } +func (c *Client) sendStatistic(m stat.Method, dur time.Duration, err error) { + c.prm.statisticCallback(c.nodeKey, c.endpoint, m, dur, err) } // PrmInit groups initialization parameters of Client instances. diff --git a/client/container.go b/client/container.go index 58287ad3..9e4c87fe 100644 --- a/client/container.go +++ b/client/container.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" v2container "github.com/nspcc-dev/neofs-api-go/v2/container" protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" @@ -71,9 +72,12 @@ func (x *PrmContainerPut) AttachSignature(sig neofscrypto.Signature) { // - [ErrMissingSigner] func (c *Client) ContainerPut(ctx context.Context, cont container.Container, signer neofscrypto.Signer, prm PrmContainerPut) (cid.ID, error) { var err error - defer func() { - c.sendStatistic(stat.MethodContainerPut, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerPut, time.Since(startTime), err) + }() + } if signer == nil { return cid.ID{}, ErrMissingSigner @@ -174,9 +178,12 @@ type PrmContainerGet struct { // Context is required and must not be nil. It is used for network communication. func (c *Client) ContainerGet(ctx context.Context, id cid.ID, prm PrmContainerGet) (container.Container, error) { var err error - defer func() { - c.sendStatistic(stat.MethodContainerGet, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerGet, time.Since(startTime), err) + }() + } var cidV2 refs.ContainerID id.WriteToV2(&cidV2) @@ -248,9 +255,12 @@ type PrmContainerList struct { // Context is required and must not be nil. It is used for network communication. func (c *Client) ContainerList(ctx context.Context, ownerID user.ID, prm PrmContainerList) ([]cid.ID, error) { var err error - defer func() { - c.sendStatistic(stat.MethodContainerList, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerList, time.Since(startTime), err) + }() + } // form request body var ownerV2 refs.OwnerID @@ -359,9 +369,12 @@ func (x *PrmContainerDelete) AttachSignature(sig neofscrypto.Signature) { // - [ErrMissingSigner] func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscrypto.Signer, prm PrmContainerDelete) error { var err error - defer func() { - c.sendStatistic(stat.MethodContainerDelete, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerDelete, time.Since(startTime), err) + }() + } if signer == nil { return ErrMissingSigner @@ -450,9 +463,12 @@ type PrmContainerEACL struct { // Context is required and must not be nil. It is used for network communication. func (c *Client) ContainerEACL(ctx context.Context, id cid.ID, prm PrmContainerEACL) (eacl.Table, error) { var err error - defer func() { - c.sendStatistic(stat.MethodContainerEACL, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerEACL, time.Since(startTime), err) + }() + } var cidV2 refs.ContainerID id.WriteToV2(&cidV2) @@ -565,9 +581,12 @@ func (x *PrmContainerSetEACL) AttachSignature(sig neofscrypto.Signature) { // Context is required and must not be nil. It is used for network communication. func (c *Client) ContainerSetEACL(ctx context.Context, table eacl.Table, signer user.Signer, prm PrmContainerSetEACL) error { var err error - defer func() { - c.sendStatistic(stat.MethodContainerSetEACL, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerSetEACL, time.Since(startTime), err) + }() + } if signer == nil { return ErrMissingSigner @@ -665,9 +684,12 @@ type PrmAnnounceSpace struct { // - [ErrMissingAnnouncements] func (c *Client) ContainerAnnounceUsedSpace(ctx context.Context, announcements []container.SizeEstimation, prm PrmAnnounceSpace) error { var err error - defer func() { - c.sendStatistic(stat.MethodContainerAnnounceUsedSpace, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodContainerAnnounceUsedSpace, time.Since(startTime), err) + }() + } if len(announcements) == 0 { err = ErrMissingAnnouncements diff --git a/client/netmap.go b/client/netmap.go index 46861367..823d55d8 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "time" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" @@ -60,9 +61,12 @@ func (x ResEndpointInfo) NodeInfo() netmap.NodeInfo { // Reflects all internal errors in second return value (transport problems, response processing, etc.). func (c *Client) EndpointInfo(ctx context.Context, prm PrmEndpointInfo) (*ResEndpointInfo, error) { var err error - defer func() { - c.sendStatistic(stat.MethodEndpointInfo, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodEndpointInfo, time.Since(startTime), err) + }() + } // form request var req v2netmap.LocalNodeInfoRequest @@ -146,9 +150,12 @@ type PrmNetworkInfo struct { // Reflects all internal errors in second return value (transport problems, response processing, etc.). func (c *Client) NetworkInfo(ctx context.Context, prm PrmNetworkInfo) (netmap.NetworkInfo, error) { var err error - defer func() { - c.sendStatistic(stat.MethodNetworkInfo, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodNetworkInfo, time.Since(startTime), err) + }() + } // form request var req v2netmap.NetworkInfoRequest @@ -215,9 +222,12 @@ type PrmNetMapSnapshot struct { // Reflects all internal errors in second return value (transport problems, response processing, etc.). func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (netmap.NetMap, error) { var err error - defer func() { - c.sendStatistic(stat.MethodNetMapSnapshot, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodNetMapSnapshot, time.Since(startTime), err) + }() + } // form request body var body v2netmap.SnapshotRequestBody diff --git a/client/object_delete.go b/client/object_delete.go index 435e1709..f4e93470 100644 --- a/client/object_delete.go +++ b/client/object_delete.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" @@ -74,9 +75,12 @@ func (c *Client) ObjectDelete(ctx context.Context, containerID cid.ID, objectID err error ) - defer func() { - c.sendStatistic(stat.MethodObjectDelete, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectDelete, time.Since(startTime), err) + }() + } containerID.WriteToV2(&cidV2) addr.SetContainerID(&cidV2) diff --git a/client/object_get.go b/client/object_get.go index 592d05c4..40c3407d 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -88,6 +88,7 @@ type PayloadReader struct { remainingPayloadLen int statisticCallback shortStatisticCallback + startTime time.Time // if statisticCallback is set only } // readHeader reads header of the object. Result means success. @@ -219,7 +220,7 @@ func (x *PayloadReader) Close() error { var err error if x.statisticCallback != nil { defer func() { - x.statisticCallback(err) + x.statisticCallback(time.Since(x.startTime), err) }() } err = x.close(true) @@ -276,9 +277,12 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID err error ) - defer func() { - c.sendStatistic(stat.MethodObjectGet, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectGet, time.Since(startTime), err) + }() + } if signer == nil { return hdr, nil, ErrMissingSigner @@ -320,8 +324,11 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID r.stream = stream r.singleMsgTimeout = c.streamTimeout r.client = c - r.statisticCallback = func(err error) { - c.sendStatistic(stat.MethodObjectGetStream, err) + if c.prm.statisticCallback != nil { + r.startTime = time.Now() + r.statisticCallback = func(dur time.Duration, err error) { + c.sendStatistic(stat.MethodObjectGetStream, dur, err) + } } if !r.readHeader(&hdr) { @@ -366,9 +373,12 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi err error ) - defer func() { - c.sendStatistic(stat.MethodObjectHead, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectHead, time.Since(startTime), err) + }() + } if signer == nil { return nil, ErrMissingSigner @@ -466,6 +476,7 @@ type ObjectRangeReader struct { remainingPayloadLen int statisticCallback shortStatisticCallback + startTime time.Time // if statisticCallback is set only } func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) { @@ -569,7 +580,7 @@ func (x *ObjectRangeReader) Close() error { var err error if x.statisticCallback != nil { defer func() { - x.statisticCallback(err) + x.statisticCallback(time.Since(x.startTime), err) }() } err = x.close(true) @@ -622,9 +633,12 @@ func (c *Client) ObjectRangeInit(ctx context.Context, containerID cid.ID, object err error ) - defer func() { - c.sendStatistic(stat.MethodObjectRange, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectRange, time.Since(startTime), err) + }() + } if length == 0 { err = ErrZeroRangeLength @@ -678,8 +692,11 @@ func (c *Client) ObjectRangeInit(ctx context.Context, containerID cid.ID, object r.stream = stream r.singleMsgTimeout = c.streamTimeout r.client = c - r.statisticCallback = func(err error) { - c.sendStatistic(stat.MethodObjectRangeStream, err)() + if c.prm.statisticCallback != nil { + r.startTime = time.Now() + r.statisticCallback = func(dur time.Duration, err error) { + c.sendStatistic(stat.MethodObjectRangeStream, dur, err) + } } return &r, nil diff --git a/client/object_hash.go b/client/object_hash.go index a8b633c6..2564597e 100644 --- a/client/object_hash.go +++ b/client/object_hash.go @@ -3,6 +3,7 @@ package client import ( "context" "fmt" + "time" "github.com/nspcc-dev/neofs-api-go/v2/acl" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" @@ -109,9 +110,12 @@ func (c *Client) ObjectHash(ctx context.Context, containerID cid.ID, objectID oi err error ) - defer func() { - c.sendStatistic(stat.MethodObjectHash, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectHash, time.Since(startTime), err) + }() + } if len(prm.body.GetRanges()) == 0 { err = ErrMissingRanges diff --git a/client/object_put.go b/client/object_put.go index 4adf6619..e02c1b34 100644 --- a/client/object_put.go +++ b/client/object_put.go @@ -38,7 +38,7 @@ type putObjectStream interface { // shortStatisticCallback is a shorter version of [stat.OperationCallback] which is calling from [client.Client]. // The difference is the client already know some info about itself. Despite it the client doesn't know // duration and error from writer/reader. -type shortStatisticCallback func(err error) +type shortStatisticCallback func(dur time.Duration, err error) // PrmObjectPutInit groups parameters of ObjectPutInit operation. type PrmObjectPutInit struct { @@ -92,6 +92,7 @@ type DefaultObjectWriter struct { partChunk v2object.PutObjectPartChunk statisticCallback shortStatisticCallback + startTime time.Time // if statisticCallback is set only buf []byte bufCleanCallback func() @@ -234,7 +235,7 @@ func (x *DefaultObjectWriter) Write(chunk []byte) (n int, err error) { func (x *DefaultObjectWriter) Close() error { if x.statisticCallback != nil { defer func() { - x.statisticCallback(x.err) + x.statisticCallback(time.Since(x.startTime), x.err) }() } @@ -309,12 +310,18 @@ func (x *DefaultObjectWriter) GetResult() ResObjectPut { // - [ErrMissingSigner] func (c *Client) ObjectPutInit(ctx context.Context, hdr object.Object, signer user.Signer, prm PrmObjectPutInit) (ObjectWriter, error) { var err error - defer func() { - c.sendStatistic(stat.MethodObjectPut, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectPut, time.Since(startTime), err) + }() + } var w DefaultObjectWriter - w.statisticCallback = func(err error) { - c.sendStatistic(stat.MethodObjectPutStream, err)() + if c.prm.statisticCallback != nil { + w.startTime = time.Now() + w.statisticCallback = func(dur time.Duration, err error) { + c.sendStatistic(stat.MethodObjectPutStream, dur, err) + } } if signer == nil { diff --git a/client/object_search.go b/client/object_search.go index 52a60f2f..4cdf7598 100644 --- a/client/object_search.go +++ b/client/object_search.go @@ -77,6 +77,7 @@ type ObjectListReader struct { tail []v2refs.ObjectID statisticCallback shortStatisticCallback + startTime time.Time // if statisticCallback is set only } // Read reads another list of the object identifiers. Works similar to @@ -178,7 +179,7 @@ func (x *ObjectListReader) Close() error { var err error if x.statisticCallback != nil { defer func() { - x.statisticCallback(err) + x.statisticCallback(time.Since(x.startTime), err) }() } @@ -207,9 +208,12 @@ func (x *ObjectListReader) Close() error { // - [ErrMissingSigner] func (c *Client) ObjectSearchInit(ctx context.Context, containerID cid.ID, signer user.Signer, prm PrmObjectSearch) (*ObjectListReader, error) { var err error - defer func() { - c.sendStatistic(stat.MethodObjectSearch, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodObjectSearch, time.Since(startTime), err) + }() + } if signer == nil { return nil, ErrMissingSigner @@ -246,8 +250,11 @@ func (c *Client) ObjectSearchInit(ctx context.Context, containerID cid.ID, signe } r.singleMsgTimeout = c.streamTimeout r.client = c - r.statisticCallback = func(err error) { - c.sendStatistic(stat.MethodObjectSearchStream, err)() + if c.prm.statisticCallback != nil { + r.startTime = time.Now() + r.statisticCallback = func(dur time.Duration, err error) { + c.sendStatistic(stat.MethodObjectSearchStream, dur, err) + } } return &r, nil diff --git a/client/reputation.go b/client/reputation.go index 444c5012..d96a5496 100644 --- a/client/reputation.go +++ b/client/reputation.go @@ -2,6 +2,7 @@ package client import ( "context" + "time" v2reputation "github.com/nspcc-dev/neofs-api-go/v2/reputation" protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" @@ -29,9 +30,12 @@ type PrmAnnounceLocalTrust struct { // Parameter trusts must not be empty. func (c *Client) AnnounceLocalTrust(ctx context.Context, epoch uint64, trusts []reputation.Trust, prm PrmAnnounceLocalTrust) error { var err error - defer func() { - c.sendStatistic(stat.MethodAnnounceLocalTrust, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodAnnounceLocalTrust, time.Since(startTime), err) + }() + } // check parameters switch { @@ -117,9 +121,12 @@ func (x *PrmAnnounceIntermediateTrust) SetIteration(iter uint32) { // Parameter epoch must not be zero. func (c *Client) AnnounceIntermediateTrust(ctx context.Context, epoch uint64, trust reputation.PeerToPeerTrust, prm PrmAnnounceIntermediateTrust) error { var err error - defer func() { - c.sendStatistic(stat.MethodAnnounceIntermediateTrust, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodAnnounceIntermediateTrust, time.Since(startTime), err) + }() + } if epoch == 0 { err = ErrZeroEpoch diff --git a/client/session.go b/client/session.go index 99631ab8..46930e11 100644 --- a/client/session.go +++ b/client/session.go @@ -2,6 +2,7 @@ package client import ( "context" + "time" "github.com/nspcc-dev/neofs-api-go/v2/refs" v2session "github.com/nspcc-dev/neofs-api-go/v2/session" @@ -78,9 +79,12 @@ func (x ResSessionCreate) PublicKey() []byte { // - [ErrMissingSigner] func (c *Client) SessionCreate(ctx context.Context, signer user.Signer, prm PrmSessionCreate) (*ResSessionCreate, error) { var err error - defer func() { - c.sendStatistic(stat.MethodSessionCreate, err)() - }() + if c.prm.statisticCallback != nil { + startTime := time.Now() + defer func() { + c.sendStatistic(stat.MethodSessionCreate, time.Since(startTime), err) + }() + } if signer == nil { return nil, ErrMissingSigner