From 0b7300f88c309cfb5f24eeae02909f33621b37f6 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 13:44:20 +0300 Subject: [PATCH 1/9] *: Improve UUID-related errors This: - groups with wrong length case; - mentions the correct version. Signed-off-by: Leonard Lyubich --- container/container.go | 2 +- container/container_test.go | 6 +++--- object/object.go | 4 ++-- object/object_test.go | 38 ++++++++++++++++++------------------- object/splitinfo.go | 2 +- object/splitinfo_test.go | 6 +++--- object/tombstone.go | 2 +- object/tombstone_test.go | 6 +++--- session/common.go | 2 +- session/common_test.go | 8 ++++---- 10 files changed, 38 insertions(+), 38 deletions(-) diff --git a/container/container.go b/container/container.go index 084b128e..64342c3d 100644 --- a/container/container.go +++ b/container/container.go @@ -114,7 +114,7 @@ func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) e if err != nil { return fmt.Errorf("invalid nonce: %w", err) } else if ver := nonce.Version(); ver != 4 { - return fmt.Errorf("invalid nonce UUID version %d", ver) + return fmt.Errorf("invalid nonce: wrong UUID version %d, expected 4", ver) } } else if checkFieldPresence { return errors.New("missing nonce") diff --git a/container/container_test.go b/container/container_test.go index fddda709..9d72d0c5 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -528,7 +528,7 @@ func TestContainer_ReadFromV2(t *testing.T) { corrupt: func(m *v2container.Container) { m.SetNonce(anyValidNonce[:15]) }}, {name: "nonce/oversize", err: "invalid nonce: invalid UUID (got 17 bytes)", corrupt: func(m *v2container.Container) { m.SetNonce(append(anyValidNonce[:], 1)) }}, - {name: "nonce/wrong version", err: "invalid nonce UUID version 3", + {name: "nonce/wrong version", err: "invalid nonce: wrong UUID version 3, expected 4", corrupt: func(m *v2container.Container) { b := bytes.Clone(anyValidNonce[:]) b[6] = 3 << 4 @@ -715,7 +715,7 @@ func TestContainer_Unmarshal(t *testing.T) { b: []byte{26, 15, 229, 22, 237, 42, 123, 159, 78, 139, 136, 206, 237, 126, 224, 125, 147}}, {name: "nonce/oversize", err: "invalid nonce: invalid UUID (got 17 bytes)", b: []byte{26, 17, 229, 22, 237, 42, 123, 159, 78, 139, 136, 206, 237, 126, 224, 125, 147, 223, 1}}, - {name: "nonce/wrong version", err: "invalid nonce UUID version 3", + {name: "nonce/wrong version", err: "invalid nonce: wrong UUID version 3, expected 4", b: []byte{26, 16, 229, 22, 237, 42, 123, 159, 48, 139, 136, 206, 237, 126, 224, 125, 147, 223}}, {name: "policy/replicas/missing", err: "invalid placement policy: missing replicas", b: []byte{50, 0}}, @@ -773,7 +773,7 @@ func TestContainer_UnmarshalJSON(t *testing.T) { j: `{"nonce":"5RbtKnufTouIzu1+4H2T"}`}, {name: "nonce/oversize", err: "invalid nonce: invalid UUID (got 17 bytes)", j: `{"nonce":"5RbtKnufTouIzu1+4H2T3wE="}`}, - {name: "nonce/wrong version", err: "invalid nonce UUID version 3", + {name: "nonce/wrong version", err: "invalid nonce: wrong UUID version 3, expected 4", j: `{"nonce":"5RbtKnufMIuIzu1+4H2T3w=="}`}, {name: "policy/replicas/missing", err: "invalid placement policy: missing replicas", j: `{"placementPolicy":{}}`}, diff --git a/object/object.go b/object/object.go index 5e1e3989..d7c0e180 100644 --- a/object/object.go +++ b/object/object.go @@ -80,9 +80,9 @@ func verifySplitHeaderV2(m object.SplitHeader) error { if b := m.GetSplitID(); len(b) > 0 { var uid uuid.UUID if err := uid.UnmarshalBinary(b); err != nil { - return fmt.Errorf("invalid split UUID: %w", err) + return fmt.Errorf("invalid split ID: %w", err) } else if ver := uid.Version(); ver != 4 { - return fmt.Errorf("invalid split UUID version %d", ver) + return fmt.Errorf("invalid split ID: wrong UUID version %d, expected 4", ver) } } // children diff --git a/object/object_test.go b/object/object_test.go index 11f3e0e1..103b45e6 100644 --- a/object/object_test.go +++ b/object/object_test.go @@ -944,7 +944,7 @@ func TestObject_ReadFromV2(t *testing.T) { h.SetSessionToken(&mt) m.SetHeader(&h) }}, - {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session UUID version 3", + {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session ID: wrong UUID version 3, expected 4", corrupt: func(m *apiobject.Object) { h := *m.GetHeader() mt := *h.GetSessionToken() @@ -1256,7 +1256,7 @@ func TestObject_ReadFromV2(t *testing.T) { h.SetSplit(&sh) m.SetHeader(&h) }}, - {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 15 bytes)", + {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 15 bytes)", corrupt: func(m *apiobject.Object) { h := *m.GetHeader() sh := *h.GetSplit() @@ -1264,7 +1264,7 @@ func TestObject_ReadFromV2(t *testing.T) { h.SetSplit(&sh) m.SetHeader(&h) }}, - {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 17 bytes)", + {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 17 bytes)", corrupt: func(m *apiobject.Object) { h := *m.GetHeader() sh := *h.GetSplit() @@ -1272,7 +1272,7 @@ func TestObject_ReadFromV2(t *testing.T) { h.SetSplit(&sh) m.SetHeader(&h) }}, - {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split UUID version 3", + {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split ID: wrong UUID version 3, expected 4", corrupt: func(m *apiobject.Object) { h := *m.GetHeader() sh := *h.GetSplit() @@ -1648,7 +1648,7 @@ func TestObject_Unmarshal(t *testing.T) { 233, 102, 232, 136, 68, 233, 22, 158, 100, 49, 20, 181, 95, 219, 143, 53, 250, 237, 113, 64, 25, 48, 11, 54, 207, 56, 98, 99, 136, 207, 21, 18, 41, 10, 14, 115, 101, 115, 115, 105, 111, 110, 95, 115, 105, 103, 110, 101, 114, 18, 17, 115, 101, 115, 115, 105, 111, 110, 32, 115, 105, 103, 110, 97, 116, 117, 114, 101, 24, 170, 137, 252, 156, 4}}, - {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session UUID version 3", + {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session ID: wrong UUID version 3, expected 4", b: []byte{26, 155, 2, 74, 152, 2, 10, 234, 1, 10, 16, 118, 23, 219, 249, 117, 70, 48, 33, 157, 229, 102, 253, 142, 52, 17, 144, 18, 27, 10, 25, 53, 248, 195, 15, 196, 254, 124, 23, 169, 198, 208, 15, 219, 229, 62, 150, 151, 159, 221, 73, 224, 229, 106, 42, 222, 26, 32, 8, 210, 204, 150, 183, 128, 222, 183, 128, 228, 1, 16, @@ -1864,11 +1864,11 @@ func TestObject_Unmarshal(t *testing.T) { {name: "header/split/first/zero", err: "invalid header: invalid split header: invalid first split member ID: zero object ID", b: []byte{26, 38, 90, 36, 58, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, - {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 15 bytes)", + {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 15 bytes)", b: []byte{26, 19, 90, 17, 50, 15, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147}}, - {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 17 bytes)", + {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 17 bytes)", b: []byte{26, 21, 90, 19, 50, 17, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147, 41, 1}}, - {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split UUID version 3", + {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split ID: wrong UUID version 3, expected 4", b: []byte{26, 20, 90, 18, 50, 16, 224, 132, 3, 80, 32, 44, 48, 184, 185, 32, 226, 201, 206, 196, 147, 41}}, {name: "header/split/children/empty value", err: "invalid header: invalid split header: invalid child split member ID #1: invalid length 0", b: []byte{26, 40, 90, 38, 42, 34, 10, 32, 178, 74, 58, 219, 46, 3, 110, 125, 220, 81, 238, 35, 27, 6, 228, 193, @@ -1944,7 +1944,7 @@ func TestObject_Unmarshal(t *testing.T) { 25, 48, 11, 54, 207, 56, 98, 99, 136, 207, 21, 18, 41, 10, 14, 115, 101, 115, 115, 105, 111, 110, 95, 115, 105, 103, 110, 101, 114, 18, 17, 115, 101, 115, 115, 105, 111, 110, 32, 115, 105, 103, 110, 97, 116, 117, 114, 101, 24, 170, 137, 252, 156, 4}}, - {name: "header/split/parent/session/body/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid session token: invalid session UUID version 3", + {name: "header/split/parent/session/body/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid session token: invalid session ID: wrong UUID version 3, expected 4", b: []byte{26, 161, 2, 90, 158, 2, 34, 155, 2, 74, 152, 2, 10, 234, 1, 10, 16, 118, 23, 219, 249, 117, 70, 48, 33, 157, 229, 102, 253, 142, 52, 17, 144, 18, 27, 10, 25, 53, 248, 195, 15, 196, 254, 124, 23, 169, 198, 208, 15, 219, 229, 62, 150, 151, 159, 221, 73, 224, 229, 106, 42, 222, 26, 32, 8, 210, 204, 150, 183, 128, 222, @@ -2154,11 +2154,11 @@ func TestObject_Unmarshal(t *testing.T) { {name: "header/split/parent/split/first/zero", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid first split member ID: zero object ID", b: []byte{26, 42, 90, 40, 34, 38, 90, 36, 58, 34, 10, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, - {name: "header/split/parent/split/ID/undersize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID: invalid UUID (got 15 bytes)", + {name: "header/split/parent/split/ID/undersize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: invalid UUID (got 15 bytes)", b: []byte{26, 23, 90, 21, 34, 19, 90, 17, 50, 15, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147}}, - {name: "header/split/parent/split/ID/oversize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID: invalid UUID (got 17 bytes)", + {name: "header/split/parent/split/ID/oversize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: invalid UUID (got 17 bytes)", b: []byte{26, 25, 90, 23, 34, 21, 90, 19, 50, 17, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147, 41, 1}}, - {name: "header/split/parent/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID version 3", + {name: "header/split/parent/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: wrong UUID version 3, expected 4", b: []byte{26, 24, 90, 22, 34, 20, 90, 18, 50, 16, 224, 132, 3, 80, 32, 44, 48, 184, 185, 32, 226, 201, 206, 196, 147, 41}}, {name: "header/split/parent/split/children/empty value", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid child split member ID #1: invalid length 0", b: []byte{26, 80, 90, 78, 34, 76, 90, 74, 42, 34, 10, 32, 178, 74, 58, 219, 46, 3, 110, 125, 220, 81, 238, 35, 27, @@ -2253,7 +2253,7 @@ func TestObject_UnmarshalJSON(t *testing.T) { j: `{"header":{"sessionToken":{"body":{"id":"dhfb+XVGQCGd5Wb9jjQR"}}}}`}, {name: "header/session/body/ID/oversize", err: "invalid header: invalid session token: invalid session ID: invalid UUID (got 17 bytes)", j: `{"header":{"sessionToken":{"body":{"id":"dhfb+XVGQCGd5Wb9jjQRkAE="}}}}`}, - {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session UUID version 3", + {name: "header/session/body/ID/wrong UUID version", err: "invalid header: invalid session token: invalid session ID: wrong UUID version 3, expected 4", j: `{"header":{"sessionToken":{"body":{"id":"dhfb+XVGMCGd5Wb9jjQRkA=="}}}}`}, {name: "header/session/body/issuer/value/empty", err: "invalid header: invalid session token: invalid session issuer: invalid length 0, expected 25", j: `{"header":{"sessionToken":{"body":{"id":"dhfb+XVGQCGd5Wb9jjQRkA==", "ownerID":{}}}}}`}, @@ -2541,11 +2541,11 @@ func TestObject_UnmarshalJSON(t *testing.T) { j: `{"header": {"split":{"first":{"value":"sko62y4Dbn3cUe4jGwbkwb7gTSwSOHWtRvYIi/euNTwB"}}}}`}, {name: "header/split/first/zero", err: "invalid header: invalid split header: invalid first split member ID: zero object ID", j: `{"header": {"split":{"first":{"value":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}}}}`}, - {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 15 bytes)", + {name: "header/split/ID/undersize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 15 bytes)", j: `{"header": {"split":{"splitID":"4IQDUCAsRbi5IOLJzsST"}}}`}, - {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split UUID: invalid UUID (got 17 bytes)", + {name: "header/split/ID/oversize", err: "invalid header: invalid split header: invalid split ID: invalid UUID (got 17 bytes)", j: `{"header": {"split":{"splitID":"4IQDUCAsRbi5IOLJzsSTKQE="}}}`}, - {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split UUID version 3", + {name: "header/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid split ID: wrong UUID version 3, expected 4", j: `{"header": {"split":{"splitID":"4IQDUCAsMLi5IOLJzsSTKQ=="}}}`}, {name: "header/split/children/empty value", err: "invalid header: invalid split header: invalid child split member ID #1: invalid length 0", j: `{"header": {"split":{"children":[{"value":"sko62y4Dbn3cUe4jGwbkwb7gTSwSOHWtRvYIi/euNTw="}, {}, {"value":"zuT32Sn3n9dP4jWZhRBmaALqI9zscGUY636t5aHKxfI="}]}}}`}, @@ -2603,11 +2603,11 @@ func TestObject_UnmarshalJSON(t *testing.T) { j: `{"header": {"split": {"parentHeader": {"split":{"first":{"value":"sko62y4Dbn3cUe4jGwbkwb7gTSwSOHWtRvYIi/euNTwB"}}}}}}`}, {name: "header/split/parent/split/first/zero", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid first split member ID: zero object ID", j: `{"header": {"split": {"parentHeader": {"split":{"first":{"value":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="}}}}}}`}, - {name: "header/split/parent/split/ID/undersize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID: invalid UUID (got 15 bytes)", + {name: "header/split/parent/split/ID/undersize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: invalid UUID (got 15 bytes)", j: `{"header": {"split": {"parentHeader": {"split":{"splitID":"4IQDUCAsRbi5IOLJzsST"}}}}}`}, - {name: "header/split/parent/split/ID/oversize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID: invalid UUID (got 17 bytes)", + {name: "header/split/parent/split/ID/oversize", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: invalid UUID (got 17 bytes)", j: `{"header": {"split": {"parentHeader": {"split":{"splitID":"4IQDUCAsRbi5IOLJzsSTKQE="}}}}}`}, - {name: "header/split/parent/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split UUID version 3", + {name: "header/split/parent/split/ID/wrong UUID version", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid split ID: wrong UUID version 3, expected 4", j: `{"header": {"split": {"parentHeader": {"split":{"splitID":"4IQDUCAsMLi5IOLJzsSTKQ=="}}}}}`}, {name: "header/split/parent/split/children/empty value", err: "invalid header: invalid split header: invalid parent header: invalid split header: invalid child split member ID #1: invalid length 0", j: `{"header": {"split": {"parentHeader": {"split":{"children":[{"value":"sko62y4Dbn3cUe4jGwbkwb7gTSwSOHWtRvYIi/euNTw="},{},{"value":"zuT32Sn3n9dP4jWZhRBmaALqI9zscGUY636t5aHKxfI="}]}}}}}`}, diff --git a/object/splitinfo.go b/object/splitinfo.go index fd39fd88..fea7cf00 100644 --- a/object/splitinfo.go +++ b/object/splitinfo.go @@ -207,7 +207,7 @@ func (s *SplitInfo) ReadFromV2(m object.SplitInfo) error { if err := uid.UnmarshalBinary(b); err != nil { return fmt.Errorf("invalid split ID: %w", err) } else if v := uid.Version(); v != 4 { - return fmt.Errorf("invalid split UUID version %d", v) + return fmt.Errorf("invalid split ID: wrong UUID version %d, expected 4", v) } } diff --git a/object/splitinfo_test.go b/object/splitinfo_test.go index cef1b3f1..1e14fa67 100644 --- a/object/splitinfo_test.go +++ b/object/splitinfo_test.go @@ -271,7 +271,7 @@ func TestSplitInfo_ReadFromV2(t *testing.T) { corrupt: func(m *apiobject.SplitInfo) { m.SetSplitID(anyValidSplitIDBytes[:15]) }}, {name: "split ID/oversize", err: "invalid split ID: invalid UUID (got 17 bytes)", corrupt: func(m *apiobject.SplitInfo) { m.SetSplitID(append(anyValidSplitIDBytes[:], 1)) }}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", corrupt: func(m *apiobject.SplitInfo) { b := bytes.Clone(anyValidSplitIDBytes[:]) b[6] = 3 << 4 @@ -358,7 +358,7 @@ func TestSplitInfo_Unmarshal(t *testing.T) { b: []byte{10, 17, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147, 41, 1, 18, 34, 10, 32, 178, 74, 58, 219, 46, 3, 110, 125, 220, 81, 238, 35, 27, 6, 228, 193, 190, 224, 77, 44, 18, 56, 117, 173, 70, 246, 8, 139, 247, 174, 53, 60}}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", b: []byte{10, 16, 224, 132, 3, 80, 32, 44, 48, 184, 185, 32, 226, 201, 206, 196, 147, 41, 18, 34, 10, 32, 178, 74, 58, 219, 46, 3, 110, 125, 220, 81, 238, 35, 27, 6, 228, 193, 190, 224, 77, 44, 18, 56, 117, 173, 70, 246, 8, 139, 247, 174, 53, 60}}, @@ -459,7 +459,7 @@ func TestSplitInfo_UnmarshalJSON(t *testing.T) { j: `{"splitId":"4IQDUCAsRbi5IOLJzsST"}`}, {name: "split ID/oversize", err: "invalid split ID: invalid UUID (got 17 bytes)", j: `{"splitId":"4IQDUCAsRbi5IOLJzsSTKQE="}`}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", j: `{"splitId":"4IQDUCAsMLi5IOLJzsSTKQ=="}`}, {name: "last part/empty value", err: "could not convert last part object ID: invalid length 0", j: `{"lastPart":{"value":""}}`}, diff --git a/object/tombstone.go b/object/tombstone.go index dc7376b3..83691b1d 100644 --- a/object/tombstone.go +++ b/object/tombstone.go @@ -49,7 +49,7 @@ func (t *Tombstone) ReadFromV2(m tombstone.Tombstone) error { if err := uid.UnmarshalBinary(b); err != nil { return fmt.Errorf("invalid split ID: %w", err) } else if v := uid.Version(); v != 4 { - return fmt.Errorf("invalid split UUID version %d", v) + return fmt.Errorf("invalid split ID: wrong UUID version %d, expected 4", v) } } *t = Tombstone(m) diff --git a/object/tombstone_test.go b/object/tombstone_test.go index ec6224d1..37d330ba 100644 --- a/object/tombstone_test.go +++ b/object/tombstone_test.go @@ -108,7 +108,7 @@ func TestTombstone_ReadFromV2(t *testing.T) { corrupt: func(m *tombstone.Tombstone) { m.SetSplitID(anyValidSplitIDBytes[:15]) }}, {name: "split ID/oversize", err: "invalid split ID: invalid UUID (got 17 bytes)", corrupt: func(m *tombstone.Tombstone) { m.SetSplitID(append(anyValidSplitIDBytes[:], 1)) }}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", corrupt: func(m *tombstone.Tombstone) { b := bytes.Clone(anyValidSplitIDBytes[:]) b[6] = 3 << 4 @@ -174,7 +174,7 @@ func TestContainer_Unmarshal(t *testing.T) { b: []byte{18, 15, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147}}, {name: "split ID/oversize", err: "invalid split ID: invalid UUID (got 17 bytes)", b: []byte{18, 17, 224, 132, 3, 80, 32, 44, 69, 184, 185, 32, 226, 201, 206, 196, 147, 41, 1}}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", b: []byte{18, 16, 224, 132, 3, 80, 32, 44, 48, 184, 185, 32, 226, 201, 206, 196, 147, 41}}, } { t.Run(tc.name, func(t *testing.T) { @@ -217,7 +217,7 @@ func TestTombstone_UnmarshalJSON(t *testing.T) { j: `{"splitID":"4IQDUCAsRbi5IOLJzsST"}`}, {name: "split ID/oversize", err: "invalid split ID: invalid UUID (got 17 bytes)", j: `{"splitID":"4IQDUCAsRbi5IOLJzsSTKQE="}`}, - {name: "split ID/wrong version", err: "invalid split UUID version 3", + {name: "split ID/wrong version", err: "invalid split ID: wrong UUID version 3, expected 4", j: `{"splitID":"4IQDUCAsMLi5IOLJzsSTKQ=="}`}, } { t.Run(tc.name, func(t *testing.T) { diff --git a/session/common.go b/session/common.go index f40a4610..4246cb1e 100644 --- a/session/common.go +++ b/session/common.go @@ -60,7 +60,7 @@ func (x *commonData) readFromV2(m session.Token, checkFieldPresence bool, r cont if err != nil { return fmt.Errorf("invalid session ID: %w", err) } else if ver := x.id.Version(); ver != 4 { - return fmt.Errorf("invalid session UUID version %d", ver) + return fmt.Errorf("invalid session ID: wrong UUID version %d, expected 4", ver) } } else if checkFieldPresence { return errors.New("missing session ID") diff --git a/session/common_test.go b/session/common_test.go index 923ac2f5..89c71233 100644 --- a/session/common_test.go +++ b/session/common_test.go @@ -84,7 +84,7 @@ var invalidProtoTokenCommonTestcases = []invalidProtoTokenTestcase{ {name: "body/ID/undersize", err: "invalid session ID: invalid UUID (got 15 bytes)", corrupt: func(st *apisession.Token) { st.GetBody().SetID(make([]byte, 15)) }}, - {name: "body/ID/wrong UUID version", err: "invalid session UUID version 3", corrupt: func(st *apisession.Token) { + {name: "body/ID/wrong UUID version", err: "invalid session ID: wrong UUID version 3, expected 4", corrupt: func(st *apisession.Token) { st.GetBody().GetID()[6] = 3 << 4 }}, {name: "body/ID/oversize", err: "invalid session ID: invalid UUID (got 17 bytes)", corrupt: func(st *apisession.Token) { @@ -139,7 +139,7 @@ var invalidBinTokenCommonTestcases = []invalidBinTokenTestcase{ b: []byte{10, 17, 10, 15, 188, 255, 42, 107, 236, 249, 78, 152, 169, 7, 2, 87, 36, 139, 31}}, {name: "body/ID/oversize", err: "invalid session ID: invalid UUID (got 17 bytes)", b: []byte{10, 19, 10, 17, 109, 141, 40, 16, 21, 245, 76, 128, 150, 236, 154, 53, 157, 172, 12, 195, 1}}, - {name: "body/ID/wrong UUID version", err: "invalid session UUID version 3", + {name: "body/ID/wrong UUID version", err: "invalid session ID: wrong UUID version 3, expected 4", b: []byte{10, 18, 10, 16, 97, 47, 243, 131, 222, 201, 48, 64, 135, 195, 177, 240, 107, 12, 2, 42}}, {name: "body/issuer/value/empty", err: "invalid session issuer: invalid length 0, expected 25", b: []byte{10, 2, 18, 0}}, @@ -164,7 +164,7 @@ var invalidSignedTokenCommonTestcases = []invalidBinTokenTestcase{ b: []byte{10, 15, 188, 255, 42, 107, 236, 249, 78, 152, 169, 7, 2, 87, 36, 139, 31}}, {name: "ID/oversize", err: "invalid session ID: invalid UUID (got 17 bytes)", b: []byte{10, 17, 109, 141, 40, 16, 21, 245, 76, 128, 150, 236, 154, 53, 157, 172, 12, 195, 1}}, - {name: "ID/wrong UUID version", err: "invalid session UUID version 3", + {name: "ID/wrong UUID version", err: "invalid session ID: wrong UUID version 3, expected 4", b: []byte{10, 16, 97, 47, 243, 131, 222, 201, 48, 64, 135, 195, 177, 240, 107, 12, 2, 42}}, {name: "issuer/value/empty", err: "invalid session issuer: invalid length 0, expected 25", b: []byte{18, 0}}, @@ -194,7 +194,7 @@ var invalidJSONTokenCommonTestcases = []invalidJSONTokenTestcase{ {name: "body/ID/oversize", err: "invalid session ID: invalid UUID (got 17 bytes)", j: ` {"body":{"id":"YxhvRhasSBSLu69iCv/nvAE="}} `}, - {name: "body/ID/wrong UUID version", err: "invalid session UUID version 3", j: ` + {name: "body/ID/wrong UUID version", err: "invalid session ID: wrong UUID version 3, expected 4", j: ` {"body":{"id":"YxhvRhasMBSLu69iCv/nvA=="}} `}, {name: "body/issuer/value/empty", err: "invalid session issuer: invalid length 0, expected 25", j: ` From 2f9146c7e4a22826b9e2f94f8d64652c85e2570d Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Mon, 25 Nov 2024 21:26:50 +0300 Subject: [PATCH 2/9] client: Increase test coverage of all NeoFS ops Continues 0de224ef363114a0268f2104f28d2f80d789db39. Signed-off-by: Leonard Lyubich --- client/accounting_test.go | 599 +++------ client/client_test.go | 1334 ++++++++++++++++++- client/container_statistic_test.go | 471 ------- client/container_test.go | 1879 ++++++++++++++++++++++++-- client/crypto_test.go | 189 +++ client/messages_test.go | 1899 ++++++++++++++++++++++++++ client/netmap_test.go | 933 ++++++++++--- client/object_delete_test.go | 279 +++- client/object_get_test.go | 1983 ++++++++++++++++++++++++++-- client/object_hash_test.go | 360 ++++- client/object_put_test.go | 806 ++++++++++- client/object_search_test.go | 679 ++++++++-- client/object_test.go | 352 ++++- client/reputation_test.go | 486 ++++++- client/session_test.go | 294 ++++- 15 files changed, 10971 insertions(+), 1572 deletions(-) delete mode 100644 client/container_statistic_test.go create mode 100644 client/crypto_test.go create mode 100644 client/messages_test.go diff --git a/client/accounting_test.go b/client/accounting_test.go index 570d724a..5abc2f00 100644 --- a/client/accounting_test.go +++ b/client/accounting_test.go @@ -1,55 +1,53 @@ package client import ( - "bytes" "context" "errors" "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" - 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/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" "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" + "google.golang.org/protobuf/proto" ) -func newDefaultAccountingService(srv protoaccounting.AccountingServiceServer) testService { +// returns Client-compatible Accounting service handled by given server. +// Provided server must implement [protoaccounting.AccountingServiceServer]: the +// parameter is not of this type to support generics. +func newDefaultAccountingService(t testing.TB, srv any) testService { + require.Implements(t, (*protoaccounting.AccountingServiceServer)(nil), srv) 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, newDefaultAccountingService(srv)) +// returns Client of Accounting service provided by given server. Provided +// server must implement [protoaccounting.AccountingServiceServer]: the +// parameter is not of this type to support generics. +func newTestAccountingClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultAccountingService(t, srv)) } type testGetBalanceServer struct { protoaccounting.UnimplementedAccountingServiceServer - - reqXHdrs []string - reqAcc []byte - - handlerErr error - - respSleep time.Duration - respUnsigned bool - respSigner neofscrypto.Signer - respMeta *protosession.ResponseMetaHeader - respBodyCons func() *protoaccounting.BalanceResponse_Body + testCommonUnaryServerSettings[ + *protoaccounting.BalanceRequest_Body, + v2accounting.BalanceRequestBody, + *v2accounting.BalanceRequestBody, + *protoaccounting.BalanceRequest, + v2accounting.BalanceRequest, + *v2accounting.BalanceRequest, + *protoaccounting.BalanceResponse_Body, + v2accounting.BalanceResponseBody, + *v2accounting.BalanceResponseBody, + *protoaccounting.BalanceResponse, + v2accounting.BalanceResponse, + *v2accounting.BalanceResponse, + ] + reqAcc *user.ID } // returns [protoaccounting.AccountingServiceServer] supporting Balance method @@ -57,128 +55,24 @@ type testGetBalanceServer struct { // 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[:] -} - -// tells the server whether to sign all the responses or not. By default, any -// response is signed. -// -// 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 signing is disabled using setEnabledResponseSigning. -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 -} - -// 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 + x.reqAcc = &acc } -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) +func (x *testGetBalanceServer) verifyRequest(req *protoaccounting.BalanceRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return 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)) + switch metaHdr := req.MetaHeader; { case metaHdr.Ttl != 2: - return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Epoch)) + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) 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 @@ -188,54 +82,38 @@ func (x *testGetBalanceServer) verifyBalanceRequest(req *protoaccounting.Balance 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")) + if x.reqAcc != nil { + if err := checkUserIDTransport(*x.reqAcc, body.OwnerId); err != nil { + return newErrInvalidRequestField("account", err) + } } return nil } func (x *testGetBalanceServer) Balance(_ context.Context, req *protoaccounting.BalanceRequest) (*protoaccounting.BalanceResponse, error) { - time.Sleep(x.respSleep) - - if err := x.verifyBalanceRequest(req); err != nil { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { return nil, err } - if x.handlerErr != nil { return nil, x.handlerErr } - resp := protoaccounting.BalanceResponse{ + resp := &protoaccounting.BalanceResponse{ MetaHeader: x.respMeta, } - if x.respBodyCons != nil { - resp.Body = x.respBodyCons() + if x.respBodyForced { + resp.Body = x.respBody } else { - resp.Body = &protoaccounting.BalanceResponse_Body{ - Balance: &protoaccounting.Decimal{ - Value: rand.Int63(), - Precision: rand.Uint32(), - }, - } + resp.Body = proto.Clone(validMinBalanceResponseBody).(*protoaccounting.BalanceResponse_Body) } - if x.respUnsigned { - return &resp, nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) } - - var respV2 v2accounting.BalanceResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) - } - 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) - } - - return respV2.ToGRPCMessage().(*protoaccounting.BalanceResponse), nil + return resp, nil } func TestClient_BalanceGet(t *testing.T) { @@ -251,291 +129,136 @@ func TestClient_BalanceGet(t *testing.T) { require.ErrorIs(t, err, ErrMissingAccount) }) }) - t.Run("exact in-out", func(t *testing.T) { + t.Run("messages", 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. */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestGetBalanceServer() + c := newTestAccountingClient(t, srv) - 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) + var prm PrmBalanceGet + prm.SetAccount(anyUsr) + srv.checkRequestAccount(anyUsr) + srv.authenticateRequest(c.prm.signer) _, err := c.BalanceGet(ctx, prm) - require.ErrorAs(t, err, &tc.err) - tcCustom.assert(t, tc.err) - } - } + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestGetBalanceServer, newTestAccountingClient, func(c *Client, xhs []string) error { + opts := anyValidPrm + opts.WithXHeaders(xhs...) + _, err := c.BalanceGet(ctx, opts) + return err + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoaccounting.BalanceResponse_Body + }{ + {name: "min", body: validMinBalanceResponseBody}, + {name: "full", body: validFullBalanceResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetBalanceServer() + c := newTestAccountingClient(t, srv) + + var prm PrmBalanceGet + prm.SetAccount(anyUsr) + + srv.respondWithBody(tc.body) + balance, err := c.BalanceGet(ctx, anyValidPrm) + require.NoError(t, err) + require.NoError(t, checkBalanceTransport(balance, tc.body.GetBalance())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestGetBalanceServer, newTestAccountingClient, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "accounting.AccountingService", "Balance", func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestGetBalanceServer, newTestAccountingClient, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + tcs := []invalidResponseBodyTestcase[protoaccounting.BalanceResponse_Body]{ + {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") + }}, + {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") + }}, + } + + testInvalidResponseBodies(t, newTestGetBalanceServer, newTestAccountingClient, tcs, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestGetBalanceServer, newTestAccountingClient, func(ctx context.Context, c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return 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") + testSignRequestFailure(t, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) }) 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.Contains(t, st.Message(), transportErr.Error()) - }) - 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.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) - - _, 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) - }) - } + testTransportFailure(t, newTestGetBalanceServer, newTestAccountingClient, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return 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. - 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 - c := newClientWithResponseCallback(t, func(meta ResponseMetaInfo) error { - collected = append(collected, meta) - return cbErr - }, - newDefaultNetmapServiceDesc(nodeInfoSrv), - newDefaultAccountingService(balanceSrv), - ) - - _, err := c.BalanceGet(ctx, anyValidPrm) - require.NoError(t, err) - require.Equal(t, []ResponseMetaInfo{ - {key: nodeInfoSrvSigner.PublicKeyBytes, epoch: nodeInfoSrvEpoch}, - {key: balanceSrvSigner.PublicKeyBytes, epoch: balanceSrvEpoch}, - }, 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]) + testUnaryResponseCallback(t, newTestGetBalanceServer, newDefaultAccountingService, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }) }) 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), + testStatistic(t, newTestGetBalanceServer, newDefaultAccountingService, stat.MethodBalanceGet, + nil, + []testedClientOp{func(c *Client) error { + _, err := c.BalanceGet(ctx, PrmBalanceGet{}) + return err + }}, func(c *Client) error { + _, err := c.BalanceGet(ctx, anyValidPrm) + return err + }, ) - // 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 03004157..2d26d6f9 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -2,17 +2,39 @@ package client import ( "context" + "crypto/ecdsa" + "crypto/sha256" + "errors" "fmt" + "math/rand" "net" + "slices" + "strconv" "testing" + "time" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apigrpc "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" + apisession "github.com/nspcc-dev/neofs-api-go/v2/session" + 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" + neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/stat" + 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/grpc/test/bufconn" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" ) /* @@ -25,6 +47,16 @@ func init() { statusErr.SetMessage("test status error") } +// flattens all slices into one. +// TODO: propose to [slices] package. +func join[SS ~[]S, S ~[]E, E any](ss SS) S { + var res S + for i := range ss { + res = append(res, ss[i]...) + } + return res +} + func newInvalidRequestErr(cause error) error { return fmt.Errorf("invalid request: %w", cause) } @@ -49,6 +81,19 @@ func newErrInvalidRequestField(name string, err error) error { return newInvalidRequestBodyErr(fmt.Errorf("invalid %s field: %w", name, err)) } +// static server settings used for [Client] testing. +var ( + testServerEndpoint = "localhost:8080" + testServerSignerOnDial = neofscryptotest.Signer() + testServerStateOnDial = struct { + pub []byte + epoch uint64 + }{ + pub: neofscrypto.PublicKeyBytes(testServerSignerOnDial.Public()), + epoch: rand.Uint64(), + } +) + // pairs service spec and implementation to-be-registered in some [grpc.Server]. type testService struct { desc *grpc.ServiceDesc @@ -57,7 +102,7 @@ type testService struct { // 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 { +func newCustomClient(t testing.TB, setPrm func(*PrmInit), svcs ...testService) *Client { var prm PrmInit if setPrm != nil { setPrm(&prm) @@ -66,6 +111,66 @@ func newCustomClient(t testing.TB, endpoint string, setPrm func(*PrmInit), svcs c, err := New(prm) require.NoError(t, err) + // serve dial RPC + const netmapSvcName = "neo.fs.v2.netmap.NetmapService" + const nodeInfoMtdName = "LocalNodeInfo" + netmapSvcInd := -1 + nodeInfoMtdInd := -1 +loop: + for i := range svcs { + if svcs[i].desc.ServiceName == netmapSvcName { + netmapSvcInd = i + for j := range svcs[i].desc.Methods { + if svcs[i].desc.Methods[j].MethodName == nodeInfoMtdName { + nodeInfoMtdInd = j + break loop + } + } + } + } + + type nodeInfoServer interface { + LocalNodeInfo(context.Context, *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) + } + dialSrv := newTestGetNodeInfoServer() + dialSrv.signResponsesBy(testServerSignerOnDial.ECDSAPrivateKey) + dialSrv.respondWithNodePublicKey(testServerStateOnDial.pub) + dialSrv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: testServerStateOnDial.epoch}) + handleDial := func(_ any, ctx context.Context, dec func(any) error, _ grpc.UnaryServerInterceptor) (any, error) { + var req protonetmap.LocalNodeInfoRequest + if err := dec(&req); err != nil { + return nil, err + } + return dialSrv.LocalNodeInfo(ctx, &req) + } + + if netmapSvcInd < 0 { + svcs = append(svcs, testService{ + desc: &grpc.ServiceDesc{ + ServiceName: netmapSvcName, + HandlerType: (*nodeInfoServer)(nil), + Methods: []grpc.MethodDesc{{MethodName: nodeInfoMtdName, Handler: handleDial}}, + }, + }) + } else { + dcp := *svcs[netmapSvcInd].desc // safe copy prevents mutation + dcp.Methods = slices.Clone(dcp.Methods) + if nodeInfoMtdInd < 0 { + dcp.Methods = append(dcp.Methods, grpc.MethodDesc{MethodName: nodeInfoMtdName, Handler: handleDial}) + } else { + originalHandler := dcp.Methods[nodeInfoMtdInd].Handler + called := false + dcp.Methods[nodeInfoMtdInd].Handler = func(srv any, ctx context.Context, dec func(any) error, in grpc.UnaryServerInterceptor) (any, error) { + if !called { + called = true + return handleDial(srv, ctx, dec, in) + } + return originalHandler(srv, ctx, dec, in) + } + } + svcs[netmapSvcInd].desc = &dcp + } + srv := grpc.NewServer() for _, svc := range svcs { srv.RegisterService(svc.desc, svc.impl) @@ -75,34 +180,24 @@ func newCustomClient(t testing.TB, endpoint string, setPrm func(*PrmInit), svcs go func() { _ = srv.Serve(lis) }() var dialPrm PrmDial - if endpoint == "" { - endpoint = "grpc://localhost:8080" - } - dialPrm.SetServerURI(endpoint) // any valid + dialPrm.SetServerURI(testServerEndpoint) 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()) - } + require.NoError(t, err) 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. // -// If caller registers stat callback (like [PrmInit.SetStatisticCallback] does) -// processing nodeKey, it must include NetmapService with implemented -// LocalNodeInfo method. +// Note: [Client] uses NetmapService.LocalNodeInfo RPC to dial the server. Test +// [Client] always receives testServerStateOnDial. Take this into account if the +// test keeps track of all ops like stat test. func newClient(t testing.TB, svcs ...testService) *Client { - return newCustomClient(t, "", nil, svcs...) + return newCustomClient(t, nil, svcs...) } func TestClient_Dial(t *testing.T) { @@ -210,3 +305,1208 @@ type nopSigner struct{} func (nopSigner) Scheme() neofscrypto.Scheme { return neofscrypto.ECDSA_SHA512 } func (nopSigner) Sign([]byte) ([]byte, error) { return []byte("signature"), nil } func (x nopSigner) Public() neofscrypto.PublicKey { return nopPublicKey{} } + +// various cross-service protocol messages. Any message (incl. set elements) +// must be cloned via [proto.Clone] before passing anywhere. +var ( + // correct NeoFS protocol version with required fields only. + validMinProtoVersion = &protorefs.Version{} + // correct NeoFS protocol version with all fields. + validFullProtoVersion = &protorefs.Version{Major: 538919038, Minor: 3957317479} + // set of correct container IDs. + validProtoContainerIDs = []*protorefs.ContainerID{ + {Value: []byte{198, 137, 143, 192, 231, 50, 106, 89, 225, 118, 7, 42, 40, 225, 197, 183, 9, 205, 71, 140, 233, 30, 63, 73, 224, 244, 235, 18, 205, 45, 155, 236}}, + {Value: []byte{26, 71, 99, 242, 146, 121, 0, 142, 95, 50, 78, 190, 222, 104, 252, 72, 48, 219, 67, 226, 30, 90, 103, 51, 1, 234, 136, 143, 200, 240, 75, 250}}, + {Value: []byte{51, 124, 45, 83, 227, 119, 66, 76, 220, 196, 118, 197, 116, 44, 138, 83, 103, 102, 134, 191, 108, 124, 162, 255, 184, 137, 193, 242, 178, 10, 23, 29}}, + } +) + +// TODO: use eacltest.Table() after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 +var anyValidEACL = eacl.NewTableForContainer(cidtest.ID(), []eacl.Record{ + eacl.ConstructRecord(eacl.ActionDeny, eacl.OperationPut, + []eacl.Target{ + eacl.NewTargetByRole(eacl.RoleOthers), + eacl.NewTargetByAccounts(usertest.IDs(3)), + }, + eacl.NewFilterObjectOwnerEquals(usertest.ID()), + eacl.NewObjectPropertyFilter("attr1", eacl.MatchStringEqual, "val1"), + ), +}) + +type ( + invalidSessionTokenProtoTestcase = struct { + name, msg string + corrupt func(*protosession.SessionToken) + } +) + +// various sets of cross-service testcases. +var ( + invalidUUIDProtoTestcases = []struct { + name, msg string + corrupt func(valid []byte) []byte + }{ + {name: "undersize", msg: "invalid UUID (got 15 bytes)", corrupt: func(valid []byte) []byte { + return valid[:15] + }}, + {name: "oversize", msg: "invalid UUID (got 17 bytes)", corrupt: func(valid []byte) []byte { + return append(valid, 1) + }}, + {name: "wrong version", msg: "wrong UUID version 3, expected 4", corrupt: func(valid []byte) []byte { + valid[6] = 3 << 4 + return valid + }}, + } + invalidContainerIDProtoTestcases = []struct { + name, msg string + corrupt func(valid *protorefs.ContainerID) + }{ + {name: "nil", msg: "invalid length 0", corrupt: func(valid *protorefs.ContainerID) { + valid.Value = nil + }}, + {name: "empty", msg: "invalid length 0", corrupt: func(valid *protorefs.ContainerID) { + valid.Value = []byte{} + }}, + {name: "undersize", msg: "invalid length 31", corrupt: func(valid *protorefs.ContainerID) { + valid.Value = valid.Value[:31] + }}, + {name: "oversize", msg: "invalid length 33", corrupt: func(valid *protorefs.ContainerID) { + valid.Value = append(valid.Value, 1) + }}, + {name: "zero", msg: "zero container ID", corrupt: func(valid *protorefs.ContainerID) { + for i := range valid.Value { + valid.Value[i] = 0 + } + }}, + } + invalidUserIDProtoTestcases = []struct { + name, msg string + corrupt func(valid *protorefs.OwnerID) + }{ + {name: "nil", msg: "invalid length 0, expected 25", corrupt: func(valid *protorefs.OwnerID) { + valid.Value = nil + }}, + {name: "empty", msg: "invalid length 0, expected 25", corrupt: func(valid *protorefs.OwnerID) { + valid.Value = []byte{} + }}, + {name: "owner/undersize", msg: "invalid length 24, expected 25", corrupt: func(valid *protorefs.OwnerID) { + valid.Value = valid.Value[:24] + }}, + {name: "owner/oversize", msg: "invalid length 26, expected 25", corrupt: func(valid *protorefs.OwnerID) { + valid.Value = append(valid.Value, 1) + }}, + {name: "owner/wrong prefix", msg: "invalid prefix byte 0x42, expected 0x35", corrupt: func(valid *protorefs.OwnerID) { + valid.Value[0] = 0x42 + h := sha256.Sum256(valid.Value[:21]) + hh := sha256.Sum256(h[:]) + copy(valid.Value[21:], hh[:]) + }}, + {name: "owner/wrong checksum", msg: "checksum mismatch", corrupt: func(valid *protorefs.OwnerID) { + valid.Value[24]++ + }}, + {name: "owner/zero", msg: "invalid prefix byte 0x0, expected 0x35", corrupt: func(valid *protorefs.OwnerID) { + valid.Value = make([]byte, 25) + }}, + } + invalidObjectIDProtoTestcases = []struct { + name, msg string + corrupt func(valid *protorefs.ObjectID) + }{ + {name: "nil", msg: "invalid length 0", corrupt: func(valid *protorefs.ObjectID) { + valid.Value = nil + }}, + {name: "empty", msg: "invalid length 0", corrupt: func(valid *protorefs.ObjectID) { + valid.Value = []byte{} + }}, + {name: "undersize", msg: "invalid length 31", corrupt: func(valid *protorefs.ObjectID) { + valid.Value = valid.Value[:31] + }}, + {name: "oversize", msg: "invalid length 33", corrupt: func(valid *protorefs.ObjectID) { + valid.Value = append(valid.Value, 1) + }}, + {name: "zero", msg: "zero object ID", corrupt: func(valid *protorefs.ObjectID) { + for i := range valid.Value { + valid.Value[i] = 0 + } + }}, + } + invalidChecksumTestcases = []struct { + name, msg string + corrupt func(valid *protorefs.Checksum) + }{ + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "negative scheme", msg: "negative type -1", corrupt: func(valid *protorefs.Checksum) { + // valid.Type = -1 + // }}, + {name: "value/nil", msg: "missing value", corrupt: func(valid *protorefs.Checksum) { + valid.Sum = nil + }}, + {name: "value/empty", msg: "missing value", corrupt: func(valid *protorefs.Checksum) { + valid.Sum = []byte{} + }}, + } + invalidSignatureProtoTestcases = []struct { + name, msg string + corrupt func(valid *protorefs.Signature) + }{ + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "negative scheme", msg: "negative scheme -1", corrupt: func(valid *protorefs.Signature) { + // valid.Scheme = -1 + // }}, + } + invalidCommonSessionTokenProtoTestcases = []invalidSessionTokenProtoTestcase{ + {name: "body/nil", msg: "missing token body", corrupt: func(valid *protosession.SessionToken) { + valid.Body = nil + }}, + {name: "body/ID/nil", msg: "missing session ID", corrupt: func(valid *protosession.SessionToken) { + valid.Body.Id = nil + }}, + {name: "body/ID/empty", msg: "missing session ID", corrupt: func(valid *protosession.SessionToken) { + valid.Body.Id = []byte{} + }}, + // + other ID cases in init + {name: "body/issuer/nil", msg: "missing session issuer", corrupt: func(valid *protosession.SessionToken) { + valid.Body.OwnerId = nil + }}, + // + other issuer cases in init + {name: "body/lifetime", msg: "missing token lifetime", corrupt: func(valid *protosession.SessionToken) { + valid.Body.Lifetime = nil + }}, + {name: "body/session key/nil", msg: "missing session public key", corrupt: func(valid *protosession.SessionToken) { + valid.Body.SessionKey = nil + }}, + {name: "body/session key/empty", msg: "missing session public key", corrupt: func(valid *protosession.SessionToken) { + valid.Body.SessionKey = []byte{} + }}, + {name: "body/context/nil", msg: "missing session context", corrupt: func(valid *protosession.SessionToken) { + valid.Body.Context = nil + }}, + {name: "signature/nil", msg: "missing body signature", corrupt: func(valid *protosession.SessionToken) { + valid.Signature = nil + }}, + // + other signature cases in init + } +) + +func init() { + for _, tc := range invalidUUIDProtoTestcases { + invalidCommonSessionTokenProtoTestcases = append(invalidCommonSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "body/ID/" + tc.name, msg: "invalid session ID: " + tc.msg, + corrupt: func(valid *protosession.SessionToken) { valid.Body.Id = tc.corrupt(valid.Body.Id) }, + }) + } + for _, tc := range invalidUserIDProtoTestcases { + invalidCommonSessionTokenProtoTestcases = append(invalidCommonSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "body/issuer/" + tc.name, msg: "invalid session issuer: " + tc.msg, + corrupt: func(valid *protosession.SessionToken) { tc.corrupt(valid.Body.OwnerId) }, + }) + } + for _, tc := range invalidSignatureProtoTestcases { + invalidCommonSessionTokenProtoTestcases = append(invalidCommonSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "signature/" + tc.name, msg: "invalid body signature: " + tc.msg, + corrupt: func(valid *protosession.SessionToken) { tc.corrupt(valid.Signature) }, + }) + } +} + +// for sharing between servers of requests with required container ID. +type testRequiredContainerIDServerSettings struct { + expectedReqCnrID *cid.ID +} + +// makes the server to assert that any request carries given container ID. By +// default, any ID is accepted. +func (x *testRequiredContainerIDServerSettings) checkRequestContainerID(id cid.ID) { + x.expectedReqCnrID = &id +} + +func (x testRequiredContainerIDServerSettings) verifyRequestContainerID(m *protorefs.ContainerID) error { + if m == nil { + return newErrMissingRequestBodyField("container ID") + } + if x.expectedReqCnrID != nil { + if err := checkContainerIDTransport(*x.expectedReqCnrID, m); err != nil { + return newErrInvalidRequestField("container ID", err) + } + } + return nil +} + +// provides generic server code for various NeoFS API RPC servers. +type testCommonServerSettings struct { + handlerSleepDur time.Duration + handlerErrForced bool + handlerErr error +} + +// makes the server to return given error as a gRPC status from the handler. By +// default, and if nil, some response message is returned. +func (x *testCommonServerSettings) setHandlerError(err error) { + x.handlerErrForced, x.handlerErr = true, err +} + +// makes the server to sleep specified time before any request processing. By +// default, and if non-positive, request is handled instantly. +func (x *testCommonServerSettings) setSleepDuration(dur time.Duration) { x.handlerSleepDur = dur } + +// provides generic server code for various NeoFS API unary RPC servers. +type testCommonUnaryServerSettings[ + REQBODY apigrpc.Message, + REQBODYV2 any, + REQBODYV2PTR interface { + *REQBODYV2 + signedMessageV2 + }, + REQ interface { + GetBody() REQBODY + GetMetaHeader() *protosession.RequestMetaHeader + GetVerifyHeader() *protosession.RequestVerificationHeader + }, + REQV2 any, + REQV2PTR interface { + *REQV2 + FromGRPCMessage(apigrpc.Message) error + }, + RESPBODY proto.Message, + RESPBODYV2 any, + RESPBODYV2PTR interface { + *RESPBODYV2 + signedMessageV2 + }, + RESP interface { + GetBody() RESPBODY + GetMetaHeader() *protosession.ResponseMetaHeader + }, + RESPV2 any, + RESPV2PTR interface { + *RESPV2 + ToGRPCMessage() apigrpc.Message + FromGRPCMessage(apigrpc.Message) error + }, +] struct { + testCommonServerSettings + testCommonRequestServerSettings[REQBODY, REQBODYV2, REQBODYV2PTR, REQ, REQV2, REQV2PTR] + testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR] +} + +// provides generic server code for various NeoFS API server-side stream RPC +// servers. +type testCommonServerStreamServerSettings[ + REQBODY apigrpc.Message, + REQBODYV2 any, + REQBODYV2PTR interface { + *REQBODYV2 + signedMessageV2 + }, + REQ interface { + GetBody() REQBODY + GetMetaHeader() *protosession.RequestMetaHeader + GetVerifyHeader() *protosession.RequestVerificationHeader + }, + REQV2 any, + REQV2PTR interface { + *REQV2 + FromGRPCMessage(apigrpc.Message) error + }, + RESPBODY proto.Message, + RESPBODYV2 any, + RESPBODYV2PTR interface { + *RESPBODYV2 + signedMessageV2 + }, + RESP interface { + GetBody() RESPBODY + GetMetaHeader() *protosession.ResponseMetaHeader + }, + RESPV2 any, + RESPV2PTR interface { + *RESPV2 + ToGRPCMessage() apigrpc.Message + FromGRPCMessage(apigrpc.Message) error + }, +] struct { + testCommonServerSettings + testCommonRequestServerSettings[REQBODY, REQBODYV2, REQBODYV2PTR, REQ, REQV2, REQV2PTR] + resps map[uint]testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR] + respErrN uint + respErr error +} + +// tunes processing of N-th response starting from 0. +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) tuneNResp(n uint, + tune func(*testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR])) { + type t = testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR] + if x.resps == nil { + x.resps = make(map[uint]t, 1) + } + s := x.resps[n] + tune(&s) + x.resps[n] = s +} + +// tells the server whether to sign the n-th response or not. By default, any +// response is signed. +// +// Overrides signResponsesBy. +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) respondWithoutSigning(n uint) { + x.tuneNResp(n, func(s *testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) { + s.respondWithoutSigning() + }) +} + +// makes the server to sign n-th response using given signer. By default, and +// if nil, random signer is used. +// +// No-op if signing is disabled using respondWithoutSigning. +// nolint:unused // will be needed for https://github.com/nspcc-dev/neofs-sdk-go/issues/653 +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) signResponsesBy(n uint, signer ecdsa.PrivateKey) { + x.tuneNResp(n, func(s *testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) { + s.signResponsesBy(signer) + }) +} + +// makes the server to return n-th response with given meta header. By default, +// and if nil, no header is attached. +// +// Overrides respondWithStatus. +// nolint:unused // will be needed for https://github.com/nspcc-dev/neofs-sdk-go/issues/653 +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) respondWithMeta(n uint, meta *protosession.ResponseMetaHeader) { + x.tuneNResp(n, func(s *testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) { + s.respondWithMeta(meta) + }) +} + +// makes the server to return given status in the n-th response. By default, +// status OK is returned. +// +// Overrides respondWithMeta. +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) respondWithStatus(n uint, st *protostatus.Status) { + x.tuneNResp(n, func(s *testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) { + s.respondWithStatus(st) + }) +} + +// makes the server to return n-th request with the given body. By default, any +// valid body is returned. +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) respondWithBody(n uint, body RESPBODY) { + x.tuneNResp(n, func(s *testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) { + s.respondWithBody(body) + }) +} + +// makes the server to return given error as a gRPC status from the handler +// after the n-th response transmission. If n is zero, handler returns +// immediately. By default, all responses are sent. Note that nil error is also +// returned since it leads to a particular gRPC status. +// +// Overrides respondWithStatus. +func (x *testCommonServerStreamServerSettings[_, _, _, _, _, _, _, _, _, _, _, _]) abortHandlerAfterResponse(n uint, err error) { + if n == 0 { + x.setHandlerError(err) + } else { + x.respErrN, x.respErr = n, err + } +} + +// provides generic server code for various NeoFS API client-side stream RPC +// servers. +type testCommonClientStreamServerSettings[ + REQBODY apigrpc.Message, + REQBODYV2 any, + REQBODYV2PTR interface { + *REQBODYV2 + signedMessageV2 + }, + REQ interface { + GetBody() REQBODY + GetMetaHeader() *protosession.RequestMetaHeader + GetVerifyHeader() *protosession.RequestVerificationHeader + }, + REQV2 any, + REQV2PTR interface { + *REQV2 + FromGRPCMessage(apigrpc.Message) error + }, + RESPBODY proto.Message, + RESPBODYV2 any, + RESPBODYV2PTR interface { + *RESPBODYV2 + signedMessageV2 + }, + RESP interface { + GetBody() RESPBODY + GetMetaHeader() *protosession.ResponseMetaHeader + }, + RESPV2 any, + RESPV2PTR interface { + *RESPV2 + ToGRPCMessage() apigrpc.Message + FromGRPCMessage(apigrpc.Message) error + }, +] struct { + testCommonServerSettings + testCommonRequestServerSettings[REQBODY, REQBODYV2, REQBODYV2PTR, REQ, REQV2, REQV2PTR] + testCommonResponseServerSettings[RESPBODY, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR] + reqCounter uint + reqErrN uint + reqErr error + respN uint +} + +// makes the server to return given error as a gRPC status from the handler +// after the n-th request receipt. If n is zero, handler returns immediately. By +// default, all requests are processed and response message is returned. Note +// that nil error is also returned since it leads to a particular gRPC status. +// +// Overrides respondWithStatusOnRequest. +func (x *testCommonClientStreamServerSettings[_, _, _, _, _, _, _, _, _, _, _, _]) abortHandlerAfterRequest(n uint, err error) { + if n == 0 { + x.setHandlerError(err) + } else { + x.reqErrN, x.reqErr = n, err + } +} + +// makes the server to immediately respond right after the n-th request +// received. +func (x *testCommonClientStreamServerSettings[_, _, _, _, _, _, _, _, _, _, _, _]) respondAfterRequest(n uint) { + x.respN = n +} + +type testCommonRequestServerSettings[ + REQBODY apigrpc.Message, + REQBODYV2 any, + REQBODYV2PTR interface { + *REQBODYV2 + signedMessageV2 + }, + REQ interface { + GetBody() REQBODY + GetMetaHeader() *protosession.RequestMetaHeader + GetVerifyHeader() *protosession.RequestVerificationHeader + }, + REQV2 any, + REQV2PTR interface { + *REQV2 + FromGRPCMessage(apigrpc.Message) error + }, +] struct { + reqCreds *authCredentials + reqXHdrs []string +} + +// makes the server to assert that any request has given X-headers. By default, +// and if empty, no headers are expected. +func (x *testCommonRequestServerSettings[_, _, _, _, _, _]) 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 signed by s. By default, any +// signer is accepted. +// +// Has no effect with checkRequestDataSignature. +func (x *testCommonRequestServerSettings[_, _, _, _, _, _]) authenticateRequest(s neofscrypto.Signer) { + c := authCredentialsFromSigner(s) + x.reqCreds = &c +} + +func (x testCommonRequestServerSettings[REQBODY, REQBODYV2, REQBODYV2PTR, REQ, _, _]) verifyRequest(req REQ) error { + body := req.GetBody() + metaHdr := req.GetMetaHeader() + verifyHdr := req.GetVerifyHeader() + + // signatures + if verifyHdr == nil { + return newInvalidRequestErr(errors.New("missing verification header")) + } + if verifyHdr.Origin != nil { + return newInvalidRequestVerificationHeaderErr(errors.New("origin field is set while should not be")) + } + if err := verifyMessageSignature[REQBODY, REQBODYV2, REQBODYV2PTR]( + body, verifyHdr.BodySignature, x.reqCreds); err != nil { + return newInvalidRequestVerificationHeaderErr(fmt.Errorf("body signature: %w", err)) + } + if err := verifyMessageSignature[*protosession.RequestMetaHeader, apisession.RequestMetaHeader, *apisession.RequestMetaHeader]( + metaHdr, verifyHdr.MetaSignature, x.reqCreds); err != nil { + return newInvalidRequestVerificationHeaderErr(fmt.Errorf("meta signature: %w", err)) + } + if err := verifyMessageSignature[*protosession.RequestVerificationHeader, apisession.RequestVerificationHeader, *apisession.RequestVerificationHeader]( + verifyHdr.Origin, verifyHdr.OriginSignature, x.reqCreds); err != nil { + return newInvalidRequestVerificationHeaderErr(fmt.Errorf("verification header's origin signature: %w", err)) + } + // meta header + 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.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])) + } + } + return nil +} + +type testCommonResponseServerSettings[ + RESPBODY proto.Message, + RESPBODYV2 any, + RESPBODYV2PTR interface { + *RESPBODYV2 + signedMessageV2 + }, + RESP interface { + GetBody() RESPBODY + GetMetaHeader() *protosession.ResponseMetaHeader + }, + RESPV2 any, + RESPV2PTR interface { + *RESPV2 + ToGRPCMessage() apigrpc.Message + FromGRPCMessage(apigrpc.Message) error + }, +] struct { + respUnsigned bool + respSigner *ecdsa.PrivateKey + respMeta *protosession.ResponseMetaHeader + respBody RESPBODY + respBodyForced bool // if respBody = nil is explicitly set +} + +// tells the server whether to sign all the responses or not. By default, any +// response is signed. +// +// Overrides signResponsesBy. +func (x *testCommonResponseServerSettings[_, _, _, _, _, _]) respondWithoutSigning() { + x.respUnsigned = true +} + +// makes the server to always sign responses using given signer. By default, and +// if nil, random signer is used. +// +// No-op if signing is disabled using respondWithoutSigning. +func (x *testCommonResponseServerSettings[_, _, _, _, _, _]) signResponsesBy(key ecdsa.PrivateKey) { + x.respSigner = &key +} + +// makes the server to always respond with the given meta header. By default, +// and if nil, no header is attached. +// +// Overrides respondWithStatus. +func (x *testCommonResponseServerSettings[_, _, _, _, _, _]) respondWithMeta(meta *protosession.ResponseMetaHeader) { + x.respMeta = meta +} + +// makes the server to always respond with the given status. By default, status +// OK is returned. +// +// Overrides respondWithMeta. +func (x *testCommonResponseServerSettings[_, _, _, _, _, _]) respondWithStatus(st *protostatus.Status) { + x.respondWithMeta(&protosession.ResponseMetaHeader{Status: st}) +} + +// makes the server to always respond with the given body. By default, any valid +// body is returned. +func (x *testCommonResponseServerSettings[RESPBODY, _, _, _, _, _]) respondWithBody(body RESPBODY) { + x.respBody = proto.Clone(body).(RESPBODY) + x.respBodyForced = true +} + +func (x testCommonResponseServerSettings[_, RESPBODYV2, RESPBODYV2PTR, RESP, RESPV2, RESPV2PTR]) signResponse(resp RESP) (*protosession.ResponseVerificationHeader, error) { + if x.respUnsigned { + return nil, nil + } + var signer ecdsa.PrivateKey + if x.respSigner != nil { + signer = *x.respSigner + } else { + signer = neofscryptotest.ECDSAPrivateKey() + } + // body + bs, err := signMessage(signer, resp.GetBody(), RESPBODYV2PTR(nil)) + if err != nil { + return nil, fmt.Errorf("sign body: %w", err) + } + // meta + ms, err := signMessage(signer, resp.GetMetaHeader(), (*apisession.ResponseMetaHeader)(nil)) + if err != nil { + return nil, fmt.Errorf("sign meta: %w", err) + } + // origin + ors, err := signMessage(signer, (*protosession.ResponseVerificationHeader)(nil), (*apisession.ResponseVerificationHeader)(nil)) + if err != nil { + return nil, fmt.Errorf("sign verification header's origin: %w", err) + } + return &protosession.ResponseVerificationHeader{ + BodySignature: bs, + MetaSignature: ms, + OriginSignature: ors, + }, nil +} + +// func signature shortener. +type testedClientOp = func(*Client) error + +// asserts that built test server expecting particular X-headers receives them +// from the connected [Client] through on specified op execution. The op must be +// executed with all the correct parameters to return no error. +func testRequestXHeaders[SRV interface{ checkRequestXHeaders([]string) }]( + t *testing.T, + newSrv func() SRV, + connect func(testing.TB, any /* SRV */) *Client, + op func(*Client, []string) error, +) { + xhdrs := []string{ + "x-key1", "x-val1", + "x-key2", "x-val2", + } + + srv := newSrv() + c := connect(t, srv) + + srv.checkRequestXHeaders(xhdrs) + err := op(c, xhdrs) + require.NoError(t, err) +} + +func assertSignRequestErr(t testing.TB, err error) { require.ErrorContains(t, err, "sign request") } + +// asserts that given op returns an error when the [Client]'s underlying signer +// fails to sign the request. The op must be executed with all the correct +// parameters. +func testSignRequestFailure(t testing.TB, op testedClientOp) { + c := newClient(t) + c.prm.signer = neofscryptotest.FailSigner(neofscryptotest.Signer()) + assertSignRequestErr(t, op(c)) +} + +func assertTransportErr(t testing.TB, transport, err error) { + require.ErrorContains(t, err, "rpc failure") + st, ok := status.FromError(err) + require.True(t, ok, err) + require.Equal(t, codes.Unknown, st.Code()) + require.Contains(t, st.Message(), transport.Error()) +} + +// asserts that given [Client] op returns an expected error when built test +// server always responds with gRPC status error. The op must be executed with +// all the correct parameters. +func testTransportFailure[SRV interface{ setHandlerError(error) }]( + t testing.TB, + newSrv func() SRV, + connect func(t testing.TB, srv any) *Client, + op testedClientOp, +) { + transportErr := errors.New("any transport failure") + srv := newSrv() + srv.setHandlerError(transportErr) + c := connect(t, srv) + + err := op(c) + // 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. + assertTransportErr(t, transportErr, err) +} + +// asserts that given [Client] op returns an expected error when built test +// server responds with incorrect verification header. The op must be executed +// with all the correct parameters. +func testInvalidResponseVerificationHeader[SRV interface{ respondWithoutSigning() }]( + t testing.TB, + newSrv func() SRV, + connect func(t testing.TB, srv any) *Client, + op testedClientOp, +) { + srv := newSrv() + srv.respondWithoutSigning() + // TODO: add cases with less radical corruption such as replacing one byte or + // dropping only one of the signatures. + // Note: TBD during transition to proto/* packages in current repository. + c := connect(t, srv) + require.ErrorContains(t, op(c), "invalid response signature") +} + +type invalidResponseBodyTestcase[BODY any] struct { + name string + body *BODY + assertErr func(testing.TB, error) +} + +// asserts that given [Client] op returns expected errors when built test server +// responds with various invalid bodies. The op must be executed with all the +// correct parameters. +func testInvalidResponseBodies[BODY any, SRV interface{ respondWithBody(*BODY) }]( + t *testing.T, + newSrv func() SRV, + connect func(t testing.TB, srv any) *Client, + tcs []invalidResponseBodyTestcase[BODY], + op testedClientOp, +) { + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + srv := newSrv() + srv.respondWithBody(tc.body) + c := connect(t, srv) + err := op(c) + tc.assertErr(t, err) + }) + } +} + +// asserts that given [Client] op returns expected context errors when user +// passes done context. The op must be executed with the provided context and +// correct other parameters. +func testContextErrors[SRV any]( + t *testing.T, + newSrv func() SRV, + connect func(t testing.TB, srv any) *Client, + op func(context.Context, *Client) error, +) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/624") + srv := newSrv() + c := connect(t, srv) + require.NoError(t, op(context.Background(), c)) + t.Run("cancelled", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := op(ctx, c) + require.ErrorIs(t, err, context.Canceled) + }) + t.Run("timed out", func(t *testing.T) { + ctx, cancel := context.WithDeadline(context.Background(), time.Now()) + t.Cleanup(cancel) + err := op(ctx, c) + require.ErrorIs(t, err, context.DeadlineExceeded) + }) +} + +// asserts that given [Client] op returns expected errors when server responds +// with various NeoFS statuses. The op must be executed with all the correct +// parameters. +func testStatusResponses[SRV interface { + respondWithStatus(*protostatus.Status) +}]( + t *testing.T, + newSrv func() SRV, + connect func(t testing.TB, srv any) *Client, + op testedClientOp, +) { + execWithStatus := func(code uint32, msg string, details []*protostatus.Status_Detail) error { + srv := newSrv() + st := &protostatus.Status{Code: code, Message: msg, Details: details} + srv.respondWithStatus(st) + c := connect(t, srv) + return op(c) + } + + t.Run("OK", func(t *testing.T) { + err := execWithStatus(0, "", make([]*protostatus.Status_Detail, 2)) + require.NoError(t, err) + }) + t.Run("unrecognized", func(t *testing.T) { + for _, code := range []uint32{ + 1, + 1023, + 1028, + 2054, + 3074, + 4098, + } { + t.Run("unrecognized_"+strconv.FormatUint(uint64(code), 10), func(t *testing.T) { + err := execWithStatus(code, "any message", make([]*protostatus.Status_Detail, 2)) + require.ErrorContains(t, err, "status: code = unrecognized message = any message") + require.ErrorIs(t, err, apistatus.ErrUnrecognizedStatusV2) + require.ErrorAs(t, err, new(*apistatus.UnrecognizedStatusV2)) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/648") + require.ErrorIs(t, err, apistatus.Error) + }) + } + }) + + type testcase struct { + name string + code uint32 + details []*protostatus.Status_Detail + defaultErrMsg string + err, constErr error + extraAssert func(t testing.TB, msg string, err error) + } + tcs := []testcase{ + {name: "internal server error", + code: 1024, details: make([]*protostatus.Status_Detail, 2), + err: new(apistatus.ServerInternal), constErr: apistatus.ErrServerInternal, + extraAssert: func(t testing.TB, msg string, err error) { + var e *apistatus.ServerInternal + require.ErrorAs(t, err, &e) + require.Equal(t, msg, e.Message()) + }, + }, + {name: "invalid response signature", + code: 1026, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "signature verification failed", + err: new(apistatus.SignatureVerification), constErr: apistatus.ErrSignatureVerification, + extraAssert: func(t testing.TB, msg string, err error) { + var e *apistatus.SignatureVerification + require.ErrorAs(t, err, &e) + require.Equal(t, msg, e.Message()) + }, + }, + {name: "node maintenance", + code: 1027, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "node is under maintenance", + err: new(apistatus.NodeUnderMaintenance), constErr: apistatus.ErrNodeUnderMaintenance, + extraAssert: func(t testing.TB, msg string, err error) { + var e *apistatus.NodeUnderMaintenance + require.ErrorAs(t, err, &e) + require.Equal(t, msg, e.Message()) + }, + }, + {name: "missing object", + code: 2049, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "object not found", + err: new(apistatus.ObjectNotFound), constErr: apistatus.ErrObjectNotFound, + }, + {name: "locked object", + code: 2050, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "object is locked", + err: new(apistatus.ObjectLocked), constErr: apistatus.ErrObjectLocked, + }, + {name: "lock irregular object", + code: 2051, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "locking non-regular object is forbidden", + err: new(apistatus.LockNonRegularObject), constErr: apistatus.ErrLockNonRegularObject, + }, + {name: "already removed object", + code: 2052, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "object already removed", + err: new(apistatus.ObjectAlreadyRemoved), constErr: apistatus.ErrObjectAlreadyRemoved, + }, + {name: "out of object range", + code: 2053, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "out of range", + err: new(apistatus.ObjectOutOfRange), constErr: apistatus.ErrObjectOutOfRange, + }, + {name: "missing container", + code: 3072, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "container not found", + err: new(apistatus.ContainerNotFound), constErr: apistatus.ErrContainerNotFound, + }, + {name: "missing eACL", + code: 3073, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "eACL not found", + err: new(apistatus.EACLNotFound), constErr: apistatus.ErrEACLNotFound, + }, + {name: "missing session token", + code: 4096, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "session token not found", + err: new(apistatus.SessionTokenNotFound), constErr: apistatus.ErrSessionTokenNotFound, + }, + {name: "expired session token", + code: 4097, details: make([]*protostatus.Status_Detail, 2), + defaultErrMsg: "expired session token", + err: new(apistatus.SessionTokenExpired), constErr: apistatus.ErrSessionTokenExpired, + }, + } + for _, tc := range []struct { + name string + correctMagicBytes []byte + assert func(testing.TB, *apistatus.WrongMagicNumber) + }{ + { // default + assert: func(tb testing.TB, e *apistatus.WrongMagicNumber) { + _, ok := e.CorrectMagic() + require.Zero(t, ok) + }}, + {name: "undersize", + correctMagicBytes: make([]byte, 7), + assert: func(tb testing.TB, e *apistatus.WrongMagicNumber) { + _, ok := e.CorrectMagic() + require.EqualValues(t, -1, ok) + }}, + {name: "oversize", + correctMagicBytes: make([]byte, 9), + assert: func(tb testing.TB, e *apistatus.WrongMagicNumber) { + _, ok := e.CorrectMagic() + require.EqualValues(t, -1, ok) + }}, + {name: "valid", + correctMagicBytes: []byte{140, 15, 162, 245, 219, 236, 37, 191}, + assert: func(tb testing.TB, e *apistatus.WrongMagicNumber) { + magic, ok := e.CorrectMagic() + require.EqualValues(t, 1, ok) + require.EqualValues(t, uint64(10092464466800944575), magic) + }}, + } { + name := "wrong magic number" + var details []*protostatus.Status_Detail + if tc.correctMagicBytes != nil { + details = []*protostatus.Status_Detail{{Id: 0, Value: tc.correctMagicBytes}} + name += "/with correct magic/" + tc.name + } else { + name += "/default" + } + tcs = append(tcs, testcase{name: name, + code: 1025, details: details, + err: new(apistatus.WrongMagicNumber), constErr: apistatus.ErrWrongMagicNumber, + extraAssert: func(t testing.TB, _ string, err error) { + var e *apistatus.WrongMagicNumber + require.ErrorAs(t, err, &e) + tc.assert(t, e) + }, + }) + } + for _, tc := range []struct { + name string + reason string + assert func(testing.TB, *apistatus.ObjectAccessDenied) + }{ + { // default + assert: func(tb testing.TB, e *apistatus.ObjectAccessDenied) { require.Zero(t, e.Reason()) }}, + {name: "with reason", + reason: "Hello, world!", + assert: func(tb testing.TB, e *apistatus.ObjectAccessDenied) { require.Equal(t, "Hello, world!", e.Reason()) }}, + } { + name := "object access denial" + var details []*protostatus.Status_Detail + if tc.reason != "" { + details = []*protostatus.Status_Detail{{Id: 0, Value: []byte(tc.reason)}} + name += "/with reason/" + tc.name + } else { + name += "/default" + } + tcs = append(tcs, testcase{name: name, + code: 2048, details: details, + err: new(apistatus.ObjectAccessDenied), constErr: apistatus.ErrObjectAccessDenied, + defaultErrMsg: "access to object operation denied", + extraAssert: func(t testing.TB, _ string, err error) { + var e *apistatus.ObjectAccessDenied + require.ErrorAs(t, err, &e) + tc.assert(t, e) + }, + }) + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + checkWithMsg := func(msg string) { + err := execWithStatus(tc.code, msg, tc.details) + require.ErrorIs(t, err, apistatus.Error) + require.ErrorAs(t, err, &tc.err) + require.ErrorIs(t, err, tc.constErr) + var expectedErrMsg string + if msg != "" { + expectedErrMsg = fmt.Sprintf("status: code = %d message = %s", tc.code, msg) + } else { + if tc.defaultErrMsg != "" { + expectedErrMsg = fmt.Sprintf("status: code = %d message = %s", tc.code, tc.defaultErrMsg) + } else { + expectedErrMsg = fmt.Sprintf("status: code = %d", tc.code) + } + } + require.ErrorContains(t, err, expectedErrMsg) + if tc.extraAssert != nil { + tc.extraAssert(t, msg, tc.err) + } + } + checkWithMsg("") + checkWithMsg("Hello, world!") + }) + } +} + +// asserts that given [Client] op returns an expected error when some server +// responds with the incorrect message format. The op must be executed with all +// the correct parameters. +func testIncorrectUnaryRPCResponseFormat(t testing.TB, svcName, method string, op testedClientOp) { + svc := testService{ + desc: &grpc.ServiceDesc{ServiceName: "neo.fs.v2." + svcName, Methods: []grpc.MethodDesc{ + { + MethodName: method, + 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) + require.ErrorContains(t, op(c), "invalid response signature") + // TODO(https://github.com/nspcc-dev/neofs-sdk-go/issues/661): 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. +} + +// asserts that given [Client] op correctly reports meta information received +// from built test server when consuming the specified service. The op must be +// executed with all the correct parameters. +func testUnaryResponseCallback[SRV interface { + respondWithMeta(*protosession.ResponseMetaHeader) + signResponsesBy(ecdsa.PrivateKey) +}]( + t testing.TB, + newSrv func() SRV, + newSvc func(t testing.TB, srv any) testService, + op testedClientOp, +) { + srv := newSrv() + srvSigner := neofscryptotest.Signer() + srvPub := neofscrypto.PublicKeyBytes(srvSigner.Public()) + srv.signResponsesBy(srvSigner.ECDSAPrivateKey) + srvEpoch := rand.Uint64() + srv.respondWithMeta(&protosession.ResponseMetaHeader{Epoch: srvEpoch}) + + var collected []ResponseMetaInfo + var handlerErr error + handler := func(meta ResponseMetaInfo) error { + collected = append(collected, meta) + return handlerErr + } + assert := func(expEpoch uint64, expPub []byte) { + require.Len(t, collected, 1) + require.Equal(t, expEpoch, collected[0].Epoch()) + require.Equal(t, expPub, collected[0].ResponderKey()) + collected = nil + } + + c := newCustomClient(t, func(prm *PrmInit) { prm.SetResponseInfoCallback(handler) }, newSvc(t, srv)) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + assert(testServerStateOnDial.epoch, testServerStateOnDial.pub) + + err := op(c) + require.NoError(t, err) + assert(srvEpoch, srvPub) + + handlerErr = errors.New("any response meta handler failure") + err = op(c) + require.ErrorContains(t, err, "response callback error") + require.ErrorIs(t, err, handlerErr) + assert(srvEpoch, srvPub) +} + +// checks that the [Client] correctly keeps exec statistics of specified ops +// performing communication with built test server. All operations must comply +// with the tested service. +// +// If non-stat failure cases are specified, they must include request signature +// failure caused by the op signer parameter. +func testStatistic[SRV interface { + setSleepDuration(time.Duration) + setHandlerError(error) +}]( + t testing.TB, + newSrv func() SRV, + newSvc func(t testing.TB, srv any) testService, + expMtd stat.Method, + customNonStatFailures []testedClientOp, + customStatFailures []testedClientOp, + validInputCall testedClientOp, +) { + srv := newSrv() + svc := newSvc(t, srv) + + type collectedItem struct { + pub []byte + endpoint string + mtd stat.Method + dur time.Duration + err error + } + var collected []collectedItem + handler := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + collected = append(collected, collectedItem{pub: pub, endpoint: endpoint, mtd: mtd, dur: dur, err: err}) + } + assertCommon := func(mtd stat.Method, pub []byte, err error) { + require.Len(t, collected, 1) + require.Equal(t, pub, collected[0].pub) + require.Equal(t, testServerEndpoint, collected[0].endpoint) + require.Equal(t, mtd, collected[0].mtd) + require.Positive(t, collected[0].dur) + require.Equal(t, err, collected[0].err) + } + + c := newCustomClient(t, func(prm *PrmInit) { prm.SetStatisticCallback(handler) }, svc) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + assertCommon(stat.MethodEndpointInfo, nil, nil) // server key is not yet received + collected = nil + + assert := func(err error) { + assertCommon(expMtd, testServerStateOnDial.pub, err) + } + + // custom non-stat failures + for _, getNonStatErr := range customNonStatFailures { + err := getNonStatErr(c) + require.Error(t, err) + assert(nil) + collected = nil + } + + // custom stat failures + for _, getStatErr := range customStatFailures { + err := getStatErr(c) + require.Error(t, err) + assert(err) + collected = nil + } + + if len(customNonStatFailures) == 0 { + // sign request failure + signerCp := c.prm.signer + c.prm.signer = neofscryptotest.FailSigner(c.prm.signer) + + err := validInputCall(c) + assertSignRequestErr(t, err) + assert(err) + collected = nil + + c.prm.signer = signerCp + } + + // transport + transportErr := errors.New("any transport failure") + srv.setHandlerError(transportErr) + + err := validInputCall(c) + assertTransportErr(t, transportErr, err) + assert(err) + collected = nil + + srv.setHandlerError(nil) + + // OK + const sleepDur = 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + srv.setSleepDuration(sleepDur) + + err = validInputCall(c) + require.NoError(t, err) + assert(err) + require.Greater(t, collected[0].dur, sleepDur) +} diff --git a/client/container_statistic_test.go b/client/container_statistic_test.go deleted file mode 100644 index 2412d494..00000000 --- a/client/container_statistic_test.go +++ /dev/null @@ -1,471 +0,0 @@ -package client - -import ( - "context" - "crypto/rand" - "io" - mathRand "math/rand/v2" - "strconv" - "testing" - "time" - - "github.com/google/uuid" - "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" - "github.com/nspcc-dev/neofs-sdk-go/eacl" - "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" - reputation2 "github.com/nspcc-dev/neofs-sdk-go/reputation" - session2 "github.com/nspcc-dev/neofs-sdk-go/session" - "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/stretchr/testify/require" -) - -type ( - methodStatistic struct { - requests int - errors int - duration time.Duration - } - - testStatCollector struct { - methods map[stat.Method]*methodStatistic - } -) - -func newCollector() *testStatCollector { - c := testStatCollector{ - methods: make(map[stat.Method]*methodStatistic), - } - - for i := stat.MethodBalanceGet; i < stat.MethodLast; i++ { - c.methods[i] = &methodStatistic{} - } - - return &c -} - -func (c *testStatCollector) Collect(_ []byte, _ string, method stat.Method, duration time.Duration, err error) { - data, ok := c.methods[method] - if ok { - data.duration += duration - if duration > 0 { - data.requests++ - } - - if err != nil { - data.errors++ - } - } -} - -func randBytes(l int) []byte { - r := make([]byte, l) - _, _ = rand.Read(r) - - return r -} - -func prepareContainer(accountID user.ID) container.Container { - cont := container.Container{} - cont.Init() - cont.SetOwner(accountID) - cont.SetBasicACL(acl.PublicRW) - - cont.SetName(strconv.FormatInt(time.Now().UnixNano(), 16)) - cont.SetCreationTime(time.Now().UTC()) - - var pp netmap.PlacementPolicy - var rd netmap.ReplicaDescriptor - rd.SetNumberOfObjects(1) - - pp.SetContainerBackupFactor(1) - pp.SetReplicas([]netmap.ReplicaDescriptor{rd}) - cont.SetPlacementPolicy(pp) - - return cont -} - -func testEaclTable(containerID cid.ID) eacl.Table { - var table eacl.Table - table.SetCID(containerID) - - r := eacl.ConstructRecord(eacl.ActionAllow, eacl.OperationPut, []eacl.Target{eacl.NewTargetByRole(eacl.RoleOthers)}) - table.AddRecord(&r) - - return table -} - -func TestClientStatistic_ContainerPut(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testPutContainerServer - c := newTestContainerClient(t, &srv) - cont := prepareContainer(usr.ID) - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerPut - _, err := c.ContainerPut(ctx, cont, usr.RFC6979, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerPut].requests) -} - -func TestClientStatistic_ContainerGet(t *testing.T) { - ctx := context.Background() - var srv testGetContainerServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerGet - _, err := c.ContainerGet(ctx, cid.ID{}, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerGet].requests) -} - -func TestClientStatistic_ContainerList(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testListContainersServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerList - _, err := c.ContainerList(ctx, usr.ID, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerList].requests) -} - -func TestClientStatistic_ContainerDelete(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testDeleteContainerServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerDelete - err := c.ContainerDelete(ctx, cid.ID{}, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerDelete].requests) -} - -func TestClientStatistic_ContainerEacl(t *testing.T) { - ctx := context.Background() - var srv testGetEACLServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerEACL - _, err := c.ContainerEACL(ctx, cid.ID{}, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerEACL].requests) -} - -func TestClientStatistic_ContainerSetEacl(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testSetEACLServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmContainerSetEACL - table := testEaclTable(cidtest.ID()) - err := c.ContainerSetEACL(ctx, table, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerSetEACL].requests) -} - -func TestClientStatistic_ContainerAnnounceUsedSpace(t *testing.T) { - ctx := context.Background() - var srv testAnnounceContainerSpaceServer - c := newTestContainerClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - estimation := container.SizeEstimation{} - estimation.SetContainer(cidtest.ID()) - estimation.SetValue(mathRand.Uint64()) - estimation.SetEpoch(mathRand.Uint64()) - - var prm PrmAnnounceSpace - err := c.ContainerAnnounceUsedSpace(ctx, []container.SizeEstimation{estimation}, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodContainerAnnounceUsedSpace].requests) -} - -func TestClientStatistic_ContainerSyncContainerWithNetwork(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testGetNetworkInfoServer - c := newTestNetmapClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - cont := prepareContainer(usr.ID) - - err := SyncContainerWithNetwork(ctx, &cont, c) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodNetworkInfo].requests) -} - -func TestClientStatistic_ContainerEndpointInfo(t *testing.T) { - ctx := context.Background() - srv := newTestGetNodeInfoServer() - c := newTestNetmapClient(t, srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - _, err := c.EndpointInfo(ctx, PrmEndpointInfo{}) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodEndpointInfo].requests) -} - -func TestClientStatistic_ContainerNetMapSnapshot(t *testing.T) { - ctx := context.Background() - var srv testNetmapSnapshotServer - c := newTestNetmapClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - _, err := c.NetMapSnapshot(ctx, PrmNetMapSnapshot{}) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodNetMapSnapshot].requests) -} - -func TestClientStatistic_CreateSession(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testCreateSessionServer - c := newTestSessionClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmSessionCreate - - _, err := c.SessionCreate(ctx, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodSessionCreate].requests) -} - -func TestClientStatistic_ObjectPut(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testPutObjectServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var tokenSession session2.Object - tokenSession.SetID(uuid.New()) - tokenSession.SetExp(1) - tokenSession.BindContainer(containerID) - tokenSession.ForVerb(session2.VerbObjectPut) - tokenSession.SetAuthKey(usr.Public()) - tokenSession.SetIssuer(usr.ID) - - err := tokenSession.Sign(usr) - require.NoError(t, err) - - var prm PrmObjectPutInit - prm.WithinSession(tokenSession) - - var hdr object.Object - hdr.SetOwner(usr.ID) - hdr.SetContainerID(containerID) - - writer, err := c.ObjectPutInit(ctx, hdr, usr, prm) - require.NoError(t, err) - - _, err = writer.Write(randBytes(10)) - require.NoError(t, err) - - err = writer.Close() - require.NoError(t, err) - - 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() - var srv testDeleteObjectServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - objectID := oid.ID{} - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectDelete - - _, err := c.ObjectDelete(ctx, containerID, objectID, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectDelete].requests) -} - -func TestClientStatistic_ObjectGet(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testGetObjectServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - objectID := oid.ID{} - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectGet - - _, reader, err := c.ObjectGetInit(ctx, containerID, objectID, usr, prm) - require.NoError(t, err) - _, err = io.Copy(io.Discard, reader) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectGet].requests) -} - -func TestClientStatistic_ObjectHead(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testHeadObjectServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - objectID := oid.ID{} - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectHead - - _, err := c.ObjectHead(ctx, containerID, objectID, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectHead].requests) -} - -func TestClientStatistic_ObjectRange(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testGetObjectPayloadRangeServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - objectID := oid.ID{} - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectRange - - reader, err := c.ObjectRangeInit(ctx, containerID, objectID, 0, 1, usr, prm) - require.NoError(t, err) - _, err = io.Copy(io.Discard, reader) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectRange].requests) -} - -func TestClientStatistic_ObjectHash(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testHashObjectPayloadRangesServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - objectID := oid.ID{} - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectHash - prm.SetRangeList(0, 2) - - _, err := c.ObjectHash(ctx, containerID, objectID, usr, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectHash].requests) -} - -func TestClientStatistic_ObjectSearch(t *testing.T) { - usr := usertest.User() - ctx := context.Background() - var srv testSearchObjectsServer - c := newTestObjectClient(t, &srv) - containerID := cidtest.ID() - - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var prm PrmObjectSearch - - reader, err := c.ObjectSearchInit(ctx, containerID, usr, prm) - require.NoError(t, err) - - iterator := func(oid.ID) bool { - return false - } - - err = reader.Iterate(iterator) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodObjectSearch].requests) -} - -func TestClientStatistic_AnnounceIntermediateTrust(t *testing.T) { - ctx := context.Background() - var srv testAnnounceIntermediateReputationServer - c := newTestReputationClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var trust reputation2.PeerToPeerTrust - var prm PrmAnnounceIntermediateTrust - - err := c.AnnounceIntermediateTrust(ctx, 1, trust, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodAnnounceIntermediateTrust].requests) -} - -func TestClientStatistic_MethodAnnounceLocalTrust(t *testing.T) { - ctx := context.Background() - var srv testAnnounceLocalTrustServer - c := newTestReputationClient(t, &srv) - collector := newCollector() - c.prm.statisticCallback = collector.Collect - - var peer reputation2.PeerID - var trust reputation2.Trust - trust.SetPeer(peer) - - var prm PrmAnnounceLocalTrust - - err := c.AnnounceLocalTrust(ctx, 1, []reputation2.Trust{trust}, prm) - require.NoError(t, err) - - require.Equal(t, 1, collector.methods[stat.MethodAnnounceLocalTrust].requests) -} diff --git a/client/container_test.go b/client/container_test.go index 0392ea24..58e572f5 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -2,203 +2,1882 @@ package client import ( "context" + "errors" "fmt" "testing" + "time" + v2acl "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" protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + "github.com/nspcc-dev/neofs-api-go/v2/refs" protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apigrpc "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/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" containertest "github.com/nspcc-dev/neofs-sdk-go/container/test" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" + neofsecdsa "github.com/nspcc-dev/neofs-sdk-go/crypto/ecdsa" neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" "github.com/nspcc-dev/neofs-sdk-go/eacl" + "github.com/nspcc-dev/neofs-sdk-go/session" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/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/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) -// 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}) +// returns Client-compatible Container service handled by given server. Provided +// server must implement [protocontainer.ContainerServiceServer]: the parameter +// is not of this type to support generics. +func newDefaultContainerService(t testing.TB, srv any) testService { + require.Implements(t, (*protocontainer.ContainerServiceServer)(nil), srv) + return testService{desc: &protocontainer.ContainerService_ServiceDesc, impl: srv} +} + +// returns Client of Container service provided by given server. Provided server +// must implement [protocontainer.ContainerServiceServer]: the parameter is +// not of this type to support generics. +func newTestContainerClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultContainerService(t, srv)) +} + +// for sharing between servers of requests with RFC 6979 signature of particular +// data. +type testRFC6979DataSignatureServerSettings[ + SIGNED apigrpc.Message, + SIGNEDV2 any, + SIGNEDV2PTR interface { + *SIGNEDV2 + signedMessageV2 + }, +] struct { + reqCreds *authCredentials + reqDataSignature *neofscrypto.Signature +} + +// makes the server to assert that any request's payload is signed by s. By +// default, any signer is accepted. +// +// Has no effect with checkRequestDataSignature. +func (x *testRFC6979DataSignatureServerSettings[_, _, _]) authenticateRequestPayload(s neofscrypto.Signer) { + c := authCredentialsFromSigner(s) + x.reqCreds = &c +} + +// makes the server to assert that any request carries given signature without +// verification. By default, any signature matching the data is accepted. +// +// Overrides checkRequestDataSignerKey. +func (x *testRFC6979DataSignatureServerSettings[_, _, _]) checkRequestDataSignature(s neofscrypto.Signature) { + x.reqDataSignature = &s +} + +func (x testRFC6979DataSignatureServerSettings[_, _, _]) verifyDataSignature(signedField string, data []byte, m *protorefs.SignatureRFC6979) error { + field := signedField + " signature" + if m == nil { + return newErrMissingRequestBodyField(field) + } + if x.reqDataSignature != nil { + if err := checkSignatureRFC6979Transport(*x.reqDataSignature, m); err != nil { + return newErrInvalidRequestField(field, err) + } + return nil + } + if err := verifyDataSignature(data, &protorefs.Signature{ + Key: m.Key, + Sign: m.Sign, + Scheme: protorefs.SignatureScheme_ECDSA_RFC6979_SHA256, + }, x.reqCreds); err != nil { + return newErrInvalidRequestField(field, err) + } + return nil +} + +func (x testRFC6979DataSignatureServerSettings[SIGNED, SIGNEDV2, SIGNEDV2PTR]) verifyMessageSignature(signedField string, signed SIGNED, m *protorefs.SignatureRFC6979) error { + mV2 := SIGNEDV2PTR(new(SIGNEDV2)) + if err := mV2.FromGRPCMessage(signed); err != nil { + panic(err) + } + return x.verifyDataSignature(signedField, mV2.StableMarshal(nil), m) +} + +// for sharing between servers of requests with a container session token. +type testContainerSessionServerSettings struct { + expectedToken *session.Container +} + +// makes the server to assert that any request carries given session token. By +// default, session token must not be attached. +func (x *testContainerSessionServerSettings) checkRequestSessionToken(st session.Container) { + x.expectedToken = &st +} + +func (x testContainerSessionServerSettings) verifySessionToken(m *protosession.SessionToken) error { + if m == nil { + if x.expectedToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("session token is missing while should not be")) + } + return nil + } + if x.expectedToken == nil { + return newInvalidRequestMetaHeaderErr(errors.New("session token attached while should not be")) + } + if err := checkContainerSessionTransport(*x.expectedToken, m); err != nil { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("session token: %w", err)) + } + return nil } type testPutContainerServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.PutRequest_Body, + apicontainer.PutRequestBody, + *apicontainer.PutRequestBody, + *protocontainer.PutRequest, + apicontainer.PutRequest, + *apicontainer.PutRequest, + *protocontainer.PutResponse_Body, + apicontainer.PutResponseBody, + *apicontainer.PutResponseBody, + *protocontainer.PutResponse, + apicontainer.PutResponse, + *apicontainer.PutResponse, + ] + testContainerSessionServerSettings + testRFC6979DataSignatureServerSettings[*protocontainer.Container, apicontainer.Container, *apicontainer.Container] + reqContainer *container.Container +} + +// returns [protocontainer.ContainerServiceServer] supporting Put method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestPutContainerServer() *testPutContainerServer { return new(testPutContainerServer) } + +// makes the server to assert that any request carries given container. By +// default, any valid container is accepted. +func (x *testPutContainerServer) checkRequestContainer(cnr container.Container) { + x.reqContainer = &cnr } -func (x *testPutContainerServer) Put(context.Context, *protocontainer.PutRequest) (*protocontainer.PutResponse, error) { - id := cidtest.ID() - resp := protocontainer.PutResponse{ - Body: &protocontainer.PutResponse_Body{ - ContainerId: &protorefs.ContainerID{Value: id[:]}, - }, +func (x *testPutContainerServer) verifyRequest(req *protocontainer.PutRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + case metaHdr.BearerToken != nil: + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + if err := x.verifySessionToken(req.MetaHeader.SessionToken); err != nil { + return err } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // container + if body.Container == nil { + return newErrMissingRequestBodyField("container") + } + if x.reqContainer != nil { + if err := checkContainerTransport(*x.reqContainer, body.Container); err != nil { + return newErrInvalidRequestField("container", err) + } + } + // signature + return x.verifyMessageSignature("container", body.Container, body.Signature) +} - var respV2 apicontainer.PutResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testPutContainerServer) Put(_ context.Context, req *protocontainer.PutRequest) (*protocontainer.PutResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr + } + + resp := &protocontainer.PutResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinPutContainerResponseBody).(*protocontainer.PutResponse_Body) } - return respV2.ToGRPCMessage().(*protocontainer.PutResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testGetContainerServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.GetRequest_Body, + apicontainer.GetRequestBody, + *apicontainer.GetRequestBody, + *protocontainer.GetRequest, + apicontainer.GetRequest, + *apicontainer.GetRequest, + *protocontainer.GetResponse_Body, + apicontainer.GetResponseBody, + *apicontainer.GetResponseBody, + *protocontainer.GetResponse, + apicontainer.GetResponse, + *apicontainer.GetResponse, + ] + testRequiredContainerIDServerSettings } -func (x *testGetContainerServer) Get(context.Context, *protocontainer.GetRequest) (*protocontainer.GetResponse, error) { - cnr := containertest.Container() - var cnrV2 apicontainer.Container - cnr.WriteToV2(&cnrV2) - resp := protocontainer.GetResponse{ - Body: &protocontainer.GetResponse_Body{ - Container: cnrV2.ToGRPCMessage().(*protocontainer.Container), - }, +// returns [protocontainer.ContainerServiceServer] supporting Get method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestGetContainerServer() *testGetContainerServer { return new(testGetContainerServer) } + +func (x *testGetContainerServer) verifyRequest(req *protocontainer.GetRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // session token + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) } + // bearer token + if req.MetaHeader.BearerToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + return x.verifyRequestContainerID(body.ContainerId) +} - var respV2 apicontainer.GetResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testGetContainerServer) Get(_ context.Context, req *protocontainer.GetRequest) (*protocontainer.GetResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.handlerErr != nil { + return nil, x.handlerErr } - return respV2.ToGRPCMessage().(*protocontainer.GetResponse), nil + resp := &protocontainer.GetResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinGetContainerResponseBody).(*protocontainer.GetResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testListContainersServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.ListRequest_Body, + apicontainer.ListRequestBody, + *apicontainer.ListRequestBody, + *protocontainer.ListRequest, + apicontainer.ListRequest, + *apicontainer.ListRequest, + *protocontainer.ListResponse_Body, + apicontainer.ListResponseBody, + *apicontainer.ListResponseBody, + *protocontainer.ListResponse, + apicontainer.ListResponse, + *apicontainer.ListResponse, + ] + reqOwner *user.ID } -func (x *testListContainersServer) List(context.Context, *protocontainer.ListRequest) (*protocontainer.ListResponse, error) { - var resp protocontainer.ListResponse +// returns [protocontainer.ContainerServiceServer] supporting List method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestListContainersServer() *testListContainersServer { return new(testListContainersServer) } - var respV2 apicontainer.ListResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +// makes the server to assert that any request carries given owner. By default, +// any user is accepted. +func (x *testListContainersServer) checkOwner(owner user.ID) { x.reqOwner = &owner } + +func (x *testListContainersServer) verifyRequest(req *protocontainer.ListRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + // owner + if body.OwnerId == nil { + return newErrMissingRequestBodyField("owner") + } + if x.reqOwner != nil { + if err := checkUserIDTransport(*x.reqOwner, body.OwnerId); err != nil { + return newErrInvalidRequestField("owner", err) + } + } + return nil +} + +func (x *testListContainersServer) List(_ context.Context, req *protocontainer.ListRequest) (*protocontainer.ListResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr } - return respV2.ToGRPCMessage().(*protocontainer.ListResponse), nil + resp := &protocontainer.ListResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinListContainersResponseBody).(*protocontainer.ListResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testDeleteContainerServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.DeleteRequest_Body, + apicontainer.DeleteRequestBody, + *apicontainer.DeleteRequestBody, + *protocontainer.DeleteRequest, + apicontainer.DeleteRequest, + *apicontainer.DeleteRequest, + *protocontainer.DeleteResponse_Body, + apicontainer.DeleteResponseBody, + *apicontainer.DeleteResponseBody, + *protocontainer.DeleteResponse, + apicontainer.DeleteResponse, + *apicontainer.DeleteResponse, + ] + testContainerSessionServerSettings + testRequiredContainerIDServerSettings + testRFC6979DataSignatureServerSettings[*protorefs.ContainerID, refs.ContainerID, *refs.ContainerID] } -func (x *testDeleteContainerServer) Delete(context.Context, *protocontainer.DeleteRequest) (*protocontainer.DeleteResponse, error) { - var resp protocontainer.DeleteResponse +// returns [protocontainer.ContainerServiceServer] supporting Delete method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestDeleteContainerServer() *testDeleteContainerServer { return new(testDeleteContainerServer) } - var respV2 apicontainer.DeleteResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testDeleteContainerServer) verifyRequest(req *protocontainer.DeleteRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // session token + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + case metaHdr.BearerToken != nil: + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + if err := x.verifySessionToken(req.MetaHeader.SessionToken); err != nil { + return err + } + // bearer token + if req.MetaHeader.BearerToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // ID + mc := body.GetContainerId() + if err := x.verifyRequestContainerID(mc); err != nil { + return err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + // signature + return x.verifyDataSignature("container ID", mc.GetValue(), body.Signature) +} + +func (x *testDeleteContainerServer) Delete(_ context.Context, req *protocontainer.DeleteRequest) (*protocontainer.DeleteResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr + } + + resp := &protocontainer.DeleteResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinDeleteContainerResponseBody).(*protocontainer.DeleteResponse_Body) } - return respV2.ToGRPCMessage().(*protocontainer.DeleteResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testGetEACLServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.GetExtendedACLRequest_Body, + apicontainer.GetExtendedACLRequestBody, + *apicontainer.GetExtendedACLRequestBody, + *protocontainer.GetExtendedACLRequest, + apicontainer.GetExtendedACLRequest, + *apicontainer.GetExtendedACLRequest, + *protocontainer.GetExtendedACLResponse_Body, + apicontainer.GetExtendedACLResponseBody, + *apicontainer.GetExtendedACLResponseBody, + *protocontainer.GetExtendedACLResponse, + apicontainer.GetExtendedACLResponse, + *apicontainer.GetExtendedACLResponse, + ] + testRequiredContainerIDServerSettings +} + +// returns [protocontainer.ContainerServiceServer] supporting GetExtendedACL +// method only. Default implementation performs common verification of any +// request, and responds with any valid message. Some methods allow to tune the +// behavior. +func newTestGetEACLServer() *testGetEACLServer { return new(testGetEACLServer) } + +func (x *testGetEACLServer) verifyRequest(req *protocontainer.GetExtendedACLRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // ID + return x.verifyRequestContainerID(body.ContainerId) } -func (x *testGetEACLServer) GetExtendedACL(context.Context, *protocontainer.GetExtendedACLRequest) (*protocontainer.GetExtendedACLResponse, error) { - resp := protocontainer.GetExtendedACLResponse{ - Body: &protocontainer.GetExtendedACLResponse_Body{ - Eacl: new(protoacl.EACLTable), - }, +func (x *testGetEACLServer) GetExtendedACL(_ context.Context, req *protocontainer.GetExtendedACLRequest) (*protocontainer.GetExtendedACLResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr } - var respV2 apicontainer.GetExtendedACLResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + resp := &protocontainer.GetExtendedACLResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinEACLResponseBody).(*protocontainer.GetExtendedACLResponse_Body) } - return respV2.ToGRPCMessage().(*protocontainer.GetExtendedACLResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testSetEACLServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.SetExtendedACLRequest_Body, + apicontainer.SetExtendedACLRequestBody, + *apicontainer.SetExtendedACLRequestBody, + *protocontainer.SetExtendedACLRequest, + apicontainer.SetExtendedACLRequest, + *apicontainer.SetExtendedACLRequest, + *protocontainer.SetExtendedACLResponse_Body, + apicontainer.SetExtendedACLResponseBody, + *apicontainer.SetExtendedACLResponseBody, + *protocontainer.SetExtendedACLResponse, + apicontainer.SetExtendedACLResponse, + *apicontainer.SetExtendedACLResponse, + ] + testContainerSessionServerSettings + testRFC6979DataSignatureServerSettings[*protoacl.EACLTable, v2acl.Table, *v2acl.Table] + reqEACL *eacl.Table } -func (x *testSetEACLServer) SetExtendedACL(context.Context, *protocontainer.SetExtendedACLRequest) (*protocontainer.SetExtendedACLResponse, error) { - var resp protocontainer.SetExtendedACLResponse +// returns [protocontainer.ContainerServiceServer] supporting SetExtendedACL +// method only. Default implementation performs common verification of any +// request, and responds with any valid message. Some methods allow to tune the +// behavior. +func newTestSetEACLServer() *testSetEACLServer { return new(testSetEACLServer) } - var respV2 apicontainer.SetExtendedACLResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +// makes the server to assert that any request carries given eACL. By +// default, any eACL is accepted. +func (x *testSetEACLServer) checkRequestEACL(eACL eacl.Table) { x.reqEACL = &eACL } + +func (x *testSetEACLServer) verifyRequest(req *protocontainer.SetExtendedACLRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // session token + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + case metaHdr.BearerToken != nil: + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + if err := x.verifySessionToken(req.MetaHeader.SessionToken); err != nil { + return err + } + // bearer token + if req.MetaHeader.BearerToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // eACL + if body.Eacl == nil { + return newErrMissingRequestBodyField("eACL") + } + if x.reqEACL != nil { + if err := checkEACLTransport(*x.reqEACL, body.Eacl); err != nil { + return newErrInvalidRequestField("eACL", err) + } + } + // signature + return x.verifyMessageSignature("eACL", body.Eacl, body.Signature) +} + +func (x *testSetEACLServer) SetExtendedACL(_ context.Context, req *protocontainer.SetExtendedACLRequest) (*protocontainer.SetExtendedACLResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.handlerErr != nil { + return nil, x.handlerErr + } + resp := &protocontainer.SetExtendedACLResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinSetEACLResponseBody).(*protocontainer.SetExtendedACLResponse_Body) } - return respV2.ToGRPCMessage().(*protocontainer.SetExtendedACLResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testAnnounceContainerSpaceServer struct { protocontainer.UnimplementedContainerServiceServer + testCommonUnaryServerSettings[ + *protocontainer.AnnounceUsedSpaceRequest_Body, + apicontainer.AnnounceUsedSpaceRequestBody, + *apicontainer.AnnounceUsedSpaceRequestBody, + *protocontainer.AnnounceUsedSpaceRequest, + apicontainer.AnnounceUsedSpaceRequest, + *apicontainer.AnnounceUsedSpaceRequest, + *protocontainer.AnnounceUsedSpaceResponse_Body, + apicontainer.AnnounceUsedSpaceResponseBody, + *apicontainer.AnnounceUsedSpaceResponseBody, + *protocontainer.AnnounceUsedSpaceResponse, + apicontainer.AnnounceUsedSpaceResponse, + *apicontainer.AnnounceUsedSpaceResponse, + ] + reqAnnouncements []container.SizeEstimation +} + +// returns [protocontainer.ContainerServiceServer] supporting AnnounceUsedSpace +// method only. Default implementation performs common verification of any +// request, and responds with any valid message. Some methods allow to tune the +// behavior. +func newTestAnnounceContainerSpaceServer() *testAnnounceContainerSpaceServer { + return new(testAnnounceContainerSpaceServer) } -func (x *testAnnounceContainerSpaceServer) AnnounceUsedSpace(context.Context, *protocontainer.AnnounceUsedSpaceRequest) (*protocontainer.AnnounceUsedSpaceResponse, error) { - var resp protocontainer.AnnounceUsedSpaceResponse +// makes the server to assert that any request carries given announcements. By +// default, any valid values are accepted. +func (x *testAnnounceContainerSpaceServer) checkRequestAnnouncements(els []container.SizeEstimation) { + x.reqAnnouncements = els +} - var respV2 apicontainer.AnnounceUsedSpaceResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testAnnounceContainerSpaceServer) verifyRequest(req *protocontainer.AnnounceUsedSpaceRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // mead header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // announcements + if len(body.Announcements) == 0 { + return newErrMissingRequestBodyField("announcements") + } + if x.reqAnnouncements != nil { + if v1, v2 := len(x.reqAnnouncements), len(body.Announcements); v1 != v2 { + return fmt.Errorf("number of records (client: %d, message: %d)", v1, v2) + } + for i := range x.reqAnnouncements { + if err := checkContainerSizeEstimationTransport(x.reqAnnouncements[i], body.Announcements[i]); err != nil { + return newErrInvalidRequestField("announcements", fmt.Errorf("elements#%d: %w", i, err)) + } + } + } + return nil +} + +func (x *testAnnounceContainerSpaceServer) AnnounceUsedSpace(_ context.Context, req *protocontainer.AnnounceUsedSpaceRequest) (*protocontainer.AnnounceUsedSpaceResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.handlerErr != nil { + return nil, x.handlerErr } - return respV2.ToGRPCMessage().(*protocontainer.AnnounceUsedSpaceResponse), nil + resp := &protocontainer.AnnounceUsedSpaceResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinUsedSpaceResponseBody).(*protocontainer.AnnounceUsedSpaceResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } -func TestClient_Container(t *testing.T) { - c := newClient(t) +func TestClient_ContainerPut(t *testing.T) { ctx := context.Background() + var anyValidOpts PrmContainerPut + anyValidContainer := containertest.Container() + anyValidSigner := neofscryptotest.Signer().RFC6979 + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestPutContainerServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestContainer(anyValidContainer) + srv.authenticateRequestPayload(anyValidSigner) + srv.authenticateRequest(c.prm.signer) + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, PrmContainerPut{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testStatusResponses(t, newTestPutContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("precalculated container signature", func(t *testing.T) { + srv := newTestPutContainerServer() + c := newTestContainerClient(t, srv) + + var sig neofscrypto.Signature + sig.SetPublicKeyBytes([]byte("any public key")) + sig.SetValue([]byte("any value")) + opts := anyValidOpts + opts.AttachSignature(sig) + + srv.checkRequestDataSignature(sig) + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestPutContainerServer() + c := newTestContainerClient(t, srv) + + st := sessiontest.ContainerSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.PutResponse_Body + }{ + {name: "min", body: validMinPutContainerResponseBody}, + {name: "full", body: validFullPutContainerResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestPutContainerServer() + c := newTestContainerClient(t, srv) - t.Run("missing signer", func(t *testing.T) { - tt := []struct { - name string - methodCall func() error - }{ - { - "put", - func() error { - _, err := c.ContainerPut(ctx, container.Container{}, nil, PrmContainerPut{}) - return err - }, + srv.respondWithBody(tc.body) + id, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkContainerIDTransport(id, tc.body.GetContainerId())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestPutContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "Put", func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestPutContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protocontainer.PutResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing container ID field in the response") + }}, + {name: "empty", body: new(protocontainer.PutResponse_Body), + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing container ID field in the response") + }}, + } + // 1. container ID + for _, tc := range invalidContainerIDProtoTestcases { + id := proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID) + tc.corrupt(id) + body := &protocontainer.PutResponse_Body{ContainerId: id} + tcs = append(tcs, testcase{name: "container ID/" + tc.name, body: body, assertErr: func(tb testing.TB, err error) { + require.EqualError(t, err, "invalid container ID field in the response: "+tc.msg) + }}) + } + + testInvalidResponseBodies(t, newTestPutContainerServer, newTestContainerClient, tcs, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ContainerPut(ctx, anyValidContainer, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("sign container failure", func(t *testing.T) { + c := newClient(t) + t.Run("wrong scheme", func(t *testing.T) { + _, err := c.ContainerPut(ctx, anyValidContainer, neofsecdsa.Signer(neofscryptotest.ECDSAPrivateKey()), anyValidOpts) + require.EqualError(t, err, "calculate container signature: incorrect signer: expected ECDSA_DETERMINISTIC_SHA256 scheme") + }) + t.Run("signer failure", func(t *testing.T) { + _, err := c.ContainerPut(ctx, anyValidContainer, neofscryptotest.FailSigner(neofscryptotest.Signer()), anyValidOpts) + require.ErrorContains(t, err, "calculate container signature") + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestPutContainerServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestPutContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestPutContainerServer, newDefaultContainerService, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestPutContainerServer, newDefaultContainerService, stat.MethodContainerPut, + []testedClientOp{func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, nil, anyValidOpts) + return err + }}, + []testedClientOp{func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, neofscryptotest.FailSigner(anyValidSigner), anyValidOpts) + return err + }}, func(c *Client) error { + _, err := c.ContainerPut(ctx, anyValidContainer, anyValidSigner, anyValidOpts) + return err }, - { - "delete", - func() error { - return c.ContainerDelete(ctx, cid.ID{}, nil, PrmContainerDelete{}) - }, + ) + }) +} + +func TestClient_ContainerGet(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmContainerGet + anyID := cidtest.ID() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestGetContainerServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestContainerID(anyID) + srv.authenticateRequest(c.prm.signer) + _, err := c.ContainerGet(ctx, anyID, PrmContainerGet{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestGetContainerServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ContainerGet(ctx, anyID, opts) + return err + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.GetResponse_Body + }{ + {name: "min", body: validMinGetContainerResponseBody}, + {name: "full", body: validFullGetContainerResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetContainerServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + cnr, err := c.ContainerGet(ctx, anyID, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkContainerTransport(cnr, tc.body.GetContainer())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestGetContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "Get", func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestGetContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protocontainer.GetResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing container in response") + }}, + {name: "empty", body: new(protocontainer.GetResponse_Body), + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing container in response") + }}, + } + // 1. container + type invalidContainerTestcase = struct { + name, msg string + corrupt func(valid *protocontainer.Container) + } + // 1.1 version + ctcs := []invalidContainerTestcase{{name: "version/missing", msg: "missing version", corrupt: func(valid *protocontainer.Container) { + valid.Version = nil + }}} + // 1.2 owner + ctcs = append(ctcs, invalidContainerTestcase{name: "owner/missing", msg: "missing owner", corrupt: func(valid *protocontainer.Container) { + valid.OwnerId = nil + }}) + for _, tc := range invalidUserIDProtoTestcases { + ctcs = append(ctcs, invalidContainerTestcase{ + name: "owner/" + tc.name, msg: "invalid owner: " + tc.msg, + corrupt: func(valid *protocontainer.Container) { tc.corrupt(valid.OwnerId) }, + }) + } + // 1.3 nonce + ctcs = append(ctcs, invalidContainerTestcase{name: "nonce/missing", msg: "missing nonce", corrupt: func(valid *protocontainer.Container) { + valid.Nonce = nil + }}) + for _, tc := range invalidUUIDProtoTestcases { + ctcs = append(ctcs, invalidContainerTestcase{ + name: "nonce/" + tc.name, msg: "invalid nonce: " + tc.msg, + corrupt: func(valid *protocontainer.Container) { valid.Nonce = tc.corrupt(valid.Nonce) }, + }) + } + // 1.4 basic ACL + // 1.5 attributes + for _, tc := range []struct { + name, msg string + attrs []string + }{ + {name: "attributes/empty key", msg: "empty attribute key", + attrs: []string{"k1", "v1", "", "v2", "k3", "v3"}}, + {name: "attributes/empty value", msg: "empty attribute value k2", + attrs: []string{"k1", "v1", "k2", "", "k3", "v3"}}, + {name: "attributes/duplicated", msg: "duplicated attribute k1", + attrs: []string{"k1", "v1", "k2", "v2", "k1", "v3"}}, + {name: "attributes/timestamp/invalid", msg: `invalid attribute value Timestamp: foo (strconv.ParseInt: parsing "foo": invalid syntax)`, + attrs: []string{"k1", "v1", "Timestamp", "foo", "k1", "v3"}}, + } { + require.Zero(t, len(tc.attrs)%2) + as := make([]*protocontainer.Container_Attribute, 0, len(tc.attrs)/2) + for i := range len(tc.attrs) / 2 { + as = append(as, &protocontainer.Container_Attribute{Key: tc.attrs[2*i], Value: tc.attrs[2*i+1]}) + } + ctcs = append(ctcs, invalidContainerTestcase{ + name: "attributes/" + tc.name, msg: tc.msg, + corrupt: func(valid *protocontainer.Container) { valid.Attributes = as }, + }) + } + // 1.6 policy + ctcs = append(ctcs, invalidContainerTestcase{name: "policy/missing", msg: "missing placement policy", corrupt: func(valid *protocontainer.Container) { + valid.PlacementPolicy = nil + }}) + for _, tc := range []struct { + name, msg string + corrupt func(valid *protonetmap.PlacementPolicy) + }{ + {name: "missing replicas", msg: "missing replicas", corrupt: func(valid *protonetmap.PlacementPolicy) { + valid.Replicas = nil + }}, + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "selectors/clause/negative", msg: "invalid selector #1: negative clause -1", corrupt: func(valid *protonetmap.PlacementPolicy) { + // valid.Selectors[1].Clause = -1 + // }}, + // {name: "filters/op/negative", msg: "invalid filter #1: negative op -1", corrupt: func(valid *protonetmap.PlacementPolicy) { + // valid.Filters[1].Op = -1 + // }}, + } { + ctcs = append(ctcs, invalidContainerTestcase{ + name: "policy" + tc.name, msg: "invalid placement policy: " + tc.msg, + corrupt: func(valid *protocontainer.Container) { tc.corrupt(valid.PlacementPolicy) }, + }) + } + + for _, tc := range ctcs { + body := proto.Clone(validFullGetContainerResponseBody).(*protocontainer.GetResponse_Body) + tc.corrupt(body.Container) + tcs = append(tcs, testcase{ + name: "container/" + tc.name, body: body, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid container in response: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newTestGetContainerServer, newTestContainerClient, tcs, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestGetContainerServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestGetContainerServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestGetContainerServer, newDefaultContainerService, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestGetContainerServer, newDefaultContainerService, stat.MethodContainerGet, + nil, nil, func(c *Client) error { + _, err := c.ContainerGet(ctx, anyID, anyValidOpts) + return err }, - { - "set_eacl", - func() error { - return c.ContainerSetEACL(ctx, eacl.Table{}, nil, PrmContainerSetEACL{}) - }, + ) + }) +} + +func TestClient_ContainerList(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmContainerList + anyUser := usertest.ID() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestListContainersServer() + c := newTestContainerClient(t, srv) + + srv.checkOwner(anyUser) + srv.authenticateRequest(c.prm.signer) + _, err := c.ContainerList(ctx, anyUser, PrmContainerList{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestListContainersServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ContainerList(ctx, anyUser, opts) + return err + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.ListResponse_Body + }{ + {name: "nil", body: nil}, + {name: "min", body: validMinListContainersResponseBody}, + {name: "full", body: validFullListContainersResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestListContainersServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + res, err := c.ContainerList(ctx, anyUser, anyValidOpts) + require.NoError(t, err) + mids := tc.body.GetContainerIds() + require.Len(t, res, len(mids)) + for i := range mids { + require.NoError(t, checkContainerIDTransport(res[i], mids[i]), i) + } + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestListContainersServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "List", func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestListContainersServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protocontainer.ListResponse_Body] + var tcs []testcase + // 1. container IDs + type invalidIDsTestcase = struct { + name, msg string + corrupt func(valid []*protorefs.ContainerID) // 3 elements + } + tcsIDs := []invalidIDsTestcase{ + { + name: "nil element", + msg: "invalid length 0", + corrupt: func(valid []*protorefs.ContainerID) { valid[1] = nil }, + }, + } + for _, tc := range invalidContainerIDProtoTestcases { + tcsIDs = append(tcsIDs, invalidIDsTestcase{ + name: "invalid element/" + tc.name, + msg: tc.msg, + corrupt: func(valid []*protorefs.ContainerID) { tc.corrupt(valid[1]) }, + }) + } + for _, tc := range tcsIDs { + ids := make([]*protorefs.ContainerID, len(validProtoContainerIDs)) + for i, id := range validProtoContainerIDs { + ids[i] = proto.Clone(id).(*protorefs.ContainerID) + } + tc.corrupt(ids) + body := &protocontainer.ListResponse_Body{ContainerIds: ids} + tcs = append(tcs, testcase{name: "container IDs/" + tc.name, body: body, assertErr: func(tb testing.TB, err error) { + require.EqualError(t, err, "invalid ID in the response: "+tc.msg) + }}) + } + + testInvalidResponseBodies(t, newTestListContainersServer, newTestContainerClient, tcs, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestListContainersServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestListContainersServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestListContainersServer, newDefaultContainerService, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestListContainersServer, newDefaultContainerService, stat.MethodContainerList, + nil, nil, func(c *Client) error { + _, err := c.ContainerList(ctx, anyUser, anyValidOpts) + return err }, - } + ) + }) +} - for _, test := range tt { - t.Run(test.name, func(t *testing.T) { - require.ErrorIs(t, test.methodCall(), ErrMissingSigner) +func TestClient_ContainerDelete(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmContainerDelete + anyValidSigner := neofscryptotest.Signer().RFC6979 + anyID := cidtest.ID() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestDeleteContainerServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestContainerID(anyID) + srv.authenticateRequestPayload(anyValidSigner) + srv.authenticateRequest(c.prm.signer) + err := c.ContainerDelete(ctx, anyID, anyValidSigner, PrmContainerDelete{}) + require.NoError(t, err) }) - } + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestDeleteContainerServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + return c.ContainerDelete(ctx, anyID, anyValidSigner, opts) + }) + }) + t.Run("precalculated container signature", func(t *testing.T) { + srv := newTestDeleteContainerServer() + c := newTestContainerClient(t, srv) + + var sig neofscrypto.Signature + sig.SetPublicKeyBytes([]byte("any public key")) + sig.SetValue([]byte("any value")) + opts := anyValidOpts + opts.AttachSignature(sig) + + srv.checkRequestDataSignature(sig) + err := c.ContainerDelete(ctx, anyID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestDeleteContainerServer() + c := newTestContainerClient(t, srv) + + st := sessiontest.ContainerSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + err := c.ContainerDelete(ctx, anyID, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.DeleteResponse_Body + }{ + {name: "min", body: validMinDeleteContainerResponseBody}, + {name: "full", body: validFullDeleteContainerResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestDeleteContainerServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + err := c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestDeleteContainerServer, newTestContainerClient, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "Delete", func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestDeleteContainerServer, newTestContainerClient, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + err := c.ContainerDelete(ctx, anyID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("sign ID failure", func(t *testing.T) { + c := newTestContainerClient(t, newTestDeleteContainerServer()) + t.Run("wrong scheme", func(t *testing.T) { + err := c.ContainerDelete(ctx, anyID, neofsecdsa.Signer(neofscryptotest.ECDSAPrivateKey()), anyValidOpts) + require.EqualError(t, err, "write request: rpc failure: rpc error: code = Unknown desc = invalid request: "+ + "invalid body: invalid container ID signature field: invalid signature length 65, should be 64") + }) + t.Run("signer failure", func(t *testing.T) { + err := c.ContainerDelete(ctx, anyID, neofscryptotest.FailSigner(anyValidSigner), anyValidOpts) + require.ErrorContains(t, err, "calculate signature") + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestDeleteContainerServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestDeleteContainerServer, newTestContainerClient, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestDeleteContainerServer, newDefaultContainerService, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestDeleteContainerServer, newDefaultContainerService, stat.MethodContainerDelete, + []testedClientOp{func(c *Client) error { + return c.ContainerDelete(ctx, anyID, nil, anyValidOpts) + }}, []testedClientOp{func(c *Client) error { + return c.ContainerDelete(ctx, anyID, neofscryptotest.FailSigner(anyValidSigner), anyValidOpts) + }}, func(c *Client) error { + return c.ContainerDelete(ctx, anyID, anyValidSigner, anyValidOpts) + }, + ) + }) +} + +func TestClient_ContainerEACL(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmContainerEACL + anyID := cidtest.ID() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestGetEACLServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestContainerID(anyID) + srv.authenticateRequest(c.prm.signer) + _, err := c.ContainerEACL(ctx, anyID, PrmContainerEACL{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestGetEACLServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ContainerEACL(ctx, anyID, opts) + return err + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.GetExtendedACLResponse_Body + }{ + {name: "min", body: validMinEACLResponseBody}, + {name: "full", body: validFullEACLResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetEACLServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + eACL, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkEACLTransport(eACL, tc.body.GetEacl())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestGetEACLServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "GetExtendedACL", func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestGetEACLServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protocontainer.GetExtendedACLResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing eACL field in the response") + }}, + {name: "empty", body: new(protocontainer.GetExtendedACLResponse_Body), assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing eACL field in the response") + }}, + } + // 1. eACL + type invalidEACLTestcase = struct { + name, msg string + corrupt func(valid *protoacl.EACLTable) + } + var etcs []invalidEACLTestcase + // 1.2 container ID + for _, tc := range invalidContainerIDProtoTestcases { + etcs = append(etcs, invalidEACLTestcase{ + name: "container ID/" + tc.name, msg: "invalid container ID: " + tc.msg, + corrupt: func(valid *protoacl.EACLTable) { tc.corrupt(valid.ContainerId) }, + }) + } + // 1.3 records + for _, tc := range []struct { + name, msg string + corrupt func(valid *protoacl.EACLRecord) + }{ + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "op/negative", msg: "negative op -1", corrupt: func(valid *protoacl.EACLRecord) { + // valid.Operation = -1 + // }}, + // {name: "action/negative", msg: "negative action -1", corrupt: func(valid *protoacl.EACLRecord) { + // valid.Action = -1 + // }}, + // {name: "filters/header type/negative", msg: "invalid filter #1: negative header type -1", corrupt: func(valid *protoacl.EACLRecord) { + // valid.Filters = []*protoacl.EACLRecord_Filter{{}, {HeaderType: -1}} + // }}, + // {name: "filters/matcher/negative", msg: "invalid filter #1: negative matcher -1", corrupt: func(valid *protoacl.EACLRecord) { + // valid.Filters = []*protoacl.EACLRecord_Filter{{}, {MatchType: -1}} + // }}, + // {name: "targets/role/negative", msg: "invalid target #1: negative role -1", corrupt: func(valid *protoacl.EACLRecord) { + // valid.Targets = []*protoacl.EACLRecord_Target{{}, {Role: -1}} + // }}, + } { + etcs = append(etcs, invalidEACLTestcase{ + name: "records/" + tc.name, msg: "invalid record #1: " + tc.msg, + corrupt: func(valid *protoacl.EACLTable) { tc.corrupt(valid.Records[1]) }, + }) + } + + for _, tc := range etcs { + body := proto.Clone(validFullEACLResponseBody).(*protocontainer.GetExtendedACLResponse_Body) + tc.corrupt(body.Eacl) + tcs = append(tcs, testcase{name: "eACL/" + tc.name, body: body, assertErr: func(tb testing.TB, err error) { + require.EqualError(t, err, "invalid eACL field in the response: "+tc.msg) + }}) + } + + testInvalidResponseBodies(t, newTestGetEACLServer, newTestContainerClient, tcs, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestGetEACLServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestGetEACLServer, newTestContainerClient, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestGetEACLServer, newDefaultContainerService, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestGetEACLServer, newDefaultContainerService, stat.MethodContainerEACL, + nil, nil, func(c *Client) error { + _, err := c.ContainerEACL(ctx, anyID, anyValidOpts) + return err + }, + ) + }) +} + +func TestClient_ContainerSetEACL(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmContainerSetEACL + anyValidSigner := usertest.User().RFC6979 + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestSetEACLServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestEACL(anyValidEACL) + srv.authenticateRequestPayload(anyValidSigner) + srv.authenticateRequest(c.prm.signer) + err := c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, PrmContainerSetEACL{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestSetEACLServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, opts) + }) + }) + t.Run("precalculated container signature", func(t *testing.T) { + srv := newTestSetEACLServer() + c := newTestContainerClient(t, srv) + + var sig neofscrypto.Signature + sig.SetPublicKeyBytes([]byte("any public key")) + sig.SetValue([]byte("any value")) + opts := anyValidOpts + opts.AttachSignature(sig) + + srv.checkRequestDataSignature(sig) + err := c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestSetEACLServer() + c := newTestContainerClient(t, srv) + + st := sessiontest.ContainerSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + err := c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.SetExtendedACLResponse_Body + }{ + {name: "min", body: validMinSetEACLResponseBody}, + {name: "full", body: validFullSetEACLResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestSetEACLServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + err := c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + require.NoError(t, err) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestSetEACLServer, newTestContainerClient, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "SetExtendedACL", func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestSetEACLServer, newTestContainerClient, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newTestContainerClient(t, newTestDeleteContainerServer()) + t.Run("missing signer", func(t *testing.T) { + err := c.ContainerSetEACL(ctx, anyValidEACL, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + t.Run("missing container ID in eACL", func(t *testing.T) { + eACL := anyValidEACL + eACL.SetCID(cid.ID{}) + err := c.ContainerSetEACL(ctx, eACL, anyValidSigner, anyValidOpts) + require.ErrorIs(t, err, ErrMissingEACLContainer) + }) + }) + t.Run("sign container failure", func(t *testing.T) { + c := newTestContainerClient(t, newTestSetEACLServer()) + t.Run("wrong scheme", func(t *testing.T) { + err := c.ContainerSetEACL(ctx, anyValidEACL, user.NewAutoIDSigner(neofscryptotest.ECDSAPrivateKey()), anyValidOpts) + require.EqualError(t, err, "write request: rpc failure: rpc error: code = Unknown desc = invalid request: "+ + "invalid body: invalid eACL signature field: invalid signature length 65, should be 64") + }) + t.Run("signer failure", func(t *testing.T) { + err := c.ContainerSetEACL(ctx, anyValidEACL, usertest.FailSigner(anyValidSigner), anyValidOpts) + require.ErrorContains(t, err, "calculate signature") + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestSetEACLServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestSetEACLServer, newTestContainerClient, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestSetEACLServer, newDefaultContainerService, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestSetEACLServer, newDefaultContainerService, stat.MethodContainerSetEACL, + []testedClientOp{func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, nil, anyValidOpts) + }}, []testedClientOp{func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, usertest.FailSigner(anyValidSigner), anyValidOpts) + }}, func(c *Client) error { + return c.ContainerSetEACL(ctx, anyValidEACL, anyValidSigner, anyValidOpts) + }, + ) + }) +} + +func TestClient_ContainerAnnounceUsedSpace(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmAnnounceSpace + anyValidAnnouncements := []container.SizeEstimation{containertest.SizeEstimation(), containertest.SizeEstimation()} + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestAnnounceContainerSpaceServer() + c := newTestContainerClient(t, srv) + + srv.checkRequestAnnouncements(anyValidAnnouncements) + srv.authenticateRequest(c.prm.signer) + err := c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, PrmAnnounceSpace{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestAnnounceContainerSpaceServer, newTestContainerClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, opts) + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protocontainer.AnnounceUsedSpaceResponse_Body + }{ + {name: "min", body: validMinUsedSpaceResponseBody}, + {name: "full", body: validFullUsedSpaceResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestAnnounceContainerSpaceServer() + c := newTestContainerClient(t, srv) + + srv.respondWithBody(tc.body) + err := c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, PrmAnnounceSpace{}) + require.NoError(t, err) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestAnnounceContainerSpaceServer, newTestContainerClient, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "container.ContainerService", "AnnounceUsedSpace", func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestAnnounceContainerSpaceServer, newTestContainerClient, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + t.Run("missing announcements", func(t *testing.T) { + c := newClient(t) + err := c.ContainerAnnounceUsedSpace(ctx, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingAnnouncements) + err = c.ContainerAnnounceUsedSpace(ctx, []container.SizeEstimation{}, anyValidOpts) + require.ErrorIs(t, err, ErrMissingAnnouncements) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestAnnounceContainerSpaceServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestAnnounceContainerSpaceServer, newTestContainerClient, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestAnnounceContainerSpaceServer, newDefaultContainerService, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestAnnounceContainerSpaceServer, newDefaultContainerService, stat.MethodContainerAnnounceUsedSpace, + nil, []testedClientOp{func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, nil, anyValidOpts) + }}, func(c *Client) error { + return c.ContainerAnnounceUsedSpace(ctx, anyValidAnnouncements, anyValidOpts) + }, + ) }) } diff --git a/client/crypto_test.go b/client/crypto_test.go new file mode 100644 index 00000000..f5f74817 --- /dev/null +++ b/client/crypto_test.go @@ -0,0 +1,189 @@ +package client + +import ( + "bytes" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "math/big" + + "github.com/nspcc-dev/neo-go/pkg/crypto/keys" + "github.com/nspcc-dev/neo-go/pkg/io" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + apigrpc "github.com/nspcc-dev/neofs-api-go/v2/rpc/grpc" + neofscrypto "github.com/nspcc-dev/neofs-sdk-go/crypto" +) + +var p256Curve = elliptic.P256() + +type signedMessageV2 interface { + FromGRPCMessage(apigrpc.Message) error + StableMarshal([]byte) []byte +} + +// represents tested NeoFS authentication credentials. +type authCredentials struct { + scheme protorefs.SignatureScheme + pub []byte +} + +func authCredentialsFromSigner(s neofscrypto.Signer) authCredentials { + var res authCredentials + res.pub = neofscrypto.PublicKeyBytes(s.Public()) + switch scheme := s.Scheme(); scheme { + default: + res.scheme = protorefs.SignatureScheme(scheme) + case neofscrypto.ECDSA_SHA512: + res.scheme = protorefs.SignatureScheme_ECDSA_SHA512 + case neofscrypto.ECDSA_DETERMINISTIC_SHA256: + res.scheme = protorefs.SignatureScheme_ECDSA_RFC6979_SHA256 + case neofscrypto.ECDSA_WALLETCONNECT: + res.scheme = protorefs.SignatureScheme_ECDSA_RFC6979_SHA256_WALLET_CONNECT + } + return res +} + +func checkAuthCredendials(exp, act authCredentials) error { + if exp.scheme != act.scheme { + return fmt.Errorf("unexpected scheme (client: %v, message: %v)", exp.scheme, act.scheme) + } + if !bytes.Equal(exp.pub, act.pub) { + return fmt.Errorf("unexpected public key (client: %x, message: %x)", exp.pub, act.pub) + } + return nil +} + +func signMessage[MESSAGE apigrpc.Message, MESSAGEV2 any, MESSAGEV2PTR interface { + *MESSAGEV2 + signedMessageV2 +}](key ecdsa.PrivateKey, m MESSAGE, _ MESSAGEV2PTR) (*protorefs.Signature, error) { + mV2 := MESSAGEV2PTR(new(MESSAGEV2)) + if err := mV2.FromGRPCMessage(m); err != nil { + panic(err) + } + b := mV2.StableMarshal(nil) + h := sha512.Sum512(b) + r, s, err := ecdsa.Sign(rand.Reader, &key, h[:]) + if err != nil { + return nil, fmt.Errorf("sign ECDSA: %w", err) + } + sig := make([]byte, 1+64) + sig[0] = 4 + r.FillBytes(sig[1:33]) + s.FillBytes(sig[33:]) + return &protorefs.Signature{Key: elliptic.MarshalCompressed(p256Curve, key.X, key.Y), Sign: sig}, nil +} + +func verifyMessageSignature[MESSAGE apigrpc.Message, MESSAGEV2 any, MESSAGEV2PTR interface { + *MESSAGEV2 + signedMessageV2 +}](m MESSAGE, s *protorefs.Signature, expectedCreds *authCredentials) error { + mV2 := MESSAGEV2PTR(new(MESSAGEV2)) + if err := mV2.FromGRPCMessage(m); err != nil { + panic(err) + } + return verifyDataSignature(mV2.StableMarshal(nil), s, expectedCreds) +} + +func verifyDataSignature(data []byte, s *protorefs.Signature, expectedCreds *authCredentials) error { + if s == nil { + return errors.New("missing") + } + creds := authCredentials{scheme: s.Scheme, pub: s.Key} + if err := verifyProtoSignature(creds, s.Sign, data); err != nil { + return err + } + if expectedCreds != nil { + if err := checkAuthCredendials(*expectedCreds, creds); err != nil { + return fmt.Errorf("unexpected credendials: %w", err) + } + } + return nil +} + +func verifyProtoSignature(creds authCredentials, sig, data []byte) error { + switch creds.scheme { + default: + return fmt.Errorf("unsupported scheme: %v", creds.scheme) + case protorefs.SignatureScheme_ECDSA_SHA512: + if len(sig) != keys.SignatureLen+1 { + return fmt.Errorf("invalid signature length %d, should be %d", len(sig), keys.SignatureLen+1) + } + r, s := unmarshalECP256Point([keys.SignatureLen + 1]byte(sig)) + if r == nil { + return fmt.Errorf("invalid signature format %x", sig) + } + x, y := elliptic.UnmarshalCompressed(p256Curve, creds.pub) + if x == nil { + return fmt.Errorf("invalid public key: %x", sig) + } + h := sha512.Sum512(data) + if !ecdsa.Verify(&ecdsa.PublicKey{Curve: p256Curve, X: x, Y: y}, h[:], r, s) { + return errors.New("signature mismatch") + } + case protorefs.SignatureScheme_ECDSA_RFC6979_SHA256: + if len(sig) != keys.SignatureLen { + return fmt.Errorf("invalid signature length %d, should be %d", len(sig), keys.SignatureLen) + } + x, y := elliptic.UnmarshalCompressed(p256Curve, creds.pub) + if x == nil { + return fmt.Errorf("invalid signature's public key: %x", sig) + } + h := sha256.Sum256(data) + r, s := ecP256PointFromBytes([keys.SignatureLen]byte(sig)) + if !ecdsa.Verify(&ecdsa.PublicKey{Curve: p256Curve, X: x, Y: y}, h[:], r, s) { + return errors.New("signature mismatch") + } + case protorefs.SignatureScheme_ECDSA_RFC6979_SHA256_WALLET_CONNECT: + const saltLen = 16 + if len(sig) != keys.SignatureLen+saltLen { + return fmt.Errorf("invalid signature length %d, should be %d", + len(sig), keys.SignatureLen) + } + x, y := elliptic.UnmarshalCompressed(p256Curve, creds.pub) + if x == nil { + return fmt.Errorf("invalid public key: %x", creds.pub) + } + + b64 := make([]byte, base64.StdEncoding.EncodedLen(len(data))) + base64.StdEncoding.Encode(b64, data) + payloadLen := 2*saltLen + len(b64) + b := make([]byte, 4+io.GetVarSize(payloadLen)+payloadLen+2) + n := copy(b, []byte{0x01, 0x00, 0x01, 0xf0}) + n += io.PutVarUint(b[n:], uint64(payloadLen)) + n += hex.Encode(b[n:], sig[keys.SignatureLen:]) + n += copy(b[n:], b64) + copy(b[n:], []byte{0x00, 0x00}) + + h := sha256.Sum256(b) + r, s := ecP256PointFromBytes([keys.SignatureLen]byte(sig)) + if !ecdsa.Verify(&ecdsa.PublicKey{Curve: p256Curve, X: x, Y: y}, h[:], r, s) { + return errors.New("signature mismatch") + } + } + return nil +} + +func ecP256PointFromBytes(b [keys.SignatureLen]byte) (*big.Int, *big.Int) { + return new(big.Int).SetBytes(b[:32]), new(big.Int).SetBytes(b[32:]) +} + +// decodes a serialized [elliptic.P256] point. It is an error if the point is +// not in uncompressed form, or is the point at infinity. On error, x = nil. +func unmarshalECP256Point(b [keys.SignatureLen + 1]byte) (x, y *big.Int) { + if b[0] != 4 { // uncompressed form + return + } + p := p256Curve.Params().P + x, y = ecP256PointFromBytes([keys.SignatureLen]byte(b[1:])) + if x.Cmp(p) >= 0 || y.Cmp(p) >= 0 { + return nil, nil + } + return x, y +} diff --git a/client/messages_test.go b/client/messages_test.go new file mode 100644 index 00000000..ff29b5b2 --- /dev/null +++ b/client/messages_test.go @@ -0,0 +1,1899 @@ +package client + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "math" + "math/rand" + "strconv" + "strings" + + "github.com/google/uuid" + protoaccounting "github.com/nspcc-dev/neofs-api-go/v2/accounting/grpc" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl/grpc" + apicontainer "github.com/nspcc-dev/neofs-api-go/v2/container" + protocontainer "github.com/nspcc-dev/neofs-api-go/v2/container/grpc" + apinetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" + protonetmap "github.com/nspcc-dev/neofs-api-go/v2/netmap/grpc" + protoobject "github.com/nspcc-dev/neofs-api-go/v2/object/grpc" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + protoreputation "github.com/nspcc-dev/neofs-api-go/v2/reputation/grpc" + protosession "github.com/nspcc-dev/neofs-api-go/v2/session/grpc" + "github.com/nspcc-dev/neofs-sdk-go/accounting" + "github.com/nspcc-dev/neofs-sdk-go/bearer" + "github.com/nspcc-dev/neofs-sdk-go/checksum" + "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" + 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/eacl" + "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" + "github.com/nspcc-dev/neofs-sdk-go/reputation" + "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/nspcc-dev/neofs-sdk-go/user" + "github.com/nspcc-dev/neofs-sdk-go/version" + "google.golang.org/protobuf/proto" +) + +/* +Various NeoFS protocol messages. Any message (incl. each group element) must be +cloned via [proto.Clone] for the network transmission. +*/ + +// Cross-service. +var ( + // set of correct object IDs. + validProtoObjectIDs = []*protorefs.ObjectID{ + {Value: []byte{218, 203, 9, 142, 129, 249, 13, 159, 198, 60, 153, 148, 70, 216, 50, 17, 15, 87, 47, 104, 143, 0, 187, 211, 120, 105, 250, 170, 220, 36, 108, 171}}, + {Value: []byte{28, 74, 243, 168, 65, 185, 194, 228, 239, 47, 76, 99, 131, 154, 18, 4, 91, 243, 28, 47, 183, 252, 203, 17, 32, 194, 193, 55, 213, 43, 15, 157}}, + {Value: []byte{64, 228, 234, 193, 115, 188, 136, 160, 127, 238, 221, 164, 4, 75, 158, 61, 82, 183, 241, 130, 189, 122, 192, 191, 244, 181, 98, 91, 179, 36, 197, 47}}, + } + // correct object address with all fields. + validFullProtoObjectAddress = &protorefs.Address{ + ContainerId: proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID), + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + } + // correct signature with required fields only. + validMinProtoSignature = &protorefs.Signature{} + // correct signature with all fields. + validFullProtoSignature = &protorefs.Signature{ + Key: []byte("any_key"), + Sign: []byte("any_signature"), + Scheme: protorefs.SignatureScheme(rand.Int31()), + } +) + +// Accounting service. +var ( + // correct balance with required fields only. + validMinProtoBalance = &protoaccounting.Decimal{} + // correct balance with all fields. + validFullProtoBalance = &protoaccounting.Decimal{Value: 1609926665709559552, Precision: 2322521745} + // correct AccountingService.Balance response payload with required fields only. + validMinBalanceResponseBody = &protoaccounting.BalanceResponse_Body{ + Balance: proto.Clone(validMinProtoBalance).(*protoaccounting.Decimal), + } + // correct AccountingService.Balance response payload with all fields. + validFullBalanceResponseBody = &protoaccounting.BalanceResponse_Body{ + Balance: proto.Clone(validFullProtoBalance).(*protoaccounting.Decimal), + } +) + +// Container service. +var ( + // correct container with required fields only. + validMinProtoContainer = &protocontainer.Container{ + Version: proto.Clone(validMinProtoVersion).(*protorefs.Version), + OwnerId: &protorefs.OwnerID{Value: []byte{53, 233, 31, 174, 37, 64, 241, 22, 182, 130, 7, 210, 222, 150, 85, 18, 106, 4, + 253, 122, 191, 90, 168, 187, 245}}, + Nonce: []byte{207, 5, 57, 28, 224, 103, 76, 207, 133, 186, 108, 96, 185, 52, 37, 205}, + PlacementPolicy: &protonetmap.PlacementPolicy{ + Replicas: make([]*protonetmap.Replica, 1), + }, + } + // correct container with all fields. + validFullProtoContainer = &protocontainer.Container{ + Version: proto.Clone(validFullProtoVersion).(*protorefs.Version), + OwnerId: proto.Clone(validMinProtoContainer.OwnerId).(*protorefs.OwnerID), + Nonce: bytes.Clone(validMinProtoContainer.Nonce), + BasicAcl: 1043832770, + Attributes: []*protocontainer.Container_Attribute{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + {Key: "Name", Value: "any container name"}, + {Key: "Timestamp", Value: "1732577694"}, + {Key: "__NEOFS__NAME", Value: "any domain name"}, + {Key: "__NEOFS__ZONE", Value: "any domain zone"}, + {Key: "__NEOFS__DISABLE_HOMOMORPHIC_HASHING", Value: "true"}, + }, + PlacementPolicy: &protonetmap.PlacementPolicy{ + Replicas: []*protonetmap.Replica{ + {Count: 3060437, Selector: "selector1"}, + {Count: 156936495, Selector: "selector2"}, + }, + ContainerBackupFactor: 920231904, + Selectors: []*protonetmap.Selector{ + {Name: "selector1", Count: 1663184999, Clause: 1, Attribute: "attribute1", Filter: "filter1"}, + {Name: "selector2", Count: 2649065896, Clause: 2, Attribute: "attribute2", Filter: "filter2"}, + {Name: "selector_max", Count: 2649065896, Clause: math.MaxInt32, Attribute: "attribute_max", Filter: "filter_max"}, + }, + Filters: []*protonetmap.Filter{ + {Name: "filter1", Key: "key1", Op: 0, Value: "value1", Filters: []*protonetmap.Filter{ + {}, + {}, + }}, + {Op: 1}, + {Op: 2}, + {Op: 3}, + {Op: 4}, + {Op: 5}, + {Op: 6}, + {Op: 7}, + {Op: 8}, + {Op: math.MaxInt32}, + }, + SubnetId: &protorefs.SubnetID{Value: 987533317}, + }, + } + // correct eACL with required fields only. + validMinEACL = &protoacl.EACLTable{} + // correct eACL with required all fields. + validFullEACL = &protoacl.EACLTable{ + Version: &protorefs.Version{Major: 538919038, Minor: 3957317479}, + ContainerId: proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID), + Records: []*protoacl.EACLRecord{ + {}, + {Operation: 1, Action: 1}, + {Operation: 2, Action: 2}, + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {Operation: 3, Action: 3}, + // {Operation: 4, Action: math.MaxInt32}, + {Operation: 5}, + {Operation: 6}, + {Operation: 7}, + // {Operation: math.MaxInt32}, + {Filters: []*protoacl.EACLRecord_Filter{ + {HeaderType: 0, MatchType: 0, Key: "key1", Value: "val1"}, + {HeaderType: 1, MatchType: 1}, + {HeaderType: 2, MatchType: 2}, + {HeaderType: 3, MatchType: 3}, + // {HeaderType: math.MaxInt32, MatchType: 4}, + {MatchType: 5}, + {MatchType: 6}, + {MatchType: 7}, + // {MatchType: math.MaxInt32}, + }}, + {Targets: []*protoacl.EACLRecord_Target{ + {Role: 0, Keys: [][]byte{[]byte("key1"), []byte("key2")}}, + {Role: 1}, + {Role: 2}, + {Role: 3}, + // {Role: math.MaxInt32}, + }}, + }, + } + // correct ContainerService.Put response payload with required fields only. + validMinPutContainerResponseBody = &protocontainer.PutResponse_Body{ + ContainerId: proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID), + } + // correct ContainerService.Put response payload with all fields. + validFullPutContainerResponseBody = &protocontainer.PutResponse_Body{ + ContainerId: proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID), + } + // correct ContainerService.Get response payload with required fields only. + validMinGetContainerResponseBody = &protocontainer.GetResponse_Body{ + Container: proto.Clone(validMinProtoContainer).(*protocontainer.Container), + } + // correct ContainerService.Get response payload with all fields. + validFullGetContainerResponseBody = &protocontainer.GetResponse_Body{ + Container: proto.Clone(validFullProtoContainer).(*protocontainer.Container), + Signature: &protorefs.SignatureRFC6979{Key: []byte("any_key"), Sign: []byte("any_signature")}, + SessionToken: &protosession.SessionToken{ + Body: &protosession.SessionToken_Body{ + Id: []byte("any_ID"), + OwnerId: &protorefs.OwnerID{Value: []byte("any_user")}, + Lifetime: &protosession.SessionToken_Body_TokenLifetime{Exp: 1, Nbf: 2, Iat: 3}, + SessionKey: []byte("any_session_key"), + }, + Signature: proto.Clone(validFullProtoSignature).(*protorefs.Signature), + }, + } + // correct ContainerService.List response payload with required fields only. + validMinListContainersResponseBody = (*protocontainer.ListResponse_Body)(nil) + // correct ContainerService.List response payload with all fields. + validFullListContainersResponseBody = &protocontainer.ListResponse_Body{ + ContainerIds: []*protorefs.ContainerID{ + proto.Clone(validProtoContainerIDs[0]).(*protorefs.ContainerID), + proto.Clone(validProtoContainerIDs[1]).(*protorefs.ContainerID), + proto.Clone(validProtoContainerIDs[2]).(*protorefs.ContainerID), + }, + } + // correct ContainerService.Delete response payload with required fields only. + validMinDeleteContainerResponseBody = (*protocontainer.DeleteResponse_Body)(nil) + // correct ContainerService.Delete response payload with all fields. + validFullDeleteContainerResponseBody = &protocontainer.DeleteResponse_Body{} + // correct ContainerService.GetExtendedACL response payload with required fields only. + validMinEACLResponseBody = &protocontainer.GetExtendedACLResponse_Body{ + Eacl: proto.Clone(validMinEACL).(*protoacl.EACLTable), + } + // correct ContainerService.GetExtendedACL response payload with all fields. + validFullEACLResponseBody = &protocontainer.GetExtendedACLResponse_Body{ + Eacl: proto.Clone(validFullEACL).(*protoacl.EACLTable), + Signature: proto.Clone(validFullGetContainerResponseBody.Signature).(*protorefs.SignatureRFC6979), + SessionToken: proto.Clone(validFullGetContainerResponseBody.SessionToken).(*protosession.SessionToken), + } + // correct ContainerService.SetExtendedACL response payload with required fields only. + validMinSetEACLResponseBody = (*protocontainer.SetExtendedACLResponse_Body)(nil) + // correct ContainerService.SetExtendedACL response payload with all fields. + validFullSetEACLResponseBody = &protocontainer.SetExtendedACLResponse_Body{} + // correct ContainerService.AnnounceUsedSpace response payload with required fields only. + validMinUsedSpaceResponseBody = (*protocontainer.AnnounceUsedSpaceResponse_Body)(nil) + // correct ContainerService.AnnounceUsedSpace response payload with all fields. + validFullUsedSpaceResponseBody = &protocontainer.AnnounceUsedSpaceResponse_Body{} +) + +// Netmap service. +var ( + // correct node info with required fields only. + validMinNodeInfo = &protonetmap.NodeInfo{ + PublicKey: []byte("any_pub"), + Addresses: []string{"any_endpoint"}, + } + // correct node info with all fields. + validFullNodeInfo = newValidFullNodeInfo(0) + // correct network map with required fields only. + validMinProtoNetmap = &protonetmap.Netmap{} + // correct network map with all fields. + validFullProtoNetmap = &protonetmap.Netmap{ + Epoch: 17416815529850981458, + Nodes: []*protonetmap.NodeInfo{newValidFullNodeInfo(0), newValidFullNodeInfo(1), newValidFullNodeInfo(2)}, + } + // correct network info with required fields only. + validMinProtoNetInfo = &protonetmap.NetworkInfo{ + NetworkConfig: &protonetmap.NetworkConfig{ + Parameters: []*protonetmap.NetworkConfig_Parameter{ + {Value: []byte("any")}, + }, + }, + } + // correct network info with all fields. + validFullProtoNetInfo = &protonetmap.NetworkInfo{ + CurrentEpoch: 17416815529850981458, + MagicNumber: 8576993077569092248, + MsPerBlock: 9059417785180743518, + NetworkConfig: &protonetmap.NetworkConfig{ + Parameters: []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("k1"), Value: []byte("v1")}, + {Key: []byte("k2"), Value: []byte("v2")}, + {Key: []byte("AuditFee"), Value: []byte{148, 103, 221, 13, 230, 131, 76, 41}}, // 2975898477883385748 + {Key: []byte("BasicIncomeRate"), Value: []byte{75, 10, 132, 219, 93, 88, 10, 159}}, // 11460069361935714891 + {Key: []byte("ContainerFee"), Value: []byte{138, 229, 49, 0, 30, 129, 67, 130}}, // 9386488014222517642 + {Key: []byte("ContainerAliasFee"), Value: []byte{138, 229, 49, 0, 30, 129, 67, 130}}, // 9386488014222517642 + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/653 + // {Key: []byte("EigenTrustAlpha"), Value: []byte("5.551764501727871")}, + {Key: []byte("EigenTrustIterations"), Value: []byte{130, 92, 74, 224, 95, 59, 146, 249}}, // 17983501545014713474 + {Key: []byte("EpochDuration"), Value: []byte{161, 231, 2, 119, 184, 52, 66, 217}}, // 15655133221568571297 + {Key: []byte("HomomorphicHashingDisabled"), Value: []byte("any")}, + {Key: []byte("InnerRingCandidateFee"), Value: []byte{0, 11, 236, 200, 112, 164, 1, 217}}, // 15636960185521277696 + {Key: []byte("MaintenanceModeAllowed"), Value: []byte("any")}, + {Key: []byte("MaxObjectSize"), Value: []byte{109, 133, 46, 32, 118, 66, 240, 72}}, // 5255773840254862701 + {Key: []byte("WithdrawFee"), Value: []byte{216, 63, 55, 77, 56, 24, 171, 101}}, // 7325975848940945368 + }, + }, + } + // correct NetmapService.LocalNodeInfo response payload with required fields only. + validMinNodeInfoResponseBody = &protonetmap.LocalNodeInfoResponse_Body{ + Version: proto.Clone(validMinProtoVersion).(*protorefs.Version), + NodeInfo: proto.Clone(validMinNodeInfo).(*protonetmap.NodeInfo), + } + // correct NetmapService.LocalNodeInfo response payload with all fields. + validFullNodeInfoResponseBody = &protonetmap.LocalNodeInfoResponse_Body{ + Version: proto.Clone(validFullProtoVersion).(*protorefs.Version), + NodeInfo: proto.Clone(validFullNodeInfo).(*protonetmap.NodeInfo), + } + // correct NetmapService.NetmapSnapshot response payload with required fields only. + validMinNetmapResponseBody = &protonetmap.NetmapSnapshotResponse_Body{ + Netmap: proto.Clone(validMinProtoNetmap).(*protonetmap.Netmap), + } + // correct NetmapService.NetmapSnapshot response payload with all fields. + validFullNetmapResponseBody = &protonetmap.NetmapSnapshotResponse_Body{ + Netmap: proto.Clone(validFullProtoNetmap).(*protonetmap.Netmap), + } + // correct NetmapService.NetworkInfo response payload with required fields only. + validMinNetInfoResponseBody = &protonetmap.NetworkInfoResponse_Body{ + NetworkInfo: proto.Clone(validMinProtoNetInfo).(*protonetmap.NetworkInfo), + } + // correct NetmapService.NetworkInfo response payload with all fields. + validFullNetInfoResponseBody = &protonetmap.NetworkInfoResponse_Body{ + NetworkInfo: proto.Clone(validFullProtoNetInfo).(*protonetmap.NetworkInfo), + } +) + +// Object service. +var ( + // valid object header with required fields only. + validMinObjectHeader = &protoobject.Header{} + // correct object header with all fields. + validFullObjectHeader = &protoobject.Header{ + Version: &protorefs.Version{Major: 2551725017, Minor: 2526948189}, + ContainerId: &protorefs.ContainerID{Value: []byte{80, 212, 0, 200, 84, 144, 252, 77, 205, 169, 28, 36, 61, 25, 4, 32, + 182, 161, 107, 148, 193, 86, 1, 252, 224, 65, 204, 176, 27, 189, 63, 198}}, + OwnerId: &protorefs.OwnerID{Value: []byte{53, 36, 208, 131, 238, 151, 230, 27, 245, 87, 156, 55, 90, 144, 192, 82, + 205, 97, 243, 240, 98, 0, 4, 202, 190}}, + CreationEpoch: 535166283637641128, + PayloadLength: 7493095166286485665, + PayloadHash: &protorefs.Checksum{Type: 745469659, Sum: []byte("payload_checksum")}, + ObjectType: 1336146323, + HomomorphicHash: &protorefs.Checksum{Type: 56973732, Sum: []byte("homomorphic_checksum")}, + SessionToken: &protosession.SessionToken{ + Body: &protosession.SessionToken_Body{ + Id: []byte{219, 53, 231, 42, 56, 82, 65, 196, 175, 34, 22, 36, 170, 248, 64, 45}, + OwnerId: &protorefs.OwnerID{Value: []byte{53, 79, 105, 50, 97, 214, 227, 217, 243, 111, 24, 28, 164, 116, 174, 36, + 217, 111, 165, 197, 109, 225, 168, 165, 133}}, + Lifetime: &protosession.SessionToken_Body_TokenLifetime{ + Exp: 2306780414485650416, Nbf: 17091941679101563337, Iat: 10428481937388069414, + }, + SessionKey: []byte{3, 47, 174, 204, 218, 71, 223, 103, 27, 142, 185, 141, 190, 177, 199, 235, 100, 168, 68, 216, 253, + 4, 124, 162, 237, 187, 141, 28, 109, 121, 22, 77, 77}, + Context: &protosession.SessionToken_Body_Object{ + Object: &protosession.ObjectSessionContext{ + // TODO: must work with big verb (e.g. 1849442930) after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + Verb: 3, + Target: &protosession.ObjectSessionContext_Target{ + Container: &protorefs.ContainerID{Value: []byte{43, 155, 220, 2, 70, 86, 249, 4, 211, 12, 14, 152, 15, 165, + 141, 240, 15, 199, 82, 245, 32, 86, 49, 60, 3, 15, 235, 107, 227, 21, 201, 226}}, + Objects: []*protorefs.ObjectID{ + {Value: []byte{168, 182, 85, 123, 227, 177, 127, 228, 62, 192, 73, 61, 38, 102, 136, 138, 20, 155, 175, + 89, 95, 241, 200, 148, 156, 142, 215, 78, 34, 223, 238, 62}}, + {Value: []byte{104, 187, 144, 239, 201, 242, 213, 136, 32, 1, 74, 125, 157, 143, 114, 57, 57, 182, 218, + 172, 126, 69, 157, 62, 119, 45, 116, 152, 225, 222, 16, 243}}, + {Value: []byte{106, 193, 15, 88, 111, 154, 77, 182, 11, 190, 3, 154, 84, 249, 1, 165, 220, 23, 234, 101, + 210, 105, 114, 230, 251, 102, 164, 142, 128, 6, 35, 131}}, + }, + }}, + }, + }, + Signature: &protorefs.Signature{Key: []byte("any_public_key"), Sign: []byte("any_signature"), Scheme: 343874216}, + }, + Attributes: []*protoobject.Header_Attribute{ + {Key: "k1", Value: "v1"}, + {Key: "k2", Value: "v2"}, + {Key: "__NEOFS__EXPIRATION_EPOCH", Value: "15108052785492221606"}, + }, + Split: &protoobject.Header_Split{ + Parent: &protorefs.ObjectID{Value: []byte{136, 16, 11, 39, 44, 190, 117, 150, 28, 108, 97, 182, 137, 71, 116, 141, + 39, 3, 240, 58, 177, 143, 185, 171, 139, 189, 87, 178, 168, 91, 108, 49}}, + Previous: &protorefs.ObjectID{Value: []byte{70, 184, 70, 223, 213, 136, 169, 221, 63, 103, 244, 43, 109, 226, 9, + 243, 154, 177, 74, 6, 128, 100, 237, 126, 81, 203, 210, 206, 97, 16, 12, 145}}, + ParentSignature: &protorefs.Signature{Key: []byte("any_parent_key"), Sign: []byte("any_parent_signature"), Scheme: 343874216}, + ParentHeader: &protoobject.Header{ + Version: &protorefs.Version{Major: 1650885558, Minor: 1215827697}, + ContainerId: &protorefs.ContainerID{Value: []byte{180, 73, 166, 38, 121, 174, 19, 54, 183, 40, 110, 62, 221, 124, 243, + 108, 222, 97, 21, 41, 154, 159, 92, 217, 99, 136, 75, 2, 71, 243, 230, 33}}, + OwnerId: &protorefs.OwnerID{Value: []byte{53, 147, 252, 32, 131, 247, 225, 223, 238, 111, 227, 232, 235, 86, 220, 225, 95, 68, 242, 143, 250, 19, 209, 207, 137}}, + CreationEpoch: 13908636632389871906, + PayloadLength: 9446280261481989231, + PayloadHash: &protorefs.Checksum{Type: 1764227836, Sum: []byte("parent_payload_checksum")}, + ObjectType: 950142306, + HomomorphicHash: &protorefs.Checksum{Type: 2086030953, Sum: []byte("parent_homomorphic_checksum")}, + Attributes: []*protoobject.Header_Attribute{ + {Key: "parent_k1", Value: "parent_v1"}, + {Key: "parent_k2", Value: "parent_v2"}, + {Key: "__NEOFS__EXPIRATION_EPOCH", Value: "5546294308840974481"}, + }, + }, + Children: []*protorefs.ObjectID{ + {Value: []byte{62, 123, 103, 12, 105, 55, 53, 123, 78, 108, 241, 217, 90, 252, 200, 18, 237, 194, 154, 76, 101, 254, + 10, 80, 245, 97, 195, 227, 184, 247, 23, 2}}, + {Value: []byte{127, 105, 152, 33, 27, 219, 170, 156, 77, 47, 133, 82, 253, 100, 203, 229, 12, 231, 39, 223, 155, 199, + 124, 164, 78, 208, 243, 23, 220, 13, 101, 91}}, + {Value: []byte{232, 111, 102, 246, 179, 18, 108, 53, 36, 150, 64, 248, 108, 100, 161, 85, 82, 27, 39, 90, 97, 184, 146, + 230, 139, 162, 43, 171, 65, 184, 255, 238}}, + }, + SplitId: []byte{161, 132, 100, 12, 194, 100, 65, 179, 165, 156, 156, 2, 173, 208, 33, 45}, + First: &protorefs.ObjectID{Value: []byte{43, 82, 110, 195, 252, 103, 56, 184, 106, 229, 94, 136, 213, 63, 133, + 47, 174, 125, 1, 181, 102, 158, 110, 102, 115, 41, 204, 232, 44, 176, 233, 78}}, + }, + } + // correct split info with required fields only. + validMinSplitInfo = &protoobject.SplitInfo{ + LastPart: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + } + // correct split info with all fields. + validFullSplitInfo = &protoobject.SplitInfo{ + SplitId: []byte{181, 76, 71, 204, 73, 230, 65, 146, 156, 76, 98, 233, 55, 162, 45, 223}, + LastPart: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + Link: proto.Clone(validProtoObjectIDs[1]).(*protorefs.ObjectID), + FirstPart: proto.Clone(validProtoObjectIDs[2]).(*protorefs.ObjectID), + } + // correct ObjectService.Put response payload with required fields only. + validMinPutObjectResponseBody = &protoobject.PutResponse_Body{ + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + } + // correct ObjectService.Put response payload with all fields. + validFullPutObjectResponseBody = &protoobject.PutResponse_Body{ + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + } + // correct ObjectService.Delete response payload with required fields only. + validMinDeleteObjectResponseBody = &protoobject.DeleteResponse_Body{ + Tombstone: &protorefs.Address{ + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + }, + } + // correct ObjectService.Delete response payload with all fields. + validFullDeleteObjectResponseBody = &protoobject.DeleteResponse_Body{ + Tombstone: proto.Clone(validFullProtoObjectAddress).(*protorefs.Address), + } + // correct ObjectService.GetRangeHash response payload with required fields only. + validMinObjectHashResponseBody = &protoobject.GetRangeHashResponse_Body{ + HashList: [][]byte{[]byte("one")}, + } + // correct ObjectService.GetRangeHash response payload with all fields. + validFullObjectHashResponseBody = &protoobject.GetRangeHashResponse_Body{ + Type: protorefs.ChecksumType(rand.Int31()), + HashList: [][]byte{[]byte("one"), []byte("two")}, + } + // correct ObjectService.Head split info response payload with required fields only. + validMinObjectSplitInfoHeadResponseBody = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validMinSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.Head split info response payload with all fields. + validFullObjectSplitInfoHeadResponseBody = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validFullSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.Head response payload with required fields only. + validMinObjectHeadResponseBody = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: proto.Clone(validMinObjectHeader).(*protoobject.Header), + Signature: proto.Clone(validMinProtoSignature).(*protorefs.Signature), + }, + }, + } + // correct ObjectService.Head response payload with all fields. + validFullObjectHeadResponseBody = &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: proto.Clone(validFullObjectHeader).(*protoobject.Header), + Signature: proto.Clone(validFullProtoSignature).(*protorefs.Signature), + }, + }, + } + // correct ObjectService.Get heading response payload with required fields only. + validMinHeadingObjectGetResponseBody = &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{ + Init: &protoobject.GetResponse_Body_Init{ + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + Signature: proto.Clone(validMinProtoSignature).(*protorefs.Signature), + Header: proto.Clone(validMinObjectHeader).(*protoobject.Header), + }, + }, + } + // correct ObjectService.Get heading response payload with all fields. + validFullHeadingObjectGetResponseBody = &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{ + Init: &protoobject.GetResponse_Body_Init{ + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + Signature: proto.Clone(validFullProtoSignature).(*protorefs.Signature), + Header: proto.Clone(validFullObjectHeader).(*protoobject.Header), + }, + }, + } + // correct ObjectService.Get chunk response payload with all fields. + validFullChunkObjectGetResponseBody = &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Chunk{ + Chunk: []byte("Hello, world!"), + }, + } + // correct ObjectService.Get split info response payload with required fields only. + validMinObjectSplitInfoGetResponseBody = &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validMinSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.Get split info response payload with all fields. + validFullObjectSplitInfoGetResponseBody = &protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validFullSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.GetRange chunk response payload with all fields. + validFullChunkObjectRangeResponseBody = &protoobject.GetRangeResponse_Body{ + RangePart: &protoobject.GetRangeResponse_Body_Chunk{ + Chunk: []byte("Hello, world!"), + }, + } + // correct ObjectService.GetRange split info response payload with required fields only. + validMinObjectSplitInfoRangeResponseBody = &protoobject.GetRangeResponse_Body{ + RangePart: &protoobject.GetRangeResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validMinSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.GetRange split info response payload with all fields. + validFullObjectSplitInfoRangeResponseBody = &protoobject.GetRangeResponse_Body{ + RangePart: &protoobject.GetRangeResponse_Body_SplitInfo{ + SplitInfo: proto.Clone(validFullSplitInfo).(*protoobject.SplitInfo), + }, + } + // correct ObjectService.Search response payload with required fields only. + validMinSearchResponseBody = &protoobject.SearchResponse_Body{} + // correct ObjectService.Search response payload with all fields. + validFullSearchResponseBody = &protoobject.SearchResponse_Body{ + IdList: []*protorefs.ObjectID{ + proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[1]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[2]).(*protorefs.ObjectID), + }, + } +) + +// Reputation service. +var ( + // correct ReputationService.AnnounceIntermediateResult response payload with + // required fields only. + validMinAnnounceIntermediateRepResponseBody = (*protoreputation.AnnounceIntermediateResultResponse_Body)(nil) + // correct ReputationService.AnnounceIntermediateResult response payload with + // all fields. + validFullAnnounceIntermediateRepResponseBody = &protoreputation.AnnounceIntermediateResultResponse_Body{} + // correct ReputationService.AnnounceLocalTrust response payload with required + // fields only. + validMinAnnounceLocalTrustResponseBody = (*protoreputation.AnnounceLocalTrustResponse_Body)(nil) + // correct ReputationService.AnnounceLocalTrust response payload with all + // fields. + validFullAnnounceLocalTrustRepResponseBody = &protoreputation.AnnounceLocalTrustResponse_Body{} +) + +// Session service. +var ( + // correct SessionService.Create response payload with required fields + // only. + validMinCreateSessionResponseBody = &protosession.CreateResponse_Body{ + Id: []byte("any_ID"), + SessionKey: []byte("any_pub"), + } + // correct SessionService.Create response payload with all fields. + validFullCreateSessionResponseBody = proto.Clone(validMinCreateSessionResponseBody).(*protosession.CreateResponse_Body) +) + +func newValidFullNodeInfo(ind int) *protonetmap.NodeInfo { + si := strconv.Itoa(ind) + return &protonetmap.NodeInfo{ + PublicKey: []byte("pub_" + si), + Addresses: []string{"endpoint_" + si + "_0", "endpoint_" + si + "_1"}, + Attributes: []*protonetmap.NodeInfo_Attribute{ + {Key: "attr_key_" + si + "_0", Value: "attr_val_" + si + "_0"}, + {Key: "attr_key_" + si + "_1", Value: "attr_val_" + si + "_1"}, + }, + State: protonetmap.NodeInfo_State(ind), + } +} + +func checkContainerIDTransport(id cid.ID, m *protorefs.ContainerID) error { + if v1, v2 := id[:], m.GetValue(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message: %x)", v1, v2) + } + return nil +} + +func checkObjectIDTransport(id oid.ID, m *protorefs.ObjectID) error { + if v1, v2 := id[:], m.GetValue(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message: %x)", v1, v2) + } + return nil +} + +func checkUserIDTransport(id user.ID, m *protorefs.OwnerID) error { + if v1, v2 := id[:], m.GetValue(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message: %x)", v1, v2) + } + return nil +} + +func checkSignatureTransport(sig neofscrypto.Signature, m *protorefs.Signature) error { + scheme := sig.Scheme() + var expScheme protorefs.SignatureScheme + switch scheme { + default: + expScheme = protorefs.SignatureScheme(scheme) + case neofscrypto.ECDSA_SHA512: + expScheme = protorefs.SignatureScheme_ECDSA_SHA512 + case neofscrypto.ECDSA_DETERMINISTIC_SHA256: + expScheme = protorefs.SignatureScheme_ECDSA_RFC6979_SHA256 + case neofscrypto.ECDSA_WALLETCONNECT: + expScheme = protorefs.SignatureScheme_ECDSA_RFC6979_SHA256_WALLET_CONNECT + } + if actScheme := m.GetScheme(); actScheme != expScheme { + return fmt.Errorf("scheme field (client: %v, message: %v)", actScheme, expScheme) + } + if v1, v2 := sig.PublicKeyBytes(), m.GetKey(); !bytes.Equal(v1, v2) { + return fmt.Errorf("public key field (client: %x, message: %x)", v1, v2) + } + if v1, v2 := sig.Value(), m.GetSign(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message: %x)", v1, v2) + } + return nil +} + +func checkSignatureRFC6979Transport(sig neofscrypto.Signature, m *protorefs.SignatureRFC6979) error { + if v1, v2 := sig.PublicKeyBytes(), m.GetKey(); !bytes.Equal(v1, v2) { + return fmt.Errorf("public key field (client: %x, message: %x)", v1, v2) + } + if v1, v2 := sig.Value(), m.GetSign(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message: %x)", v1, v2) + } + return nil +} + +// returns context oneof field of unexported type. +func checkCommonSessionTransport(t interface { + ID() uuid.UUID + Issuer() user.ID + Exp() uint64 + Nbf() uint64 + Iat() uint64 + IssuerPublicKeyBytes() []byte + AssertAuthKey(neofscrypto.PublicKey) bool + Signature() (neofscrypto.Signature, bool) +}, m *protosession.SessionToken) (any, error) { + body := m.GetBody() + if body == nil { + return nil, errors.New("missing body field in the message") + } + // 1. ID + id := t.ID() + if v1, v2 := id[:], body.GetId(); !bytes.Equal(v1, v2) { + return nil, fmt.Errorf("ID field (client: %x, message: %x)", v1, v2) + } + // 2. issuer + if err := checkUserIDTransport(t.Issuer(), body.GetOwnerId()); err != nil { + return nil, fmt.Errorf("issuer field: %w", err) + } + // 3. lifetime + lt := body.GetLifetime() + if v1, v2 := t.Exp(), lt.GetExp(); v1 != v2 { + return nil, fmt.Errorf("exp lifetime field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := t.Nbf(), lt.GetNbf(); v1 != v2 { + return nil, fmt.Errorf("nbf lifetime field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := t.Iat(), lt.GetIat(); v1 != v2 { + return nil, fmt.Errorf("iat lifetime field (client: %d, message: %d)", v1, v2) + } + // 4. session key + var k neofsecdsa.PublicKey + kb := body.GetSessionKey() + if err := k.Decode(kb); err != nil { + return nil, fmt.Errorf("invalid session key in the message: %w", err) + } + if !t.AssertAuthKey(&k) { + return nil, errors.New("session key mismatch") + } + // 5+. context + c := body.GetContext() + if c == nil { + return nil, errors.New("missing context") + } + + msig := m.GetSignature() + if sig, ok := t.Signature(); ok { + if msig == nil { + return nil, errors.New("missing signature field in the message") + } + if err := checkSignatureTransport(sig, msig); err != nil { + return nil, fmt.Errorf("signature field: %w", err) + } + } else if msig != nil { + return nil, errors.New("signature field is set while should not be") + } + return c, nil +} + +func checkContainerSessionTransport(t session.Container, m *protosession.SessionToken) error { + c, err := checkCommonSessionTransport(&t, m) + if err != nil { + return err + } + cb, ok := c.(*protosession.SessionToken_Body_Container) + if !ok { + return fmt.Errorf("wrong oneof context field type (client: %T, message: %T)", cb, c) + } + cc := cb.Container + // 1. verb + var expVerb session.ContainerVerb + actVerb := cc.GetVerb() + switch actVerb { + default: + expVerb = session.ContainerVerb(actVerb) + case protosession.ContainerSessionContext_PUT: + expVerb = session.VerbContainerPut + case protosession.ContainerSessionContext_DELETE: + expVerb = session.VerbContainerDelete + case protosession.ContainerSessionContext_SETEACL: + expVerb = session.VerbContainerSetEACL + } + if !t.AssertVerb(expVerb) { + return fmt.Errorf("wrong verb in the context field: %v", actVerb) + } + // 1.2, 1.3 container(s) + wc := cc.GetWildcard() + mc := cc.GetContainerId() + if mc == nil != wc { + return errors.New("wildcard flag conflicts with container ID in the context") + } + if wc { + if !t.AppliedTo(cidtest.ID()) { + return errors.New("wildcard flag is set while should not be") + } + } else { + var expCnr cid.ID + actCnr := mc.GetValue() + if copy(expCnr[:], actCnr); !t.AppliedTo(expCnr) { + return fmt.Errorf("wrong container in the context field: %x", actCnr) + } + } + return nil +} + +func checkObjectSessionTransport(t session.Object, m *protosession.SessionToken) error { + c, err := checkCommonSessionTransport(&t, m) + if err != nil { + return err + } + co, ok := c.(*protosession.SessionToken_Body_Object) + if !ok { + return fmt.Errorf("wrong oneof context field type (client: %T, message: %T)", co, c) + } + oo := co.Object + // 1. verb + var expVerb session.ObjectVerb + actVerb := oo.GetVerb() + switch actVerb { + default: + expVerb = session.ObjectVerb(actVerb) + case protosession.ObjectSessionContext_PUT: + expVerb = session.VerbObjectPut + case protosession.ObjectSessionContext_GET: + expVerb = session.VerbObjectGet + case protosession.ObjectSessionContext_HEAD: + expVerb = session.VerbObjectHead + case protosession.ObjectSessionContext_SEARCH: + expVerb = session.VerbObjectSearch + case protosession.ObjectSessionContext_DELETE: + expVerb = session.VerbObjectDelete + case protosession.ObjectSessionContext_RANGE: + expVerb = session.VerbObjectRange + case protosession.ObjectSessionContext_RANGEHASH: + expVerb = session.VerbObjectRangeHash + } + if !t.AssertVerb(expVerb) { + return fmt.Errorf("wrong verb in the context field: %v", actVerb) + } + // 2. target + // 2.1. container + mtgt := oo.GetTarget() + mc := mtgt.GetContainer() + if mc == nil { + return errors.New("missing container in the context field") + } + var expCnr cid.ID + actCnr := mc.GetValue() + if copy(expCnr[:], actCnr); !t.AssertContainer(expCnr) { + return fmt.Errorf("wrong container in the context field: %x", actCnr) + } + // 2.2. objects + mo := mtgt.GetObjects() + var expObj oid.ID + for i := range mo { + actObj := mo[i].GetValue() + if copy(expObj[:], actObj); !t.AssertObject(expObj) { + return fmt.Errorf("wrong object #%d in the context field: %x", i, actObj) + } + } + // FIXME: t can have more objects, this is wrong but won't be detected. Full + // list should be accessible to verify. + return nil +} + +func checkBearerTokenTransport(b bearer.Token, m *protoacl.BearerToken) error { + body := m.GetBody() + if body == nil { + return errors.New("missing body field in the message") + } + // 1. eACL + me := body.GetEaclTable() + if me == nil { + return errors.New("missing eACL in the message") + } + if err := checkEACLTransport(b.EACLTable(), me); err != nil { + return fmt.Errorf("eACL field: %w", err) + } + // 2. owner + mo := body.GetOwnerId() + if mo == nil { + return errors.New("missing owner field") + } + var expUsr user.ID + actUsr := mo.GetValue() + if copy(expUsr[:], actUsr); !b.AssertUser(expUsr) { + return fmt.Errorf("wrong owner: %x", actUsr) + } + // 3. lifetime + lt := body.GetLifetime() + if v1, v2 := b.Exp(), lt.GetExp(); v1 != v2 { + return fmt.Errorf("exp lifetime field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := b.Nbf(), lt.GetNbf(); v1 != v2 { + return fmt.Errorf("nbf lifetime field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := b.Iat(), lt.GetIat(); v1 != v2 { + return fmt.Errorf("iat lifetime field (client: %d, message: %d)", v1, v2) + } + // 4. issuer + if err := checkUserIDTransport(b.Issuer(), body.GetIssuer()); err != nil { + return fmt.Errorf("issuer field: %w", err) + } + return nil +} + +func checkVersionTransport(v version.Version, m *protorefs.Version) error { + if v1, v2 := v.Major(), m.GetMajor(); v1 != v2 { + return fmt.Errorf("major field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := v.Minor(), m.GetMinor(); v1 != v2 { + return fmt.Errorf("minor field (client: %d, message: %d)", v1, v2) + } + return nil +} + +func checkBalanceTransport(b accounting.Decimal, m *protoaccounting.Decimal) error { + if v1, v2 := b.Value(), m.GetValue(); v1 != v2 { + return fmt.Errorf("value field (client: %d, message: %d)", v1, v2) + } + if v1, v2 := b.Precision(), m.GetPrecision(); v1 != v2 { + return fmt.Errorf("precision field (client: %d, message: %d)", v1, v2) + } + return nil +} + +func checkStoragePolicyFilterTransport(f netmap.Filter, m *protonetmap.Filter) error { + // 1. name + if v1, v2 := f.Name(), m.GetName(); v1 != v2 { + return fmt.Errorf("name (client: %q, message: %q)", v1, v2) + } + // 2. key + if v1, v2 := f.Key(), m.GetKey(); v1 != v2 { + return fmt.Errorf("key (client: %q, message: %q)", v1, v2) + } + // 3. op + var expOp protonetmap.Operation + switch op := f.Op(); op { + default: + expOp = protonetmap.Operation(op) + case netmap.FilterOpEQ: + expOp = protonetmap.Operation_EQ + case netmap.FilterOpNE: + expOp = protonetmap.Operation_NE + case netmap.FilterOpGT: + expOp = protonetmap.Operation_GT + case netmap.FilterOpGE: + expOp = protonetmap.Operation_GE + case netmap.FilterOpLT: + expOp = protonetmap.Operation_LT + case netmap.FilterOpLE: + expOp = protonetmap.Operation_LE + case netmap.FilterOpOR: + expOp = protonetmap.Operation_OR + case netmap.FilterOpAND: + expOp = protonetmap.Operation_AND + } + if actOp := m.GetOp(); actOp != expOp { + return fmt.Errorf("op (client: %v, message: %v)", expOp, actOp) + } + // 4. value + if v1, v2 := f.Value(), m.GetValue(); v1 != v2 { + return fmt.Errorf("value (client: %q, message: %q)", v1, v2) + } + // 5. sub-filters + cfs, mfs := f.SubFilters(), m.GetFilters() + if v1, v2 := len(cfs), len(mfs); v1 != v2 { + return fmt.Errorf("number of sub-filters (client: %d, message: %d)", v1, v2) + } + for i := range cfs { + if err := checkStoragePolicyFilterTransport(cfs[i], mfs[i]); err != nil { + return fmt.Errorf("sub-filter#%d: %w", i, err) + } + } + return nil +} + +func checkStoragePolicyTransport(p netmap.PlacementPolicy, m *protonetmap.PlacementPolicy) error { + // 1. replicas + crs, mrs := p.Replicas(), m.GetReplicas() + if v1, v2 := len(crs), len(mrs); v1 != v2 { + return fmt.Errorf("number of replicas (client: %d, message: %d)", v1, v2) + } + for i, cr := range crs { + mr := mrs[i] + if v1, v2 := cr.NumberOfObjects(), mr.GetCount(); v1 != v2 { + return fmt.Errorf("replica#%d field: object count (client: %d, message: %d)", i, v1, v2) + } + if v1, v2 := cr.SelectorName(), mr.GetSelector(); v1 != v2 { + return fmt.Errorf("replica#%d field: selector (client: %v, message: %v)", i, v1, v2) + } + } + // 2. backup factor + if v1, v2 := p.ContainerBackupFactor(), m.GetContainerBackupFactor(); v1 != v2 { + return fmt.Errorf("backup factor (client: %d, message: %d)", v1, v2) + } + // 3. selectors + css, mss := p.Selectors(), m.GetSelectors() + if v1, v2 := len(css), len(mss); v1 != v2 { + return fmt.Errorf("number of selectors (client: %d, message: %d)", v1, v2) + } + for i, cs := range css { + ms := mss[i] + // 1. name + if v1, v2 := cs.Name(), ms.GetName(); v1 != v2 { + return fmt.Errorf("selector#%d field: name (client: %q, message: %q)", i, v1, v2) + } + // 2. count + if v1, v2 := cs.NumberOfNodes(), ms.GetCount(); v1 != v2 { + return fmt.Errorf("selector#%d field: node count (client: %d, message: %d)", i, v1, v2) + } + // 3. clause + var expClause protonetmap.Clause + actClause := ms.GetClause() + switch { + default: + var pV2 apinetmap.PlacementPolicy + p.WriteToV2(&pV2) + expClause = pV2.ToGRPCMessage().(*protonetmap.PlacementPolicy).Selectors[i].Clause + case cs.IsSame(): + expClause = protonetmap.Clause_SAME + case cs.IsDistinct(): + expClause = protonetmap.Clause_DISTINCT + } + if actClause != expClause { + return fmt.Errorf("selector#%d field: clause (client: %v, message: %v)", i, expClause, actClause) + } + // 4. attribute + if v1, v2 := cs.BucketAttribute(), ms.GetAttribute(); v1 != v2 { + return fmt.Errorf("selector#%d field: attribute (client: %q, message: %q)", i, v1, v2) + } + // 5. filter + if v1, v2 := cs.FilterName(), ms.GetFilter(); v1 != v2 { + return fmt.Errorf("selector#%d field: filter (client: %q, message: %q)", i, v1, v2) + } + } + // filters + cfs, mfs := p.Filters(), m.GetFilters() + if v1, v2 := len(cfs), len(mfs); v1 != v2 { + return fmt.Errorf("number of filters (client: %d, message: %d)", v1, v2) + } + for i, mf := range mfs { + if err := checkStoragePolicyFilterTransport(cfs[i], mf); err != nil { + return fmt.Errorf("filter#%d field: %w", i, err) + } + } + return nil +} + +func checkContainerTransport(c container.Container, m *protocontainer.Container) error { + // 1. version + if err := checkVersionTransport(c.Version(), m.GetVersion()); err != nil { + return fmt.Errorf("version field: %w", err) + } + // 2. owner + if err := checkUserIDTransport(c.Owner(), m.GetOwnerId()); err != nil { + return fmt.Errorf("owner field: %w", err) + } + // 3. nonce + // TODO(https://github.com/nspcc-dev/neofs-sdk-go/issues/664): access nonce from c directly + var cV2 apicontainer.Container + c.WriteToV2(&cV2) + if v1, v2 := cV2.GetNonce(), m.GetNonce(); !bytes.Equal(v1, v2) { + return fmt.Errorf("nonce field (client: %x, message: %x)", v1, v2) + } + // 4. basic ACL + if v1, v2 := c.BasicACL().Bits(), m.GetBasicAcl(); v1 != v2 { + return fmt.Errorf("basic ACL field (client: %d, message: %d)", v1, v2) + } + // 5. attributes + var mas [][2]string + var name, dmn, zone string + var disableHomoHash bool + var timestamp int64 + for _, ma := range m.GetAttributes() { + k, v := ma.GetKey(), ma.GetValue() + mas = append(mas, [2]string{k, v}) + switch k { + case "Name": + name = v + case "Timestamp": + var err error + if timestamp, err = strconv.ParseInt(v, 10, 64); err != nil { + return fmt.Errorf("invalid timestamp attribute value %q in the message: %w", v, err) + } + } + if tail, ok := strings.CutPrefix(k, "__NEOFS__"); ok { + switch tail { + case "NAME": + dmn = v + case "ZONE": + zone = v + case "DISABLE_HOMOMORPHIC_HASHING": + disableHomoHash = v == "true" + } + } + } + var cas [][2]string + c.IterateAttributes(func(k, v string) { cas = append(cas, [2]string{k, v}) }) + if v1, v2 := len(cas), len(mas); v1 != v2 { + return fmt.Errorf("number of attributes (client: %d, message: %d)", v1, v2) + } + for i, ca := range cas { + if ma := mas[i]; ca != ma { + return fmt.Errorf("attribute #%d (client: %v, message: %v)", i, ca, ma) + } + } + if v1, v2 := c.Name(), name; v1 != v2 { + return fmt.Errorf("name attribute (client: %q, message: %q)", v1, v2) + } + if v1, v2 := c.IsHomomorphicHashingDisabled(), disableHomoHash; v2 != v1 { + return fmt.Errorf("homomorphic hashing flag attribute (client: %t, message: %t)", v1, v2) + } + if v1, v2 := c.CreatedAt().Unix(), timestamp; v2 != v1 { + return fmt.Errorf("timestamp attribute (client: %d, message: %d)", v1, v2) + } + if v1, v2 := c.ReadDomain().Name(), dmn; v1 != v2 { + return fmt.Errorf("domain name attribute (client: %q, message: %q)", v1, v2) + } + if zone == "" { + zone = "container" + } + if v1, v2 := c.ReadDomain().Zone(), zone; v2 != v1 { + return fmt.Errorf("domain zone attribute (client: %q, message: %q)", v1, v2) + } + // 6. policy + mp := m.GetPlacementPolicy() + if mp == nil { + return errors.New("missing storage policy field in the message") + } + if err := checkStoragePolicyTransport(c.PlacementPolicy(), mp); err != nil { + return fmt.Errorf("storage policy field: %w", err) + } + return nil +} + +func checkEACLFilterTransport(f eacl.Filter, m *protoacl.EACLRecord_Filter) error { + // 1. header type + var expHdr protoacl.HeaderType + switch ht := f.From(); ht { + default: + expHdr = protoacl.HeaderType(ht) + case eacl.HeaderFromRequest: + expHdr = protoacl.HeaderType_REQUEST + case eacl.HeaderFromObject: + expHdr = protoacl.HeaderType_OBJECT + case eacl.HeaderFromService: + expHdr = protoacl.HeaderType_SERVICE + } + if act := m.GetHeaderType(); act != expHdr { + return fmt.Errorf("header type (client: %v, message: %v)", act, expHdr) + } + // matcher + var expMatcher protoacl.MatchType + switch m := f.Matcher(); m { + default: + expMatcher = protoacl.MatchType(m) + case eacl.MatchStringEqual: + expMatcher = protoacl.MatchType_STRING_EQUAL + case eacl.MatchStringNotEqual: + expMatcher = protoacl.MatchType_STRING_NOT_EQUAL + case eacl.MatchNotPresent: + expMatcher = protoacl.MatchType_NOT_PRESENT + case eacl.MatchNumGT: + expMatcher = protoacl.MatchType_NUM_GT + case eacl.MatchNumGE: + expMatcher = protoacl.MatchType_NUM_GE + case eacl.MatchNumLT: + expMatcher = protoacl.MatchType_NUM_LT + case eacl.MatchNumLE: + expMatcher = protoacl.MatchType_NUM_LE + } + if act := m.GetMatchType(); act != expMatcher { + return fmt.Errorf("match type (client: %v, message: %v)", act, expMatcher) + } + // 4. key + if v1, v2 := f.Key(), m.GetKey(); v1 != v2 { + return fmt.Errorf("key field (client: %q, message: %q)", v1, v2) + } + // 4. value + if v1, v2 := f.Value(), m.GetValue(); v1 != v2 { + return fmt.Errorf("value field (client: %q, message: %q)", v1, v2) + } + return nil +} + +func checkEACLTargetTransport(t eacl.Target, m *protoacl.EACLRecord_Target) error { + // role + var expRole protoacl.Role + switch r := t.Role(); r { + default: + expRole = protoacl.Role(r) + case eacl.RoleUser: + expRole = protoacl.Role_USER + case eacl.RoleSystem: + expRole = protoacl.Role_SYSTEM + case eacl.RoleOthers: + expRole = protoacl.Role_OTHERS + } + if act := m.GetRole(); act != expRole { + return fmt.Errorf("role (client: %v, message: %v)", act, expRole) + } + // 2. subjects + cks, mks := t.RawSubjects(), m.GetKeys() + if v1, v2 := len(cks), len(mks); v1 != v2 { + return fmt.Errorf("number of subjects (client: %d, message: %d)", v1, v2) + } + for i := range cks { + if !bytes.Equal(cks[i], mks[i]) { + return fmt.Errorf("subject#%d (client: %x, message: %x)", i, cks[i], mks[i]) + } + } + return nil +} + +func checkEACLRecordTransport(r eacl.Record, m *protoacl.EACLRecord) error { + // 1. op + var expOp protoacl.Operation + switch op := r.Operation(); op { + default: + expOp = protoacl.Operation(op) + case eacl.OperationGet: + expOp = protoacl.Operation_GET + case eacl.OperationHead: + expOp = protoacl.Operation_HEAD + case eacl.OperationPut: + expOp = protoacl.Operation_PUT + case eacl.OperationDelete: + expOp = protoacl.Operation_DELETE + case eacl.OperationSearch: + expOp = protoacl.Operation_SEARCH + case eacl.OperationRange: + expOp = protoacl.Operation_GETRANGE + case eacl.OperationRangeHash: + expOp = protoacl.Operation_GETRANGEHASH + } + if act := m.GetOperation(); act != expOp { + return fmt.Errorf("op (client: %v, message: %v)", act, expOp) + } + // 2. action + var expAction protoacl.Action + switch a := r.Action(); a { + default: + expAction = protoacl.Action(a) + case eacl.ActionAllow: + expAction = protoacl.Action_ALLOW + case eacl.ActionDeny: + expAction = protoacl.Action_DENY + } + if act := m.GetAction(); act != expAction { + return fmt.Errorf("action (client: %v, message: %v)", act, expAction) + } + // 3. filters + mfs, cfs := m.GetFilters(), r.Filters() + if v1, v2 := len(cfs), len(mfs); v1 != v2 { + return fmt.Errorf("number of filters (client: %d, message: %d)", v1, v2) + } + for i := range cfs { + if err := checkEACLFilterTransport(cfs[i], mfs[i]); err != nil { + return fmt.Errorf("filter#%d field: %w", i, err) + } + } + // 4. targets + mts, cts := m.GetTargets(), r.Targets() + if v1, v2 := len(cfs), len(mfs); v1 != v2 { + return fmt.Errorf("number of targets (client: %d, message: %d)", v1, v2) + } + for i := range mts { + if err := checkEACLTargetTransport(cts[i], mts[i]); err != nil { + return fmt.Errorf("target#%d field: %w", i, err) + } + } + return nil +} + +func checkEACLTransport(e eacl.Table, m *protoacl.EACLTable) error { + // 1. version + if err := checkVersionTransport(e.Version(), m.GetVersion()); err != nil { + return fmt.Errorf("version field: %w", err) + } + // 2. container ID + mc := m.GetContainerId() + if c := e.GetCID(); c.IsZero() { + if mc != nil { + return errors.New("container ID field is set while should not be") + } + } else { + if mc == nil { + return errors.New("missing container ID field") + } + if err := checkContainerIDTransport(c, mc); err != nil { + return fmt.Errorf("container ID field: %w", err) + } + } + // 3. records + mrs, crs := m.GetRecords(), e.Records() + if v1, v2 := len(crs), len(mrs); v1 != v2 { + return fmt.Errorf("number of records (client: %d, message: %d)", v1, v2) + } + for i := range mrs { + if err := checkEACLRecordTransport(crs[i], mrs[i]); err != nil { + return fmt.Errorf("record#%d field: %w", i, err) + } + } + return nil +} + +func checkContainerSizeEstimationTransport(e container.SizeEstimation, m *protocontainer.AnnounceUsedSpaceRequest_Body_Announcement) error { + // 1. epoch + if v1, v2 := e.Epoch(), m.GetEpoch(); v1 != v2 { + return fmt.Errorf("epoch field (client: %d, message: %d)", v1, v2) + } + // 1. container ID + mc := m.GetContainerId() + if mc == nil { + return newErrMissingRequestBodyField("container ID") + } + if err := checkContainerIDTransport(e.Container(), mc); err != nil { + return fmt.Errorf("container ID field: %w", err) + } + // 3. value + if v1, v2 := e.Value(), m.GetUsedSpace(); v1 != v2 { + return fmt.Errorf("value field (client: %d, message: %d)", v1, v2) + } + return nil +} + +func checkNodeInfoTransport(n netmap.NodeInfo, m *protonetmap.NodeInfo) error { + // 1. public key + if v1, v2 := n.PublicKey(), m.GetPublicKey(); !bytes.Equal(v1, v2) { + return fmt.Errorf("public key field (client: %x, message: %x)", v1, v2) + } + // 2. addresses + maddrs := m.GetAddresses() + var caddrs []string + netmap.IterateNetworkEndpoints(n, func(e string) { caddrs = append(caddrs, e) }) + if v1, v2 := len(caddrs), len(maddrs); v1 != v2 { + return fmt.Errorf("number of addresses (client: %d, message: %d)", v1, v2) + } + for i := range caddrs { + if v1, v2 := caddrs[i], maddrs[i]; v1 != v2 { + return fmt.Errorf("name (client: %q, message: %q)", v1, v2) + } + } + // 3. attributes + attrs, mattrs := n.GetAttributes(), m.GetAttributes() + if v1, v2 := len(attrs), len(mattrs); v1 != v2 { + return fmt.Errorf("number of attributes (client: %d, message: %d)", v1, v2) + } + for i, ma := range mattrs { + a := attrs[i] + if v1, v2 := a[0], ma.GetKey(); v1 != v2 { + return fmt.Errorf("attribute#%d field: key (client: %q, message: %q)", i, v1, v2) + } + if v1, v2 := a[1], ma.GetValue(); v1 != v2 { + return fmt.Errorf("attribute#%d field: value (client: %q, message: %q)", i, v1, v2) + } + if len(ma.GetParents()) > 0 { + return fmt.Errorf("attribute#%d field: parents field is set while should not be", i) + } + } + // 4. state + var expState protonetmap.NodeInfo_State + switch { + default: + var pV2 apinetmap.NodeInfo + n.WriteToV2(&pV2) + expState = pV2.ToGRPCMessage().(*protonetmap.NodeInfo).State + case n.IsOnline(): + expState = protonetmap.NodeInfo_ONLINE + case n.IsOffline(): + expState = protonetmap.NodeInfo_OFFLINE + case n.IsMaintenance(): + expState = protonetmap.NodeInfo_MAINTENANCE + } + if act := m.GetState(); act != expState { + return fmt.Errorf("state field (client: %v, message: %v)", expState, act) + } + return nil +} + +func checkNetmapTransport(n netmap.NetMap, m *protonetmap.Netmap) error { + // 1. epoch + if v1, v2 := n.Epoch(), m.GetEpoch(); v1 != v2 { + return fmt.Errorf("epoch field (client: %d, message: %d)", v1, v2) + } + // 2. nodes + cns, mns := n.Nodes(), m.GetNodes() + if v1, v2 := len(cns), len(mns); v1 != v2 { + return fmt.Errorf("number of nodes (client: %d, message: %d)", v1, v2) + } + for i := range cns { + if err := checkNodeInfoTransport(cns[i], mns[i]); err != nil { + return fmt.Errorf("node#%d field: %w", i, err) + } + } + return nil +} + +func checkNetInfoTransport(n netmap.NetworkInfo, m *protonetmap.NetworkInfo) error { + // 1. current epoch + if v1, v2 := n.CurrentEpoch(), m.GetCurrentEpoch(); v1 != v2 { + return fmt.Errorf("current epoch field (client: %d, message: %d)", v1, v2) + } + // 2. magic + if v1, v2 := n.MagicNumber(), m.GetMagicNumber(); v1 != v2 { + return fmt.Errorf("network magic field (client: %d, message: %d)", v1, v2) + } + // 3. ms per block + if v1, v2 := n.MsPerBlock(), m.GetMsPerBlock(); v1 != v2 { + return fmt.Errorf("ms per block field (client: %d, message: %d)", v1, v2) + } + // 4. config + mps := m.GetNetworkConfig().GetParameters() + var raw []string + n.IterateRawNetworkParameters(func(name string, value []byte) { raw = append(raw, name, string(value)) }) + + var mraw []string + var auditFee, storagePrice, cnrDmnFee, cnrFee, etIters, epochDur, irFee, maxObjSize, withdrawFee uint64 + var etAlpha float64 + var homoHashDisabled, maintenanceAllowed bool + for _, mp := range mps { + k, v := mp.GetKey(), mp.GetValue() + switch string(k) { + default: + mraw = append(mraw, string(k), string(v)) + case "AuditFee": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + auditFee = binary.LittleEndian.Uint64(v) + case "BasicIncomeRate": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + storagePrice = binary.LittleEndian.Uint64(v) + case "ContainerAliasFee": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + cnrDmnFee = binary.LittleEndian.Uint64(v) + case "ContainerFee": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + cnrFee = binary.LittleEndian.Uint64(v) + case "EigenTrustAlpha": + var err error + if etAlpha, err = strconv.ParseFloat(string(v), 64); err != nil { + return fmt.Errorf("invalid parameter %q value %q: %w", k, v, err) + } + case "EigenTrustIterations": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + etIters = binary.LittleEndian.Uint64(v) + case "EpochDuration": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + epochDur = binary.LittleEndian.Uint64(v) + case "HomomorphicHashingDisabled": + for _, b := range v { + if homoHashDisabled = b != 0; homoHashDisabled { + break + } + } + case "InnerRingCandidateFee": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + irFee = binary.LittleEndian.Uint64(v) + case "MaintenanceModeAllowed": + for _, b := range v { + if maintenanceAllowed = b != 0; maintenanceAllowed { + break + } + } + case "MaxObjectSize": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + maxObjSize = binary.LittleEndian.Uint64(v) + case "WithdrawFee": + if l := len(v); l < 8 { + return fmt.Errorf("too short parameter %q value: %d bytes", k, l) + } + withdrawFee = binary.LittleEndian.Uint64(v) + } + } + if v1, v2 := len(raw), len(mraw); v1 != v2 { + return fmt.Errorf("number of raw config values: (client: %d, message: %d)", v1, v2) + } + for i := 0; i < len(raw); i += 2 { + if raw[i] != mraw[i] { + return fmt.Errorf("raw config #%d: key (client: %q, message: %q)", i, raw[i], mraw[i]) + } + if raw[i+1] != mraw[i+1] { + return fmt.Errorf("raw config #%d: value (client: %q, message: %q)", i, raw[i+1], mraw[i+1]) + } + } + if v1, v2 := n.AuditFee(), auditFee; v1 != v2 { + return fmt.Errorf("audit fee parameter (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.StoragePrice(), storagePrice; v1 != v2 { + return fmt.Errorf("storage price parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.ContainerFee(), cnrFee; v1 != v2 { + return fmt.Errorf("container fee parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.NamedContainerFee(), cnrDmnFee; v1 != v2 { + return fmt.Errorf("container domain fee parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.NumberOfEigenTrustIterations(), etIters; v1 != v2 { + return fmt.Errorf("number of Eigen-Trust iterations parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.EigenTrustAlpha(), etAlpha; v1 != v2 { + return fmt.Errorf("Eigen-Trust alpha parameter value (client: %v, message: %v)", v1, v2) + } + if v1, v2 := n.EpochDuration(), epochDur; v1 != v2 { + return fmt.Errorf("epoch duration parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.IRCandidateFee(), irFee; v1 != v2 { + return fmt.Errorf("IR candidate fee parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.MaintenanceModeAllowed(), maintenanceAllowed; v1 != v2 { + return fmt.Errorf("maintenance mode allowance parameter value (client: %t, message: %t)", v1, v2) + } + if v1, v2 := n.MaxObjectSize(), maxObjSize; v1 != v2 { + return fmt.Errorf("max object size parameter value (client: %d, message: %d)", v1, v2) + } + if v1, v2 := n.WithdrawalFee(), withdrawFee; v1 != v2 { + return fmt.Errorf("withdrawal fee parameter value (client: %d, message: %d)", v1, v2) + } + return nil +} + +func checkReputationPeerTransport(p reputation.PeerID, m *protoreputation.PeerID) error { + if m == nil { + return errors.New("missing peer field") + } + if v1, v2 := p.PublicKey(), m.GetPublicKey(); !bytes.Equal(v1, v2) { + return fmt.Errorf("peer field (client: %x, message: %x)", v1, v2) + } + return nil +} + +func checkTrustTransport(t reputation.Trust, m *protoreputation.Trust) error { + if err := checkReputationPeerTransport(t.Peer(), m.GetPeer()); err != nil { + return fmt.Errorf("peer field: %w", err) + } + if v1, v2 := t.Value(), m.GetValue(); v1 != v2 { + return fmt.Errorf("value field (client: %v, message: %v)", v1, v2) + } + return nil +} + +func checkP2PTrustTransport(t reputation.PeerToPeerTrust, m *protoreputation.PeerToPeerTrust) error { + if err := checkReputationPeerTransport(t.TrustingPeer(), m.GetTrustingPeer()); err != nil { + return fmt.Errorf("trusting peer field: %w", err) + } + if err := checkTrustTransport(t.Trust(), m.GetTrust()); err != nil { + return fmt.Errorf("trust field: %w", err) + } + return nil +} + +func checkHashTransport(c checksum.Checksum, m *protorefs.Checksum) error { + var expType protorefs.ChecksumType + switch typ := c.Type(); typ { + default: + expType = protorefs.ChecksumType(typ) + case checksum.SHA256: + expType = protorefs.ChecksumType_SHA256 + case checksum.TillichZemor: + expType = protorefs.ChecksumType_TZ + } + if actType := m.GetType(); actType != expType { + return fmt.Errorf("type field (client: %v, message %v)", expType, actType) + } + if v1, v2 := c.Value(), m.GetSum(); !bytes.Equal(v1, v2) { + return fmt.Errorf("value field (client: %x, message %x)", v1, v2) + } + return nil +} + +func checkObjectHeaderWithSignatureTransport(o object.Object, m *protoobject.HeaderWithSignature) error { + if err := checkObjectHeaderTransport(o, m.GetHeader()); err != nil { + return fmt.Errorf("header field: %w", err) + } + s := o.Signature() + ms := m.GetSignature() + if s != nil { + if ms == nil { + return errors.New("missing signature field") + } + if err := checkSignatureTransport(*s, ms); err != nil { + return fmt.Errorf("signature field: %w", err) + } + } else { + if ms != nil { + return errors.New("signature field is set while should not be") + } + } + return nil +} + +func checkObjectHeaderTransport(h object.Object, m *protoobject.Header) error { + // 1. version + ver := h.Version() + if ver != nil { + if err := checkVersionTransport(*ver, m.GetVersion()); err != nil { + return fmt.Errorf("version field: %w", err) + } + } else { + if m.GetVersion() != nil { + return errors.New("version field is set while should not") + } + } + // 2. container + cnr := h.GetContainerID() + if cnr.IsZero() { + if m.GetContainerId() != nil { + return errors.New("container field is set while should not") + } + } else { + if err := checkContainerIDTransport(cnr, m.GetContainerId()); err != nil { + return fmt.Errorf("container field: %w", err) + } + } + // 3. owner + ownr := h.Owner() + if ownr.IsZero() { + if m.GetOwnerId() != nil { + return errors.New("owner field is set while should not be") + } + } else { + if err := checkUserIDTransport(ownr, m.GetOwnerId()); err != nil { + return fmt.Errorf("owner field: %w", err) + } + } + // 4. creation epoch + if v1, v2 := h.CreationEpoch(), m.GetCreationEpoch(); v1 != v2 { + return fmt.Errorf("creation epoch field (client: %d, message: %d)", v1, v2) + } + // 5. payload length + if v1, v2 := h.PayloadSize(), m.GetPayloadLength(); v1 != v2 { + return fmt.Errorf("payload length field (client: %d, message: %d)", v1, v2) + } + // 6. payload checksum + cs, ok := h.PayloadChecksum() + mcs := m.GetPayloadHash() + if ok { + if mcs == nil { + return errors.New("missing payload checksum field") + } + if err := checkHashTransport(cs, mcs); err != nil { + return fmt.Errorf("payload checksum field: %w", err) + } + } else { + if mcs != nil { + return errors.New("payload checksum field is set while should not be") + } + } + // 7. type + var expType protoobject.ObjectType + switch typ := h.Type(); typ { + default: + expType = protoobject.ObjectType(typ) + case object.TypeRegular: + expType = protoobject.ObjectType_REGULAR + case object.TypeTombstone: + expType = protoobject.ObjectType_TOMBSTONE + case object.TypeStorageGroup: + expType = protoobject.ObjectType_STORAGE_GROUP + case object.TypeLock: + expType = protoobject.ObjectType_LOCK + case object.TypeLink: + expType = protoobject.ObjectType_LINK + } + if actType := m.GetObjectType(); actType != expType { + return fmt.Errorf("type field (client: %v, message %v)", expType, actType) + } + // 8. payload homomorphic checksum + cs, ok = h.PayloadHomomorphicHash() + mcs = m.GetHomomorphicHash() + if ok { + if mcs == nil { + return errors.New("missing payload homomorphic checksum field") + } + if err := checkHashTransport(cs, mcs); err != nil { + return fmt.Errorf("payload homomorphic checksum field: %w", err) + } + } else { + if mcs != nil { + return errors.New("payload homomorphic checksum field is set while should not be") + } + } + // 9. session token + st := h.SessionToken() + mst := m.GetSessionToken() + if st != nil { + if mst == nil { + return errors.New("missing session token field") + } + if err := checkObjectSessionTransport(*st, mst); err != nil { + return fmt.Errorf("session token field: %w", err) + } + } else { + if mst != nil { + return errors.New("session token field is set while should not be") + } + } + // 10. attributes + as := h.Attributes() + mas := m.GetAttributes() + if v1, v2 := len(as), len(mas); v1 != v2 { + return fmt.Errorf("number of attributes (client: %d, message: %d)", v1, v2) + } + for i := range as { + if v1, v2 := as[i].Key(), mas[i].GetKey(); v1 != v2 { + return fmt.Errorf("attribute#%d: key (client: %q, message: %q)", i, v1, v2) + } + if v1, v2 := as[i].Value(), mas[i].GetValue(); v1 != v2 { + return fmt.Errorf("attribute#%d: value (client: %q, message: %q)", i, v1, v2) + } + } + // 11. split + parID := h.GetParentID() + prev := h.GetPreviousID() + first := h.GetFirstID() + children := h.Children() + parHdr := h.Parent() + splitID := h.SplitID() + sh := m.GetSplit() + if parID.IsZero() && parHdr == nil && prev.IsZero() && first.IsZero() && len(children) == 0 && splitID == nil { + if sh != nil { + return errors.New("split header field is set while should not be") + } + } else { + if err := checkObjectSplitTransport(parID, prev, parHdr, children, splitID, first, sh); err != nil { + return fmt.Errorf("split header field: %w", err) + } + } + return nil +} + +func checkObjectSplitTransport(parID oid.ID, prev oid.ID, parHdrSig *object.Object, children []oid.ID, + splitID *object.SplitID, first oid.ID, m *protoobject.Header_Split) error { + // 1. parent ID + mid := m.GetParent() + if parID.IsZero() { + if mid != nil { + return errors.New("parent ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing parent ID field") + } + if err := checkObjectIDTransport(parID, mid); err != nil { + return fmt.Errorf("parent ID field: %w", err) + } + } + // 2. previous ID + mid = m.GetPrevious() + if prev.IsZero() { + if mid != nil { + return errors.New("previous ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing previous ID field") + } + if err := checkObjectIDTransport(prev, mid); err != nil { + return fmt.Errorf("previous ID field: %w", err) + } + } + // 3,4. parent signature, header + mph := m.GetParentHeader() + mps := m.GetParentSignature() + if parHdrSig != nil { + if mph == nil && mps == nil { + return errors.New("missing both parent header and signature") + } + if mph != nil { + if err := checkObjectHeaderTransport(*parHdrSig, mph); err != nil { + return fmt.Errorf("parent header field: %w", err) + } + } + if ps := parHdrSig.Signature(); ps != nil { + if mps == nil { + return errors.New("missing parent header field") + } + if err := checkSignatureTransport(*ps, mps); err != nil { + return fmt.Errorf("parent signature field: %w", err) + } + } else { + if mps != nil { + return errors.New("parent signature field is set while should not be") + } + } + } else { + if mph != nil { + return errors.New("parent header field is set while should not be") + } + if mps != nil { + return errors.New("parent signature field is set while should not be") + } + } + // 5. children + mc := m.GetChildren() + if v1, v2 := len(children), len(mc); v1 != v2 { + return fmt.Errorf("number of children (client: %d, message: %d)", v1, v2) + } + for i := range children { + if mc[i] == nil { + return fmt.Errorf("children field: nil element #%d", i) + } + if err := checkObjectIDTransport(children[i], mc[i]); err != nil { + return fmt.Errorf("children field: child#%d: %w", i, err) + } + } + // 6. split ID + actSplitID := m.GetSplitId() + if splitID != nil { + if expSplitID := splitID.ToV2(); !bytes.Equal(actSplitID, expSplitID) { + return fmt.Errorf("split ID field (client: %x, message: %x)", expSplitID, actSplitID) + } + } else { + if len(actSplitID) > 0 { + return errors.New("split ID field is set while should not be") + } + } + // 7. first ID + mid = m.GetFirst() + if first.IsZero() { + if mid != nil { + return errors.New("first ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing first ID field") + } + if err := checkObjectIDTransport(first, mid); err != nil { + return fmt.Errorf("first ID field: %w", err) + } + } + return nil +} + +func checkSplitInfoTransport(s object.SplitInfo, m *protoobject.SplitInfo) error { + // 1. split ID + splitID := s.SplitID() + actSplitID := m.GetSplitId() + if splitID != nil { + if expSplitID := splitID.ToV2(); !bytes.Equal(actSplitID, expSplitID) { + return fmt.Errorf("split ID field (client: %x, message: %x)", expSplitID, actSplitID) + } + } else { + if len(actSplitID) > 0 { + return errors.New("split ID field is set while should not be") + } + } + // 2. last ID + id := s.GetLastPart() + mid := m.GetLastPart() + if id.IsZero() { + if mid != nil { + return errors.New("last ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing last ID field") + } + if err := checkObjectIDTransport(id, mid); err != nil { + return fmt.Errorf("last ID field: %w", err) + } + } + // 3. linker + id = s.GetLink() + mid = m.GetLink() + if id.IsZero() { + if mid != nil { + return errors.New("linker ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing linker ID field") + } + if err := checkObjectIDTransport(id, mid); err != nil { + return fmt.Errorf("linker ID field: %w", err) + } + } + // 4. first part + id = s.GetFirstPart() + mid = m.GetFirstPart() + if id.IsZero() { + if mid != nil { + return errors.New("first ID field is set while should not be") + } + } else { + if mid == nil { + return errors.New("missing first ID field") + } + if err := checkObjectIDTransport(id, mid); err != nil { + return fmt.Errorf("first ID field: %w", err) + } + } + return nil +} + +func checkObjectSearchFilterTransport(f object.SearchFilter, m *protoobject.SearchRequest_Body_Filter) error { + // 1. matcher + var expMatcher protoobject.MatchType + switch m := f.Operation(); m { + default: + expMatcher = protoobject.MatchType(m) + case object.MatchStringEqual: + expMatcher = protoobject.MatchType_STRING_EQUAL + case object.MatchStringNotEqual: + expMatcher = protoobject.MatchType_STRING_NOT_EQUAL + case object.MatchNotPresent: + expMatcher = protoobject.MatchType_NOT_PRESENT + case object.MatchCommonPrefix: + expMatcher = protoobject.MatchType_COMMON_PREFIX + case object.MatchNumGT: + expMatcher = protoobject.MatchType_NUM_GT + case object.MatchNumGE: + expMatcher = protoobject.MatchType_NUM_GE + case object.MatchNumLT: + expMatcher = protoobject.MatchType_NUM_LT + case object.MatchNumLE: + expMatcher = protoobject.MatchType_NUM_LE + } + if mtch := m.GetMatchType(); mtch != expMatcher { + return fmt.Errorf("matcher (client: %v, message: %v)", expMatcher, mtch) + } + // 2. key + if v1, v2 := f.Header(), m.GetKey(); v1 != v2 { + return fmt.Errorf("key (client: %q, message: %q)", v1, v2) + } + // 3. value + if v1, v2 := f.Value(), m.GetValue(); v1 != v2 { + return fmt.Errorf("value (client: %q, message: %q)", v1, v2) + } + return nil +} + +func checkObjectSearchFiltersTransport(fs []object.SearchFilter, ms []*protoobject.SearchRequest_Body_Filter) error { + if v1, v2 := len(fs), len(ms); v1 != v2 { + return fmt.Errorf("number of attributes (client: %d, message: %d)", v1, v2) + } + for i := range fs { + if err := checkObjectSearchFilterTransport(fs[i], ms[i]); err != nil { + return fmt.Errorf("filter #%d: %w", i, err) + } + } + return nil +} diff --git a/client/netmap_test.go b/client/netmap_test.go index 5719631e..7ac97df6 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -5,131 +5,330 @@ import ( "errors" "fmt" "testing" + "time" v2netmap "github.com/nspcc-dev/neofs-api-go/v2/netmap" 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/nspcc-dev/neofs-sdk-go/stat" "github.com/stretchr/testify/require" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) -func newDefaultNetmapServiceDesc(srv protonetmap.NetmapServiceServer) testService { +// various sets of Netmap service testcases. +var ( + invalidNodeInfoProtoTestcases = []struct { + name, msg string + corrupt func(valid *protonetmap.NodeInfo) + }{ + {name: "public key/nil", msg: "missing public key", corrupt: func(valid *protonetmap.NodeInfo) { + valid.PublicKey = nil + }}, + {name: "public key/empty", msg: "missing public key", corrupt: func(valid *protonetmap.NodeInfo) { + valid.PublicKey = []byte{} + }}, + {name: "addresses/nil", msg: "missing network endpoints", corrupt: func(valid *protonetmap.NodeInfo) { + valid.Addresses = nil + }}, + {name: "addresses/empty", msg: "missing network endpoints", corrupt: func(valid *protonetmap.NodeInfo) { + valid.Addresses = nil + }}, + {name: "attributes/no key", msg: "empty key of the attribute #1", corrupt: func(valid *protonetmap.NodeInfo) { + valid.Attributes = []*protonetmap.NodeInfo_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "", Value: "v2"}, {Key: "k3", Value: "v3"}, + } + }}, + {name: "attributes/no value", msg: "empty value of the attribute k2", corrupt: func(valid *protonetmap.NodeInfo) { + valid.Attributes = []*protonetmap.NodeInfo_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "k2", Value: ""}, {Key: "k3", Value: "v3"}, + } + }}, + {name: "attributes/capacity", msg: "invalid Capacity attribute: strconv.ParseUint: parsing \"foo\": invalid syntax", + corrupt: func(valid *protonetmap.NodeInfo) { + valid.Attributes = []*protonetmap.NodeInfo_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "Capacity", Value: "foo"}, {Key: "k3", Value: "v3"}, + } + }}, + {name: "attributes/price", msg: "invalid Price attribute: strconv.ParseUint: parsing \"foo\": invalid syntax", + corrupt: func(valid *protonetmap.NodeInfo) { + valid.Attributes = []*protonetmap.NodeInfo_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "Price", Value: "foo"}, {Key: "k3", Value: "v3"}, + } + }}, + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "state/negative", msg: "negative state -1", corrupt: func(valid *protonetmap.NodeInfo) { valid.State = -1 }}, + } + invalidNetInfoProtoTestcases = []struct { + name, msg string + corrupt func(valid *protonetmap.NetworkInfo) + }{ + {name: "netconfig/missing", msg: "missing network config", + corrupt: func(valid *protonetmap.NetworkInfo) { valid.NetworkConfig = nil }}, + {name: "netconfig/prms/missing", msg: "missing network parameters", + corrupt: func(valid *protonetmap.NetworkInfo) { valid.NetworkConfig = new(protonetmap.NetworkConfig) }}, + {name: "netconfig/prms/no value", msg: "empty attribute value k2", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("k1"), Value: []byte("v1")}, {Key: []byte("k2"), Value: nil}, {Key: []byte("k3"), Value: []byte("v3")}, + } + }}, + {name: "netconfig/prms/duplicated", msg: "duplicated parameter name: k1", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("k1"), Value: []byte("v1")}, {Key: []byte("k2"), Value: []byte("v2")}, {Key: []byte("k1"), Value: []byte("v3")}, + } + }}, + {name: "netconfig/prms/eigen trust alpha/overflow", msg: "invalid EigenTrustAlpha parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("EigenTrustAlpha"), Value: []byte("123456789")}, + } + }}, + {name: "netconfig/prms/eigen trust alpha/negative", msg: "invalid EigenTrustAlpha parameter: EigenTrust alpha value -0.50 is out of range [0, 1]", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("EigenTrustAlpha"), Value: []byte{0, 0, 0, 0, 0, 0, 224, 191}}, + } + }}, + {name: "netconfig/prms/eigen trust alpha/too big", msg: "invalid EigenTrustAlpha parameter: EigenTrust alpha value 1.50 is out of range [0, 1]", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("EigenTrustAlpha"), Value: []byte{0, 0, 0, 0, 0, 0, 248, 63}}, + } + }}, + {name: "netconfig/prms/homo hash disabled/overflow", msg: "invalid HomomorphicHashingDisabled parameter: invalid bool parameter contract format too big: integer", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("HomomorphicHashingDisabled"), Value: make([]byte, 33)}, + } + }}, + {name: "netconfig/prms/maintenance allowed/overflow", msg: "invalid MaintenanceModeAllowed parameter: invalid bool parameter contract format too big: integer", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("MaintenanceModeAllowed"), Value: make([]byte, 33)}, + } + }}, + {name: "netconfig/prms/audit fee/overflow", msg: "invalid AuditFee parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("AuditFee"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/storage price/overflow", msg: "invalid BasicIncomeRate parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("BasicIncomeRate"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/container fee/overflow", msg: "invalid ContainerFee parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("ContainerFee"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/named container fee/overflow", msg: "invalid ContainerAliasFee parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("ContainerAliasFee"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/eigen trust iterations/overflow", msg: "invalid EigenTrustIterations parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("EigenTrustIterations"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/epoch duration/overflow", msg: "invalid EpochDuration parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("EpochDuration"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/ir candidate fee/overflow", msg: "invalid InnerRingCandidateFee parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("InnerRingCandidateFee"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/max object size/overflow", msg: "invalid MaxObjectSize parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("MaxObjectSize"), Value: make([]byte, 9)}, + } + }}, + {name: "netconfig/prms/withdrawal fee/overflow", msg: "invalid WithdrawFee parameter: invalid uint64 parameter length 9", + corrupt: func(valid *protonetmap.NetworkInfo) { + valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ + {Key: []byte("WithdrawFee"), Value: make([]byte, 9)}, + } + }}, + } +) + +// returns Client-compatible Netmap service handled by given server. Provided +// server must implement [protocontainer.NetmapServiceServer]: the parameter is +// not of this type to support generics. +func newDefaultNetmapServiceDesc(t testing.TB, srv any) testService { + require.Implements(t, (*protonetmap.NetmapServiceServer)(nil), srv) 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, newDefaultNetmapServiceDesc(srv)) +// returns Client of Netmap service provided by given server. Provided server +// must implement [protonetmap.NetmapServiceServer]: the parameter is not of +// this type to support generics. +func newTestNetmapClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultNetmapServiceDesc(t, srv)) } type testNetmapSnapshotServer struct { protonetmap.UnimplementedNetmapServiceServer + testCommonUnaryServerSettings[ + *protonetmap.NetmapSnapshotRequest_Body, + v2netmap.SnapshotRequestBody, + *v2netmap.SnapshotRequestBody, + *protonetmap.NetmapSnapshotRequest, + v2netmap.SnapshotRequest, + *v2netmap.SnapshotRequest, + *protonetmap.NetmapSnapshotResponse_Body, + v2netmap.SnapshotResponseBody, + *v2netmap.SnapshotResponseBody, + *protonetmap.NetmapSnapshotResponse, + v2netmap.SnapshotResponse, + *v2netmap.SnapshotResponse, + ] +} - errTransport error - - unsignedResponse bool - - statusFail bool +// returns [protonetmap.NetmapServiceServer] supporting NetmapSnapshot method +// only. Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestNetmapSnapshotServer() *testNetmapSnapshotServer { return new(testNetmapSnapshotServer) } - unsetNetMap bool - netMap *protonetmap.Netmap +// makes the server to always respond with the given network map. By default, +// any valid network map is returned. +// +// Overrides with respondWithBody. - signer neofscrypto.Signer +func (x *testNetmapSnapshotServer) verifyRequest(req *protonetmap.NetmapSnapshotRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + return nil } 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 { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { return nil, err } - - if x.errTransport != nil { - return nil, x.errTransport + if x.handlerErr != nil { + return nil, x.handlerErr } - var nm *protonetmap.Netmap - if !x.unsetNetMap { - if x.netMap != nil { - nm = x.netMap - } else { - nm = new(protonetmap.Netmap) - } - } - resp := protonetmap.NetmapSnapshotResponse{ - Body: &protonetmap.NetmapSnapshotResponse_Body{ - Netmap: nm, - }, + resp := &protonetmap.NetmapSnapshotResponse{ + MetaHeader: x.respMeta, } - if x.statusFail { - resp.MetaHeader = &protosession.ResponseMetaHeader{ - Status: statusErr.ErrorToV2().ToGRPCMessage().(*protostatus.Status), - } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinNetmapResponseBody).(*protonetmap.NetmapSnapshotResponse_Body) } - 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, &respV2, nil) - if err != nil { - panic(fmt.Sprintf("sign response: %v", err)) - } + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) } - - return respV2.ToGRPCMessage().(*protonetmap.NetmapSnapshotResponse), nil + return resp, nil } type testGetNetworkInfoServer struct { protonetmap.UnimplementedNetmapServiceServer + testCommonUnaryServerSettings[ + *protonetmap.NetworkInfoRequest_Body, + v2netmap.NetworkInfoRequestBody, + *v2netmap.NetworkInfoRequestBody, + *protonetmap.NetworkInfoRequest, + v2netmap.NetworkInfoRequest, + *v2netmap.NetworkInfoRequest, + *protonetmap.NetworkInfoResponse_Body, + v2netmap.NetworkInfoResponseBody, + *v2netmap.NetworkInfoResponseBody, + *protonetmap.NetworkInfoResponse, + v2netmap.NetworkInfoResponse, + *v2netmap.NetworkInfoResponse, + ] } -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")}, - }, - }, - }, - }, +// returns [protonetmap.NetmapServiceServer] supporting NetworkInfo method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestNetworkInfoServer() *testGetNetworkInfoServer { return new(testGetNetworkInfoServer) } + +func (x *testGetNetworkInfoServer) verifyRequest(req *protonetmap.NetworkInfoRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) } + return nil +} - var respV2 v2netmap.NetworkInfoResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testGetNetworkInfoServer) NetworkInfo(_ context.Context, req *protonetmap.NetworkInfoRequest) (*protonetmap.NetworkInfoResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.handlerErr != nil { + return nil, x.handlerErr + } + resp := &protonetmap.NetworkInfoResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinNetInfoResponseBody).(*protonetmap.NetworkInfoResponse_Body) } - return respV2.ToGRPCMessage().(*protonetmap.NetworkInfoResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testGetNodeInfoServer struct { protonetmap.UnimplementedNetmapServiceServer - - respSigner neofscrypto.Signer - respMeta *protosession.ResponseMetaHeader - respNodePub []byte + testCommonUnaryServerSettings[ + *protonetmap.LocalNodeInfoRequest_Body, + v2netmap.LocalNodeInfoRequestBody, + *v2netmap.LocalNodeInfoRequestBody, + *protonetmap.LocalNodeInfoRequest, + v2netmap.LocalNodeInfoRequest, + *v2netmap.LocalNodeInfoRequest, + *protonetmap.LocalNodeInfoResponse_Body, + v2netmap.LocalNodeInfoResponseBody, + *v2netmap.LocalNodeInfoResponseBody, + *protonetmap.LocalNodeInfoResponse, + v2netmap.LocalNodeInfoResponse, + *v2netmap.LocalNodeInfoResponse, + ] } // returns [protonetmap.NetmapServiceServer] supporting LocalNodeInfo method @@ -137,126 +336,484 @@ type testGetNodeInfoServer struct { // 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 node public key. By +// default, any valid key is returned. +// +// Overrides respondWithBody. +func (x *testGetNodeInfoServer) respondWithNodePublicKey(pub []byte) { + b := proto.Clone(validMinNodeInfoResponseBody).(*protonetmap.LocalNodeInfoResponse_Body) + b.NodeInfo.PublicKey = pub + x.respondWithBody(b) } -// 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) verifyRequest(req *protonetmap.LocalNodeInfoRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + return nil } -// 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, req *protonetmap.LocalNodeInfoRequest) (*protonetmap.LocalNodeInfoResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr + } -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{ - Addresses: []string{"any"}, - }, - }, + resp := &protonetmap.LocalNodeInfoResponse{ MetaHeader: x.respMeta, } - if x.respNodePub != nil { - resp.Body.NodeInfo.PublicKey = x.respNodePub + if x.respBodyForced { + resp.Body = x.respBody } else { - resp.Body.NodeInfo.PublicKey = []byte("any") + resp.Body = proto.Clone(validMinNodeInfoResponseBody).(*protonetmap.LocalNodeInfoResponse_Body) } - var respV2 v2netmap.LocalNodeInfoResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) - } - 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) + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) } + return resp, nil +} - return respV2.ToGRPCMessage().(*protonetmap.LocalNodeInfoResponse), nil +func TestClient_EndpointInfo(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmEndpointInfo + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestGetNodeInfoServer() + c := newTestNetmapClient(t, srv) + + srv.authenticateRequest(c.prm.signer) + _, err := c.EndpointInfo(ctx, PrmEndpointInfo{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestGetNodeInfoServer, newTestNetmapClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.EndpointInfo(ctx, opts) + return err + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protonetmap.LocalNodeInfoResponse_Body + }{ + {name: "min", body: validMinNodeInfoResponseBody}, + {name: "full", body: validFullNodeInfoResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetNodeInfoServer() + c := newTestNetmapClient(t, srv) + + srv.respondWithBody(tc.body) + res, err := c.EndpointInfo(ctx, anyValidOpts) + require.NoError(t, err) + require.NotNil(t, res) + require.NoError(t, checkVersionTransport(res.LatestVersion(), tc.body.GetVersion())) + require.NoError(t, checkNodeInfoTransport(res.NodeInfo(), tc.body.GetNodeInfo())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestGetNodeInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "netmap.NetmapService", "LocalNodeInfo", func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestGetNodeInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protonetmap.LocalNodeInfoResponse_Body] + tcs := []testcase{{name: "missing", body: nil, assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing version field in the response") + }}} + + type corruptedBodyTestcase = struct { + name string + corrupt func(valid *protonetmap.LocalNodeInfoResponse_Body) + assertErr func(testing.TB, error) + } + // missing fields + ctcs := []corruptedBodyTestcase{ + {name: "version/missing", corrupt: func(valid *protonetmap.LocalNodeInfoResponse_Body) { valid.Version = nil }, + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing version field in the response") + }}, + {name: "node info/missing", corrupt: func(valid *protonetmap.LocalNodeInfoResponse_Body) { valid.NodeInfo = nil }, + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing node info field in the response") + }}, + } + // invalid node info + for _, tc := range invalidNodeInfoProtoTestcases { + ctcs = append(ctcs, corruptedBodyTestcase{ + name: "node info/" + tc.name, + corrupt: func(valid *protonetmap.LocalNodeInfoResponse_Body) { tc.corrupt(valid.NodeInfo) }, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid node info field in the response: "+tc.msg) + }, + }) + } + + for _, tc := range ctcs { + body := proto.Clone(validMinNodeInfoResponseBody).(*protonetmap.LocalNodeInfoResponse_Body) + tc.corrupt(body) + tcs = append(tcs, testcase{name: tc.name, body: body, assertErr: tc.assertErr}) + } + + testInvalidResponseBodies(t, newTestGetNodeInfoServer, newTestNetmapClient, tcs, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestGetNodeInfoServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestGetNodeInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestGetNodeInfoServer, newDefaultNetmapServiceDesc, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestGetNodeInfoServer, newDefaultNetmapServiceDesc, stat.MethodEndpointInfo, + nil, nil, func(c *Client) error { + _, err := c.EndpointInfo(ctx, anyValidOpts) + return err + }, + ) + }) } func TestClient_NetMapSnapshot(t *testing.T) { - var err error - var prm PrmNetMapSnapshot - var res netmap.NetMap - var srv testNetmapSnapshotServer - - signer := neofscryptotest.Signer() - - srv.signer = signer - - c := newTestNetmapClient(t, &srv) ctx := context.Background() + var anyValidOpts PrmNetMapSnapshot + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestNetmapSnapshotServer() + c := newTestNetmapClient(t, srv) + + srv.authenticateRequest(c.prm.signer) + _, err := c.NetMapSnapshot(ctx, PrmNetMapSnapshot{}) + require.NoError(t, err) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protonetmap.NetmapSnapshotResponse_Body + }{ + {name: "min", body: validMinNetmapResponseBody}, + {name: "full", body: validFullNetmapResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestNetmapSnapshotServer() + c := newTestNetmapClient(t, srv) + + srv.respondWithBody(tc.body) + res, err := c.NetMapSnapshot(ctx, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkNetmapTransport(res, tc.body.GetNetmap())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestNetmapSnapshotServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "netmap.NetmapService", "NetmapSnapshot", func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestNetmapSnapshotServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protonetmap.NetmapSnapshotResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing network map field in the response") + }}, + {name: "empty", body: new(protonetmap.NetmapSnapshotResponse_Body), + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, ErrMissingResponseField) + require.EqualError(t, err, "missing network map field in the response") + }}, + } + + // 1. network map + for _, tc := range invalidNodeInfoProtoTestcases { + body := &protonetmap.NetmapSnapshotResponse_Body{ + Netmap: proto.Clone(validFullProtoNetmap).(*protonetmap.Netmap), + } + tc.corrupt(body.Netmap.Nodes[1]) + tcs = append(tcs, testcase{ + name: "network map/node info/" + tc.name, + body: body, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid network map field in the response: invalid node info: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newTestNetmapSnapshotServer, newTestNetmapClient, tcs, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestNetmapSnapshotServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/654") + testTransportFailure(t, newTestNetmapSnapshotServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/654") + testUnaryResponseCallback(t, newTestNetmapSnapshotServer, newDefaultNetmapServiceDesc, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestNetmapSnapshotServer, newDefaultNetmapServiceDesc, stat.MethodNetMapSnapshot, + nil, nil, func(c *Client) error { + _, err := c.NetMapSnapshot(ctx, anyValidOpts) + return err + }, + ) + }) +} - // transport error - srv.errTransport = errors.New("any error") - - _, err = c.NetMapSnapshot(ctx, prm) - 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 - - // unsigned response - srv.unsignedResponse = true - _, err = c.NetMapSnapshot(ctx, prm) - require.Error(t, err) - srv.unsignedResponse = false - - // failure error - srv.statusFail = true - _, err = c.NetMapSnapshot(ctx, prm) - require.Error(t, err) - require.ErrorIs(t, err, apistatus.ErrServerInternal) - - srv.statusFail = false - - // missing netmap field - srv.unsetNetMap = true - _, err = c.NetMapSnapshot(ctx, prm) - require.Error(t, err) - - srv.unsetNetMap = false - - // invalid network map - 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 - 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.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) +func TestClient_NetworkInfo(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmNetworkInfo + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestNetworkInfoServer() + c := newTestNetmapClient(t, srv) + + srv.authenticateRequest(c.prm.signer) + _, err := c.NetworkInfo(ctx, anyValidOpts) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + testRequestXHeaders(t, newTestNetworkInfoServer, newTestNetmapClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.NetworkInfo(ctx, opts) + return err + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protonetmap.NetworkInfoResponse_Body + }{ + {name: "min", body: validMinNetInfoResponseBody}, + {name: "full", body: validFullNetInfoResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestNetworkInfoServer() + c := newTestNetmapClient(t, srv) + + srv.respondWithBody(tc.body) + res, err := c.NetworkInfo(ctx, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkNetInfoTransport(res, tc.body.GetNetworkInfo())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestNetworkInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "netmap.NetmapService", "NetworkInfo", func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestNetworkInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protonetmap.NetworkInfoResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing network info field in the response") + }}, + {name: "empty", body: new(protonetmap.NetworkInfoResponse_Body), + assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, ErrMissingResponseField) + require.EqualError(t, err, "missing network info field in the response") + }}, + } + + // 1. net info + for _, tc := range invalidNetInfoProtoTestcases { + body := &protonetmap.NetworkInfoResponse_Body{ + NetworkInfo: proto.Clone(validFullProtoNetInfo).(*protonetmap.NetworkInfo), + } + tc.corrupt(body.NetworkInfo) + tcs = append(tcs, testcase{ + name: "network info/" + tc.name, + body: body, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid network info field in the response: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newTestNetworkInfoServer, newTestNetmapClient, tcs, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestNetworkInfoServer, newTestContainerClient, func(ctx context.Context, c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestNetworkInfoServer, newTestNetmapClient, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestNetworkInfoServer, newDefaultNetmapServiceDesc, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestNetworkInfoServer, newDefaultNetmapServiceDesc, stat.MethodNetworkInfo, + nil, nil, func(c *Client) error { + _, err := c.NetworkInfo(ctx, anyValidOpts) + return err + }, + ) + }) } diff --git a/client/object_delete_test.go b/client/object_delete_test.go index 93bf875e..2a41642e 100644 --- a/client/object_delete_test.go +++ b/client/object_delete_test.go @@ -2,49 +2,286 @@ package client import ( "context" + "errors" "fmt" "testing" + "time" apiobject "github.com/nspcc-dev/neofs-api-go/v2/object" 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" + bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) type testDeleteObjectServer struct { protoobject.UnimplementedObjectServiceServer + testCommonUnaryServerSettings[ + *protoobject.DeleteRequest_Body, + apiobject.DeleteRequestBody, + *apiobject.DeleteRequestBody, + *protoobject.DeleteRequest, + apiobject.DeleteRequest, + *apiobject.DeleteRequest, + *protoobject.DeleteResponse_Body, + apiobject.DeleteResponseBody, + *apiobject.DeleteResponseBody, + *protoobject.DeleteResponse, + apiobject.DeleteResponse, + *apiobject.DeleteResponse, + ] + testObjectSessionServerSettings + testBearerTokenServerSettings + testObjectAddressServerSettings } -func (x *testDeleteObjectServer) Delete(context.Context, *protoobject.DeleteRequest) (*protoobject.DeleteResponse, error) { - id := oidtest.ID() - resp := protoobject.DeleteResponse{ - Body: &protoobject.DeleteResponse_Body{ - Tombstone: &protorefs.Address{ - ObjectId: &protorefs.ObjectID{Value: id[:]}, - }, - }, +// returns [protoobject.ObjectServiceServer] supporting Delete method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestDeleteObjectServer() *testDeleteObjectServer { return new(testDeleteObjectServer) } + +func (x *testDeleteObjectServer) verifyRequest(req *protoobject.DeleteRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + if req.MetaHeader.Ttl != 2 { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", req.MetaHeader.Ttl)) + } + if err := x.verifySessionToken(req.MetaHeader.SessionToken); err != nil { + return err + } + if err := x.verifyBearerToken(req.MetaHeader.BearerToken); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyObjectAddress(body.Address); err != nil { + return err + } + return nil +} + +func (x *testDeleteObjectServer) Delete(_ context.Context, req *protoobject.DeleteRequest) (*protoobject.DeleteResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr } - var respV2 apiobject.DeleteResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + resp := &protoobject.DeleteResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validFullDeleteObjectResponseBody).(*protoobject.DeleteResponse_Body) } - return respV2.ToGRPCMessage().(*protoobject.DeleteResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } func TestClient_ObjectDelete(t *testing.T) { - t.Run("missing signer", func(t *testing.T) { - c := newClient(t) + ctx := context.Background() + var anyValidOpts PrmObjectDelete + anyCID := cidtest.ID() + anyOID := oidtest.ID() + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestDeleteObjectServer() + c := newTestObjectClient(t, srv) - _, err := c.ObjectDelete(context.Background(), cid.ID{}, oid.ID{}, nil, PrmObjectDelete{}) - require.ErrorIs(t, err, ErrMissingSigner) + srv.checkRequestObjectAddress(anyCID, anyOID) + srv.authenticateRequest(anyValidSigner) + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, PrmObjectDelete{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestDeleteObjectServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, opts) + return err + }) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestDeleteObjectServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestDeleteObjectServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoobject.DeleteResponse_Body + }{ + {name: "min", body: validMinDeleteObjectResponseBody}, + {name: "full", body: validFullDeleteObjectResponseBody}, + {name: "invalid container ID", body: &protoobject.DeleteResponse_Body{ + Tombstone: &protorefs.Address{ + ContainerId: &protorefs.ContainerID{Value: []byte("any_invalid")}, + ObjectId: proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + }, + }}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestDeleteObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(tc.body) + id, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + require.NoError(t, checkObjectIDTransport(id, tc.body.GetTombstone().GetObjectId())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestDeleteObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "Delete", func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestDeleteObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protoobject.DeleteResponse_Body] + // missing fields + tcs := []testcase{ + {name: "nil", body: nil, assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing tombstone field in the response") + }}, + {name: "empty", body: new(protoobject.DeleteResponse_Body), assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing tombstone field in the response") + }}, + {name: "tombstone address/object ID/missing", body: &protoobject.DeleteResponse_Body{ + Tombstone: new(protorefs.Address), + }, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, ErrMissingResponseField) + require.EqualError(t, err, "missing tombstone field in the response") + }}, + } + // tombstone ID + for _, tc := range invalidObjectIDProtoTestcases { + id := proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID) + tc.corrupt(id) + tcs = append(tcs, testcase{ + name: "tombstone address/object ID/" + tc.name, + body: &protoobject.DeleteResponse_Body{Tombstone: &protorefs.Address{ObjectId: id}}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid tombstone field in the response: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newTestDeleteObjectServer, newTestObjectClient, tcs, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestDeleteObjectServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).ObjectDelete(ctx, anyCID, anyOID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestDeleteObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/654") + testUnaryResponseCallback(t, newTestDeleteObjectServer, newDefaultObjectService, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestDeleteObjectServer, newDefaultObjectService, stat.MethodObjectDelete, + []testedClientOp{func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, nil, anyValidOpts) + return err + }}, nil, func(c *Client) error { + _, err := c.ObjectDelete(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }, + ) }) } diff --git a/client/object_get_test.go b/client/object_get_test.go index f9479d0a..25c6c4ec 100644 --- a/client/object_get_test.go +++ b/client/object_get_test.go @@ -2,135 +2,1954 @@ package client import ( "context" + "errors" "fmt" + "io" + "math" + "math/rand" "testing" + "testing/iotest" + "time" 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" - oid "github.com/nspcc-dev/neofs-sdk-go/object/id" + 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" + bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" + apistatus "github.com/nspcc-dev/neofs-sdk-go/client/status" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + "github.com/nspcc-dev/neofs-sdk-go/object" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) +func setPayloadLengthInHeadingGetResponse(b *protoobject.GetResponse_Body, ln uint64) *protoobject.GetResponse_Body { + b = proto.Clone(b).(*protoobject.GetResponse_Body) + in := b.GetInit() + if in == nil { + in = new(protoobject.GetResponse_Body_Init) + b.ObjectPart = &protoobject.GetResponse_Body_Init_{Init: in} + } + h := in.GetHeader() + if h == nil { + h = new(protoobject.Header) + in.Header = h + } + h.PayloadLength = ln + return b +} + +func setChunkInGetResponse(b *protoobject.GetResponse_Body, c []byte) *protoobject.GetResponse_Body { + b = proto.Clone(b).(*protoobject.GetResponse_Body) + b.ObjectPart.(*protoobject.GetResponse_Body_Chunk).Chunk = c + return b +} + +func setChunkInRangeResponse(b *protoobject.GetRangeResponse_Body, c []byte) *protoobject.GetRangeResponse_Body { + b = proto.Clone(b).(*protoobject.GetRangeResponse_Body) + b.RangePart.(*protoobject.GetRangeResponse_Body_Chunk).Chunk = c + return b +} + +func checkSuccessfulGetObjectTransport(t testing.TB, hb *protoobject.GetResponse_Body, payload []byte, h object.Object, r io.Reader, err error) { + require.NoError(t, err) + require.NoError(t, iotest.TestReader(r, payload)) + id := h.GetID() + require.False(t, id.IsZero()) + in := hb.GetInit() + require.NoError(t, checkObjectIDTransport(id, in.GetObjectId())) + require.NoError(t, checkObjectHeaderWithSignatureTransport(h, &protoobject.HeaderWithSignature{ + Header: in.GetHeader(), + Signature: in.GetSignature(), + })) +} + +type testCommonReadObjectRequestServerSettings struct { + testObjectSessionServerSettings + testBearerTokenServerSettings + testObjectAddressServerSettings + testLocalRequestServerSettings + reqRaw bool +} + +// makes the server to assert that any request is with set raw flag. By default, +// the flag must be unset. +func (x *testCommonReadObjectRequestServerSettings) checkRequestRaw() { x.reqRaw = true } + +func (x *testCommonReadObjectRequestServerSettings) verifyRawFlag(raw bool) error { + if x.reqRaw != raw { + return newErrInvalidRequestField("raw flag", fmt.Errorf("unexpected value (client: %t, message: %t)", + x.reqRaw, raw)) + } + return nil +} + +func (x *testCommonReadObjectRequestServerSettings) verifyMeta(m *protosession.RequestMetaHeader) error { + // TTL + if err := x.verifyTTL(m); err != nil { + return err + } + // session token + if err := x.verifySessionToken(m.GetSessionToken()); err != nil { + return err + } + // bearer token + if err := x.verifyBearerToken(m.GetBearerToken()); err != nil { + return err + } + return nil +} + type testGetObjectServer struct { protoobject.UnimplementedObjectServiceServer + testCommonServerStreamServerSettings[ + *protoobject.GetRequest_Body, + apiobject.GetRequestBody, + *apiobject.GetRequestBody, + *protoobject.GetRequest, + apiobject.GetRequest, + *apiobject.GetRequest, + *protoobject.GetResponse_Body, + apiobject.GetResponseBody, + *apiobject.GetResponseBody, + *protoobject.GetResponse, + apiobject.GetResponse, + *apiobject.GetResponse, + ] + testCommonReadObjectRequestServerSettings + chunk []byte } -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), - }, - }, +// returns [protoobject.ObjectServiceServer] supporting Get method only. Default +// implementation performs common verification of any request, and responds with +// any valid message stream. Some methods allow to tune the behavior. +func newTestGetObjectServer() *testGetObjectServer { return new(testGetObjectServer) } + +// makes the server to return given chunk in any chunk response. By default, and +// if nil, some non-empty data chunk is returned. +func (x *testGetObjectServer) respondWithChunk(chunk []byte) { x.chunk = chunk } + +// makes the server to respond with given heading part and chunk responses. +// Returns heading response message. +// +// Overrides configured len(chunks)+1 responses. +func (x *testGetObjectServer) respondWithObject(h *protoobject.GetResponse_Body_Init, chunks [][]byte) *protoobject.GetResponse_Body { + var ln uint64 + for i := range chunks { + b := setChunkInGetResponse(validFullChunkObjectGetResponseBody, chunks[i]) + x.respondWithBody(uint(i)+1, b) + ln += uint64(len(chunks[i])) } + b := setPayloadLengthInHeadingGetResponse(&protoobject.GetResponse_Body{ + ObjectPart: &protoobject.GetResponse_Body_Init_{Init: h}, + }, ln) + x.respondWithBody(0, b) + return b +} - var respV2 apiobject.GetResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testGetObjectServer) verifyRequest(req *protoobject.GetRequest) error { + if err := x.testCommonServerStreamServerSettings.verifyRequest(req); err != nil { + return err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return fmt.Errorf("sign response message: %w", err) + // meta header + if err := x.verifyMeta(req.MetaHeader); err != nil { + return err } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyObjectAddress(body.Address); err != nil { + return err + } + // 2. raw + return x.verifyRawFlag(body.Raw) +} - return stream.SendMsg(respV2.ToGRPCMessage().(*protoobject.GetResponse)) +func (x *testGetObjectServer) Get(req *protoobject.GetRequest, stream protoobject.ObjectService_GetServer) error { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return err + } + if x.handlerErrForced { + return x.handlerErr + } + lastRespInd := uint(1) + if x.resps != nil { + lastRespInd = 0 + } + for n := range x.resps { + if n > lastRespInd { + lastRespInd = n + } + } + if x.respErrN > lastRespInd { + lastRespInd = x.respErrN + } + chunk := x.chunk + if chunk == nil { + chunk = []byte("Hello, world!") + } + for n := range lastRespInd + 1 { + s := x.resps[n] + resp := &protoobject.GetResponse{ + MetaHeader: s.respMeta, + } + if s.respBodyForced { + resp.Body = s.respBody + } else { + if n == 0 { + resp.Body = proto.Clone(validFullHeadingObjectGetResponseBody).(*protoobject.GetResponse_Body) + if lastRespInd > 0 { + resp.Body = setPayloadLengthInHeadingGetResponse(resp.Body, uint64(lastRespInd-1)*uint64(len(chunk))) + } + } else { + resp.Body = setChunkInGetResponse(validFullChunkObjectGetResponseBody, chunk) + } + } + var err error + resp.VerifyHeader, err = s.signResponse(resp) + if err != nil { + return fmt.Errorf("sign response: %w", err) + } + if err := stream.Send(resp); err != nil { + return fmt.Errorf("send response #%d: %w", n, err) + } + if x.respErrN > 0 && n >= x.respErrN-1 { + return x.respErr + } + } + return nil } type testGetObjectPayloadRangeServer struct { protoobject.UnimplementedObjectServiceServer + testCommonServerStreamServerSettings[ + *protoobject.GetRangeRequest_Body, + apiobject.GetRangeRequestBody, + *apiobject.GetRangeRequestBody, + *protoobject.GetRangeRequest, + apiobject.GetRangeRequest, + *apiobject.GetRangeRequest, + *protoobject.GetRangeResponse_Body, + apiobject.GetRangeResponseBody, + *apiobject.GetRangeResponseBody, + *protoobject.GetRangeResponse, + apiobject.GetRangeResponse, + *apiobject.GetRangeResponse, + ] + testCommonReadObjectRequestServerSettings + chunk []byte + reqRng *protoobject.Range } -func (x *testGetObjectPayloadRangeServer) GetRange(req *protoobject.GetRangeRequest, stream protoobject.ObjectService_GetRangeServer) error { - ln := req.GetBody().GetRange().GetLength() - if ln == 0 { - return nil - } +// returns [protoobject.ObjectServiceServer] supporting GetRange method only. +// Default implementation performs common verification of any request, and +// responds with any valid message stream. Some methods allow to tune the +// behavior. +func newTestObjectPayloadRangeServer() *testGetObjectPayloadRangeServer { + return new(testGetObjectPayloadRangeServer) +} - resp := protoobject.GetRangeResponse{ - Body: &protoobject.GetRangeResponse_Body{ - RangePart: &protoobject.GetRangeResponse_Body_Chunk{ - Chunk: make([]byte, ln), - }, - }, +// makes the server to assert that any request carries given range. By default, +// any valid range is accepted. +func (x *testGetObjectPayloadRangeServer) checkRequestRange(off, ln uint64) { + x.reqRng = &protoobject.Range{Offset: off, Length: ln} +} + +// makes the server to return given chunk in any chunk response. By default, and +// if nil, some non-empty data chunk is returned. +func (x *testGetObjectPayloadRangeServer) respondWithChunk(chunk []byte) { x.chunk = chunk } + +// makes the server to respond with given chunk responses. +// +// Overrides configured len(chunks) responses. +func (x *testGetObjectPayloadRangeServer) respondWithChunks(chunks [][]byte) { + for i := range chunks { + b := setChunkInRangeResponse(validFullChunkObjectRangeResponseBody, chunks[i]) + x.respondWithBody(uint(i), b) } +} - var respV2 apiobject.GetRangeResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testGetObjectPayloadRangeServer) verifyRequest(req *protoobject.GetRangeRequest) error { + if err := x.testCommonServerStreamServerSettings.verifyRequest(req); err != nil { + return err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return fmt.Errorf("sign response message: %w", err) + // meta header + if err := x.verifyMeta(req.MetaHeader); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyObjectAddress(body.Address); err != nil { + return err + } + // 2. range + if body.Range == nil { + return newErrMissingRequestBodyField("range") + } + if body.Range.Length == 0 { + return newErrInvalidRequestField("range", errors.New("zero length")) + } + if x.reqRng != nil { + if v1, v2 := x.reqRng.GetOffset(), body.Range.GetOffset(); v1 != v2 { + return newErrInvalidRequestField("range", fmt.Errorf("offset (client: %d, message: %d)", v1, v2)) + } + if v1, v2 := x.reqRng.GetLength(), body.Range.GetLength(); v1 != v2 { + return newErrInvalidRequestField("range", fmt.Errorf("length (client: %d, message: %d)", v1, v2)) + } } + // 3. raw + return x.verifyRawFlag(body.Raw) +} - return stream.SendMsg(respV2.ToGRPCMessage().(*protoobject.GetRangeResponse)) +func (x *testGetObjectPayloadRangeServer) GetRange(req *protoobject.GetRangeRequest, stream protoobject.ObjectService_GetRangeServer) error { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return err + } + if x.handlerErrForced { + return x.handlerErr + } + lastRespInd := uint(1) + if x.resps != nil { + lastRespInd = 0 + } + for n := range x.resps { + if n > lastRespInd { + lastRespInd = n + } + } + if x.respErrN > lastRespInd { + lastRespInd = x.respErrN + } + chunk := x.chunk + if chunk == nil { + chunk = []byte("Hello, world!") + } + for n := range lastRespInd + 1 { + s := x.resps[n] + resp := &protoobject.GetRangeResponse{ + MetaHeader: s.respMeta, + } + if s.respBodyForced { + resp.Body = s.respBody + } else { + resp.Body = setChunkInRangeResponse(validFullChunkObjectRangeResponseBody, chunk) + } + var err error + resp.VerifyHeader, err = s.signResponse(resp) + if err != nil { + return fmt.Errorf("sign response: %w", err) + } + if err := stream.Send(resp); err != nil { + return fmt.Errorf("send response #%d: %w", n, err) + } + if x.respErrN > 0 && n >= x.respErrN-1 { + return x.respErr + } + } + return nil } type testHeadObjectServer struct { protoobject.UnimplementedObjectServiceServer + testCommonUnaryServerSettings[ + *protoobject.HeadRequest_Body, + apiobject.HeadRequestBody, + *apiobject.HeadRequestBody, + *protoobject.HeadRequest, + apiobject.HeadRequest, + *apiobject.HeadRequest, + *protoobject.HeadResponse_Body, + apiobject.HeadResponseBody, + *apiobject.HeadResponseBody, + *protoobject.HeadResponse, + apiobject.HeadResponse, + *apiobject.HeadResponse, + ] + testCommonReadObjectRequestServerSettings } -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), - }, - }, +// returns [protoobject.ObjectServiceServer] supporting Head method +// only. Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestHeadObjectServer() *testHeadObjectServer { + return new(testHeadObjectServer) +} + +func (x *testHeadObjectServer) verifyRequest(req *protoobject.HeadRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + if err := x.verifyMeta(req.MetaHeader); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyObjectAddress(body.Address); err != nil { + return err } + // 2. main only + if body.MainOnly { + return newErrInvalidRequestField("main only flag", fmt.Errorf("unexpected value (client: %t, message: %t)", false, body.MainOnly)) + } + // 3. raw + return x.verifyRawFlag(body.Raw) +} - var respV2 apiobject.HeadResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testHeadObjectServer) Head(_ context.Context, req *protoobject.HeadRequest) (*protoobject.HeadResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.handlerErr != nil { + return nil, x.handlerErr } - return respV2.ToGRPCMessage().(*protoobject.HeadResponse), nil + resp := &protoobject.HeadResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinObjectHeadResponseBody).(*protoobject.HeadResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } -func TestClient_Get(t *testing.T) { - t.Run("missing signer", func(t *testing.T) { +func TestClient_ObjectHead(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmObjectHead + anyCID := cidtest.ID() + anyOID := oidtest.ID() + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + srv.checkRequestObjectAddress(anyCID, anyOID) + srv.authenticateRequest(anyValidSigner) + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, PrmObjectHead{}) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestHeadObjectServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, opts) + return err + }) + }) + t.Run("local", func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("raw", func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkRaw() + + srv.checkRequestRaw() + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + type testcase = struct { + name string + body *protoobject.HeadResponse_Body + assert func(testing.TB, *protoobject.HeadResponse_Body, object.Object, error) + } + var tcs []testcase + for _, tc := range []struct { + name string + body *protoobject.HeadResponse_Body + }{ + {name: "min", body: validMinObjectSplitInfoHeadResponseBody}, + {name: "full", body: validFullObjectSplitInfoHeadResponseBody}, + } { + tcs = append(tcs, testcase{name: "split info/" + tc.name, body: tc.body, + assert: func(t testing.TB, body *protoobject.HeadResponse_Body, _ object.Object, err error) { + var e *object.SplitInfoError + require.ErrorAs(t, err, &e) + require.NoError(t, checkSplitInfoTransport(*e.SplitInfo(), body.GetSplitInfo())) + }}) + } + for _, tc := range []struct { + name string + body *protoobject.HeadResponse_Body + }{ + {name: "min", body: validMinObjectHeadResponseBody}, + {name: "full", body: validFullObjectHeadResponseBody}, + } { + tcs = append(tcs, testcase{name: "header with signature/" + tc.name, body: tc.body, + assert: func(t testing.TB, body *protoobject.HeadResponse_Body, hdr object.Object, err error) { + require.NoError(t, err) + require.NoError(t, checkObjectHeaderWithSignatureTransport(hdr, body.GetHeader())) + }}) + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + srv := newTestHeadObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(tc.body) + hdr, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + if err != nil { + tc.assert(t, tc.body, object.Object{}, err) + } else { + tc.assert(t, tc.body, *hdr, err) + } + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestHeadObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "Head", func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestHeadObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protoobject.HeadResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type ") + }}, + {name: "empty", body: new(protoobject.HeadResponse_Body), + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type ") + }}, + {name: "short header oneof/nil", body: &protoobject.HeadResponse_Body{Head: (*protoobject.HeadResponse_Body_ShortHeader)(nil)}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type ") + }}, + {name: "short header oneof/empty", body: &protoobject.HeadResponse_Body{Head: new(protoobject.HeadResponse_Body_ShortHeader)}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type *object.ShortHeader") + }}, + {name: "split info oneof/nil", body: &protoobject.HeadResponse_Body{Head: (*protoobject.HeadResponse_Body_SplitInfo)(nil)}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type ") + }}, + {name: "split info oneof/empty", body: &protoobject.HeadResponse_Body{Head: new(protoobject.HeadResponse_Body_SplitInfo)}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid split info: neither link object ID nor last part object ID is set") + }}, + {name: "header oneof/nil", body: &protoobject.HeadResponse_Body{Head: (*protoobject.HeadResponse_Body_Header)(nil)}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "unexpected header type ") + }}, + {name: "header oneof/empty", body: &protoobject.HeadResponse_Body{Head: new(protoobject.HeadResponse_Body_Header)}, + assertErr: func(t testing.TB, err error) { + require.ErrorAs(t, err, new(MissingResponseFieldErr)) + require.EqualError(t, err, "missing signature field in the response") + }}, + {name: "header oneof/missing header", body: &protoobject.HeadResponse_Body{Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Signature: proto.Clone(validMinProtoSignature).(*protorefs.Signature), + }, + }}, + assertErr: func(t testing.TB, err error) { + require.ErrorAs(t, err, new(MissingResponseFieldErr)) + require.EqualError(t, err, "missing header field in the response") + }}, + {name: "header oneof/missing signature", body: &protoobject.HeadResponse_Body{ + Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: proto.Clone(validMinObjectHeader).(*protoobject.Header), + }, + }}, + assertErr: func(t testing.TB, err error) { + require.ErrorAs(t, err, new(MissingResponseFieldErr)) + require.EqualError(t, err, "missing signature field in the response") + }}, + } + for _, tc := range invalidObjectHeaderProtoTestcases { + hdr := proto.Clone(validFullObjectHeader).(*protoobject.Header) + tc.corrupt(hdr) + tcs = append(tcs, testcase{ + name: "header oneof/header/" + tc.name, + body: &protoobject.HeadResponse_Body{Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: hdr, + Signature: proto.Clone(validMinProtoSignature).(*protorefs.Signature), + }, + }}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid header response: invalid header: "+tc.msg) + }, + }) + } + for _, tc := range invalidSignatureProtoTestcases { + sig := proto.Clone(validFullProtoSignature).(*protorefs.Signature) + tc.corrupt(sig) + tcs = append(tcs, testcase{ + name: "header oneof/signature/" + tc.name, + body: &protoobject.HeadResponse_Body{Head: &protoobject.HeadResponse_Body_Header{ + Header: &protoobject.HeaderWithSignature{ + Header: proto.Clone(validMinObjectHeader).(*protoobject.Header), + Signature: sig, + }, + }}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid header response: invalid header: "+tc.msg) + }, + }) + } + for _, tc := range invalidObjectSplitInfoProtoTestcases { + si := proto.Clone(validFullSplitInfo).(*protoobject.SplitInfo) + tc.corrupt(si) + tcs = append(tcs, testcase{ + name: "split info oneof/split info/" + tc.name, + body: &protoobject.HeadResponse_Body{Head: &protoobject.HeadResponse_Body_SplitInfo{ + SplitInfo: si, + }}, + assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid split info: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newTestHeadObjectServer, newTestObjectClient, tcs, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { c := newClient(t) - ctx := context.Background() - - var nonilAddr v2refs.Address - nonilAddr.SetObjectID(new(v2refs.ObjectID)) - nonilAddr.SetContainerID(new(v2refs.ContainerID)) - - tt := []struct { - name string - methodCall func() error - }{ - { - "get", - func() error { - _, _, err := c.ObjectGetInit(ctx, cid.ID{}, oid.ID{}, nil, PrmObjectGet{prmObjectRead: prmObjectRead{}}) - return err - }, - }, - { - "get_range", - func() error { - _, err := c.ObjectRangeInit(ctx, cid.ID{}, oid.ID{}, 0, 1, nil, PrmObjectRange{prmObjectRead: prmObjectRead{}}) - return err - }, - }, - { - "get_head", - func() error { - _, err := c.ObjectHead(ctx, cid.ID{}, oid.ID{}, nil, PrmObjectHead{prmObjectRead: prmObjectRead{}}) - return err - }, + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectHead(ctx, anyCID, anyOID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestHeadObjectServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).ObjectHead(ctx, anyCID, anyOID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestHeadObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/654") + testUnaryResponseCallback(t, newTestHeadObjectServer, newDefaultObjectService, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestHeadObjectServer, newDefaultObjectService, stat.MethodObjectHead, + []testedClientOp{func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, nil, anyValidOpts) + return err + }}, nil, func(c *Client) error { + _, err := c.ObjectHead(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err }, + ) + }) +} + +func TestClient_ObjectGetInit(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmObjectGet + anyCID := cidtest.ID() + anyOID := oidtest.ID() + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.checkRequestObjectAddress(anyCID, anyOID) + srv.authenticateRequest(anyValidSigner) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, PrmObjectGet{}) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestGetObjectServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, opts) + return err + }) + }) + t.Run("local", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("raw", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkRaw() + + srv.checkRequestRaw() + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + t.Run("split info", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoobject.GetResponse_Body + }{ + {name: "min", body: validMinObjectSplitInfoGetResponseBody}, + {name: "full", body: validFullObjectSplitInfoGetResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(0, tc.body) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + var e *object.SplitInfoError + require.ErrorAs(t, err, &e) + require.NoError(t, checkSplitInfoTransport(*e.SplitInfo(), tc.body.GetSplitInfo())) + }) + } + }) + t.Run("header", func(t *testing.T) { + const bigChunkSize = 4<<20 - object.MaxHeaderLen + bigChunkTwice := make([]byte, 2*bigChunkSize) + //nolint:staticcheck // OK for this test + rand.Read(bigChunkTwice) + type bodies = struct { + heading *protoobject.GetResponse_Body + chunks [][]byte + } + type testcase = struct { + name string + bodies + assert func(testing.TB, bodies, object.Object, io.Reader, error) + } + var tcs []testcase + assertObject := func(t testing.TB, bs bodies, hdr object.Object, r io.Reader, err error) { + checkSuccessfulGetObjectTransport(t, bs.heading, join(bs.chunks), hdr, r, err) + } + for _, tc := range []struct { + name string + heading *protoobject.GetResponse_Body + }{ + {name: "min", heading: validMinHeadingObjectGetResponseBody}, + {name: "full", heading: validFullHeadingObjectGetResponseBody}, + } { + tcs = append(tcs, + testcase{ + name: tc.name + " without payload", bodies: bodies{heading: tc.heading}, + assert: assertObject, + }, + testcase{ + name: tc.name + " with single payload chunk", bodies: bodies{ + heading: tc.heading, + chunks: [][]byte{[]byte("Hello, world!")}, + }, assert: assertObject, + }, + testcase{name: tc.name + " with multiple payload chunks", bodies: bodies{ + heading: tc.heading, + chunks: [][]byte{bigChunkTwice[:bigChunkSize], []byte("small"), {}, bigChunkTwice[bigChunkSize:]}, + }, assert: assertObject}, + ) + } + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + h := srv.respondWithObject(proto.Clone(tc.heading.GetInit()).(*protoobject.GetResponse_Body_Init), tc.chunks) + hdr, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + tc.assert(t, bodies{heading: h, chunks: tc.chunks}, hdr, r, err) + }) + } + }) + }) + t.Run("statuses", func(t *testing.T) { + t.Run("no payload", func(t *testing.T) { + t.Run("header", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + hb := srv.respondWithObject(validFullHeadingObjectGetResponseBody.GetInit(), nil) + hdr, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + checkSuccessfulGetObjectTransport(t, hb, nil, hdr, r, err) + }) + t.Run("not OK", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + var code uint32 + for code == 0 { + code = rand.Uint32() + } + + srv.respondWithObject(validFullHeadingObjectGetResponseBody.GetInit(), nil) + srv.respondWithStatus(0, &protostatus.Status{Code: code}) + //nolint:staticcheck // drop with t.Skip() + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + t.Skip("currently ignores header and returns status error") + require.EqualError(t, err, fmt.Sprintf("split info response returned with non-OK status code = %d", code)) + }) + }) + t.Run("split info", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + body := validFullObjectSplitInfoGetResponseBody + srv.respondWithBody(0, body) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + var e *object.SplitInfoError + require.ErrorAs(t, err, &e) + require.NoError(t, checkSplitInfoTransport(*e.SplitInfo(), body.GetSplitInfo())) + }) + t.Run("not OK", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + var code uint32 + for code == 0 { + code = rand.Uint32() + } + + srv.respondWithBody(0, validFullObjectSplitInfoGetResponseBody) + srv.respondWithStatus(0, &protostatus.Status{Code: code}) + //nolint:staticcheck // drop with t.Skip() + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + t.Skip("currently ignores split info and returns status error") + require.EqualError(t, err, fmt.Sprintf("split info response returned with non-OK status code = %d", code)) + }) + }) + }) + t.Run("with payload", func(t *testing.T) { + test := func(t testing.TB, code uint32, + assert func(testing.TB, *protoobject.GetResponse_Body, []byte, object.Object, io.Reader, error)) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + chunks := [][]byte{[]byte("one"), []byte("two"), []byte("three")} + payload := join(chunks) + + hb := srv.respondWithObject(validFullHeadingObjectGetResponseBody.GetInit(), chunks) + srv.respondWithStatus(uint(len(chunks)), &protostatus.Status{Code: code}) + hdr, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + assert(t, hb, payload, hdr, r, err) + } + t.Run("OK", func(t *testing.T) { + test(t, 0, func(t testing.TB, hb *protoobject.GetResponse_Body, payload []byte, hdr object.Object, r io.Reader, err error) { + checkSuccessfulGetObjectTransport(t, hb, payload, hdr, r, err) + }) + }) + t.Run("failure", func(t *testing.T) { + test := func(t testing.TB, code uint32, assert func(t testing.TB, err error)) { + test(t, code, func(t testing.TB, _ *protoobject.GetResponse_Body, _ []byte, _ object.Object, r io.Reader, err error) { + require.NoError(t, err) + _, err = io.ReadAll(r) + assert(t, err) + }) + } + t.Run("internal server error", func(t *testing.T) { + test(t, 1024, func(t testing.TB, err error) { + require.ErrorAs(t, err, new(*apistatus.ServerInternal)) + }) + }) + t.Run("any other failure", func(t *testing.T) { + var code uint32 + for code == 0 || code == 1024 { + code = rand.Uint32() + } + test(t, code, func(t testing.TB, err error) { + t.Skip("client sees no problem and just returns status") + require.EqualError(t, err, fmt.Sprintf("unexpected status code = %d while reading payload", code)) + }) + }) + }) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "Get", func(c *Client) error { + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + t.Run("heading message", func(t *testing.T) { + srv := newTestGetObjectServer() + srv.respondWithoutSigning(0) + c := newTestObjectClient(t, srv) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.ErrorContains(t, err, "invalid response signature") + }) + t.Run("payload chunk message", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + const n = 10 + chunks := make([][]byte, n) + for i := range chunks { + chunks[i] = []byte(fmt.Sprintf("chunk#%d", i)) + } + + srv.respondWithObject(validFullHeadingObjectGetResponseBody.GetInit(), chunks) + srv.respondWithoutSigning(n) // remember that 1st message is heading + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + read, err := io.ReadAll(r) + require.ErrorContains(t, err, "invalid response signature") + require.Equal(t, join(chunks[:n-1]), read) + }) + }) + t.Run("payloads", func(t *testing.T) { + t.Run("split info", func(t *testing.T) { + for _, tc := range invalidObjectSplitInfoProtoTestcases { + t.Run("split info/"+tc.name, func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + b := proto.Clone(validFullObjectSplitInfoGetResponseBody).(*protoobject.GetResponse_Body) + tc.corrupt(b.GetSplitInfo()) + + srv.respondWithBody(0, b) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "header: invalid split info: "+tc.msg) + }) + } + }) + t.Run("heading", func(t *testing.T) { + type testcase = struct { + name, msg string + corrupt func(valid *protoobject.GetResponse_Body) + } + tcs := []testcase{ + {name: "nil", msg: "missing object ID field in the response", corrupt: func(valid *protoobject.GetResponse_Body) { + valid.ObjectPart.(*protoobject.GetResponse_Body_Init_).Init = nil + }}, + {name: "nil oneof", msg: "missing object ID field in the response", corrupt: func(valid *protoobject.GetResponse_Body) { + valid.ObjectPart = &protoobject.GetResponse_Body_Init_{} + }}, + } + type initTescase = struct { + name, msg string + corrupt func(valid *protoobject.GetResponse_Body_Init) + } + itcs := []initTescase{ + {name: "object ID/missing", msg: "missing object ID field in the response", corrupt: func(valid *protoobject.GetResponse_Body_Init) { + valid.ObjectId = nil + }}, + {name: "signature/missing", msg: "missing signature field in the response", corrupt: func(valid *protoobject.GetResponse_Body_Init) { + valid.Signature = nil + }}, + {name: "header/missing", msg: "missing header field in the response", corrupt: func(valid *protoobject.GetResponse_Body_Init) { + valid.Header = nil + }}, + } + for _, tc := range invalidObjectIDProtoTestcases { + itcs = append(itcs, initTescase{ + name: "object ID/" + tc.name, msg: "invalid ID: " + tc.msg, + corrupt: func(valid *protoobject.GetResponse_Body_Init) { tc.corrupt(valid.ObjectId) }, + }) + } + for _, tc := range invalidSignatureProtoTestcases { + itcs = append(itcs, initTescase{ + name: "signature/" + tc.name, msg: "invalid signature: " + tc.msg, + corrupt: func(valid *protoobject.GetResponse_Body_Init) { tc.corrupt(valid.Signature) }, + }) + } + for _, tc := range invalidObjectHeaderProtoTestcases { + itcs = append(itcs, initTescase{ + name: "header/" + tc.name, msg: "invalid header: " + tc.msg, + corrupt: func(valid *protoobject.GetResponse_Body_Init) { tc.corrupt(valid.Header) }, + }) + } + + for _, tc := range itcs { + tcs = append(tcs, testcase{ + name: tc.name, msg: tc.msg, + corrupt: func(valid *protoobject.GetResponse_Body) { + tc.corrupt(valid.ObjectPart.(*protoobject.GetResponse_Body_Init_).Init) + }, + }) + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + b := proto.Clone(validFullHeadingObjectGetResponseBody).(*protoobject.GetResponse_Body) + tc.corrupt(b) + + srv.respondWithBody(0, b) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "header: "+tc.msg) + }) + } + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestGetObjectServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, _, err := newClient(t).ObjectGetInit(ctx, anyCID, anyOID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + t.Run("on stream init", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + transportErr := errors.New("any transport failure") + + srv.setHandlerError(transportErr) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + assertObjectStreamTransportErr(t, transportErr, err) + }) + t.Run("after heading response", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + transportErr := errors.New("any transport failure") + + srv.abortHandlerAfterResponse(1, transportErr) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + assertObjectStreamTransportErr(t, transportErr, err) + }) + t.Run("on payload transmission", func(t *testing.T) { + for _, n := range []uint{0, 2, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + transportErr := errors.New("any transport failure") + + srv.respondWithChunk(chunk) + srv.abortHandlerAfterResponse(1+n, transportErr) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for range n * uint(len(chunk)) { + _, err = r.Read([]byte{1}) + require.NoError(t, err) + } + _, err = r.Read([]byte{1}) + assertObjectStreamTransportErr(t, transportErr, err) + }) + } + }) + t.Run("too large chunk message", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + cb := setChunkInGetResponse(validFullChunkObjectGetResponseBody, make([]byte, 4194305)) + + srv.respondWithBody(1, cb) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + st, ok := status.FromError(err) + require.True(t, ok, err) + require.Equal(t, codes.ResourceExhausted, st.Code()) + require.Contains(t, st.Message(), "grpc: received message larger than max (") + require.Contains(t, st.Message(), " vs. 4194304)") + }) + }) + t.Run("invalid message sequence", func(t *testing.T) { + t.Run("no messages", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.setHandlerError(nil) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + _, ok := status.FromError(err) + require.False(t, ok) + require.EqualError(t, err, "header: %!w()") + }) + t.Run("chunk message first", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(0, proto.Clone(validFullChunkObjectGetResponseBody).(*protoobject.GetResponse_Body)) + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.EqualError(t, err, "header: unexpected message instead of heading part: *object.GetObjectPartChunk") + }) + t.Run("repeated heading message", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(2, proto.Clone(validMinHeadingObjectGetResponseBody).(*protoobject.GetResponse_Body)) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.EqualError(t, err, "unexpected message instead of chunk part: *object.GetObjectPartInit") + }) + t.Run("non-first split info message", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(2, validMinObjectSplitInfoGetResponseBody) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.EqualError(t, err, "unexpected message instead of chunk part: *object.SplitInfo") + }) + t.Run("chunk after split info", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(1, validMinObjectSplitInfoGetResponseBody) + srv.respondWithBody(2, validFullChunkObjectGetResponseBody) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = io.Copy(io.Discard, r) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/659") + require.EqualError(t, err, "unexpected message after split info response") + }) + t.Run("cut payload", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + hb := setPayloadLengthInHeadingGetResponse(validFullHeadingObjectGetResponseBody, uint64(len(chunk)+1)) + cb := setChunkInGetResponse(validFullChunkObjectGetResponseBody, chunk) + + srv.respondWithBody(0, hb) + srv.respondWithBody(1, cb) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.ErrorIs(t, err, io.ErrUnexpectedEOF) + }) + t.Run("payload size overflow", func(t *testing.T) { + srv := newTestGetObjectServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + hb := setPayloadLengthInHeadingGetResponse(validFullHeadingObjectGetResponseBody, uint64(len(chunk)-1)) + cb := setChunkInGetResponse(validFullChunkObjectGetResponseBody, chunk) + + srv.respondWithBody(0, hb) + srv.respondWithBody(1, cb) + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = io.Copy(io.Discard, r) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/658") + require.EqualError(t, err, "payload size overflow") + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/653") + // TODO: implement + }) + t.Run("exec statistics", func(t *testing.T) { + type collectedItem struct { + pub []byte + endpoint string + mtd stat.Method + dur time.Duration + err error + } + bind := func() (*testGetObjectServer, *Client, *[]collectedItem) { + srv := newTestGetObjectServer() + svc := newDefaultObjectService(t, srv) + var collected []collectedItem + handler := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + collected = append(collected, collectedItem{pub: pub, endpoint: endpoint, mtd: mtd, dur: dur, err: err}) + } + c := newCustomClient(t, func(prm *PrmInit) { prm.SetStatisticCallback(handler) }, svc) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + require.Len(t, collected, 1) + require.Nil(t, collected[0].pub) // server key is not yet received + require.Equal(t, testServerEndpoint, collected[0].endpoint) + require.Equal(t, stat.MethodEndpointInfo, collected[0].mtd) + require.Positive(t, collected[0].dur) + require.NoError(t, collected[0].err) + collected = nil + return srv, c, &collected } + assertCommon := func(c *[]collectedItem) { + collected := *c + for i := range collected { + require.Equal(t, testServerStateOnDial.pub, collected[i].pub) + require.Equal(t, testServerEndpoint, collected[i].endpoint) + require.Positive(t, collected[i].dur) + } + } + t.Run("missing signer", func(t *testing.T) { + _, c, cl := bind() + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + assertCommon(cl) + collected := *cl + require.Len(t, *cl, 1) + require.Equal(t, stat.MethodObjectGet, collected[0].mtd) + require.NoError(t, collected[0].err) + }) + t.Run("sign request", func(t *testing.T) { + _, c, cl := bind() + _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 1) + require.Equal(t, stat.MethodObjectGet, collected[0].mtd) + require.Equal(t, err, collected[0].err) + }) + t.Run("transport failure", func(t *testing.T) { + srv, c, cl := bind() + transportErr := errors.New("any transport failure") + srv.abortHandlerAfterResponse(3, transportErr) + + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = r.Read([]byte{1}) + } + assertObjectStreamTransportErr(t, transportErr, err) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectGet, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectGetStream, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("OK", func(t *testing.T) { + srv, c, cl := bind() + const sleepDur = 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + srv.setSleepDuration(sleepDur) + + _, r, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectGet, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectGetStream, collected[1].mtd) + require.NoError(t, collected[1].err) + require.Greater(t, collected[1].dur, sleepDur) + }) + }) +} + +func TestClient_ObjectRangeInit(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmObjectRange + anyCID := cidtest.ID() + anyOID := oidtest.ID() + anyValidOff, anyValidLn := uint64(1), uint64(2) + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + srv.checkRequestObjectAddress(anyCID, anyOID) + srv.checkRequestRange(anyValidOff, anyValidLn) + srv.authenticateRequest(anyValidSigner) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, PrmObjectRange{}) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestObjectPayloadRangeServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, opts) + if err == nil { + _, err = io.Copy(io.Discard, r) + } + return err + }) + }) + t.Run("local", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("raw", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkRaw() + + srv.checkRequestRaw() + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) - for _, test := range tt { - t.Run(test.name, func(t *testing.T) { - require.ErrorIs(t, test.methodCall(), ErrMissingSigner) + srv.checkRequestSessionToken(st) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, opts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + t.Run("split info", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoobject.GetRangeResponse_Body + }{ + {name: "min", body: validMinObjectSplitInfoRangeResponseBody}, + {name: "full", body: validFullObjectSplitInfoRangeResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(0, tc.body) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + var e *object.SplitInfoError + require.ErrorAs(t, err, &e) + require.NoError(t, checkSplitInfoTransport(*e.SplitInfo(), tc.body.GetSplitInfo())) + }) + } + }) + t.Run("header", func(t *testing.T) { + const bigChunkSize = 4<<20 - object.MaxHeaderLen + bigChunkTwice := make([]byte, 2*bigChunkSize) + //nolint:staticcheck // OK for this test + rand.Read(bigChunkTwice) + for _, tc := range []struct { + name string + chunks [][]byte + }{ + {name: "with single payload chunk", chunks: [][]byte{[]byte("Hello, world!")}}, + {name: "with multiple payload chunks", + chunks: [][]byte{bigChunkTwice[:bigChunkSize], []byte("small"), {}, bigChunkTwice[bigChunkSize:]}}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + payload := join(tc.chunks) + + srv.respondWithChunks(tc.chunks) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(len(payload)), anyValidSigner, anyValidOpts) + require.NoError(t, err) + require.NoError(t, iotest.TestReader(r, payload)) + }) + } + }) + }) + t.Run("statuses", func(t *testing.T) { + t.Run("split info", func(t *testing.T) { + t.Run("OK", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + body := validFullObjectSplitInfoRangeResponseBody + srv.respondWithBody(0, body) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + var e *object.SplitInfoError + require.ErrorAs(t, err, &e) + require.NoError(t, checkSplitInfoTransport(*e.SplitInfo(), body.GetSplitInfo())) + }) + t.Run("not OK", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + var code uint32 + for code == 0 { + code = rand.Uint32() + } + + srv.respondWithBody(0, validFullObjectSplitInfoRangeResponseBody) + srv.respondWithStatus(0, &protostatus.Status{Code: code}) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = r.Read([]byte{1}) + t.Skip("currently ignores split info and returns status error") + require.EqualError(t, err, fmt.Sprintf("split info response returned with non-OK status code = %d", code)) + }) + }) + t.Run("payload", func(t *testing.T) { + test := func(t testing.TB, code uint32, + assert func(testing.TB, []byte, io.Reader, error)) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + chunks := [][]byte{[]byte("one"), []byte("two"), []byte("three")} + payload := join(chunks) + + srv.respondWithChunks(chunks) + srv.respondWithStatus(uint(len(chunks))-1, &protostatus.Status{Code: code}) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(len(payload)), anyValidSigner, anyValidOpts) + assert(t, payload, r, err) + } + t.Run("OK", func(t *testing.T) { + test(t, 0, func(t testing.TB, payload []byte, r io.Reader, err error) { + require.NoError(t, err) + require.NoError(t, iotest.TestReader(r, payload)) + }) + }) + t.Run("failure", func(t *testing.T) { + test := func(t testing.TB, code uint32, assert func(t testing.TB, err error)) { + test(t, code, func(t testing.TB, _ []byte, r io.Reader, err error) { + require.NoError(t, err) + _, err = io.ReadAll(r) + assert(t, err) + }) + } + t.Run("internal server error", func(t *testing.T) { + test(t, 1024, func(t testing.TB, err error) { + require.ErrorAs(t, err, new(*apistatus.ServerInternal)) + }) + }) + t.Run("any other failure", func(t *testing.T) { + var code uint32 + for code == 0 || code == 1024 { + code = rand.Uint32() + } + test(t, code, func(t testing.TB, err error) { + t.Skip("client sees no problem and just returns status") + require.EqualError(t, err, fmt.Sprintf("unexpected status code = %d while reading payload", code)) + }) + }) + }) + }) + }) }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "GetRange", func(c *Client) error { + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + for err == nil { + _, err = r.Read([]byte{1}) + } + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + const n = 10 + chunks := make([][]byte, n) + for i := range chunks { + chunks[i] = []byte(fmt.Sprintf("chunk#%d", i)) + } + + srv.respondWithChunks(chunks) + srv.respondWithoutSigning(n - 1) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(n*len(chunks)), anyValidSigner, anyValidOpts) + require.NoError(t, err) + read, err := io.ReadAll(r) + require.ErrorContains(t, err, "invalid response signature") + require.Equal(t, join(chunks[:n-1]), read) + }) + t.Run("payloads", func(t *testing.T) { + t.Run("split info", func(t *testing.T) { + type testcase = struct { + name, msg string + splitInfo *protoobject.SplitInfo + } + tcs := []testcase{{ + name: "missing", + msg: "invalid split info: neither link object ID nor last part object ID is set", + // nil becomes a zero-pointer after transport + splitInfo: nil, + }} + for _, tc := range invalidObjectSplitInfoProtoTestcases { + si := proto.Clone(validFullSplitInfo).(*protoobject.SplitInfo) + tc.corrupt(si) + tcs = append(tcs, testcase{name: tc.name, msg: "invalid split info: " + tc.msg, splitInfo: si}) + } + for _, tc := range tcs { + t.Run("split info/"+tc.name, func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + b := proto.Clone(validFullObjectSplitInfoRangeResponseBody).(*protoobject.GetRangeResponse_Body) + b.RangePart.(*protoobject.GetRangeResponse_Body_SplitInfo).SplitInfo = tc.splitInfo + + srv.respondWithBody(0, b) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + require.EqualError(t, err, tc.msg) + }) + } + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + t.Run("zero length", func(t *testing.T) { + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, 0, anyValidSigner, anyValidOpts) + require.ErrorIs(t, err, ErrZeroRangeLength) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestObjectPayloadRangeServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + t.Run("on stream init", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + transportErr := errors.New("any transport failure") + + srv.setHandlerError(transportErr) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + assertObjectStreamTransportErr(t, transportErr, err) + }) + t.Run("on payload transmission", func(t *testing.T) { + for _, n := range []uint{0, 2, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + transportErr := errors.New("any transport failure") + + srv.respondWithChunk(chunk) + srv.abortHandlerAfterResponse(n, transportErr) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(len(chunk))*uint64(n+1), anyValidSigner, anyValidOpts) + require.NoError(t, err) + for range n * uint(len(chunk)) { + _, err = r.Read([]byte{1}) + require.NoError(t, err) + } + _, err = r.Read([]byte{1}) + assertObjectStreamTransportErr(t, transportErr, err) + }) + } + }) + t.Run("too large chunk message", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + cb := setChunkInRangeResponse(validFullChunkObjectRangeResponseBody, make([]byte, 4194305)) + + srv.respondWithBody(1, cb) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + st, ok := status.FromError(err) + require.True(t, ok, err) + require.Equal(t, codes.ResourceExhausted, st.Code()) + require.Contains(t, st.Message(), "grpc: received message larger than max (") + require.Contains(t, st.Message(), " vs. 4194304)") + }) + }) + t.Run("invalid message sequence", func(t *testing.T) { + t.Run("no messages", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + srv.setHandlerError(nil) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]byte{1}) + require.ErrorIs(t, err, io.ErrUnexpectedEOF) + }) + t.Run("non-first split info message", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(1, validMinObjectSplitInfoRangeResponseBody) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = io.Copy(io.Discard, r) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/659") + require.EqualError(t, err, "unexpected message instead of chunk part: *object.SplitInfo") + }) + t.Run("chunk after split info", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(0, validMinObjectSplitInfoRangeResponseBody) + srv.respondWithBody(1, validFullChunkObjectRangeResponseBody) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = io.Copy(io.Discard, r) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/659") + require.EqualError(t, err, "unexpected message after split info response") + }) + t.Run("cut payload", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + cb := setChunkInRangeResponse(validFullChunkObjectRangeResponseBody, chunk) + + srv.respondWithBody(0, cb) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(len(chunk))+1, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.ErrorIs(t, err, io.ErrUnexpectedEOF) + }) + t.Run("payload size overflow", func(t *testing.T) { + srv := newTestObjectPayloadRangeServer() + c := newTestObjectClient(t, srv) + + chunk := []byte("Hello, world!") + cb := setChunkInRangeResponse(validFullChunkObjectRangeResponseBody, chunk) + + srv.respondWithBody(0, cb) + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, uint64(len(chunk))-1, anyValidSigner, anyValidOpts) + require.NoError(t, err) + //nolint:staticcheck // drop with t.Skip() + _, err = io.Copy(io.Discard, r) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/658") + require.EqualError(t, err, "payload size overflow") + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/653") + // TODO: implement + }) + t.Run("exec statistics", func(t *testing.T) { + type collectedItem struct { + pub []byte + endpoint string + mtd stat.Method + dur time.Duration + err error + } + bind := func() (*testGetObjectPayloadRangeServer, *Client, *[]collectedItem) { + srv := newTestObjectPayloadRangeServer() + svc := newDefaultObjectService(t, srv) + var collected []collectedItem + handler := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + collected = append(collected, collectedItem{pub: pub, endpoint: endpoint, mtd: mtd, dur: dur, err: err}) + } + c := newCustomClient(t, func(prm *PrmInit) { prm.SetStatisticCallback(handler) }, svc) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + require.Len(t, collected, 1) + require.Nil(t, collected[0].pub) // server key is not yet received + require.Equal(t, testServerEndpoint, collected[0].endpoint) + require.Equal(t, stat.MethodEndpointInfo, collected[0].mtd) + require.Positive(t, collected[0].dur) + require.NoError(t, collected[0].err) + collected = nil + return srv, c, &collected } + assertCommon := func(c *[]collectedItem) { + collected := *c + for i := range collected { + require.Equal(t, testServerStateOnDial.pub, collected[i].pub) + require.Equal(t, testServerEndpoint, collected[i].endpoint) + require.Positive(t, collected[i].dur) + } + } + t.Run("zero range length", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, 0, anyValidSigner, anyValidOpts) + require.ErrorIs(t, err, ErrZeroRangeLength) + assertCommon(cl) + collected := *cl + require.Len(t, *cl, 1) + require.Equal(t, stat.MethodObjectRange, collected[0].mtd) + require.Equal(t, err, collected[0].err) + }) + t.Run("missing signer", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + assertCommon(cl) + collected := *cl + require.Len(t, *cl, 1) + require.Equal(t, stat.MethodObjectRange, collected[0].mtd) + require.NoError(t, collected[0].err) + }) + t.Run("sign request", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 1) + require.Equal(t, stat.MethodObjectRange, collected[0].mtd) + require.Equal(t, err, collected[0].err) + }) + t.Run("transport failure", func(t *testing.T) { + srv, c, cl := bind() + transportErr := errors.New("any transport failure") + srv.abortHandlerAfterResponse(2, transportErr) + + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, math.MaxInt, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = r.Read([]byte{1}) + } + assertObjectStreamTransportErr(t, transportErr, err) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectRange, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectRangeStream, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("OK", func(t *testing.T) { + srv, c, cl := bind() + const sleepDur = 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + srv.setSleepDuration(sleepDur) + + r, err := c.ObjectRangeInit(ctx, anyCID, anyOID, anyValidOff, anyValidLn, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = io.Copy(io.Discard, r) + require.NoError(t, err) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectRange, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectRangeStream, collected[1].mtd) + require.NoError(t, collected[1].err) + require.Greater(t, collected[1].dur, sleepDur) + }) }) } diff --git a/client/object_hash_test.go b/client/object_hash_test.go index 7b4d1188..d618952a 100644 --- a/client/object_hash_test.go +++ b/client/object_hash_test.go @@ -1,51 +1,369 @@ package client import ( + "bytes" "context" + "errors" "fmt" + "math" "testing" + "time" 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" + protorefs "github.com/nspcc-dev/neofs-api-go/v2/refs/grpc" + bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" + cidtest "github.com/nspcc-dev/neofs-sdk-go/container/id/test" + oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" + usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) type testHashObjectPayloadRangesServer struct { protoobject.UnimplementedObjectServiceServer + testCommonUnaryServerSettings[ + *protoobject.GetRangeHashRequest_Body, + v2object.GetRangeHashRequestBody, + *v2object.GetRangeHashRequestBody, + *protoobject.GetRangeHashRequest, + v2object.GetRangeHashRequest, + *v2object.GetRangeHashRequest, + *protoobject.GetRangeHashResponse_Body, + v2object.GetRangeHashResponseBody, + *v2object.GetRangeHashResponseBody, + *protoobject.GetRangeHashResponse, + v2object.GetRangeHashResponse, + *v2object.GetRangeHashResponse, + ] + testCommonReadObjectRequestServerSettings + reqHomo bool + reqRanges []uint64 + reqSalt []byte } -func (x *testHashObjectPayloadRangesServer) GetRangeHash(context.Context, *protoobject.GetRangeHashRequest) (*protoobject.GetRangeHashResponse, error) { - resp := protoobject.GetRangeHashResponse{ - Body: &protoobject.GetRangeHashResponse_Body{ - HashList: [][]byte{{1}}, - }, +// returns [protoobject.ObjectServiceServer] supporting GetRangeHash method +// only. Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestHashObjectServer() *testHashObjectPayloadRangesServer { + return new(testHashObjectPayloadRangesServer) +} + +// makes the server to assert that any request has given payload (offset,len) +// ranges. By default, and if nil, any valid ranges are accepted. +func (x *testHashObjectPayloadRangesServer) checkRequestRanges(rs []uint64) { + if len(rs)%2 != 0 { + panic("odd number of elements") } + x.reqRanges = rs +} - var respV2 v2object.GetRangeHashResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +// makes the server to assert that any request has given salt. By default, and +// if nil, salt must be empty. +func (x *testHashObjectPayloadRangesServer) checkRequestSalt(salt []byte) { x.reqSalt = salt } + +// makes the server to assert that any request has homomorphic checksum type. +// By default, the type must be SHA-256. +func (x *testHashObjectPayloadRangesServer) checkRequestHomomorphic() { x.reqHomo = true } + +func (x *testHashObjectPayloadRangesServer) verifyRequest(req *protoobject.GetRangeHashRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + if err := x.verifyMeta(req.MetaHeader); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyObjectAddress(body.Address); err != nil { + return err + } + // 2. ranges + if len(body.Ranges) == 0 { + return newErrMissingRequestBodyField("ranges") + } + if x.reqRanges != nil { + if exp, act := len(x.reqRanges), 2*len(body.Ranges); exp != act { + return newErrInvalidRequestField("ranges", fmt.Errorf("number of elements (client: %d, message: %d)", exp, act)) + } + for i, r := range body.Ranges { + if v1, v2 := r.GetOffset(), x.reqRanges[2*i]; v1 != v2 { + return newErrInvalidRequestField("ranges", fmt.Errorf("element#%d: offset field (client: %v, message: %v)", i, v1, v2)) + } + if v1, v2 := r.GetLength(), x.reqRanges[2*i+1]; v1 != v2 { + return newErrInvalidRequestField("ranges", fmt.Errorf("element#%d: length field (client: %v, message: %v)", i, v1, v2)) + } + } + } + // 3. salt + if x.reqSalt != nil && !bytes.Equal(body.Salt, x.reqSalt) { + return newErrInvalidRequestField("salt", fmt.Errorf("unexpected value (client: %x, message: %x)", x.reqSalt, body.Salt)) + } + // 4. type + var expType protorefs.ChecksumType + if x.reqHomo { + expType = protorefs.ChecksumType_TZ + } else { + expType = protorefs.ChecksumType_SHA256 } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if body.Type != expType { + return newErrInvalidRequestField("type", fmt.Errorf("unexpected value (client: %v, message: %v)", expType, body.Type)) + } + return nil +} + +func (x *testHashObjectPayloadRangesServer) GetRangeHash(_ context.Context, req *protoobject.GetRangeHashRequest) (*protoobject.GetRangeHashResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr } - return respV2.ToGRPCMessage().(*protoobject.GetRangeHashResponse), nil + resp := &protoobject.GetRangeHashResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinObjectHashResponseBody).(*protoobject.GetRangeHashResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } func TestClient_ObjectHash(t *testing.T) { - c := newClient(t) + ctx := context.Background() + var anyValidOpts PrmObjectHash + anyValidOpts.SetRangeList(0, 1) + anyCID := cidtest.ID() + anyOID := oidtest.ID() + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + rs := []uint64{1, 2, 3, 4, 5, 6} + var opts PrmObjectHash + opts.SetRangeList(rs...) + + srv.checkRequestRanges(rs) + srv.authenticateRequest(anyValidSigner) + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestHashObjectServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + return err + }) + }) + t.Run("salt", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) - t.Run("missing signer", func(t *testing.T) { - var reqBody v2object.GetRangeHashRequestBody - reqBody.SetRanges(make([]v2object.Range, 1)) + salt := []byte("any salt") + opts := anyValidOpts + opts.UseSalt(salt) - _, err := c.ObjectHash(context.Background(), cid.ID{}, oid.ID{}, nil, PrmObjectHash{ - body: reqBody, + srv.checkRequestSalt(salt) + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("homomorphic", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.TillichZemorAlgo() + + srv.checkRequestHomomorphic() + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("local", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoobject.GetRangeHashResponse_Body + }{ + {name: "min", body: validMinObjectHashResponseBody}, + {name: "full", body: validFullObjectHashResponseBody}, + {name: "type/negative", body: &protoobject.GetRangeHashResponse_Body{ + // https://github.com/nspcc-dev/neofs-sdk-go/issues/663 + Type: -1, HashList: validMinObjectHashResponseBody.GetHashList(), + }}, + {name: "type/unsupported", body: &protoobject.GetRangeHashResponse_Body{ + Type: math.MaxInt32, HashList: validMinObjectHashResponseBody.GetHashList(), + }}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestHashObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(tc.body) + res, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + require.Equal(t, tc.body.GetHashList(), res) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestHashObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "GetRangeHash", func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestHashObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protoobject.GetRangeHashResponse_Body] + // missing fields + tcs := []testcase{ + {name: "nil", body: nil, assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing hash list field in the response") + }}, + {name: "empty", body: new(protoobject.GetRangeHashResponse_Body), assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing hash list field in the response") + }}, + } + + testInvalidResponseBodies(t, newTestHashObjectServer, newTestObjectClient, tcs, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + }) }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectHash(ctx, anyCID, anyOID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + t.Run("missing ranges", func(t *testing.T) { + var opts PrmObjectHash + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.ErrorIs(t, err, ErrMissingRanges) - require.ErrorIs(t, err, ErrMissingSigner) + opts = anyValidOpts + opts.SetRangeList() + _, err = c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, opts) + require.ErrorIs(t, err, ErrMissingRanges) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestHashObjectServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).ObjectHash(ctx, anyCID, anyOID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestHashObjectServer, newTestObjectClient, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/653") + testUnaryResponseCallback(t, newTestHashObjectServer, newDefaultObjectService, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestHashObjectServer, newDefaultObjectService, stat.MethodObjectHash, + []testedClientOp{func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, nil, anyValidOpts) + return err + }}, []testedClientOp{func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, PrmObjectHash{}) + return err + }}, func(c *Client) error { + _, err := c.ObjectHash(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) + return err + }, + ) }) } diff --git a/client/object_put_test.go b/client/object_put_test.go index 3159034d..bca0c1ba 100644 --- a/client/object_put_test.go +++ b/client/object_put_test.go @@ -1,86 +1,802 @@ package client import ( + "bytes" "context" "errors" "fmt" "io" + "math/rand" "testing" + "time" 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" - 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" + bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" 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" + objecttest "github.com/nspcc-dev/neofs-sdk-go/object/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/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" "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) +const signOneReqCalls = 3 // body+headers + +type nFailedSigner struct { + user.Signer + n, count uint +} + +// returns [user.Signer] failing all Sign calls starting from the n-th one. +func newNFailedSigner(base user.Signer, n uint) user.Signer { + return &nFailedSigner{Signer: base, n: n} +} + +func (x *nFailedSigner) Sign(data []byte) ([]byte, error) { + x.count++ + if x.count < x.n { + return x.Signer.Sign(data) + } + return nil, errors.New("test signer forcefully fails") +} + type testPutObjectServer struct { protoobject.UnimplementedObjectServiceServer + testCommonClientStreamServerSettings[ + *protoobject.PutRequest_Body, + v2object.PutRequestBody, + *v2object.PutRequestBody, + *protoobject.PutRequest, + v2object.PutRequest, + *v2object.PutRequest, + *protoobject.PutResponse_Body, + v2object.PutResponseBody, + *v2object.PutResponseBody, + *protoobject.PutResponse, + v2object.PutResponse, + *v2object.PutResponse, + ] + testObjectSessionServerSettings + testBearerTokenServerSettings + testLocalRequestServerSettings + + reqHdr *object.Object + reqPayload []byte + reqCopies uint32 - denyAccess bool + reqPayloadLenCounter int } -func (x *testPutObjectServer) Put(stream protoobject.ObjectService_PutServer) error { - for { - req, err := stream.Recv() - if errors.Is(err, io.EOF) { - break +// returns [protoobject.ObjectServiceServer] supporting Put method only. Default +// implementation performs common verification of any request, and responds with +// any valid message. The message flow is also strictly controlled. Some methods +// allow to tune the behavior. +func newPutObjectServer() *testPutObjectServer { return new(testPutObjectServer) } + +// makes the server to assert that any heading request caries given value in +// copy num field. By default, the field must be zero. +func (x *testPutObjectServer) checkRequestCopiesNumber(n uint32) { x.reqCopies = n } + +// makes the server to assert that any heading request carries given object +// header. By default, any header is accepted. +func (x *testPutObjectServer) checkRequestHeader(hdr object.Object) { x.reqHdr = &hdr } + +// makes the server to assert that any given data is streamed as an object +// payload. By default, and if nil, any payload is accepted. +func (x *testPutObjectServer) checkRequestPayload(data []byte) { x.reqPayload = data } + +func (x *testPutObjectServer) verifyHeadingMessage(m *protoobject.PutRequest_Body_Init) error { + if m.Header == nil { + return errors.New("missing header field") + } + // 4. copies number + if x.reqCopies != m.CopiesNumber { + return fmt.Errorf("copies number field (client: %d, message: %d)", x.reqCopies, m.CopiesNumber) + } + if x.reqHdr == nil { + return nil + } + // 1. ID + id := x.reqHdr.GetID() + mid := m.GetObjectId() + if id.IsZero() { + if mid != nil { + return errors.New("object ID field is set while should not be") } - switch req.GetBody().GetObjectPart().(type) { - case *protoobject.PutRequest_Body_Init_, - *protoobject.PutRequest_Body_Chunk: - default: - return errors.New("excuse me?") + } else { + if mid == nil { + return errors.New("missing object ID field") } + if err := checkObjectIDTransport(id, mid); err != nil { + return fmt.Errorf("object ID field: %w", err) + } + } + // 2. signature + // 3. header + if err := checkObjectHeaderWithSignatureTransport(*x.reqHdr, &protoobject.HeaderWithSignature{ + Header: m.Header, Signature: m.Signature, + }); err != nil { + return fmt.Errorf("header with signature fields: %w", err) } + return nil +} - var v refs.Version - version.Current().WriteToV2(&v) - id := oidtest.ID() - resp := protoobject.PutResponse{ - Body: &protoobject.PutResponse_Body{ - ObjectId: &protorefs.ObjectID{Value: id[:]}, - }, - MetaHeader: &protosession.ResponseMetaHeader{ - Version: v.ToGRPCMessage().(*protorefs.Version), - }, +func (x *testPutObjectServer) verifyPayloadChunkMessage(chunk []byte) error { + ln := len(chunk) + if ln == 0 { + return errors.New("empty payload chunk") + } + const maxChunkLen = 3 << 20 + if ln > maxChunkLen { + return fmt.Errorf("intermediate chunk exceeds the expected size limit: %dB > %dB", ln, maxChunkLen) + } + if x.reqPayload == nil { + return nil } + if exp := x.reqPayload[x.reqPayloadLenCounter:]; !bytes.HasPrefix(exp, chunk) { + return fmt.Errorf("wrong payload chunk (remains: %dB, message: %dB)", len(exp), len(chunk)) + } + x.reqPayloadLenCounter += ln + return nil +} - if x.denyAccess { - resp.MetaHeader.Status = apistatus.ErrObjectAccessDenied.ErrorToV2().ToGRPCMessage().(*protostatus.Status) +func (x *testPutObjectServer) verifyRequest(req *protoobject.PutRequest) error { + // TODO(https://github.com/nspcc-dev/neofs-sdk-go/issues/662): why meta is + // transmitted in all stream messages when heading parts is enough? + if err := x.testCommonClientStreamServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + metaHdr := req.MetaHeader + // TTL + if err := x.verifyTTL(metaHdr); err != nil { + return err + } + // session token + if err := x.verifySessionToken(metaHdr.GetSessionToken()); err != nil { + return err + } + // bearer token + if err := x.verifyBearerToken(metaHdr.GetBearerToken()); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + switch v := body.ObjectPart.(type) { + default: + return newErrInvalidRequestField("object part", fmt.Errorf("unsupported oneof type %T", v)) + case nil: + return newErrMissingRequestBodyField("object part") + case *protoobject.PutRequest_Body_Init_: + if x.reqCounter > 1 { + return newErrInvalidRequestField("object part", fmt.Errorf("heading part must be a 1st stream message only, "+ + "but received in #%d one", x.reqCounter)) + } + if v.Init == nil { + panic("nil oneof field container") + } + if err := x.verifyHeadingMessage(v.Init); err != nil { + return newErrInvalidRequestField("heading part", err) + } + case *protoobject.PutRequest_Body_Chunk: + if x.reqCounter <= 1 { + return newErrInvalidRequestField("object part", errors.New("payload chunk must not be a 1st stream message")) + } + if err := x.verifyPayloadChunkMessage(v.Chunk); err != nil { + return newErrInvalidRequestField("chunk part", err) + } } + return nil +} - var respV2 v2object.PutResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) +func (x *testPutObjectServer) sendResponse(stream protoobject.ObjectService_PutServer) error { + resp := &protoobject.PutResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinPutObjectResponseBody).(*protoobject.PutResponse_Body) } - return stream.SendAndClose(respV2.ToGRPCMessage().(*protoobject.PutResponse)) + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return fmt.Errorf("sign response: %w", err) + } + + return stream.SendAndClose(resp) +} + +func (x *testPutObjectServer) reset() { + x.reqCounter, x.reqPayloadLenCounter = 0, 0 +} + +func (x *testPutObjectServer) Put(stream protoobject.ObjectService_PutServer) error { + defer x.reset() + time.Sleep(x.handlerSleepDur) + if x.handlerErrForced { + return x.handlerErr + } + ctx := stream.Context() + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + req, err := stream.Recv() + if err != nil { + if errors.Is(err, io.EOF) { + if x.reqCounter == 0 { + return errors.New("stream finished without messages") + } + if x.reqPayload != nil && x.reqPayloadLenCounter != len(x.reqPayload) { + return fmt.Errorf("unfinished payload (expected: %dB, received: %dB)", len(x.reqPayload), x.reqPayloadLenCounter) + } + break + } + return err + } + x.reqCounter++ + if err := x.verifyRequest(req); err != nil { + return err + } + if x.reqErrN > 0 && x.reqCounter >= x.reqErrN { + return x.reqErr + } + if x.respN > 0 && x.reqCounter >= x.respN { + break + } + } + return x.sendResponse(stream) } -func TestClient_ObjectPutInit(t *testing.T) { - t.Run("EOF-on-status-return", func(t *testing.T) { - srv := testPutObjectServer{ - denyAccess: true, +func TestClient_ObjectPut(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmObjectPutInit + anyValidHdr := objecttest.Object() + anyValidSigner := usertest.User() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + for _, tc := range []struct { + name string + payloadLen uint + }{ + {name: "no payload", payloadLen: 0}, + {name: "one byte", payloadLen: 1}, + {name: "3MB-1", payloadLen: 3<<20 - 1}, + {name: "3MB", payloadLen: 3 << 20}, + {name: "3MB+1", payloadLen: 3<<20 + 1}, + {name: "6MB-1", payloadLen: 6<<20 - 1}, + {name: "6MB", payloadLen: 6 << 20}, + {name: "6MB+1", payloadLen: 6<<20 + 1}, + {name: "10MB", payloadLen: 10 << 20}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + payload := make([]byte, tc.payloadLen) + //nolint:staticcheck // OK for this test + rand.Read(payload) + + srv.checkRequestHeader(anyValidHdr) + srv.checkRequestPayload(payload) + srv.authenticateRequest(anyValidSigner) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, PrmObjectPutInit{}) + require.NoError(t, err) + + chunkLen := len(payload)/10 + 1 + for len(payload) > 0 { + ln := min(chunkLen, len(payload)) + n, err := w.Write(payload[:ln]) + require.NoError(t, err) + require.EqualValues(t, ln, n) + payload = payload[ln:] + } + require.NoError(t, w.Close()) + }) + } + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newPutObjectServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, opts) + if err == nil { + _, err = w.Write([]byte{1}) + if err == nil { + err = w.Close() + } + } + return err + }) + }) + t.Run("local", func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, opts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + require.NoError(t, w.Close()) + }) + t.Run("session token", func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, opts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + require.NoError(t, w.Close()) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, opts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + require.NoError(t, w.Close()) + }) + t.Run("copies number", func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + n := rand.Uint32() + opts := anyValidOpts + opts.SetCopiesNumber(n) + + srv.checkRequestCopiesNumber(n) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, opts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + require.NoError(t, w.Close()) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoobject.PutResponse_Body + }{ + {name: "min", body: validMinPutObjectResponseBody}, + {name: "full", body: validFullPutObjectResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + srv.respondWithBody(tc.body) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + require.NoError(t, w.Close()) + require.NoError(t, checkObjectIDTransport(w.GetResult().StoredObjectID(), tc.body.GetObjectId())) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + for _, tc := range []struct { + name string + n uint + }{ + {name: "on stream init", n: 1}, + {name: "after heading request", n: 2}, + {name: "on payload transmission", n: 10}, + } { + t.Run("interrupting/"+tc.name, func(t *testing.T) { + test := func(ok bool) { + t.Run(fmt.Sprintf("ok=%t", ok), func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + var code uint32 + if !ok { + for code == 0 { + code = rand.Uint32() + } + } + srv.respondAfterRequest(tc.n) + srv.respondWithStatus(&protostatus.Status{Code: code}) + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) // prevent hanging + t.Cleanup(cancel) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = w.Write([]byte{1}) + time.Sleep(50 * time.Millisecond) // give the response time to come + } + if ok { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/649") + require.EqualError(t, err, "server unexpectedly interrupted the stream with a response") + } else { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/648") + require.ErrorIs(t, err, apistatus.Error) + } + }) + } + test(true) + test(false) + }) + } + t.Run("after stream finish", func(t *testing.T) { + testStatusResponses(t, newPutObjectServer, newTestObjectClient, func(c *Client) error { + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + if err == nil { + _, err = w.Write([]byte{1}) + if err == nil { + err = w.Close() + } + } + return err + }) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + exec := func(c *Client) error { + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + for err == nil { + return w.Close() + } + return err + } + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "Put", exec) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newPutObjectServer, newTestObjectClient, exec) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protoobject.PutResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, ErrMissingResponseField) + require.EqualError(t, err, "missing ID field in the response") + }}, + {name: "empty", body: new(protoobject.PutResponse_Body), assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, ErrMissingResponseField) + require.EqualError(t, err, "missing ID field in the response") + }}, + } + for _, tc := range invalidObjectIDProtoTestcases { + body := proto.Clone(validFullPutObjectResponseBody).(*protoobject.PutResponse_Body) + tc.corrupt(body.ObjectId) + tcs = append(tcs, testcase{name: "ID/" + tc.name, body: body, assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "invalid ID field in the response: "+tc.msg) + }, + }) + } + + testInvalidResponseBodies(t, newPutObjectServer, newTestObjectClient, tcs, exec) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectPutInit(ctx, anyValidHdr, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newPutObjectServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + t.Run("heading", func(t *testing.T) { + _, err := newClient(t).ObjectPutInit(ctx, anyValidHdr, usertest.FailSigner(anyValidSigner), anyValidOpts) + require.ErrorContains(t, err, "header write") + require.ErrorContains(t, err, "sign message") + }) + t.Run("payload chunks", func(t *testing.T) { + for _, n := range []int{0, 1, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + okSignings := signOneReqCalls * (n + 1) // +1 for header one + signer := newNFailedSigner(anyValidSigner, uint(okSignings+1)) + w, err := c.ObjectPutInit(ctx, anyValidHdr, signer, anyValidOpts) + require.NoError(t, err) + + for range n { + _, err = w.Write([]byte{1}) + require.NoError(t, err) + } + _, err = w.Write([]byte{1}) + require.ErrorContains(t, err, "sign message") + }) + } + }) + }) + t.Run("transport failure", func(t *testing.T) { + test := func(t testing.TB, n uint, handleInit func(testing.TB, io.WriteCloser, error) error) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + transportErr := errors.New("any transport failure") + + srv.abortHandlerAfterRequest(n, transportErr) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + if handleInit != nil { + err = handleInit(t, w, err) + } + assertObjectStreamTransportErr(t, transportErr, err) + } + t.Run("on stream init", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/649") + test(t, 0, func(t testing.TB, w io.WriteCloser, err error) error { + for err == nil { + _, err = w.Write([]byte{1}) + time.Sleep(50 * time.Millisecond) // give the response time to come + } + require.ErrorContains(t, err, "header write") + return err + }) + }) + t.Run("after heading request", func(t *testing.T) { + test := func(t testing.TB, withPayload bool) { + test(t, 1, func(t testing.TB, w io.WriteCloser, err error) error { + require.NoError(t, err) + if withPayload { + _, err = w.Write([]byte{1}) // gRPC client stream does not ACK each request + if err == nil { + // wait for the response + err = w.Close() + } // else it has already come and reflected in err + } else { + err = w.Close() + } + return err + }) + } + t.Run("with payload", func(t *testing.T) { test(t, true) }) + t.Run("without payload", func(t *testing.T) { test(t, false) }) + }) + t.Run("on payload transmission", func(t *testing.T) { + for _, n := range []uint{0, 2, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + test(t, 2+n, func(t testing.TB, w io.WriteCloser, err error) error { + require.NoError(t, err) + for range n { + _, err = w.Write([]byte{1}) + require.NoError(t, err) + } + _, err = w.Write([]byte{1}) // gRPC client stream does not ACK each request + if err == nil { + // wait for the response + err = w.Close() + } // else it has already come and reflected in err + return err + }) + }) + } + }) + }) + t.Run("no response message", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/649") + assertNoResponseErr := func(t testing.TB, err error) { + _, ok := status.FromError(err) + require.False(t, ok) + require.EqualError(t, err, "server finished stream without response") + } + test := func(t testing.TB, n uint, assertStream func(testing.TB, io.WriteCloser, error)) { + srv := newPutObjectServer() + c := newTestObjectClient(t, srv) + + srv.abortHandlerAfterRequest(n, nil) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + assertStream(t, w, err) + } + + t.Run("on stream init", func(t *testing.T) { + test(t, 0, func(t testing.TB, _ io.WriteCloser, err error) { assertNoResponseErr(t, err) }) + }) + t.Run("after heading request", func(t *testing.T) { + test := func(t testing.TB, withPayload bool) { + test(t, 1, func(t testing.TB, w io.WriteCloser, err error) { + require.NoError(t, err) + if withPayload { + _, err = w.Write([]byte{1}) // gRPC client stream does not ACK each request + if err == nil { + // wait for the response + err = w.Close() + } // else it has already come and reflected in err + } else { + err = w.Close() + } + assertNoResponseErr(t, err) + }) + } + t.Run("with payload", func(t *testing.T) { test(t, true) }) + t.Run("without payload", func(t *testing.T) { test(t, false) }) + }) + t.Run("on chunk requests", func(t *testing.T) { + for _, n := range []uint{0, 2, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + test(t, 2+n, func(t testing.TB, w io.WriteCloser, err error) { + require.NoError(t, err) + for range n { + _, err = w.Write([]byte{1}) + require.NoError(t, err) + } + _, err = w.Write([]byte{1}) // gRPC client stream does not ACK each request + if err == nil { + // wait for the response + err = w.Close() + } // else it has already come and reflected in err + assertNoResponseErr(t, err) + }) + }) + } + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/653") + // TODO: implement + }) + t.Run("exec statistics", func(t *testing.T) { + type collectedItem struct { + pub []byte + endpoint string + mtd stat.Method + dur time.Duration + err error + } + bind := func() (*testPutObjectServer, *Client, *[]collectedItem) { + srv := newPutObjectServer() + svc := newDefaultObjectService(t, srv) + var collected []collectedItem + handler := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + collected = append(collected, collectedItem{pub: pub, endpoint: endpoint, mtd: mtd, dur: dur, err: err}) + } + c := newCustomClient(t, func(prm *PrmInit) { prm.SetStatisticCallback(handler) }, svc) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + require.Len(t, collected, 1) + require.Nil(t, collected[0].pub) // server key is not yet received + require.Equal(t, testServerEndpoint, collected[0].endpoint) + require.Equal(t, stat.MethodEndpointInfo, collected[0].mtd) + require.Positive(t, collected[0].dur) + require.NoError(t, collected[0].err) + collected = nil + return srv, c, &collected + } + assertCommon := func(c *[]collectedItem) { + collected := *c + for i := range collected { + require.Equal(t, testServerStateOnDial.pub, collected[i].pub) + require.Equal(t, testServerEndpoint, collected[i].endpoint) + require.Positive(t, collected[i].dur) + } } - c := newTestObjectClient(t, &srv) - usr := usertest.User() + t.Run("missing signer", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectPutInit(ctx, anyValidHdr, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 1) + require.Equal(t, stat.MethodObjectPut, collected[0].mtd) + require.NoError(t, collected[0].err) + }) + t.Run("sign heading request failure", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectPutInit(ctx, anyValidHdr, usertest.FailSigner(anyValidSigner), anyValidOpts) + require.ErrorContains(t, err, "header write") + require.ErrorContains(t, err, "sign message") + assertCommon(cl) + collected := *cl + require.Len(t, collected, 2) + require.Equal(t, stat.MethodObjectPutStream, collected[0].mtd) + require.ErrorContains(t, collected[0].err, "sign message") + require.Equal(t, stat.MethodObjectPut, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("sign chunk request failure", func(t *testing.T) { + _, c, cl := bind() + w, err := c.ObjectPutInit(ctx, anyValidHdr, newNFailedSigner(anyValidSigner, signOneReqCalls*2+1), anyValidOpts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.ErrorContains(t, err, "sign message") + err = w.Close() + require.ErrorContains(t, err, "sign message") + assertCommon(cl) + collected := *cl + require.Len(t, collected, 2) + require.Equal(t, stat.MethodObjectPut, collected[0].mtd) + require.NoError(t, collected[0].err) + require.Equal(t, stat.MethodObjectPutStream, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("transport failure", func(t *testing.T) { + srv, c, cl := bind() + transportErr := errors.New("any transport failure") + srv.abortHandlerAfterRequest(3, transportErr) - w, err := c.ObjectPutInit(context.Background(), object.Object{}, usr, PrmObjectPutInit{}) - require.NoError(t, err) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = w.Write([]byte{1}) + time.Sleep(50 * time.Millisecond) // give the response time to come + } + err = w.Close() + assertObjectStreamTransportErr(t, transportErr, err) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 2) + require.Equal(t, stat.MethodObjectPut, collected[0].mtd) + require.NoError(t, collected[0].err) + require.Equal(t, stat.MethodObjectPutStream, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("OK", func(t *testing.T) { + srv, c, cl := bind() + const sleepDur = 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + srv.setSleepDuration(sleepDur) - err = w.Close() - require.ErrorIs(t, err, apistatus.ErrObjectAccessDenied) + w, err := c.ObjectPutInit(ctx, anyValidHdr, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = w.Write([]byte{1}) + require.NoError(t, err) + err = w.Close() + require.NoError(t, err) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 2) + require.Equal(t, stat.MethodObjectPut, collected[0].mtd) + require.NoError(t, collected[0].err) + require.Equal(t, stat.MethodObjectPutStream, collected[1].mtd) + require.NoError(t, err, collected[1].err) + require.Greater(t, collected[1].dur, sleepDur) + }) }) } diff --git a/client/object_search_test.go b/client/object_search_test.go index 33caa346..c74956a6 100644 --- a/client/object_search_test.go +++ b/client/object_search_test.go @@ -5,91 +5,104 @@ import ( "errors" "fmt" "io" + "math" + "math/rand" "testing" + "time" v2object "github.com/nspcc-dev/neofs-api-go/v2/object" 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" + bearertest "github.com/nspcc-dev/neofs-sdk-go/bearer/test" 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" - neofscryptotest "github.com/nspcc-dev/neofs-sdk-go/crypto/test" + "github.com/nspcc-dev/neofs-sdk-go/object" oid "github.com/nspcc-dev/neofs-sdk-go/object/id" oidtest "github.com/nspcc-dev/neofs-sdk-go/object/id/test" + sessiontest "github.com/nspcc-dev/neofs-sdk-go/session/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" usertest "github.com/nspcc-dev/neofs-sdk-go/user/test" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) -func TestObjectSearch(t *testing.T) { - ids := oidtest.IDs(20) - - buf := make([]oid.ID, 2) - checkRead := func(t *testing.T, r *ObjectListReader, expected []oid.ID, expectedErr error) { +func readAllObjectIDs(r *ObjectListReader) ([]oid.ID, error) { + buf := make([]oid.ID, 32) + var collected []oid.ID + for { n, err := r.Read(buf) - if expectedErr == nil { - require.NoError(t, err) - require.True(t, len(expected) == len(buf), "expected the same length") - } else { - require.Error(t, err) - require.True(t, len(expected) != len(buf), "expected different length") + collected = append(collected, buf[:n]...) + if err != nil { + if errors.Is(err, io.EOF) { + err = nil + } + return collected, err } - - require.Equal(t, len(expected), n, "expected %d items to be read", len(expected)) - require.Equal(t, expected, buf[:len(expected)]) } +} - // no data - 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 - checkRead(t, stream, ids[:2], nil) - - // one ID cached, second fetched - checkRead(t, stream, ids[2:4], nil) - - // both ID cached - 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 - checkRead(t, stream, ids[6:8], nil) - - // read from tail multiple times - stream = newTestSearchObjectsStream(t, ids[8:11]) - buf = buf[:1] - checkRead(t, stream, ids[8:9], nil) - checkRead(t, stream, ids[9:10], nil) - checkRead(t, stream, ids[10:11], nil) +func setChunkInSearchResponse(b *protoobject.SearchResponse_Body, c []oid.ID) *protoobject.SearchResponse_Body { + b = proto.Clone(b).(*protoobject.SearchResponse_Body) + b.IdList = make([]*protorefs.ObjectID, len(c)) + for i := range c { + b.IdList[i] = &protorefs.ObjectID{Value: c[i][:]} + } + return b +} - // handle EOF - buf = buf[:2] - stream = newTestSearchObjectsStream(t, ids[11:12]) - checkRead(t, stream, ids[11:12], io.EOF) +type testSearchObjectsServer struct { + protoobject.UnimplementedObjectServiceServer + testCommonServerStreamServerSettings[ + *protoobject.SearchRequest_Body, + v2object.SearchRequestBody, + *v2object.SearchRequestBody, + *protoobject.SearchRequest, + v2object.SearchRequest, + *v2object.SearchRequest, + *protoobject.SearchResponse_Body, + v2object.SearchResponseBody, + *v2object.SearchResponseBody, + *protoobject.SearchResponse, + v2object.SearchResponse, + *v2object.SearchResponse, + ] + testObjectSessionServerSettings + testBearerTokenServerSettings + testRequiredContainerIDServerSettings + testLocalRequestServerSettings + chunk []oid.ID + reqFilters []object.SearchFilter } func TestObjectIterate(t *testing.T) { ids := oidtest.IDs(3) + newTestSearchObjectsStream := func(t testing.TB, code uint32, chunks [][]oid.ID) *ObjectListReader { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + srv.respondWithChunks(chunks) + srv.respondWithStatus(uint(len(chunks)-1), &protostatus.Status{Code: code}) + + r, err := c.ObjectSearchInit(context.Background(), cidtest.ID(), usertest.User(), PrmObjectSearch{}) + require.NoError(t, err) + return r + } t.Run("no objects", func(t *testing.T) { - stream := newTestSearchObjectsStream(t) + stream := newTestSearchObjectsStream(t, 0, nil) var actual []oid.ID require.NoError(t, stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) return false })) - require.Len(t, actual, 0) + require.Empty(t, actual) }) t.Run("iterate all sequence", func(t *testing.T) { - stream := newTestSearchObjectsStream(t, ids[0:2], nil, ids[2:3]) + stream := newTestSearchObjectsStream(t, 0, [][]oid.ID{ids[0:2], nil, ids[2:3]}) var actual []oid.ID require.NoError(t, stream.Iterate(func(id oid.ID) bool { @@ -99,7 +112,7 @@ func TestObjectIterate(t *testing.T) { require.Equal(t, ids[:3], actual) }) t.Run("stop by return value", func(t *testing.T) { - stream := newTestSearchObjectsStream(t, ids) + stream := newTestSearchObjectsStream(t, 0, [][]oid.ID{ids}) var actual []oid.ID require.NoError(t, stream.Iterate(func(id oid.ID) bool { actual = append(actual, id) @@ -108,9 +121,7 @@ func TestObjectIterate(t *testing.T) { require.Equal(t, ids[:2], actual) }) t.Run("stop after error", func(t *testing.T) { - expectedErr := errors.New("test error") - - stream := newTestSearchObjectsStreamWithEndErr(t, expectedErr, ids[:2]) + stream := newTestSearchObjectsStream(t, 1024, [][]oid.ID{ids[:2], ids[2:]}) var actual []oid.ID err := stream.Iterate(func(id oid.ID) bool { @@ -122,86 +133,514 @@ func TestObjectIterate(t *testing.T) { }) } -func TestClient_ObjectSearch(t *testing.T) { - c := newClient(t) +// returns [protoobject.ObjectServiceServer] supporting Search method only. +// Default implementation performs common verification of any request, and +// responds with any valid message stream. Some methods allow to tune the +// behavior. +func newTestSearchObjectsServer() *testSearchObjectsServer { return new(testSearchObjectsServer) } - t.Run("missing signer", func(t *testing.T) { - _, err := c.ObjectSearchInit(context.Background(), cid.ID{}, nil, PrmObjectSearch{}) - require.ErrorIs(t, err, ErrMissingSigner) - }) -} +// makes the server to assert that any request carries given filter set. By +// default, and if nil, any set is accepted. +func (x *testSearchObjectsServer) checkRequestFilters(fs []object.SearchFilter) { x.reqFilters = fs } -func newTestSearchObjectsStreamWithEndErr(t *testing.T, endError error, idList ...[]oid.ID) *ObjectListReader { - usr := usertest.User() - srv := testSearchObjectsServer{ - signer: usr, - endStatus: apistatus.ErrorToV2(endError).ToGRPCMessage().(*protostatus.Status), - idList: idList, - } - stream, err := newTestObjectClient(t, &srv).ObjectSearchInit(context.Background(), cidtest.ID(), usr, PrmObjectSearch{}) - require.NoError(t, err) - return stream -} +// makes the server to return given chunk of IDs in any response. By default, +// and if nil, some non-empty data is returned. +func (x *testSearchObjectsServer) respondWithChunk(chunk []oid.ID) { x.chunk = chunk } -func newTestSearchObjectsStream(t *testing.T, idList ...[]oid.ID) *ObjectListReader { - return newTestSearchObjectsStreamWithEndErr(t, nil, idList...) +// makes the server to respond with given chunk responses. +// +// Overrides configured len(chunks) responses. +func (x *testSearchObjectsServer) respondWithChunks(chunks [][]oid.ID) { + if len(chunks) == 0 { + x.respondWithBody(0, validMinSearchResponseBody) + return + } + for i := range chunks { + b := setChunkInSearchResponse(validFullSearchResponseBody, chunks[i]) + x.respondWithBody(uint(i), b) + } } -type testSearchObjectsServer struct { - protoobject.UnimplementedObjectServiceServer - - signer neofscrypto.Signer - endStatus *protostatus.Status - idList [][]oid.ID +func (x *testSearchObjectsServer) verifyRequest(req *protoobject.SearchRequest) error { + if err := x.testCommonServerStreamServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + // TTL + if err := x.verifyTTL(req.MetaHeader); err != nil { + return err + } + // session token + if err := x.verifySessionToken(req.MetaHeader.GetSessionToken()); err != nil { + return err + } + // bearer token + if err := x.verifyBearerToken(req.MetaHeader.GetBearerToken()); err != nil { + return err + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. address + if err := x.verifyRequestContainerID(body.ContainerId); err != nil { + return err + } + // 2. version + if body.Version != 1 { + return newErrInvalidRequestField("version", fmt.Errorf("wrong value (client: 1, message: %d)", body.Version)) + } + // 3. filters + if x.reqFilters != nil { + if err := checkObjectSearchFiltersTransport(x.reqFilters, body.Filters); err != nil { + return newErrInvalidRequestField("filters", err) + } + } + return nil } -func (x *testSearchObjectsServer) Search(_ *protoobject.SearchRequest, stream protoobject.ObjectService_SearchServer) error { - signer := x.signer - if signer == nil { - signer = neofscryptotest.Signer() +func (x *testSearchObjectsServer) Search(req *protoobject.SearchRequest, stream protoobject.ObjectService_SearchServer) error { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return err + } + if x.handlerErrForced { + return x.handlerErr + } + lastRespInd := uint(1) + if x.resps != nil { + lastRespInd = 0 } - for i := range x.idList { - resp := protoobject.SearchResponse{ - Body: &protoobject.SearchResponse_Body{ - IdList: make([]*protorefs.ObjectID, len(x.idList[i])), - }, + for n := range x.resps { + if n > lastRespInd { + lastRespInd = n } - for j := range x.idList[i] { - resp.Body.IdList[j] = &protorefs.ObjectID{Value: x.idList[i][j][:]} + } + if x.respErrN > lastRespInd { + lastRespInd = x.respErrN + } + chunk := x.chunk + if chunk == nil { + chunk = oidtest.IDs(3) + } + for n := range lastRespInd + 1 { + s := x.resps[n] + resp := &protoobject.SearchResponse{ + MetaHeader: s.respMeta, } - - var respV2 v2object.SearchResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + if s.respBodyForced { + resp.Body = s.respBody + } else { + resp.Body = setChunkInSearchResponse(validFullSearchResponseBody, chunk) } - if err := signServiceMessage(signer, &respV2, nil); err != nil { - return fmt.Errorf("sign response message: %w", err) + var err error + resp.VerifyHeader, err = s.signResponse(resp) + if err != nil { + return fmt.Errorf("sign response: %w", err) } - if err := stream.Send(respV2.ToGRPCMessage().(*protoobject.SearchResponse)); err != nil { - return err + if err := stream.Send(resp); err != nil { + return fmt.Errorf("send response #%d: %w", n, err) + } + if x.respErrN > 0 && n >= x.respErrN-1 { + return x.respErr } } + return nil +} - if x.endStatus == nil { - return nil - } +func TestClient_ObjectSearch(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmObjectSearch + anyCID := cidtest.ID() + anyValidSigner := usertest.User() - resp := protoobject.SearchResponse{ - MetaHeader: &protosession.ResponseMetaHeader{ - Status: x.endStatus, - }, - } + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) - 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 - } + srv.checkRequestContainerID(anyCID) + srv.authenticateRequest(anyValidSigner) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, PrmObjectSearch{}) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestSearchObjectsServer, newTestObjectClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, opts) + if err == nil { + _, err = readAllObjectIDs(r) + } + return err + }) + }) + t.Run("local", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + opts := anyValidOpts + opts.MarkLocal() + + srv.checkRequestLocal() + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, opts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.NoError(t, err) + }) + t.Run("session token", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + st := sessiontest.ObjectSigned(usertest.User()) + opts := anyValidOpts + opts.WithinSession(st) + + srv.checkRequestSessionToken(st) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, opts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.NoError(t, err) + }) + t.Run("bearer token", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + bt := bearertest.Token() + bt.SetEACLTable(anyValidEACL) // TODO: drop after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + require.NoError(t, bt.Sign(usertest.User())) + opts := anyValidOpts + opts.WithBearerToken(bt) + + srv.checkRequestBearerToken(bt) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, opts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.NoError(t, err) + }) + t.Run("filters", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + fs := make(object.SearchFilters, 10) + fs.AddFilter("k1", "v1", object.MatchStringEqual) + fs.AddFilter("k1", "v2", object.MatchStringNotEqual) + fs.AddFilter("k3", "v3", object.MatchNotPresent) + fs.AddFilter("k4", "v4", object.MatchCommonPrefix) + fs.AddFilter("k5", "v5", object.MatchNumGT) + fs.AddFilter("k6", "v6", object.MatchNumGE) + fs.AddFilter("k7", "v7", object.MatchNumLT) + fs.AddFilter("k8", "v8", object.MatchNumLE) + fs.AddFilter("k_max", "v_max", math.MaxInt32) + + opts := anyValidOpts + opts.SetFilters(fs) - return stream.Send(respV2.ToGRPCMessage().(*protoobject.SearchResponse)) + srv.checkRequestFilters(fs) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, opts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + const bigChunkSize = (3<<20 + 500<<10) / oid.Size + bigChunkTwice := oidtest.IDs(bigChunkSize * 2) + smallChunk := oidtest.IDs(10) + for _, tc := range []struct { + name string + chunks [][]oid.ID + }{ + {name: "empty"}, + {name: "with single ID chunk", chunks: [][]oid.ID{smallChunk}}, + {name: "with multiple ID chunks", + chunks: [][]oid.ID{bigChunkTwice[:bigChunkSize], smallChunk, {}, bigChunkTwice[bigChunkSize:]}}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + srv.respondWithChunks(tc.chunks) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + res, err := readAllObjectIDs(r) + require.NoError(t, err) + require.Equal(t, join(tc.chunks), res) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + test := func(t testing.TB, code uint32, assert func(testing.TB, error)) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + chunks := [][]oid.ID{oidtest.IDs(3), oidtest.IDs(5), oidtest.IDs(4)} + + srv.respondWithChunks(chunks) + srv.respondWithStatus(uint(len(chunks))-1, &protostatus.Status{Code: code}) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + assert(t, err) + } + t.Run("OK", func(t *testing.T) { + test(t, 0, func(t testing.TB, err error) { require.NoError(t, err) }) + }) + t.Run("failure", func(t *testing.T) { + var code uint32 + for code == 0 || code == 1024 { + code = rand.Uint32() + } + test(t, code, func(t testing.TB, err error) { + require.EqualError(t, err, "status: code = unrecognized") + // TODO: replace after https://github.com/nspcc-dev/neofs-sdk-go/issues/648 + // require.ErrorIs(t, err, apistatus.Error) + }) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "object.ObjectService", "Search", func(c *Client) error { + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + if err == nil { + _, err = readAllObjectIDs(r) + } + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + const n = 10 + chunks := make([][]oid.ID, n) + for i := range chunks { + chunks[i] = oidtest.IDs(20) + } + + srv.respondWithChunks(chunks) + srv.respondWithoutSigning(n - 1) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + read, err := readAllObjectIDs(r) + require.ErrorContains(t, err, "invalid response signature") + require.Equal(t, join(chunks[:n-1]), read) + }) + t.Run("payloads", func(t *testing.T) { + t.Skip("") + type testcase = struct { + name, msg string + corrupt func(valid *protoobject.SearchResponse_Body) // with 3 valid IDs + } + tcs := []testcase{ + {name: "IDs/nil element", msg: "invalid length 0", corrupt: func(valid *protoobject.SearchResponse_Body) { + valid.IdList[1] = nil + }}, + } + for _, tc := range invalidObjectIDProtoTestcases { + tcs = append(tcs, testcase{name: "IDs/element/" + tc.name, msg: "invalid ID #1: " + tc.msg, + corrupt: func(valid *protoobject.SearchResponse_Body) { tc.corrupt(valid.IdList[1]) }, + }) + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + b := proto.Clone(validFullSearchResponseBody).(*protoobject.SearchResponse_Body) + tc.corrupt(b) + + srv.respondWithBody(0, b) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = readAllObjectIDs(r) + require.EqualError(t, err, tc.msg) + }) + } + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.ObjectSearchInit(ctx, anyCID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + t.Run("empty buffer", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + require.PanicsWithValue(t, "empty buffer in ObjectListReader.ReadList", func() { _, _ = r.Read(nil) }) + require.PanicsWithValue(t, "empty buffer in ObjectListReader.ReadList", func() { _, _ = r.Read([]oid.ID{}) }) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestSearchObjectsServer, newTestObjectClient, func(ctx context.Context, c *Client) error { + _, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).ObjectSearchInit(ctx, anyCID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + t.Run("on payload transmission", func(t *testing.T) { + for _, n := range []uint{0, 2, 10} { + t.Run(fmt.Sprintf("after %d successes", n), func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + chunk := oidtest.IDs(10) + transportErr := errors.New("any transport failure") + + srv.respondWithChunk(chunk) + srv.abortHandlerAfterResponse(n, transportErr) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for range n * uint(len(chunk)) { + _, err = r.Read([]oid.ID{{}}) + require.NoError(t, err) + } + _, err = r.Read([]oid.ID{{}}) + assertObjectStreamTransportErr(t, transportErr, err) + }) + } + }) + t.Run("too large chunk message", func(t *testing.T) { + srv := newTestSearchObjectsServer() + c := newTestObjectClient(t, srv) + + b := setChunkInSearchResponse(validFullSearchResponseBody, make([]oid.ID, 4194304/oid.Size)) + + srv.respondWithBody(0, b) + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + _, err = r.Read([]oid.ID{{}}) + st, ok := status.FromError(err) + require.True(t, ok, err) + require.Equal(t, codes.ResourceExhausted, st.Code()) + require.Contains(t, st.Message(), "grpc: received message larger than max (") + require.Contains(t, st.Message(), " vs. 4194304)") + }) + }) + t.Run("response callback", func(t *testing.T) { + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/653") + // TODO: implement + }) + t.Run("exec statistics", func(t *testing.T) { + type collectedItem struct { + pub []byte + endpoint string + mtd stat.Method + dur time.Duration + err error + } + bind := func() (*testSearchObjectsServer, *Client, *[]collectedItem) { + srv := newTestSearchObjectsServer() + svc := newDefaultObjectService(t, srv) + var collected []collectedItem + handler := func(pub []byte, endpoint string, mtd stat.Method, dur time.Duration, err error) { + collected = append(collected, collectedItem{pub: pub, endpoint: endpoint, mtd: mtd, dur: dur, err: err}) + } + c := newCustomClient(t, func(prm *PrmInit) { prm.SetStatisticCallback(handler) }, svc) + // [Client.EndpointInfo] is always called to dial the server: this is also submitted + require.Len(t, collected, 1) + require.Nil(t, collected[0].pub) // server key is not yet received + require.Equal(t, testServerEndpoint, collected[0].endpoint) + require.Equal(t, stat.MethodEndpointInfo, collected[0].mtd) + require.Positive(t, collected[0].dur) + require.NoError(t, collected[0].err) + collected = nil + return srv, c, &collected + } + assertCommon := func(c *[]collectedItem) { + collected := *c + for i := range collected { + require.Equal(t, testServerStateOnDial.pub, collected[i].pub) + require.Equal(t, testServerEndpoint, collected[i].endpoint) + require.Positive(t, collected[i].dur) + } + } + t.Run("missing signer", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectSearchInit(ctx, anyCID, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + assertCommon(cl) + collected := *cl + require.Len(t, *cl, 1) + require.Equal(t, stat.MethodObjectSearch, collected[0].mtd) + require.NoError(t, collected[0].err) + }) + t.Run("sign request", func(t *testing.T) { + _, c, cl := bind() + _, err := c.ObjectSearchInit(ctx, anyCID, usertest.FailSigner(anyValidSigner), anyValidOpts) + assertSignRequestErr(t, err) + assertCommon(cl) + collected := *cl + require.Len(t, collected, 1) + require.Equal(t, stat.MethodObjectSearch, collected[0].mtd) + require.Equal(t, err, collected[0].err) + }) + t.Run("transport failure", func(t *testing.T) { + srv, c, cl := bind() + transportErr := errors.New("any transport failure") + srv.abortHandlerAfterResponse(3, transportErr) + + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = r.Read([]oid.ID{{}}) + } + assertObjectStreamTransportErr(t, transportErr, err) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectSearch, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectSearchStream, collected[1].mtd) + require.Equal(t, err, collected[1].err) + }) + t.Run("OK", func(t *testing.T) { + srv, c, cl := bind() + const sleepDur = 100 * time.Millisecond + // duration is pretty short overall, but most likely larger than the exec time w/o sleep + srv.setSleepDuration(sleepDur) + + r, err := c.ObjectSearchInit(ctx, anyCID, anyValidSigner, anyValidOpts) + require.NoError(t, err) + for err == nil { + _, err = r.Read([]oid.ID{{}}) + } + require.ErrorIs(t, err, io.EOF) + assertCommon(cl) + collected := *cl + require.Equal(t, stat.MethodObjectSearch, collected[0].mtd) + require.NoError(t, collected[0].err) + t.Skip("https://github.com/nspcc-dev/neofs-sdk-go/issues/656") + require.Len(t, collected, 2) // move upper + require.Equal(t, stat.MethodObjectSearchStream, collected[1].mtd) + require.NoError(t, err, collected[1].err) + require.Greater(t, collected[1].dur, sleepDur) + }) + }) } diff --git a/client/object_test.go b/client/object_test.go index 86cd3650..ccd1b907 100644 --- a/client/object_test.go +++ b/client/object_test.go @@ -1,12 +1,358 @@ package client import ( + "errors" + "fmt" + "strings" "testing" + protoacl "github.com/nspcc-dev/neofs-api-go/v2/acl/grpc" 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" + "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" + "github.com/nspcc-dev/neofs-sdk-go/session" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) -// 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}) +type ( + invalidObjectSplitInfoProtoTestcase = struct { + name, msg string + corrupt func(valid *protoobject.SplitInfo) + } + invalidObjectHeaderProtoTestcase = struct { + name, msg string + corrupt func(valid *protoobject.Header) + } +) + +// various sets of Object service testcases. +var ( + invalidObjectSplitInfoProtoTestcases = []invalidObjectSplitInfoProtoTestcase{ + {name: "neither linker nor last", msg: "neither link object ID nor last part object ID is set", corrupt: func(valid *protoobject.SplitInfo) { + valid.Reset() + }}, + // + other cases in init + } + invalidObjectSessionTokenProtoTestcases = append(invalidCommonSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "context/wrong", msg: "invalid context: invalid context *session.ContainerSessionContext", + corrupt: func(valid *protosession.SessionToken) { + valid.Body.Context = new(protosession.SessionToken_Body_Container) + }}, + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // invalidSessionTokenProtoTestcase{ + // name: "context/verb/negative", msg: "invalid context: negative verb -1", + // corrupt: func(valid *protosession.SessionToken) { + // c := valid.Body.Context.(*protosession.SessionToken_Body_Object).Object + // c.Verb = -1 + // }, + // }, + invalidSessionTokenProtoTestcase{ + name: "context/container/nil", msg: "invalid context: missing target container", + corrupt: func(valid *protosession.SessionToken) { + c := valid.Body.Context.(*protosession.SessionToken_Body_Object).Object + c.Target.Container = nil + }, + }) // + other container and object ID cases in init + invalidObjectHeaderProtoTestcases = []invalidObjectHeaderProtoTestcase{ + // 1. version (any accepted, even absent) + // 2. container (init) + // 3. owner (init) + // 4. creation epoch (any accepted) + // 5. payload length (any accepted) + // 6. payload checksum (init) + // TODO: uncomment after https://github.com/nspcc-dev/neofs-sdk-go/issues/606 + // {name: "type/negative", msg: "negative type -1", corrupt: func(valid *protoobject.Header) { + // valid.ObjectType = -1 + // }}, + // 8. homomorphic payload checksum (init) + // 9. session token (init) + {name: "attributes/no key", msg: "empty key of the attribute #1", + corrupt: func(valid *protoobject.Header) { + valid.Attributes = []*protoobject.Header_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "", Value: "v2"}, {Key: "k3", Value: "v3"}, + } + }}, + {name: "attributes/no value", msg: "empty value of the attribute #1 (k2)", + corrupt: func(valid *protoobject.Header) { + valid.Attributes = []*protoobject.Header_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "k2", Value: ""}, {Key: "k3", Value: "v3"}, + } + }}, + {name: "attributes/duplicated", msg: "duplicated attribute k1", + corrupt: func(valid *protoobject.Header) { + valid.Attributes = []*protoobject.Header_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "k2", Value: "v2"}, {Key: "k1", Value: "v3"}, + } + }}, + {name: "attributes/expiration", msg: `invalid expiration attribute (must be a uint): strconv.ParseUint: parsing "foo": invalid syntax`, + corrupt: func(valid *protoobject.Header) { + valid.Attributes = []*protoobject.Header_Attribute{ + {Key: "k1", Value: "v1"}, {Key: "__NEOFS__EXPIRATION_EPOCH", Value: "foo"}, {Key: "k3", Value: "v3"}, + } + }}, + // 11. split (init) + } +) + +func init() { + // session token + for _, tc := range invalidContainerIDProtoTestcases { + invalidObjectSessionTokenProtoTestcases = append(invalidObjectSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "context/container/" + tc.name, msg: "invalid context: invalid container ID: " + tc.msg, + corrupt: func(valid *protosession.SessionToken) { + c := valid.Body.Context.(*protosession.SessionToken_Body_Object).Object + tc.corrupt(c.Target.Container) + }, + }) + } + for _, tc := range invalidObjectIDProtoTestcases { + invalidObjectSessionTokenProtoTestcases = append(invalidObjectSessionTokenProtoTestcases, invalidSessionTokenProtoTestcase{ + name: "context/objects/" + tc.name, msg: "invalid context: invalid target object: " + tc.msg, + corrupt: func(valid *protosession.SessionToken) { + c := valid.Body.Context.(*protosession.SessionToken_Body_Object).Object + c.Target.Objects = []*protorefs.ObjectID{ + proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[1]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[2]).(*protorefs.ObjectID), + } + tc.corrupt(c.Target.Objects[1]) + }, + }) + } + // split info + for _, tc := range invalidUUIDProtoTestcases { + invalidObjectSplitInfoProtoTestcases = append(invalidObjectSplitInfoProtoTestcases, invalidObjectSplitInfoProtoTestcase{ + name: "split ID/" + tc.name, msg: "invalid split ID: " + tc.msg, + corrupt: func(valid *protoobject.SplitInfo) { valid.SplitId = tc.corrupt(valid.SplitId) }, + }) + } + for _, tc := range invalidObjectIDProtoTestcases { + invalidObjectSplitInfoProtoTestcases = append(invalidObjectSplitInfoProtoTestcases, invalidObjectSplitInfoProtoTestcase{ + name: "last ID/" + tc.name, msg: "could not convert last part object ID: " + tc.msg, + corrupt: func(valid *protoobject.SplitInfo) { tc.corrupt(valid.LastPart) }, + }, invalidObjectSplitInfoProtoTestcase{ + name: "linker/" + tc.name, msg: "could not convert link object ID: " + tc.msg, + corrupt: func(valid *protoobject.SplitInfo) { tc.corrupt(valid.Link) }, + }, invalidObjectSplitInfoProtoTestcase{ + name: "first ID/" + tc.name, msg: "could not convert first part object ID: " + tc.msg, + corrupt: func(valid *protoobject.SplitInfo) { tc.corrupt(valid.FirstPart) }, + }) + } + // header + for _, tc := range invalidContainerIDProtoTestcases { + invalidObjectHeaderProtoTestcases = append(invalidObjectHeaderProtoTestcases, invalidObjectHeaderProtoTestcase{ + name: "container/" + tc.name, msg: "invalid container: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.ContainerId) }, + }) + } + for _, tc := range invalidUserIDProtoTestcases { + invalidObjectHeaderProtoTestcases = append(invalidObjectHeaderProtoTestcases, invalidObjectHeaderProtoTestcase{ + name: "owner/" + tc.name, msg: "invalid owner: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.OwnerId) }, + }) + } + for _, tc := range invalidChecksumTestcases { + invalidObjectHeaderProtoTestcases = append(invalidObjectHeaderProtoTestcases, invalidObjectHeaderProtoTestcase{ + name: "payload checksum/" + tc.name, msg: "invalid payload checksum: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.PayloadHash) }, + }, invalidObjectHeaderProtoTestcase{ + name: "payload homomorphic checksum/" + tc.name, msg: "invalid payload homomorphic checksum: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.HomomorphicHash) }, + }) + } + type splitTestcase = struct { + name, msg string + corrupt func(split *protoobject.Header_Split) + } + var splitTestcases []splitTestcase + for _, tc := range invalidObjectHeaderProtoTestcases { + splitTestcases = append(splitTestcases, splitTestcase{ + name: "parent header/" + tc.name, msg: "invalid parent header: " + strings.ReplaceAll(tc.msg, "invalid header: ", ""), + corrupt: func(valid *protoobject.Header_Split) { tc.corrupt(valid.ParentHeader) }, + }) + } + for _, tc := range invalidObjectIDProtoTestcases { + splitTestcases = append(splitTestcases, splitTestcase{ + name: "parent ID/" + tc.name, msg: "invalid parent split member ID: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { tc.corrupt(valid.Parent) }, + }, splitTestcase{ + name: "previous ID/" + tc.name, msg: "invalid previous split member ID: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { tc.corrupt(valid.Previous) }, + }, splitTestcase{ + name: "first ID/" + tc.name, msg: "invalid first split member ID: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { tc.corrupt(valid.First) }, + }, splitTestcase{ + name: "children/" + tc.name, msg: "invalid child split member ID #1: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { + valid.Children = []*protorefs.ObjectID{ + proto.Clone(validProtoObjectIDs[0]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[1]).(*protorefs.ObjectID), + proto.Clone(validProtoObjectIDs[2]).(*protorefs.ObjectID), + } + tc.corrupt(valid.Children[1]) + }, + }) + } + for _, tc := range invalidSignatureProtoTestcases { + splitTestcases = append(splitTestcases, splitTestcase{ + name: "parent signature/" + tc.name, msg: "invalid parent signature: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { tc.corrupt(valid.ParentSignature) }, + }) + } + for _, tc := range invalidUUIDProtoTestcases { + splitTestcases = append(splitTestcases, splitTestcase{ + name: "split ID/" + tc.name, msg: "invalid split ID: " + tc.msg, + corrupt: func(valid *protoobject.Header_Split) { valid.SplitId = tc.corrupt(valid.SplitId) }, + }) + } + for _, tc := range splitTestcases { + invalidObjectHeaderProtoTestcases = append(invalidObjectHeaderProtoTestcases, invalidObjectHeaderProtoTestcase{ + name: "split header/" + tc.name, + msg: "invalid split header: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.Split) }, + }) + } + for _, tc := range invalidObjectSessionTokenProtoTestcases { + invalidObjectHeaderProtoTestcases = append(invalidObjectHeaderProtoTestcases, invalidObjectHeaderProtoTestcase{ + name: "session token/" + tc.name, msg: "invalid session token: " + tc.msg, + corrupt: func(valid *protoobject.Header) { tc.corrupt(valid.SessionToken) }, + }) + } +} + +// returns Client-compatible Object service handled by given server. Provided +// server must implement [protoobject.ObjectServiceServer]: the parameter is not +// of this type to support generics. +func newDefaultObjectService(t testing.TB, srv any) testService { + require.Implements(t, (*protoobject.ObjectServiceServer)(nil), srv) + return testService{desc: &protoobject.ObjectService_ServiceDesc, impl: srv} +} + +// returns Client of Object service provided by given server. Provided server +// must implement [protoobject.ObjectServiceServer]: the parameter is not of +// this type to support generics. +func newTestObjectClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultObjectService(t, srv)) +} + +func assertObjectStreamTransportErr(t testing.TB, transportErr, err error) { + require.Error(t, err) + require.NotContains(t, err.Error(), "open stream") // gRPC client cannot catch this + st, ok := status.FromError(err) + require.True(t, ok, err) + require.Equal(t, codes.Unknown, st.Code()) + require.Contains(t, st.Message(), transportErr.Error()) +} + +// for sharing between servers of requests that can be for local execution only. +type testLocalRequestServerSettings struct { + reqLocal bool +} + +// makes the server to assert that any request has TTL = 1. By default, TTL must +// be 2. +func (x *testLocalRequestServerSettings) checkRequestLocal() { x.reqLocal = true } + +func (x testLocalRequestServerSettings) verifyTTL(m *protosession.RequestMetaHeader) error { + var exp uint32 + if x.reqLocal { + exp = 1 + } else { + exp = 2 + } + if act := m.GetTtl(); act != exp { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected %d", act, exp)) + } + return nil +} + +// for sharing between servers of requests with required object address. +type testObjectAddressServerSettings struct { + c testRequiredContainerIDServerSettings + expectedReqObjID *oid.ID +} + +// makes the server to assert that any request carries given object address. By +// default, any address is accepted. +func (x *testObjectAddressServerSettings) checkRequestObjectAddress(c cid.ID, o oid.ID) { + x.c.checkRequestContainerID(c) + x.expectedReqObjID = &o +} + +func (x testObjectAddressServerSettings) verifyObjectAddress(m *protorefs.Address) error { + if m == nil { + return newErrMissingRequestBodyField("object address") + } + if err := x.c.verifyRequestContainerID(m.ContainerId); err != nil { + return err + } + if m.ObjectId == nil { + return newErrMissingRequestBodyField("object ID") + } + if x.expectedReqObjID != nil { + if err := checkObjectIDTransport(*x.expectedReqObjID, m.ObjectId); err != nil { + return newErrInvalidRequestField("container ID", err) + } + } + return nil +} + +// for sharing between servers of requests with an object session token. +type testObjectSessionServerSettings struct { + expectedToken *session.Object +} + +// makes the server to assert that any request carries given session token. By +// default, session token must not be attached. +func (x *testObjectSessionServerSettings) checkRequestSessionToken(st session.Object) { + x.expectedToken = &st +} + +func (x testObjectSessionServerSettings) verifySessionToken(m *protosession.SessionToken) error { + if m == nil { + if x.expectedToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("session token is missing while should not be")) + } + return nil + } + if x.expectedToken == nil { + return newInvalidRequestMetaHeaderErr(errors.New("session token attached while should not be")) + } + if err := checkObjectSessionTransport(*x.expectedToken, m); err != nil { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("session token: %w", err)) + } + return nil +} + +// for sharing between servers of requests with a bearer token. +type testBearerTokenServerSettings struct { + expectedToken *bearer.Token +} + +// makes the server to assert that any request carries given bearer token. By +// default, bearer token must not be attached. +func (x *testBearerTokenServerSettings) checkRequestBearerToken(bt bearer.Token) { + x.expectedToken = &bt +} + +func (x testBearerTokenServerSettings) verifyBearerToken(m *protoacl.BearerToken) error { + if m == nil { + if x.expectedToken != nil { + return newInvalidRequestMetaHeaderErr(errors.New("bearer token is missing while should not be")) + } + return nil + } + if x.expectedToken == nil { + return newInvalidRequestMetaHeaderErr(errors.New("bearer token attached while should not be")) + } + if err := checkBearerTokenTransport(*x.expectedToken, m); err != nil { + return newInvalidRequestMetaHeaderErr(fmt.Errorf("bearer token: %w", err)) + } + return nil } diff --git a/client/reputation_test.go b/client/reputation_test.go index 975af462..ebe53e5e 100644 --- a/client/reputation_test.go +++ b/client/reputation_test.go @@ -2,53 +2,497 @@ package client import ( "context" + "errors" "fmt" + "math/rand" "testing" + "time" - "github.com/nspcc-dev/neofs-api-go/v2/reputation" + apireputation "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" + "github.com/nspcc-dev/neofs-sdk-go/reputation" + reputationtest "github.com/nspcc-dev/neofs-sdk-go/reputation/test" + "github.com/nspcc-dev/neofs-sdk-go/stat" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) -// 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}) +// returns Client-compatible Reputation service handled by given server. +// Provided server must implement [protoreputation.ReputationServiceServer]: the +// parameter is not of this type to support generics. +func newDefaultReputationServiceDesc(t testing.TB, srv any) testService { + require.Implements(t, (*protoreputation.ReputationServiceServer)(nil), srv) + return testService{desc: &protoreputation.ReputationService_ServiceDesc, impl: srv} +} + +// returns Client of Reputation service provided by given server. Provided +// server must implement [protoreputation.ReputationServiceServer]: the +// parameter is not of this type to support generics. +func newTestReputationClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultReputationServiceDesc(t, srv)) } type testAnnounceIntermediateReputationServer struct { protoreputation.UnimplementedReputationServiceServer + testCommonUnaryServerSettings[ + *protoreputation.AnnounceIntermediateResultRequest_Body, + apireputation.AnnounceIntermediateResultRequestBody, + *apireputation.AnnounceIntermediateResultRequestBody, + *protoreputation.AnnounceIntermediateResultRequest, + apireputation.AnnounceIntermediateResultRequest, + *apireputation.AnnounceIntermediateResultRequest, + *protoreputation.AnnounceIntermediateResultResponse_Body, + apireputation.AnnounceIntermediateResultResponseBody, + *apireputation.AnnounceIntermediateResultResponseBody, + *protoreputation.AnnounceIntermediateResultResponse, + apireputation.AnnounceIntermediateResultResponse, + *apireputation.AnnounceIntermediateResultResponse, + ] + reqEpoch *uint64 + reqIter uint32 + reqTrust *reputation.PeerToPeerTrust +} + +// returns [protoreputation.ReputationServiceServer] supporting +// AnnounceIntermediateResult method only. Default implementation performs +// common verification of any request, and responds with any valid message. Some +// methods allow to tune the behavior. +func newTestAnnounceIntermediateReputationServer() *testAnnounceIntermediateReputationServer { + return new(testAnnounceIntermediateReputationServer) +} + +// makes the server to assert that any request is for the given epoch. By +// default, any epoch is accepted. +func (x *testAnnounceIntermediateReputationServer) checkRequestEpoch(epoch uint64) { + x.reqEpoch = &epoch +} + +// makes the server to assert that any request is for the given iteration. By +// default, iteration must be unset. +func (x *testAnnounceIntermediateReputationServer) checkRequestIteration(iter uint32) { + x.reqIter = iter +} + +// makes the server to assert that any request has given trust. By default, +// any valid trust is accepted. +func (x *testAnnounceIntermediateReputationServer) checkRequestTrust(t reputation.PeerToPeerTrust) { + x.reqTrust = &t +} + +func (x *testAnnounceIntermediateReputationServer) verifyRequest(req *protoreputation.AnnounceIntermediateResultRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. epoch + if body.Epoch == 0 { + return newErrInvalidRequestField("epoch", errors.New("zero")) + } + if x.reqEpoch != nil && body.Epoch != *x.reqEpoch { + return newErrInvalidRequestField("epoch", errors.New("mismatches the test input")) + } + // 2. iteration + if body.Iteration != x.reqIter { + return newErrInvalidRequestField("iteration", errors.New("mismatches the test input")) + } + // 3. trust + if body.Trust == nil { + return newErrMissingRequestBodyField("trust") + } + if x.reqTrust != nil { + if err := checkP2PTrustTransport(*x.reqTrust, body.Trust); err != nil { + return newErrInvalidRequestField("trust", err) + } + } + return nil } -func (x *testAnnounceIntermediateReputationServer) AnnounceIntermediateResult(context.Context, *protoreputation.AnnounceIntermediateResultRequest, +func (x *testAnnounceIntermediateReputationServer) AnnounceIntermediateResult(_ context.Context, req *protoreputation.AnnounceIntermediateResultRequest, ) (*protoreputation.AnnounceIntermediateResultResponse, error) { - var resp protoreputation.AnnounceIntermediateResultResponse + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr + } - var respV2 reputation.AnnounceIntermediateResultResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + resp := &protoreputation.AnnounceIntermediateResultResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinAnnounceIntermediateRepResponseBody).(*protoreputation.AnnounceIntermediateResultResponse_Body) } - return respV2.ToGRPCMessage().(*protoreputation.AnnounceIntermediateResultResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } type testAnnounceLocalTrustServer struct { protoreputation.UnimplementedReputationServiceServer + testCommonUnaryServerSettings[ + *protoreputation.AnnounceLocalTrustRequest_Body, + apireputation.AnnounceLocalTrustRequestBody, + *apireputation.AnnounceLocalTrustRequestBody, + *protoreputation.AnnounceLocalTrustRequest, + apireputation.AnnounceLocalTrustRequest, + *apireputation.AnnounceLocalTrustRequest, + *protoreputation.AnnounceLocalTrustResponse_Body, + apireputation.AnnounceLocalTrustResponseBody, + *apireputation.AnnounceLocalTrustResponseBody, + *protoreputation.AnnounceLocalTrustResponse, + apireputation.AnnounceLocalTrustResponse, + *apireputation.AnnounceLocalTrustResponse, + ] + reqEpoch *uint64 + reqTrusts []reputation.Trust +} + +// returns [protoreputation.ReputationServiceServer] supporting +// AnnounceLocalTrust method only. Default implementation performs common +// verification of any request, and responds with any valid message. Some +// methods allow to tune the behavior. +func newTestAnnounceLocalTrustServer() *testAnnounceLocalTrustServer { + return new(testAnnounceLocalTrustServer) +} + +// makes the server to assert that any request is for the given epoch. By +// default, any epoch is accepted. +func (x *testAnnounceLocalTrustServer) checkRequestEpoch(epoch uint64) { x.reqEpoch = &epoch } + +// makes the server to assert that any request has given trust. By default, and +// if nil, any valid trusts are accepted. +func (x *testAnnounceLocalTrustServer) checkRequestTrusts(ts []reputation.Trust) { + x.reqTrusts = ts } -func (x *testAnnounceLocalTrustServer) AnnounceLocalTrust(context.Context, *protoreputation.AnnounceLocalTrustRequest, +func (x *testAnnounceLocalTrustServer) verifyRequest(req *protoreputation.AnnounceLocalTrustRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err + } + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) + } + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. epoch + if body.Epoch == 0 { + return newErrInvalidRequestField("epoch", errors.New("zero")) + } + if x.reqEpoch != nil && body.Epoch != *x.reqEpoch { + return newErrInvalidRequestField("epoch", errors.New("mismatches the test input")) + } + // 2. trusts + if len(body.Trusts) == 0 { + return newErrMissingRequestBodyField("trusts") + } + if x.reqTrusts != nil { + if v1, v2 := len(x.reqTrusts), len(body.Trusts); v1 != v2 { + return fmt.Errorf("number of trusts (client: %d, message: %d)", v1, v2) + } + for i := range x.reqTrusts { + if err := checkTrustTransport(x.reqTrusts[i], body.Trusts[i]); err != nil { + return newErrInvalidRequestField("trusts", fmt.Errorf("element #%d: %w", i, err)) + } + } + } + return nil +} + +func (x *testAnnounceLocalTrustServer) AnnounceLocalTrust(_ context.Context, req *protoreputation.AnnounceLocalTrustRequest, ) (*protoreputation.AnnounceLocalTrustResponse, error) { - var resp protoreputation.AnnounceLocalTrustResponse + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { + return nil, err + } + if x.handlerErr != nil { + return nil, x.handlerErr + } - var respV2 reputation.AnnounceLocalTrustResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + resp := &protoreputation.AnnounceLocalTrustResponse{ + MetaHeader: x.respMeta, } - if err := signServiceMessage(neofscryptotest.Signer(), &respV2, nil); err != nil { - return nil, fmt.Errorf("sign response message: %w", err) + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinAnnounceLocalTrustResponseBody).(*protoreputation.AnnounceLocalTrustResponse_Body) } - return respV2.ToGRPCMessage().(*protoreputation.AnnounceLocalTrustResponse), nil + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil +} + +func TestClient_AnnounceIntermediateTrust(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmAnnounceIntermediateTrust + const anyValidEpoch = 123 + anyValidTrust := reputationtest.PeerToPeerTrust() + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestAnnounceIntermediateReputationServer() + c := newTestReputationClient(t, srv) + + srv.checkRequestEpoch(anyValidEpoch) + srv.checkRequestTrust(anyValidTrust) + srv.authenticateRequest(c.prm.signer) + err := c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestAnnounceIntermediateReputationServer, newTestReputationClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, opts) + }) + }) + t.Run("iteration", func(t *testing.T) { + srv := newTestAnnounceIntermediateReputationServer() + c := newTestReputationClient(t, srv) + + iter := rand.Uint32() + opts := anyValidOpts + opts.SetIteration(iter) + + srv.checkRequestIteration(iter) + err := c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoreputation.AnnounceIntermediateResultResponse_Body + }{ + {name: "min", body: validMinAnnounceIntermediateRepResponseBody}, + {name: "full", body: validFullAnnounceIntermediateRepResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestAnnounceIntermediateReputationServer() + c := newTestReputationClient(t, srv) + + srv.respondWithBody(tc.body) + err := c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + require.NoError(t, err) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestAnnounceIntermediateReputationServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "reputation.ReputationService", "AnnounceIntermediateResult", func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestAnnounceIntermediateReputationServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("zero epoch", func(t *testing.T) { + err := c.AnnounceIntermediateTrust(ctx, 0, anyValidTrust, anyValidOpts) + require.ErrorIs(t, err, ErrZeroEpoch) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestAnnounceIntermediateReputationServer, newTestReputationClient, func(ctx context.Context, c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestAnnounceIntermediateReputationServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestAnnounceIntermediateReputationServer, newDefaultReputationServiceDesc, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestAnnounceIntermediateReputationServer, newDefaultReputationServiceDesc, stat.MethodAnnounceIntermediateTrust, + nil, []testedClientOp{func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, 0, anyValidTrust, anyValidOpts) + }, + }, func(c *Client) error { + return c.AnnounceIntermediateTrust(ctx, anyValidEpoch, anyValidTrust, anyValidOpts) + }, + ) + }) +} + +func TestClient_AnnounceLocalTrust(t *testing.T) { + ctx := context.Background() + var anyValidOpts PrmAnnounceLocalTrust + const anyValidEpoch = 123 + anyValidTrusts := []reputation.Trust{reputationtest.Trust(), reputationtest.Trust()} + + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestAnnounceLocalTrustServer() + c := newTestReputationClient(t, srv) + + srv.checkRequestEpoch(anyValidEpoch) + srv.checkRequestTrusts(anyValidTrusts) + srv.authenticateRequest(c.prm.signer) + err := c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestAnnounceLocalTrustServer, newTestReputationClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, opts) + }) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protoreputation.AnnounceLocalTrustResponse_Body + }{ + {name: "min", body: validMinAnnounceLocalTrustResponseBody}, + {name: "full", body: validFullAnnounceLocalTrustRepResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestAnnounceLocalTrustServer() + c := newTestReputationClient(t, srv) + + srv.respondWithBody(tc.body) + err := c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + require.NoError(t, err) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestAnnounceLocalTrustServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "reputation.ReputationService", "AnnounceLocalTrust", func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestAnnounceLocalTrustServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("zero epoch", func(t *testing.T) { + err := c.AnnounceLocalTrust(ctx, 0, anyValidTrusts, anyValidOpts) + require.ErrorIs(t, err, ErrZeroEpoch) + }) + t.Run("empty trusts", func(t *testing.T) { + err := c.AnnounceLocalTrust(ctx, anyValidEpoch, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingTrusts) + err = c.AnnounceLocalTrust(ctx, anyValidEpoch, []reputation.Trust{}, anyValidOpts) + require.ErrorIs(t, err, ErrMissingTrusts) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestAnnounceLocalTrustServer, newTestReputationClient, func(ctx context.Context, c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + t.Run("sign request failure", func(t *testing.T) { + testSignRequestFailure(t, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestAnnounceLocalTrustServer, newTestReputationClient, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestAnnounceLocalTrustServer, newDefaultReputationServiceDesc, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestAnnounceLocalTrustServer, newDefaultReputationServiceDesc, stat.MethodAnnounceLocalTrust, + nil, []testedClientOp{func(c *Client) error { + return c.AnnounceLocalTrust(ctx, 0, anyValidTrusts, anyValidOpts) + }, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, nil, anyValidOpts) + }}, func(c *Client) error { + return c.AnnounceLocalTrust(ctx, anyValidEpoch, anyValidTrusts, anyValidOpts) + }, + ) + }) } diff --git a/client/session_test.go b/client/session_test.go index dd9dcc0e..f4304c59 100644 --- a/client/session_test.go +++ b/client/session_test.go @@ -2,80 +2,284 @@ package client import ( "context" + "errors" + "fmt" + "math/rand" "testing" + "time" - "github.com/nspcc-dev/neofs-api-go/v2/session" + apisession "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" + "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/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) -// 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}) +// returns Client-compatible Session service handled by given server. Provided +// server must implement [protosession.SessionServiceServer]: the parameter is +// not of this type to support generics. +func newDefaultSessionServiceDesc(t testing.TB, srv any) testService { + require.Implements(t, (*protosession.SessionServiceServer)(nil), srv) + return testService{desc: &protosession.SessionService_ServiceDesc, impl: srv} +} + +// returns Client of Session service provided by given server. Provided server +// must implement [protosession.SessionServiceServer]: the parameter is not of +// this type to support generics. +func newTestSessionClient(t testing.TB, srv any) *Client { + return newClient(t, newDefaultSessionServiceDesc(t, srv)) } type testCreateSessionServer struct { protosession.UnimplementedSessionServiceServer - signer neofscrypto.Signer - - unsetID bool - unsetKey bool + testCommonUnaryServerSettings[ + *protosession.CreateRequest_Body, + apisession.CreateRequestBody, + *apisession.CreateRequestBody, + *protosession.CreateRequest, + apisession.CreateRequest, + *apisession.CreateRequest, + *protosession.CreateResponse_Body, + apisession.CreateResponseBody, + *apisession.CreateResponseBody, + *protosession.CreateResponse, + apisession.CreateResponse, + *apisession.CreateResponse, + ] + reqUsr *user.ID + reqExp uint64 } -func (m *testCreateSessionServer) Create(context.Context, *protosession.CreateRequest) (*protosession.CreateResponse, error) { - resp := protosession.CreateResponse{ - Body: new(protosession.CreateResponse_Body), - } +// returns [protosession.SessionServiceServer] supporting Create method only. +// Default implementation performs common verification of any request, and +// responds with any valid message. Some methods allow to tune the behavior. +func newTestCreateSessionInfoServer() *testCreateSessionServer { return new(testCreateSessionServer) } + +// makes the server to assert that any request is for the given user. By +// default, any user is accepted. +func (x *testCreateSessionServer) checkRequestAccount(usr user.ID) { x.reqUsr = &usr } + +// makes the server to assert that any request has given expiration epoch. By +// default, expiration must be unset. +func (x *testCreateSessionServer) checkRequestExpirationEpoch(epoch uint64) { x.reqExp = epoch } - if !m.unsetID { - resp.Body.Id = []byte{1} +func (x *testCreateSessionServer) verifyRequest(req *protosession.CreateRequest) error { + if err := x.testCommonUnaryServerSettings.verifyRequest(req); err != nil { + return err } - if !m.unsetKey { - resp.Body.SessionKey = []byte{1} + // meta header + switch metaHdr := req.MetaHeader; { + case metaHdr.Ttl != 2: + return newInvalidRequestMetaHeaderErr(fmt.Errorf("wrong TTL %d, expected 2", metaHdr.Ttl)) + 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")) } - - var respV2 session.CreateResponse - if err := respV2.FromGRPCMessage(&resp); err != nil { - panic(err) + // body + body := req.Body + if body == nil { + return newInvalidRequestBodyErr(errors.New("missing body")) + } + // 1. user + if body.OwnerId == nil { + return newErrMissingRequestBodyField("user") + } + if x.reqUsr != nil { + if err := checkUserIDTransport(*x.reqUsr, body.OwnerId); err != nil { + return newErrInvalidRequestField("user", err) + } } - signer := m.signer - if signer == nil { - signer = neofscryptotest.Signer() + // 2. expiration epoch + if body.Expiration != x.reqExp { + return newErrInvalidRequestField("expiration epoch", errors.New("mismatches the test input")) } - if err := signServiceMessage(signer, &respV2, nil); err != nil { + return nil +} + +func (x *testCreateSessionServer) Create(_ context.Context, req *protosession.CreateRequest) (*protosession.CreateResponse, error) { + time.Sleep(x.handlerSleepDur) + if err := x.verifyRequest(req); err != nil { return nil, err } + if x.handlerErr != nil { + return nil, x.handlerErr + } - return respV2.ToGRPCMessage().(*protosession.CreateResponse), nil + resp := &protosession.CreateResponse{ + MetaHeader: x.respMeta, + } + if x.respBodyForced { + resp.Body = x.respBody + } else { + resp.Body = proto.Clone(validMinCreateSessionResponseBody).(*protosession.CreateResponse_Body) + } + + var err error + resp.VerifyHeader, err = x.signResponse(resp) + if err != nil { + return nil, fmt.Errorf("sign response: %w", err) + } + return resp, nil } func TestClient_SessionCreate(t *testing.T) { ctx := context.Background() - usr := usertest.User() + var anyValidOpts PrmSessionCreate + anyUsr := usertest.User() - var prmSessionCreate PrmSessionCreate - prmSessionCreate.SetExp(1) + t.Run("messages", 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. + */ + t.Run("requests", func(t *testing.T) { + t.Run("required data", func(t *testing.T) { + srv := newTestCreateSessionInfoServer() + c := newTestSessionClient(t, srv) - t.Run("missing session id", func(t *testing.T) { - srv := testCreateSessionServer{signer: usr, unsetID: true} - c := newTestSessionClient(t, &srv) + srv.checkRequestAccount(anyUsr.ID) + srv.authenticateRequest(anyUsr) + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + require.NoError(t, err) + }) + t.Run("options", func(t *testing.T) { + t.Run("X-headers", func(t *testing.T) { + testRequestXHeaders(t, newTestCreateSessionInfoServer, newTestSessionClient, func(c *Client, xhs []string) error { + opts := anyValidOpts + opts.WithXHeaders(xhs...) + _, err := c.SessionCreate(ctx, anyUsr, opts) + return err + }) + }) + t.Run("expiration epoch", func(t *testing.T) { + srv := newTestCreateSessionInfoServer() + c := newTestSessionClient(t, srv) - result, err := c.SessionCreate(ctx, usr, prmSessionCreate) - require.Nil(t, result) - require.ErrorIs(t, err, ErrMissingResponseField) - require.Equal(t, "missing session id field in the response", err.Error()) - }) + epoch := rand.Uint64() + var opts PrmSessionCreate + opts.SetExp(epoch) + + srv.checkRequestExpirationEpoch(epoch) + _, err := c.SessionCreate(ctx, anyUsr, opts) + require.NoError(t, err) + }) + }) + }) + t.Run("responses", func(t *testing.T) { + t.Run("valid", func(t *testing.T) { + t.Run("payloads", func(t *testing.T) { + for _, tc := range []struct { + name string + body *protosession.CreateResponse_Body + }{ + {name: "min", body: validMinCreateSessionResponseBody}, + {name: "full", body: validFullCreateSessionResponseBody}, + } { + t.Run(tc.name, func(t *testing.T) { + srv := newTestCreateSessionInfoServer() + c := newTestSessionClient(t, srv) - t.Run("missing session key", func(t *testing.T) { - srv := testCreateSessionServer{signer: usr, unsetKey: true} - c := newTestSessionClient(t, &srv) + srv.respondWithBody(tc.body) + res, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, validFullCreateSessionResponseBody.Id, res.ID()) + require.Equal(t, validFullCreateSessionResponseBody.SessionKey, res.PublicKey()) + }) + } + }) + t.Run("statuses", func(t *testing.T) { + testStatusResponses(t, newTestCreateSessionInfoServer, newTestSessionClient, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + }) + t.Run("invalid", func(t *testing.T) { + t.Run("format", func(t *testing.T) { + testIncorrectUnaryRPCResponseFormat(t, "session.SessionService", "Create", func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + t.Run("verification header", func(t *testing.T) { + testInvalidResponseVerificationHeader(t, newTestCreateSessionInfoServer, newTestSessionClient, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + t.Run("payloads", func(t *testing.T) { + type testcase = invalidResponseBodyTestcase[protosession.CreateResponse_Body] + tcs := []testcase{ + {name: "missing", body: nil, assertErr: func(t testing.TB, err error) { + require.EqualError(t, err, "missing session id field in the response") + }}, + {name: "ID/missing", body: &protosession.CreateResponse_Body{ + SessionKey: validFullCreateSessionResponseBody.SessionKey, + }, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing session id field in the response") + }}, + {name: "session public key/missing", body: &protosession.CreateResponse_Body{ + Id: validFullCreateSessionResponseBody.Id, + }, assertErr: func(t testing.TB, err error) { + require.ErrorIs(t, err, MissingResponseFieldErr{}) + require.EqualError(t, err, "missing session key field in the response") + }}, + } - result, err := c.SessionCreate(ctx, usr, prmSessionCreate) - require.Nil(t, result) - require.ErrorIs(t, err, ErrMissingResponseField) - require.Equal(t, "missing session key field in the response", err.Error()) + testInvalidResponseBodies(t, newTestCreateSessionInfoServer, newTestSessionClient, tcs, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + }) + }) + }) + t.Run("invalid user input", func(t *testing.T) { + c := newClient(t) + t.Run("missing signer", func(t *testing.T) { + _, err := c.SessionCreate(ctx, nil, anyValidOpts) + require.ErrorIs(t, err, ErrMissingSigner) + }) + }) + t.Run("context", func(t *testing.T) { + testContextErrors(t, newTestCreateSessionInfoServer, newTestSessionClient, func(ctx context.Context, c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + t.Run("sign request failure", func(t *testing.T) { + _, err := newClient(t).SessionCreate(ctx, usertest.FailSigner(anyUsr), anyValidOpts) + assertSignRequestErr(t, err) + }) + t.Run("transport failure", func(t *testing.T) { + testTransportFailure(t, newTestCreateSessionInfoServer, newTestSessionClient, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + t.Run("response callback", func(t *testing.T) { + testUnaryResponseCallback(t, newTestCreateSessionInfoServer, newDefaultSessionServiceDesc, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }) + }) + t.Run("exec statistics", func(t *testing.T) { + testStatistic(t, newTestCreateSessionInfoServer, newDefaultSessionServiceDesc, stat.MethodSessionCreate, + []testedClientOp{ + func(c *Client) error { + _, err := c.SessionCreate(ctx, nil, anyValidOpts) + return err + }, + }, nil, func(c *Client) error { + _, err := c.SessionCreate(ctx, anyUsr, anyValidOpts) + return err + }, + ) }) } From 53269eb31dd0743279ef451f4e3ce9fe330a38ea Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 17:09:06 +0300 Subject: [PATCH 3/9] client: Do not lose transport errors in statistics Regression from b75db12121973c8ed9b516294bd6bc6bd0f29e46. Since then, no error were submitted to stat handler of following `Client` ops: - `NetmapSnapshot`; - `ObjectDelete`; - `ObjectHash`. Now unit tests detect such things. Signed-off-by: Leonard Lyubich --- client/netmap.go | 3 ++- client/object_delete.go | 3 ++- client/object_get.go | 3 ++- client/object_hash.go | 3 ++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/netmap.go b/client/netmap.go index 823d55d8..eb0a391b 100644 --- a/client/netmap.go +++ b/client/netmap.go @@ -250,7 +250,8 @@ func (c *Client) NetMapSnapshot(ctx context.Context, _ PrmNetMapSnapshot) (netma resp, err := c.netmap.NetmapSnapshot(ctx, req.ToGRPCMessage().(*protonetmap.NetmapSnapshotRequest)) if err != nil { - return netmap.NetMap{}, rpcErr(err) + err = rpcErr(err) + return netmap.NetMap{}, err } var respV2 v2netmap.SnapshotResponse if err = respV2.FromGRPCMessage(resp); err != nil { diff --git a/client/object_delete.go b/client/object_delete.go index f4e93470..dc9a5353 100644 --- a/client/object_delete.go +++ b/client/object_delete.go @@ -110,7 +110,8 @@ func (c *Client) ObjectDelete(ctx context.Context, containerID cid.ID, objectID resp, err := c.object.Delete(ctx, req.ToGRPCMessage().(*protoobject.DeleteRequest)) if err != nil { - return oid.ID{}, rpcErr(err) + err = rpcErr(err) + return oid.ID{}, err } var respV2 v2object.DeleteResponse if err = respV2.FromGRPCMessage(resp); err != nil { diff --git a/client/object_get.go b/client/object_get.go index 40c3407d..0ec7aa01 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -407,7 +407,8 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi resp, err := c.object.Head(ctx, req.ToGRPCMessage().(*protoobject.HeadRequest)) if err != nil { - return nil, rpcErr(err) + err = rpcErr(err) + return nil, err } var respV2 v2object.HeadResponse if err = respV2.FromGRPCMessage(resp); err != nil { diff --git a/client/object_hash.go b/client/object_hash.go index 2564597e..192f30e8 100644 --- a/client/object_hash.go +++ b/client/object_hash.go @@ -153,7 +153,8 @@ func (c *Client) ObjectHash(ctx context.Context, containerID cid.ID, objectID oi resp, err := c.object.GetRangeHash(ctx, req.ToGRPCMessage().(*protoobject.GetRangeHashRequest)) if err != nil { - return nil, rpcErr(err) + err = rpcErr(err) + return nil, err } var respV2 v2object.GetRangeHashResponse if err = respV2.FromGRPCMessage(resp); err != nil { From c4ad81829e95da7da2ade987962a57771b94eda4 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 17:31:41 +0300 Subject: [PATCH 4/9] client: Verify split info in responses of reading ops Previously, client did not check split info from server responses to `Head` / `Get` and `GetRange` requests. In addition to accepting a buggy or malicious response, this also resulted in undefined data in the method returns. Now split info field is strictly controlled according to the protocol. Signed-off-by: Leonard Lyubich --- client/object_get.go | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/client/object_get.go b/client/object_get.go index 0ec7aa01..1fd025ca 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -19,6 +19,8 @@ import ( "github.com/nspcc-dev/neofs-sdk-go/user" ) +var errInvalidSplitInfo = errors.New("invalid split info") + // shared parameters of GET/HEAD/RANGE. type prmObjectRead struct { sessionContainer @@ -120,7 +122,16 @@ func (x *PayloadReader) readHeader(dst *object.Object) bool { x.err = fmt.Errorf("unexpected message instead of heading part: %T", v) return false case *v2object.SplitInfo: - x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) + if v == nil { + x.err = fmt.Errorf("%w: nil split info field", errInvalidSplitInfo) + return false + } + var si object.SplitInfo + if x.err = si.ReadFromV2(*v); x.err != nil { + x.err = fmt.Errorf("%w: %w", errInvalidSplitInfo, x.err) + return false + } + x.err = object.NewSplitInfoError(&si) return false case *v2object.GetObjectPartInit: partInit = v @@ -424,7 +435,16 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi err = fmt.Errorf("unexpected header type %T", v) return nil, err case *v2object.SplitInfo: - err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) + if v == nil { + err = fmt.Errorf("%w: nil split info field", errInvalidSplitInfo) + return nil, err + } + var si object.SplitInfo + if err = si.ReadFromV2(*v); err != nil { + err = fmt.Errorf("%w: %w", errInvalidSplitInfo, err) + return nil, err + } + err = object.NewSplitInfoError(&si) return nil, err case *v2object.HeaderWithSignature: if v == nil { @@ -522,7 +542,16 @@ func (x *ObjectRangeReader) readChunk(buf []byte) (int, bool) { x.err = fmt.Errorf("unexpected message received: %T", v) return read, false case *v2object.SplitInfo: - x.err = object.NewSplitInfoError(object.NewSplitInfoFromV2(v)) + if v == nil { + x.err = fmt.Errorf("%w: nil split info field", errInvalidSplitInfo) + return read, false + } + var si object.SplitInfo + if x.err = si.ReadFromV2(*v); x.err != nil { + x.err = fmt.Errorf("%w: %w", errInvalidSplitInfo, x.err) + return read, false + } + x.err = object.NewSplitInfoError(&si) return read, false case *v2object.GetRangePartChunk: partChunk = v From b3778075cbfc85cf10a6486efd60ed4387bbb2bf Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 18:03:06 +0300 Subject: [PATCH 5/9] client: Check presence of required fields in `Get` and `Head` responses Previously, `ObjectGetInit` and `ObjectHead` methods of the `Client` did not check whether required response fields (object header, signature and, for GET, ID) are set or not. This was incorrect since these fields are required, missing any of them makes response inappropriate. Now `Client` returns `MissingResponseFieldErr` in this case. Corresponding unit tests PASS now. Signed-off-by: Leonard Lyubich --- client/object_get.go | 42 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/client/object_get.go b/client/object_get.go index 1fd025ca..4ed0f5b9 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -134,16 +134,36 @@ func (x *PayloadReader) readHeader(dst *object.Object) bool { x.err = object.NewSplitInfoError(&si) return false case *v2object.GetObjectPartInit: + if v == nil { + x.err = newErrMissingResponseField("init") + return false + } partInit = v } + id := partInit.GetObjectID() + if id == nil { + x.err = newErrMissingResponseField("object ID") + return false + } + sig := partInit.GetSignature() + if sig == nil { + x.err = newErrMissingResponseField("signature") + return false + } + hdr := partInit.GetHeader() + if hdr == nil { + x.err = newErrMissingResponseField("header") + return false + } + var objv2 v2object.Object - objv2.SetObjectID(partInit.GetObjectID()) - objv2.SetHeader(partInit.GetHeader()) - objv2.SetSignature(partInit.GetSignature()) + objv2.SetObjectID(id) + objv2.SetHeader(hdr) + objv2.SetSignature(sig) - x.remainingPayloadLen = int(objv2.GetHeader().GetPayloadLength()) + x.remainingPayloadLen = int(hdr.GetPayloadLength()) x.err = dst.ReadFromV2(objv2) return x.err == nil @@ -450,10 +470,20 @@ func (c *Client) ObjectHead(ctx context.Context, containerID cid.ID, objectID oi if v == nil { return nil, errors.New("empty header") } + sig := v.GetSignature() + if sig == nil { + err = newErrMissingResponseField("signature") + return nil, err + } + hdr := v.GetHeader() + if hdr == nil { + err = newErrMissingResponseField("header") + return nil, err + } var objv2 v2object.Object - objv2.SetHeader(v.GetHeader()) - objv2.SetSignature(v.GetSignature()) + objv2.SetHeader(hdr) + objv2.SetSignature(sig) var obj object.Object if err = obj.ReadFromV2(objv2); err != nil { From 8915e1bc17ab90eadf965c1bcb51da34930c602f Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 19:21:11 +0300 Subject: [PATCH 6/9] container,netmap: Clarify message for the empty value error Previous 'empty attribute value K' text could confuse because K is not empty. For network info, 'attribute' is also renamed to 'parameter'. Signed-off-by: Leonard Lyubich --- client/container_test.go | 2 +- client/netmap_test.go | 4 ++-- container/container.go | 2 +- container/container_test.go | 6 +++--- netmap/netmap_test.go | 2 +- netmap/network_info.go | 2 +- netmap/network_info_test.go | 4 ++-- netmap/node_info.go | 2 +- netmap/node_info_test.go | 6 +++--- 9 files changed, 15 insertions(+), 15 deletions(-) diff --git a/client/container_test.go b/client/container_test.go index 58e572f5..cdae412e 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -1039,7 +1039,7 @@ func TestClient_ContainerGet(t *testing.T) { }{ {name: "attributes/empty key", msg: "empty attribute key", attrs: []string{"k1", "v1", "", "v2", "k3", "v3"}}, - {name: "attributes/empty value", msg: "empty attribute value k2", + {name: "attributes/empty value", msg: `empty "k2" attribute value`, attrs: []string{"k1", "v1", "k2", "", "k3", "v3"}}, {name: "attributes/duplicated", msg: "duplicated attribute k1", attrs: []string{"k1", "v1", "k2", "v2", "k1", "v3"}}, diff --git a/client/netmap_test.go b/client/netmap_test.go index 7ac97df6..4d1ac42a 100644 --- a/client/netmap_test.go +++ b/client/netmap_test.go @@ -37,7 +37,7 @@ var ( {Key: "k1", Value: "v1"}, {Key: "", Value: "v2"}, {Key: "k3", Value: "v3"}, } }}, - {name: "attributes/no value", msg: "empty value of the attribute k2", corrupt: func(valid *protonetmap.NodeInfo) { + {name: "attributes/no value", msg: `empty "k2" attribute value`, corrupt: func(valid *protonetmap.NodeInfo) { valid.Attributes = []*protonetmap.NodeInfo_Attribute{ {Key: "k1", Value: "v1"}, {Key: "k2", Value: ""}, {Key: "k3", Value: "v3"}, } @@ -65,7 +65,7 @@ var ( corrupt: func(valid *protonetmap.NetworkInfo) { valid.NetworkConfig = nil }}, {name: "netconfig/prms/missing", msg: "missing network parameters", corrupt: func(valid *protonetmap.NetworkInfo) { valid.NetworkConfig = new(protonetmap.NetworkConfig) }}, - {name: "netconfig/prms/no value", msg: "empty attribute value k2", + {name: "netconfig/prms/no value", msg: `empty "k2" parameter value`, corrupt: func(valid *protonetmap.NetworkInfo) { valid.NetworkConfig.Parameters = []*protonetmap.NetworkConfig_Parameter{ {Key: []byte("k1"), Value: []byte("v1")}, {Key: []byte("k2"), Value: nil}, {Key: []byte("k3"), Value: []byte("v3")}, diff --git a/container/container.go b/container/container.go index 64342c3d..0a023bf6 100644 --- a/container/container.go +++ b/container/container.go @@ -155,7 +155,7 @@ func (x *Container) readFromV2(m container.Container, checkFieldPresence bool) e val = attrs[i].GetValue() if val == "" { - return fmt.Errorf("empty attribute value %s", key) + return fmt.Errorf("empty %q attribute value", key) } switch key { diff --git a/container/container_test.go b/container/container_test.go index 9d72d0c5..cff927fd 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -548,7 +548,7 @@ func TestContainer_ReadFromV2(t *testing.T) { }}, {name: "attributes/no key", err: "empty attribute key", corrupt: func(m *v2container.Container) { setContainerAttributes(m, "k1", "v1", "", "v2") }}, - {name: "attributes/no value", err: "empty attribute value k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, corrupt: func(m *v2container.Container) { setContainerAttributes(m, "k1", "v1", "k2", "") }}, {name: "attributes/duplicated", err: "duplicated attribute k1", corrupt: func(m *v2container.Container) { setContainerAttributes(m, "k1", "v1", "k2", "v2", "k1", "v3") }}, @@ -721,7 +721,7 @@ func TestContainer_Unmarshal(t *testing.T) { b: []byte{50, 0}}, {name: "attributes/no key", err: "empty attribute key", b: []byte{42, 8, 10, 2, 107, 49, 18, 2, 118, 49, 42, 4, 18, 2, 118, 50}}, - {name: "attributes/no value", err: "empty attribute value k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, b: []byte{42, 8, 10, 2, 107, 49, 18, 2, 118, 49, 42, 4, 10, 2, 107, 50}}, {name: "attributes/duplicated", err: "duplicated attribute k1", b: []byte{42, 8, 10, 2, 107, 49, 18, 2, 118, 49, 42, 8, 10, 2, 107, 50, 18, 2, 118, 50, 42, 8, 10, 2, 107, 49, @@ -779,7 +779,7 @@ func TestContainer_UnmarshalJSON(t *testing.T) { j: `{"placementPolicy":{}}`}, {name: "attributes/no key", err: "empty attribute key", j: `{"attributes":[{"key":"k1","value":"v1"},{"key":"","value":"v2"}]}`}, - {name: "attributes/no value", err: "empty attribute value k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, j: `{"attributes":[{"key":"k1", "value":"v1"}, {"key":"k2", "value":""}]}`}, {name: "attributes/duplicated", err: "duplicated attribute k1", j: `{"attributes":[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"},{"key":"k1","value":"v3"}]}`}, diff --git a/netmap/netmap_test.go b/netmap/netmap_test.go index 9513d6e7..4827a210 100644 --- a/netmap/netmap_test.go +++ b/netmap/netmap_test.go @@ -143,7 +143,7 @@ func TestNetMap_ReadFromV2(t *testing.T) { corrupt: func(m *apinetmap.NetMap) { m.Nodes()[1].SetAddresses() }}, {name: "nodes/attributes/no key", err: "invalid node info: empty key of the attribute #1", corrupt: func(m *apinetmap.NetMap) { setNodeAttributes(&m.Nodes()[1], "k1", "v1", "", "v2") }}, - {name: "nodes/attributes/no value", err: "invalid node info: empty value of the attribute k2", + {name: "nodes/attributes/no value", err: `invalid node info: empty "k2" attribute value`, corrupt: func(m *apinetmap.NetMap) { setNodeAttributes(&m.Nodes()[1], "k1", "v1", "k2", "") }}, {name: "nodes/attributes/duplicated", err: "invalid node info: duplicated attribute k1", corrupt: func(m *apinetmap.NetMap) { setNodeAttributes(&m.Nodes()[1], "k1", "v1", "k2", "v2", "k1", "v3") }}, diff --git a/netmap/network_info.go b/netmap/network_info.go index 762e1c36..668466a6 100644 --- a/netmap/network_info.go +++ b/netmap/network_info.go @@ -52,7 +52,7 @@ func (x *NetworkInfo) readFromV2(m netmap.NetworkInfo, checkFieldPresence bool) switch name { default: if len(prm.GetValue()) == 0 { - err = fmt.Errorf("empty attribute value %s", name) + err = fmt.Errorf("empty %q parameter value", name) return true } case configEigenTrustAlpha: diff --git a/netmap/network_info_test.go b/netmap/network_info_test.go index ceb97298..019f4bef 100644 --- a/netmap/network_info_test.go +++ b/netmap/network_info_test.go @@ -311,7 +311,7 @@ func TestNetworkInfo_ReadFromV2(t *testing.T) { corrupt: func(m *apinetmap.NetworkInfo) { m.SetNetworkConfig(nil) }}, {name: "netconfig/prms/missing", err: "missing network parameters", corrupt: func(m *apinetmap.NetworkInfo) { m.SetNetworkConfig(new(apinetmap.NetworkConfig)) }}, - {name: "netconfig/prms/no value", err: "empty attribute value k1", + {name: "netconfig/prms/no value", err: `empty "k1" parameter value`, corrupt: func(m *apinetmap.NetworkInfo) { setNetworkPrms(m, "k1", "") }}, {name: "netconfig/prms/duplicated", err: "duplicated parameter name: k1", corrupt: func(m *apinetmap.NetworkInfo) { setNetworkPrms(m, "k1", "v1", "k2", "v2", "k1", "v3") }}, @@ -425,7 +425,7 @@ func TestNetworkInfo_Unmarshal(t *testing.T) { err string b []byte }{ - {name: "netconfig/prms/no value", err: "empty attribute value k1", + {name: "netconfig/prms/no value", err: `empty "k1" parameter value`, b: []byte{34, 6, 10, 4, 10, 2, 107, 49}}, {name: "netconfig/prms/duplicated", err: "duplicated parameter name: k1", b: []byte{34, 30, 10, 8, 10, 2, 107, 49, 18, 2, 118, 49, 10, 8, 10, 2, 107, 50, 18, 2, 118, 50, 10, 8, 10, 2, 107, diff --git a/netmap/node_info.go b/netmap/node_info.go index d852cb88..001d4764 100644 --- a/netmap/node_info.go +++ b/netmap/node_info.go @@ -65,7 +65,7 @@ func (x *NodeInfo) readFromV2(m netmap.NodeInfo, checkFieldPresence bool) error } default: if attributes[i].GetValue() == "" { - return fmt.Errorf("empty value of the attribute %s", key) + return fmt.Errorf("empty %q attribute value", key) } } diff --git a/netmap/node_info_test.go b/netmap/node_info_test.go index 39c70e1f..d97f0dba 100644 --- a/netmap/node_info_test.go +++ b/netmap/node_info_test.go @@ -604,7 +604,7 @@ func TestNodeInfo_ReadFromV2(t *testing.T) { corrupt: func(m *apinetmap.NodeInfo) { m.SetAddresses() }}, {name: "attributes/no key", err: "empty key of the attribute #1", corrupt: func(m *apinetmap.NodeInfo) { setNodeAttributes(m, "k1", "v1", "", "v2") }}, - {name: "attributes/no value", err: "empty value of the attribute k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, corrupt: func(m *apinetmap.NodeInfo) { setNodeAttributes(m, "k1", "v1", "k2", "") }}, {name: "attributes/duplicated", err: "duplicated attribute k1", corrupt: func(m *apinetmap.NodeInfo) { setNodeAttributes(m, "k1", "v1", "k2", "v2", "k1", "v3") }}, @@ -700,7 +700,7 @@ func TestNodeInfo_Unmarshal(t *testing.T) { }{ {name: "attributes/no key", err: "empty key of the attribute #1", b: []byte{26, 8, 10, 2, 107, 49, 18, 2, 118, 49, 26, 4, 18, 2, 118, 50}}, - {name: "attributes/no value", err: "empty value of the attribute k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, b: []byte{26, 8, 10, 2, 107, 49, 18, 2, 118, 49, 26, 4, 10, 2, 107, 50}}, {name: "attributes/duplicated", err: "duplicated attribute k1", b: []byte{26, 8, 10, 2, 107, 49, 18, 2, 118, 49, 26, 8, 10, 2, 107, 50, 18, 2, 118, 50, 26, 8, 10, 2, 107, 49, @@ -753,7 +753,7 @@ func TestNodeInfo_UnmarshalJSON(t *testing.T) { for _, tc := range []struct{ name, err, j string }{ {name: "attributes/no key", err: "empty key of the attribute #1", j: `{"attributes":[{"key":"k1", "value":"v1"}, {"key":"", "value":"v2"}]}`}, - {name: "attributes/no value", err: "empty value of the attribute k2", + {name: "attributes/no value", err: `empty "k2" attribute value`, j: `{"attributes":[{"key":"k1","value":"v1"},{"key":"k2"}]}`}, {name: "attributes/duplicated", err: "duplicated attribute k1", j: `{"attributes":[{"key":"k1","value":"v1"},{"key":"k2","value":"v2"},{"key":"k1","value":"v3"}]}`}, From 5d5bbeb1ed4d77f4db99ec3bad6b526225769489 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 19:29:26 +0300 Subject: [PATCH 7/9] client: Pre-check signer scheme in container SETEACL and DELETE ops `Client` must not attempt to send obviously invalid requests. Moreover, pre-check already existed for PUT, and these operations are similar in this sense. Signed-off-by: Leonard Lyubich --- client/container.go | 6 ++++++ client/container_test.go | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/client/container.go b/client/container.go index 9e4c87fe..79c4d05f 100644 --- a/client/container.go +++ b/client/container.go @@ -379,6 +379,9 @@ func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscry if signer == nil { return ErrMissingSigner } + if signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 { + return fmt.Errorf("%w: expected ECDSA_DETERMINISTIC_SHA256 scheme", neofscrypto.ErrIncorrectSigner) + } // sign container ID var cidV2 refs.ContainerID @@ -596,6 +599,9 @@ func (c *Client) ContainerSetEACL(ctx context.Context, table eacl.Table, signer err = ErrMissingEACLContainer return err } + if signer.Scheme() != neofscrypto.ECDSA_DETERMINISTIC_SHA256 { + return fmt.Errorf("%w: expected ECDSA_DETERMINISTIC_SHA256 scheme", neofscrypto.ErrIncorrectSigner) + } // sign the eACL table eaclV2 := table.ToV2() diff --git a/client/container_test.go b/client/container_test.go index cdae412e..1141d6aa 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -1398,8 +1398,8 @@ func TestClient_ContainerDelete(t *testing.T) { c := newTestContainerClient(t, newTestDeleteContainerServer()) t.Run("wrong scheme", func(t *testing.T) { err := c.ContainerDelete(ctx, anyID, neofsecdsa.Signer(neofscryptotest.ECDSAPrivateKey()), anyValidOpts) - require.EqualError(t, err, "write request: rpc failure: rpc error: code = Unknown desc = invalid request: "+ - "invalid body: invalid container ID signature field: invalid signature length 65, should be 64") + require.EqualError(t, err, "incorrect signer: expected ECDSA_DETERMINISTIC_SHA256 scheme") + require.ErrorIs(t, err, neofscrypto.ErrIncorrectSigner) }) t.Run("signer failure", func(t *testing.T) { err := c.ContainerDelete(ctx, anyID, neofscryptotest.FailSigner(anyValidSigner), anyValidOpts) @@ -1730,8 +1730,8 @@ func TestClient_ContainerSetEACL(t *testing.T) { c := newTestContainerClient(t, newTestSetEACLServer()) t.Run("wrong scheme", func(t *testing.T) { err := c.ContainerSetEACL(ctx, anyValidEACL, user.NewAutoIDSigner(neofscryptotest.ECDSAPrivateKey()), anyValidOpts) - require.EqualError(t, err, "write request: rpc failure: rpc error: code = Unknown desc = invalid request: "+ - "invalid body: invalid eACL signature field: invalid signature length 65, should be 64") + require.EqualError(t, err, "incorrect signer: expected ECDSA_DETERMINISTIC_SHA256 scheme") + require.ErrorIs(t, err, neofscrypto.ErrIncorrectSigner) }) t.Run("signer failure", func(t *testing.T) { err := c.ContainerSetEACL(ctx, anyValidEACL, usertest.FailSigner(anyValidSigner), anyValidOpts) From 490410456782295ce50d1ec29b69f5752382d964 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 19:38:53 +0300 Subject: [PATCH 8/9] client: Clarify payload signature errors for container SETEACL / DELETE `Client` signs not just payload but request parts. So, to be more precise what's going on, it's worth to narrow generic 'calculate signature' text. Moreover, PUT already results with 'calculate container signature' in the same case. Signed-off-by: Leonard Lyubich --- client/container.go | 4 ++-- client/container_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/container.go b/client/container.go index 79c4d05f..e81f94a0 100644 --- a/client/container.go +++ b/client/container.go @@ -393,7 +393,7 @@ func (c *Client) ContainerDelete(ctx context.Context, id cid.ID, signer neofscry if !prm.sigSet { if err = prm.sig.Calculate(signer, data); err != nil { - err = fmt.Errorf("calculate signature: %w", err) + err = fmt.Errorf("calculate container ID signature: %w", err) return err } } @@ -607,7 +607,7 @@ func (c *Client) ContainerSetEACL(ctx context.Context, table eacl.Table, signer eaclV2 := table.ToV2() if !prm.sigSet { if err = prm.sig.CalculateMarshalled(signer, eaclV2, nil); err != nil { - err = fmt.Errorf("calculate signature: %w", err) + err = fmt.Errorf("calculate eACL signature: %w", err) return err } } diff --git a/client/container_test.go b/client/container_test.go index 1141d6aa..451bedd7 100644 --- a/client/container_test.go +++ b/client/container_test.go @@ -1403,7 +1403,7 @@ func TestClient_ContainerDelete(t *testing.T) { }) t.Run("signer failure", func(t *testing.T) { err := c.ContainerDelete(ctx, anyID, neofscryptotest.FailSigner(anyValidSigner), anyValidOpts) - require.ErrorContains(t, err, "calculate signature") + require.ErrorContains(t, err, "calculate container ID signature") }) }) t.Run("context", func(t *testing.T) { @@ -1735,7 +1735,7 @@ func TestClient_ContainerSetEACL(t *testing.T) { }) t.Run("signer failure", func(t *testing.T) { err := c.ContainerSetEACL(ctx, anyValidEACL, usertest.FailSigner(anyValidSigner), anyValidOpts) - require.ErrorContains(t, err, "calculate signature") + require.ErrorContains(t, err, "calculate eACL signature") }) }) t.Run("context", func(t *testing.T) { From 09b291db60fb37b9b71c71af683d8d1b46973bd9 Mon Sep 17 00:00:00 2001 From: Leonard Lyubich Date: Tue, 10 Dec 2024 19:57:57 +0300 Subject: [PATCH 9/9] client: Return more clear error on head reading from `ObjectHead` Previously, on the problem with the 1st GET stream message, `Client` returned 'header: ...' error. Such an error led to the question: header what? Now 'read header: ...' is returned. Signed-off-by: Leonard Lyubich --- client/object_get.go | 2 +- client/object_get_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/object_get.go b/client/object_get.go index 4ed0f5b9..1679d3ad 100644 --- a/client/object_get.go +++ b/client/object_get.go @@ -363,7 +363,7 @@ func (c *Client) ObjectGetInit(ctx context.Context, containerID cid.ID, objectID } if !r.readHeader(&hdr) { - err = fmt.Errorf("header: %w", r.Close()) + err = fmt.Errorf("read header: %w", r.Close()) return hdr, nil, err } diff --git a/client/object_get_test.go b/client/object_get_test.go index 25c6c4ec..d79364a9 100644 --- a/client/object_get_test.go +++ b/client/object_get_test.go @@ -1067,7 +1067,7 @@ func TestClient_ObjectGetInit(t *testing.T) { srv.respondWithBody(0, b) _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) - require.EqualError(t, err, "header: invalid split info: "+tc.msg) + require.EqualError(t, err, "read header: invalid split info: "+tc.msg) }) } }) @@ -1137,7 +1137,7 @@ func TestClient_ObjectGetInit(t *testing.T) { srv.respondWithBody(0, b) _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) - require.EqualError(t, err, "header: "+tc.msg) + require.EqualError(t, err, "read header: "+tc.msg) }) } }) @@ -1233,7 +1233,7 @@ func TestClient_ObjectGetInit(t *testing.T) { _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) _, ok := status.FromError(err) require.False(t, ok) - require.EqualError(t, err, "header: %!w()") + require.EqualError(t, err, "read header: %!w()") }) t.Run("chunk message first", func(t *testing.T) { srv := newTestGetObjectServer() @@ -1241,7 +1241,7 @@ func TestClient_ObjectGetInit(t *testing.T) { srv.respondWithBody(0, proto.Clone(validFullChunkObjectGetResponseBody).(*protoobject.GetResponse_Body)) _, _, err := c.ObjectGetInit(ctx, anyCID, anyOID, anyValidSigner, anyValidOpts) - require.EqualError(t, err, "header: unexpected message instead of heading part: *object.GetObjectPartChunk") + require.EqualError(t, err, "read header: unexpected message instead of heading part: *object.GetObjectPartChunk") }) t.Run("repeated heading message", func(t *testing.T) { srv := newTestGetObjectServer()