From 5ff41fe00f8c609b49e824f1520ce3b523e56017 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Utku=20=C3=87a=C4=9Flayan?= Date: Fri, 18 Feb 2022 13:23:59 +0300 Subject: [PATCH] [1.3.0] Add Missing Tests [API-1112] (#703) * add missing tests * add http client tests * try enabling TestClient_AddDistributedObjectListener * exclude unused codec files from test coverage * exclude unused codec files from test coverage * refactor test structs for field alignment check * test http client retry mechanism * fix bad merge * address review comments * refactor index variables "ind" to "i" * field reordering for "fieldalignment" * undo wrong file remove --- aggregate/aggregate_test.go | 88 ++++++ internal/invocation/stripe_executor_test.go | 4 +- internal/proto/codec/error_holder_codec.go | 6 - internal/proto/codec/list_iterator_codec.go | 54 ---- .../proto/codec/list_list_iterator_codec.go | 59 ---- .../proto/codec/map_fetch_entries_codec.go | 58 ---- .../proto/codec/map_submit_to_key_codec.go | 60 ---- internal/rest/http_client_test.go | 294 ++++++++++++++++++ 8 files changed, 384 insertions(+), 239 deletions(-) create mode 100644 aggregate/aggregate_test.go delete mode 100644 internal/proto/codec/list_iterator_codec.go delete mode 100644 internal/proto/codec/list_list_iterator_codec.go delete mode 100644 internal/proto/codec/map_fetch_entries_codec.go delete mode 100644 internal/proto/codec/map_submit_to_key_codec.go create mode 100644 internal/rest/http_client_test.go diff --git a/aggregate/aggregate_test.go b/aggregate/aggregate_test.go new file mode 100644 index 000000000..a3d274dc9 --- /dev/null +++ b/aggregate/aggregate_test.go @@ -0,0 +1,88 @@ +package aggregate + +import ( + "fmt" + "testing" +) + +func TestMakeString(t *testing.T) { + tcs := []struct { + name string + attrPath string + want string + }{ + { + name: "test", + attrPath: "attribute", + want: "test(attribute)", + }, + { + name: "", + attrPath: "", + want: "()", + }, + } + for i, tt := range tcs { + t.Run(fmt.Sprintf("makeString case: %d", i), func(t *testing.T) { + if got := makeString(tt.name, tt.attrPath); got != tt.want { + t.Errorf("makeString() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAggStringer(t *testing.T) { + tcs := []struct { + aggInstance fmt.Stringer + want string + }{ + { + aggInstance: Count("attribute"), + want: "Count(attribute)", + }, + { + aggInstance: DistinctValues("attribute"), + want: "DistinctValues(attribute)", + }, + { + aggInstance: DoubleSum("attribute"), + want: "DoubleSum(attribute)", + }, + { + aggInstance: DoubleAverage("attribute"), + want: "DoubleAverage(attribute)", + }, + { + aggInstance: IntSum("attribute"), + want: "IntSum(attribute)", + }, + { + aggInstance: IntAverage("attribute"), + want: "IntAverage(attribute)", + }, + { + aggInstance: LongSum("attribute"), + want: "LongSum(attribute)", + }, + { + aggInstance: LongAverage("attribute"), + want: "LongAverage(attribute)", + }, + { + aggInstance: Max("attribute"), + want: "Max(attribute)", + }, + + { + aggInstance: Min("attribute"), + want: "Min(attribute)", + }, + } + for i, tt := range tcs { + t.Run(fmt.Sprintf("makeString case: %d", i), func(t *testing.T) { + if got := tt.aggInstance.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/invocation/stripe_executor_test.go b/internal/invocation/stripe_executor_test.go index 3abbfe96e..b79ef4203 100644 --- a/internal/invocation/stripe_executor_test.go +++ b/internal/invocation/stripe_executor_test.go @@ -53,7 +53,7 @@ func Test_defaultExecFn(t *testing.T) { } func TestStripeExecutor_dispatch(t *testing.T) { - tests := []struct { + tcs := []struct { queueCount int key int expectedIndex int @@ -84,7 +84,7 @@ func TestStripeExecutor_dispatch(t *testing.T) { expectedIndex: 0, }, } - for i, tt := range tests { + for i, tt := range tcs { t.Run(fmt.Sprintf("QueueCount: %d, Key: %d", tt.queueCount, tt.key), func(t *testing.T) { se := newStripeExecutorWithConfig(tt.queueCount, 10_000) task := func() { diff --git a/internal/proto/codec/error_holder_codec.go b/internal/proto/codec/error_holder_codec.go index 9c745554b..38236b628 100644 --- a/internal/proto/codec/error_holder_codec.go +++ b/internal/proto/codec/error_holder_codec.go @@ -24,12 +24,6 @@ const ( ErrorHolderCodecErrorCodeInitialFrameSize = ErrorHolderCodecErrorCodeFieldOffset + proto.IntSizeInBytes ) -/* -type errorholderCodec struct {} - -var ErrorHolderCodec errorholderCodec -*/ - func EncodeErrorHolder(clientMessage *proto.ClientMessage, errorHolder proto.ErrorHolder) { clientMessage.AddFrame(proto.BeginFrame.Copy()) initialFrame := proto.NewFrame(make([]byte, ErrorHolderCodecErrorCodeInitialFrameSize)) diff --git a/internal/proto/codec/list_iterator_codec.go b/internal/proto/codec/list_iterator_codec.go deleted file mode 100644 index 83dd2a09c..000000000 --- a/internal/proto/codec/list_iterator_codec.go +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package codec - -import ( - "github.com/hazelcast/hazelcast-go-client/internal/proto" - iserialization "github.com/hazelcast/hazelcast-go-client/internal/serialization" -) - -const ( - // hex: 0x051600 - ListIteratorCodecRequestMessageType = int32(333312) - // hex: 0x051601 - ListIteratorCodecResponseMessageType = int32(333313) - - ListIteratorCodecRequestInitialFrameSize = proto.PartitionIDOffset + proto.IntSizeInBytes -) - -// Returns an iterator over the elements in this list in proper sequence. - -func EncodeListIteratorRequest(name string) *proto.ClientMessage { - clientMessage := proto.NewClientMessageForEncode() - clientMessage.SetRetryable(true) - - initialFrame := proto.NewFrameWith(make([]byte, ListIteratorCodecRequestInitialFrameSize), proto.UnfragmentedMessage) - clientMessage.AddFrame(initialFrame) - clientMessage.SetMessageType(ListIteratorCodecRequestMessageType) - clientMessage.SetPartitionId(-1) - - EncodeString(clientMessage, name) - - return clientMessage -} - -func DecodeListIteratorResponse(clientMessage *proto.ClientMessage) []*iserialization.Data { - frameIterator := clientMessage.FrameIterator() - // empty initial frame - frameIterator.Next() - - return DecodeListMultiFrameForData(frameIterator) -} diff --git a/internal/proto/codec/list_list_iterator_codec.go b/internal/proto/codec/list_list_iterator_codec.go deleted file mode 100644 index ad097eecf..000000000 --- a/internal/proto/codec/list_list_iterator_codec.go +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package codec - -import ( - "github.com/hazelcast/hazelcast-go-client/internal/proto" - iserialization "github.com/hazelcast/hazelcast-go-client/internal/serialization" -) - -const ( - // hex: 0x051700 - ListListIteratorCodecRequestMessageType = int32(333568) - // hex: 0x051701 - ListListIteratorCodecResponseMessageType = int32(333569) - - ListListIteratorCodecRequestIndexOffset = proto.PartitionIDOffset + proto.IntSizeInBytes - ListListIteratorCodecRequestInitialFrameSize = ListListIteratorCodecRequestIndexOffset + proto.IntSizeInBytes -) - -// Returns a list iterator over the elements in this list (in proper sequence), starting at the specified position -// in the list. The specified index indicates the first element that would be returned by an initial call to -// ListIterator#next next. An initial call to ListIterator#previous previous would return the element with the -// specified index minus one. - -func EncodeListListIteratorRequest(name string, index int32) *proto.ClientMessage { - clientMessage := proto.NewClientMessageForEncode() - clientMessage.SetRetryable(true) - - initialFrame := proto.NewFrameWith(make([]byte, ListListIteratorCodecRequestInitialFrameSize), proto.UnfragmentedMessage) - FixSizedTypesCodec.EncodeInt(initialFrame.Content, ListListIteratorCodecRequestIndexOffset, index) - clientMessage.AddFrame(initialFrame) - clientMessage.SetMessageType(ListListIteratorCodecRequestMessageType) - clientMessage.SetPartitionId(-1) - - EncodeString(clientMessage, name) - - return clientMessage -} - -func DecodeListListIteratorResponse(clientMessage *proto.ClientMessage) []*iserialization.Data { - frameIterator := clientMessage.FrameIterator() - // empty initial frame - frameIterator.Next() - - return DecodeListMultiFrameForData(frameIterator) -} diff --git a/internal/proto/codec/map_fetch_entries_codec.go b/internal/proto/codec/map_fetch_entries_codec.go deleted file mode 100644 index 22ef839b0..000000000 --- a/internal/proto/codec/map_fetch_entries_codec.go +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package codec - -import ( - "github.com/hazelcast/hazelcast-go-client/internal/proto" -) - -const ( - // hex: 0x013800 - MapFetchEntriesCodecRequestMessageType = int32(79872) - // hex: 0x013801 - MapFetchEntriesCodecResponseMessageType = int32(79873) - - MapFetchEntriesCodecRequestBatchOffset = proto.PartitionIDOffset + proto.IntSizeInBytes - MapFetchEntriesCodecRequestInitialFrameSize = MapFetchEntriesCodecRequestBatchOffset + proto.IntSizeInBytes -) - -// Fetches specified number of entries from the specified partition starting from specified table index. - -func EncodeMapFetchEntriesRequest(name string, iterationPointers []proto.Pair, batch int32) *proto.ClientMessage { - clientMessage := proto.NewClientMessageForEncode() - clientMessage.SetRetryable(true) - - initialFrame := proto.NewFrameWith(make([]byte, MapFetchEntriesCodecRequestInitialFrameSize), proto.UnfragmentedMessage) - FixSizedTypesCodec.EncodeInt(initialFrame.Content, MapFetchEntriesCodecRequestBatchOffset, batch) - clientMessage.AddFrame(initialFrame) - clientMessage.SetMessageType(MapFetchEntriesCodecRequestMessageType) - clientMessage.SetPartitionId(-1) - - EncodeString(clientMessage, name) - EncodeEntryListIntegerInteger(clientMessage, iterationPointers) - - return clientMessage -} - -func DecodeMapFetchEntriesResponse(clientMessage *proto.ClientMessage) (iterationPointers []proto.Pair, entries []proto.Pair) { - frameIterator := clientMessage.FrameIterator() - frameIterator.Next() - - iterationPointers = DecodeEntryListIntegerInteger(frameIterator) - entries = DecodeEntryListForDataAndData(frameIterator) - - return iterationPointers, entries -} diff --git a/internal/proto/codec/map_submit_to_key_codec.go b/internal/proto/codec/map_submit_to_key_codec.go deleted file mode 100644 index 3d15c21d7..000000000 --- a/internal/proto/codec/map_submit_to_key_codec.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (c) 2008-2021, Hazelcast, Inc. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License") - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package codec - -import ( - "github.com/hazelcast/hazelcast-go-client/internal/proto" - iserialization "github.com/hazelcast/hazelcast-go-client/internal/serialization" -) - -const ( - // hex: 0x012F00 - MapSubmitToKeyCodecRequestMessageType = int32(77568) - // hex: 0x012F01 - MapSubmitToKeyCodecResponseMessageType = int32(77569) - - MapSubmitToKeyCodecRequestThreadIdOffset = proto.PartitionIDOffset + proto.IntSizeInBytes - MapSubmitToKeyCodecRequestInitialFrameSize = MapSubmitToKeyCodecRequestThreadIdOffset + proto.LongSizeInBytes -) - -// Applies the user defined EntryProcessor to the entry mapped by the key. Returns immediately with a Future -// representing that task.EntryProcessor is not cancellable, so calling Future.cancel() method won't cancel the -// operation of EntryProcessor. - -func EncodeMapSubmitToKeyRequest(name string, entryProcessor *iserialization.Data, key *iserialization.Data, threadId int64) *proto.ClientMessage { - clientMessage := proto.NewClientMessageForEncode() - clientMessage.SetRetryable(false) - - initialFrame := proto.NewFrameWith(make([]byte, MapSubmitToKeyCodecRequestInitialFrameSize), proto.UnfragmentedMessage) - FixSizedTypesCodec.EncodeLong(initialFrame.Content, MapSubmitToKeyCodecRequestThreadIdOffset, threadId) - clientMessage.AddFrame(initialFrame) - clientMessage.SetMessageType(MapSubmitToKeyCodecRequestMessageType) - clientMessage.SetPartitionId(-1) - - EncodeString(clientMessage, name) - EncodeData(clientMessage, entryProcessor) - EncodeData(clientMessage, key) - - return clientMessage -} - -func DecodeMapSubmitToKeyResponse(clientMessage *proto.ClientMessage) *iserialization.Data { - frameIterator := clientMessage.FrameIterator() - // empty initial frame - frameIterator.Next() - - return CodecUtil.DecodeNullableForData(frameIterator) -} diff --git a/internal/rest/http_client_test.go b/internal/rest/http_client_test.go new file mode 100644 index 000000000..06c6786d3 --- /dev/null +++ b/internal/rest/http_client_test.go @@ -0,0 +1,294 @@ +package rest + +import ( + "bytes" + "context" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// roundTripFunc to mock and assert server side. +type roundTripFunc func(req *http.Request) (*http.Response, error) + +func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req) +} + +// setTransport modifies *http.Client by replacing Transport to avoid making real calls +func setTransport(fn roundTripFunc, client *HTTPClient) { + client.httpClient.Transport = fn +} + +func TestHTTPClient_Get(t *testing.T) { + type args struct { + url string + serverHandler roundTripFunc + headers []HTTPHeader + } + tcs := []struct { + err *Error + name string + args args + }{ + { + name: "happy path, return success and response", + args: args{ + url: "localhost:8080/some/path", + headers: []HTTPHeader{ + NewHTTPHeader("Some-Header", "value"), + }, + serverHandler: func(req *http.Request) (*http.Response, error) { + header := req.Header.Get("Some-Header") + assert.Equal(t, "value", header) + assert.Equal(t, "localhost:8080/some/path", req.URL.String()) + return &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString("OK")), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + }, + }, + { + name: "return error from server, status code <= 500", + args: args{ + serverHandler: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusInternalServerError, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString("FAIL")), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + }, + err: NewError(500, "FAIL"), + }, + } + for _, tt := range tcs { + t.Run(tt.name, func(t *testing.T) { + c := NewHTTPClient() + setTransport(tt.args.serverHandler, c) + ctx := context.Background() + resp, err := c.Get(ctx, tt.args.url, tt.args.headers...) + if tt.err != nil { + assert.Equal(t, tt.err, err) + return + } + assert.Nil(t, err) + assert.Equal(t, "OK", string(resp)) + }) + } +} + +func TestHttpClient_Retry(t *testing.T) { + c := NewHTTPClient() + ctx := context.Background() + retry := 1 + setTransport(func(req *http.Request) (*http.Response, error) { + resp := &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewBufferString("OK")), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + if retry < 3 { + // retry range + resp.StatusCode = 500 + retry++ + } + return resp, nil + }, c) + resp, err := c.Get(ctx, "somehost:8080") + assert.Nil(t, err) + assert.Equal(t, "OK", string(resp)) + assert.Equal(t, 3, retry) + // test non-retryable status code + retry = 0 + setTransport(func(req *http.Request) (*http.Response, error) { + resp := &http.Response{ + StatusCode: http.StatusBadRequest, + Body: ioutil.NopCloser(bytes.NewBufferString("OK")), + Header: make(http.Header), + } + retry++ + return resp, nil + }, c) + resp, err = c.Get(ctx, "somehost:8080") + assert.NotNil(t, err) + assert.Nil(t, resp) + assert.Equal(t, 1, retry) +} + +// type for json tests +type employee struct { + Name string `json:"name"` + Job string `json:"job"` +} + +func TestHTTPClient_GetJSONObject(t *testing.T) { + tmp := employee{ + Name: "Joe", + Job: "accountant", + } + testResp, err := json.Marshal(tmp) + assert.Nil(t, err) + // create corresponding map to assert + tmpMap := make(map[string]interface{}) + tmpMap["name"] = "Joe" + tmpMap["job"] = "accountant" + tcs := []struct { + want interface{} + err error + serverHandler roundTripFunc + name string + }{ + { + name: "happy path, return success and response", + serverHandler: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + want: tmpMap, + }, + { + name: "return invalid json", + serverHandler: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp[:len(testResp)-4])), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + err: &json.SyntaxError{}, + }, + { + name: "return error from server", + serverHandler: func(req *http.Request) (*http.Response, error) { + + return &http.Response{ + StatusCode: http.StatusInternalServerError, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp[:len(testResp)-4])), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + // rest error + err: &Error{}, + }, + } + for _, tt := range tcs { + t.Run(tt.name, func(t *testing.T) { + c := NewHTTPClient() + setTransport(tt.serverHandler, c) + ctx := context.Background() + resp, err := c.GetJSONObject(ctx, "localhost:8080") + if tt.err != nil { + assert.IsType(t, tt.err, err) + return + } + assert.Nil(t, err) + assert.Equal(t, tt.want, resp) + }) + } +} + +func TestHTTPClient_GetJSONArray(t *testing.T) { + tmp := []employee{ + { + Name: "Joe", + Job: "accountant", + }, + { + Name: "Bob", + Job: "engineering", + }, + } + testResp, err := json.Marshal(tmp) + var wantedArr []interface{} + tmpEmp := make(map[string]interface{}) + tmpEmp["name"] = "Joe" + tmpEmp["job"] = "accountant" + tmpEmp2 := make(map[string]interface{}) + tmpEmp2["name"] = "Bob" + tmpEmp2["job"] = "engineering" + wantedArr = append(wantedArr, tmpEmp, tmpEmp2) + assert.Nil(t, err) + tcs := []struct { + want interface{} + err error + serverHandler roundTripFunc + name string + }{ + { + name: "happy path, return success and response", + serverHandler: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + want: wantedArr, + }, + { + name: "return invalid json", + serverHandler: func(req *http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: http.StatusOK, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp[:len(testResp)-4])), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + err: &json.SyntaxError{}, + }, + { + name: "return error from server", + serverHandler: func(req *http.Request) (*http.Response, error) { + + return &http.Response{ + StatusCode: http.StatusInternalServerError, + // Send response to be tested + Body: ioutil.NopCloser(bytes.NewReader(testResp[:len(testResp)-4])), + // Must be set to non-nil value or it panics + Header: make(http.Header), + }, nil + }, + // rest error + err: &Error{}, + }, + } + for _, tt := range tcs { + t.Run(tt.name, func(t *testing.T) { + c := NewHTTPClient() + setTransport(tt.serverHandler, c) + ctx := context.Background() + resp, err := c.GetJSONArray(ctx, "localhost:8080") + if tt.err != nil { + assert.IsType(t, tt.err, err) + return + } + assert.Nil(t, err) + assert.ElementsMatch(t, tt.want, resp) + }) + } +}