diff --git a/foreign/go/client/tcp/tcp_access_token_management.go b/foreign/go/client/tcp/tcp_access_token_management.go index 68d14a3313..efebd10e50 100644 --- a/foreign/go/client/tcp/tcp_access_token_management.go +++ b/foreign/go/client/tcp/tcp_access_token_management.go @@ -23,7 +23,7 @@ import ( "github.com/apache/iggy/foreign/go/internal/command" ) -func (c *IggyTcpClient) CreatePersonalAccessToken(name string, expiry uint32) (*iggcon.RawPersonalAccessToken, error) { +func (c *IggyTcpClient) CreatePersonalAccessToken(name string, expiry uint64) (*iggcon.RawPersonalAccessToken, error) { buffer, err := c.do(&command.CreatePersonalAccessToken{ Name: name, Expiry: expiry, diff --git a/foreign/go/contracts/client.go b/foreign/go/contracts/client.go index 40fb82d469..66f22db7e5 100644 --- a/foreign/go/contracts/client.go +++ b/foreign/go/contracts/client.go @@ -246,7 +246,7 @@ type Client interface { DeleteUser(identifier Identifier) error // CreatePersonalAccessToken create a new personal access token for the currently authenticated user. - CreatePersonalAccessToken(name string, expiry uint32) (*RawPersonalAccessToken, error) + CreatePersonalAccessToken(name string, expiry uint64) (*RawPersonalAccessToken, error) // DeletePersonalAccessToken delete a personal access token of the currently authenticated user by unique token name. DeletePersonalAccessToken(name string) error diff --git a/foreign/go/internal/command/access_token.go b/foreign/go/internal/command/access_token.go index 68260a6984..243c7372c5 100644 --- a/foreign/go/internal/command/access_token.go +++ b/foreign/go/internal/command/access_token.go @@ -21,7 +21,7 @@ import "encoding/binary" type CreatePersonalAccessToken struct { Name string `json:"Name"` - Expiry uint32 `json:"Expiry"` + Expiry uint64 `json:"Expiry"` } func (c *CreatePersonalAccessToken) Code() Code { @@ -33,7 +33,7 @@ func (c *CreatePersonalAccessToken) MarshalBinary() ([]byte, error) { bytes := make([]byte, length) bytes[0] = byte(len(c.Name)) copy(bytes[1:], c.Name) - binary.LittleEndian.PutUint32(bytes[len(bytes)-4:], c.Expiry) + binary.LittleEndian.PutUint64(bytes[1+len(c.Name):], c.Expiry) return bytes, nil } diff --git a/foreign/go/internal/command/access_token_test.go b/foreign/go/internal/command/access_token_test.go new file mode 100644 index 0000000000..8ddea9398c --- /dev/null +++ b/foreign/go/internal/command/access_token_test.go @@ -0,0 +1,220 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" +) + +// TestSerialize_GetPersonalAccessTokens tests serialization of GetPersonalAccessTokens command +func TestSerialize_GetPersonalAccessTokens(t *testing.T) { + cmd := GetPersonalAccessTokens{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetPersonalAccessTokens: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePersonalAccessToken tests serialization with normal token name +func TestSerialize_DeletePersonalAccessToken(t *testing.T) { + cmd := DeletePersonalAccessToken{ + Name: "test_token", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePersonalAccessToken: %v", err) + } + + expected := []byte{ + 0x0A, // Name length = 10 + 0x74, 0x65, 0x73, 0x74, 0x5F, 0x74, 0x6F, 0x6B, 0x65, 0x6E, // "test_token" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePersonalAccessToken_SingleChar tests edge case with single character name +func TestSerialize_DeletePersonalAccessToken_SingleChar(t *testing.T) { + cmd := DeletePersonalAccessToken{ + Name: "a", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePersonalAccessToken with single char: %v", err) + } + + expected := []byte{ + 0x01, // Name length = 1 + 0x61, // "a" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePersonalAccessToken_EmptyName tests edge case with empty name +func TestSerialize_DeletePersonalAccessToken_EmptyName(t *testing.T) { + cmd := DeletePersonalAccessToken{ + Name: "", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePersonalAccessToken with empty name: %v", err) + } + + expected := []byte{ + 0x00, // Name length = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePersonalAccessToken tests serialization with normal values +func TestSerialize_CreatePersonalAccessToken(t *testing.T) { + cmd := CreatePersonalAccessToken{ + Name: "test", + Expiry: 3600, // 1 hour in seconds + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePersonalAccessToken: %v", err) + } + + expected := []byte{ + 0x04, // Name length = 4 + 0x74, 0x65, 0x73, 0x74, // "test" + 0x10, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Expiry = 3600 (u64 LE) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePersonalAccessToken_ZeroExpiry tests edge case with zero expiry +func TestSerialize_CreatePersonalAccessToken_ZeroExpiry(t *testing.T) { + cmd := CreatePersonalAccessToken{ + Name: "token", + Expiry: 0, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePersonalAccessToken with zero expiry: %v", err) + } + + expected := []byte{ + 0x05, // Name length = 5 + 0x74, 0x6F, 0x6B, 0x65, 0x6E, // "token" + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Expiry = 0 (u64 LE) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePersonalAccessToken_MaxExpiry tests edge case with maximum uint64 expiry +func TestSerialize_CreatePersonalAccessToken_MaxExpiry(t *testing.T) { + cmd := CreatePersonalAccessToken{ + Name: "long_token", + Expiry: 18446744073709551615, // Max uint64 value + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePersonalAccessToken with max expiry: %v", err) + } + + expected := []byte{ + 0x0A, // Name length = 10 + 0x6C, 0x6F, 0x6E, 0x67, 0x5F, 0x74, 0x6F, 0x6B, 0x65, 0x6E, // "long_token" + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Expiry = max uint64 (u64 LE) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePersonalAccessToken_EmptyName tests edge case with empty name +func TestSerialize_CreatePersonalAccessToken_EmptyName(t *testing.T) { + cmd := CreatePersonalAccessToken{ + Name: "", + Expiry: 1000, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePersonalAccessToken with empty name: %v", err) + } + + expected := []byte{ + 0x00, // Name length = 0 + 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Expiry = 1000 (u64 LE) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePersonalAccessToken_LongName tests with a longer token name +func TestSerialize_CreatePersonalAccessToken_LongName(t *testing.T) { + cmd := CreatePersonalAccessToken{ + Name: "my_very_long_personal_access_token_name", + Expiry: 86400, // 24 hours in seconds + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePersonalAccessToken with long name: %v", err) + } + + expected := []byte{ + 0x27, // Name length = 39 + // "my_very_long_personal_access_token_name" + 0x6D, 0x79, 0x5F, 0x76, 0x65, 0x72, 0x79, 0x5F, + 0x6C, 0x6F, 0x6E, 0x67, 0x5F, 0x70, 0x65, 0x72, + 0x73, 0x6F, 0x6E, 0x61, 0x6C, 0x5F, 0x61, 0x63, + 0x63, 0x65, 0x73, 0x73, 0x5F, 0x74, 0x6F, 0x6B, + 0x65, 0x6E, 0x5F, 0x6E, 0x61, 0x6D, 0x65, + 0x80, 0x51, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, // Expiry = 86400 (u64 LE) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/consumer_group_test.go b/foreign/go/internal/command/consumer_group_test.go new file mode 100644 index 0000000000..1aa96e94c0 --- /dev/null +++ b/foreign/go/internal/command/consumer_group_test.go @@ -0,0 +1,436 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +// TestSerialize_CreateConsumerGroup_NumericIds tests CreateConsumerGroup with numeric identifiers +func TestSerialize_CreateConsumerGroup_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(1)) + topicId, _ := iggcon.NewIdentifier(uint32(2)) + + cmd := CreateConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + Name: "group1", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateConsumerGroup: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x01, 0x00, 0x00, 0x00, // StreamId Value = 1 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x02, 0x00, 0x00, 0x00, // TopicId Value = 2 + 0x06, // Name Length = 6 + 0x67, 0x72, 0x6F, 0x75, 0x70, 0x31, // "group1" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreateConsumerGroup_StringIds tests CreateConsumerGroup with string identifiers +func TestSerialize_CreateConsumerGroup_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("my_stream") + topicId, _ := iggcon.NewIdentifier("my_topic") + + cmd := CreateConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + Name: "consumers", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateConsumerGroup: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x09, // StreamId Length = 9 + 0x6D, 0x79, 0x5F, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // "my_stream" + 0x02, // TopicId Kind = StringId + 0x08, // TopicId Length = 8 + 0x6D, 0x79, 0x5F, 0x74, 0x6F, 0x70, 0x69, 0x63, // "my_topic" + 0x09, // Name Length = 9 + 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x72, 0x73, // "consumers" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreateConsumerGroup_EmptyName tests edge case with empty group name +func TestSerialize_CreateConsumerGroup_EmptyName(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier("topic") + + cmd := CreateConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + Name: "", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateConsumerGroup with empty name: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x06, // StreamId Length = 6 + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // "stream" + 0x02, // TopicId Kind = StringId + 0x05, // TopicId Length = 5 + 0x74, 0x6F, 0x70, 0x69, 0x63, // "topic" + 0x00, // Name Length = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerGroups_NumericIds tests GetConsumerGroups with numeric identifiers +func TestSerialize_GetConsumerGroups_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(10)) + topicId, _ := iggcon.NewIdentifier(uint32(20)) + + cmd := GetConsumerGroups{ + StreamId: streamId, + TopicId: topicId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerGroups: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x0A, 0x00, 0x00, 0x00, // StreamId Value = 10 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x14, 0x00, 0x00, 0x00, // TopicId Value = 20 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerGroups_StringIds tests GetConsumerGroups with string identifiers +func TestSerialize_GetConsumerGroups_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("events") + topicId, _ := iggcon.NewIdentifier("logs") + + cmd := GetConsumerGroups{ + StreamId: streamId, + TopicId: topicId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerGroups: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x06, // StreamId Length = 6 + 0x65, 0x76, 0x65, 0x6E, 0x74, 0x73, // "events" + 0x02, // TopicId Kind = StringId + 0x04, // TopicId Length = 4 + 0x6C, 0x6F, 0x67, 0x73, // "logs" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerGroup_NumericIds tests GetConsumerGroup with all numeric identifiers +func TestSerialize_GetConsumerGroup_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(100)) + topicId, _ := iggcon.NewIdentifier(uint32(200)) + groupId, _ := iggcon.NewIdentifier(uint32(300)) + + cmd := GetConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerGroup: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x64, 0x00, 0x00, 0x00, // StreamId Value = 100 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0xC8, 0x00, 0x00, 0x00, // TopicId Value = 200 + 0x01, // GroupId Kind = NumericId + 0x04, // GroupId Length = 4 + 0x2C, 0x01, 0x00, 0x00, // GroupId Value = 300 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerGroup_StringIds tests GetConsumerGroup with all string identifiers +func TestSerialize_GetConsumerGroup_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream1") + topicId, _ := iggcon.NewIdentifier("topic1") + groupId, _ := iggcon.NewIdentifier("group1") + + cmd := GetConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerGroup: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x07, // StreamId Length = 7 + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x31, // "stream1" + 0x02, // TopicId Kind = StringId + 0x06, // TopicId Length = 6 + 0x74, 0x6F, 0x70, 0x69, 0x63, 0x31, // "topic1" + 0x02, // GroupId Kind = StringId + 0x06, // GroupId Length = 6 + 0x67, 0x72, 0x6F, 0x75, 0x70, 0x31, // "group1" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerGroup_MixedIds tests GetConsumerGroup with mixed identifier types +func TestSerialize_GetConsumerGroup_MixedIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(42)) + topicId, _ := iggcon.NewIdentifier("events") + groupId, _ := iggcon.NewIdentifier(uint32(999)) + + cmd := GetConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerGroup with mixed IDs: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x2A, 0x00, 0x00, 0x00, // StreamId Value = 42 + 0x02, // TopicId Kind = StringId + 0x06, // TopicId Length = 6 + 0x65, 0x76, 0x65, 0x6E, 0x74, 0x73, // "events" + 0x01, // GroupId Kind = NumericId + 0x04, // GroupId Length = 4 + 0xE7, 0x03, 0x00, 0x00, // GroupId Value = 999 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_JoinConsumerGroup tests JoinConsumerGroup serialization +func TestSerialize_JoinConsumerGroup(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("prod") + topicId, _ := iggcon.NewIdentifier("orders") + groupId, _ := iggcon.NewIdentifier(uint32(5)) + + cmd := JoinConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize JoinConsumerGroup: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x04, // StreamId Length = 4 + 0x70, 0x72, 0x6F, 0x64, // "prod" + 0x02, // TopicId Kind = StringId + 0x06, // TopicId Length = 6 + 0x6F, 0x72, 0x64, 0x65, 0x72, 0x73, // "orders" + 0x01, // GroupId Kind = NumericId + 0x04, // GroupId Length = 4 + 0x05, 0x00, 0x00, 0x00, // GroupId Value = 5 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LeaveConsumerGroup tests LeaveConsumerGroup serialization +func TestSerialize_LeaveConsumerGroup(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(7)) + topicId, _ := iggcon.NewIdentifier(uint32(8)) + groupId, _ := iggcon.NewIdentifier("my_group") + + cmd := LeaveConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LeaveConsumerGroup: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x07, 0x00, 0x00, 0x00, // StreamId Value = 7 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x08, 0x00, 0x00, 0x00, // TopicId Value = 8 + 0x02, // GroupId Kind = StringId + 0x08, // GroupId Length = 8 + 0x6D, 0x79, 0x5F, 0x67, 0x72, 0x6F, 0x75, 0x70, // "my_group" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteConsumerGroup_NumericIds tests DeleteConsumerGroup with numeric identifiers +func TestSerialize_DeleteConsumerGroup_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(11)) + topicId, _ := iggcon.NewIdentifier(uint32(22)) + groupId, _ := iggcon.NewIdentifier(uint32(33)) + + cmd := DeleteConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteConsumerGroup: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x0B, 0x00, 0x00, 0x00, // StreamId Value = 11 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x16, 0x00, 0x00, 0x00, // TopicId Value = 22 + 0x01, // GroupId Kind = NumericId + 0x04, // GroupId Length = 4 + 0x21, 0x00, 0x00, 0x00, // GroupId Value = 33 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteConsumerGroup_StringIds tests DeleteConsumerGroup with string identifiers +func TestSerialize_DeleteConsumerGroup_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("analytics") + topicId, _ := iggcon.NewIdentifier("metrics") + groupId, _ := iggcon.NewIdentifier("deprecated") + + cmd := DeleteConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteConsumerGroup: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x09, // StreamId Length = 9 + 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, // "analytics" + 0x02, // TopicId Kind = StringId + 0x07, // TopicId Length = 7 + 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, // "metrics" + 0x02, // GroupId Kind = StringId + 0x0A, // GroupId Length = 10 + 0x64, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, // "deprecated" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/marshal_nomutate_test.go b/foreign/go/internal/command/marshal_nomutate_test.go new file mode 100644 index 0000000000..a6aac48c48 --- /dev/null +++ b/foreign/go/internal/command/marshal_nomutate_test.go @@ -0,0 +1,479 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "reflect" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +// TestMarshalBinary_DoesNotMutateReceiver verifies that calling MarshalBinary +// does not modify any field on the receiver for every command type. +func TestMarshalBinary_DoesNotMutateReceiver(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(1)) + topicId, _ := iggcon.NewIdentifier("test-topic") + userId, _ := iggcon.NewIdentifier(uint32(42)) + groupId, _ := iggcon.NewIdentifier(uint32(10)) + + consumerId, _ := iggcon.NewIdentifier(uint32(1)) + consumer := iggcon.Consumer{Kind: iggcon.ConsumerKindSingle, Id: consumerId} + + partitionId := uint32(3) + username := "admin" + status := iggcon.Active + replicationFactor := uint8(1) + + perms := &iggcon.Permissions{ + Global: iggcon.GlobalPermissions{ + ManageServers: true, + ReadServers: true, + }, + } + + tests := []struct { + name string + makeCMD func() (cmd interface{ MarshalBinary() ([]byte, error) }, snapshot interface{}) + }{ + { + name: "CreatePersonalAccessToken", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreatePersonalAccessToken{Name: "token", Expiry: 3600} + snap := CreatePersonalAccessToken{Name: "token", Expiry: 3600} + return cmd, snap + }, + }, + { + name: "DeletePersonalAccessToken", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeletePersonalAccessToken{Name: "token"} + snap := DeletePersonalAccessToken{Name: "token"} + return cmd, snap + }, + }, + { + name: "GetPersonalAccessTokens", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetPersonalAccessTokens{} + snap := GetPersonalAccessTokens{} + return cmd, snap + }, + }, + { + name: "CreateUser", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreateUser{Username: "user", Password: "pass", Status: iggcon.Active, Permissions: perms} + snap := CreateUser{Username: "user", Password: "pass", Status: iggcon.Active, Permissions: perms} + return cmd, snap + }, + }, + { + name: "CreateUser_NilPermissions", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreateUser{Username: "user", Password: "pass", Status: iggcon.Active, Permissions: nil} + snap := CreateUser{Username: "user", Password: "pass", Status: iggcon.Active, Permissions: nil} + return cmd, snap + }, + }, + { + name: "GetUser", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetUser{Id: userId} + snap := GetUser{Id: userId} + return cmd, snap + }, + }, + { + name: "GetUsers", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetUsers{} + snap := GetUsers{} + return cmd, snap + }, + }, + { + name: "UpdatePermissions_WithPermissions", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &UpdatePermissions{UserID: userId, Permissions: perms} + snap := UpdatePermissions{UserID: userId, Permissions: perms} + return cmd, snap + }, + }, + { + name: "UpdatePermissions_NilPermissions", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &UpdatePermissions{UserID: userId, Permissions: nil} + snap := UpdatePermissions{UserID: userId, Permissions: nil} + return cmd, snap + }, + }, + { + name: "ChangePassword", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &ChangePassword{UserID: userId, CurrentPassword: "old", NewPassword: "new"} + snap := ChangePassword{UserID: userId, CurrentPassword: "old", NewPassword: "new"} + return cmd, snap + }, + }, + { + name: "DeleteUser", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteUser{Id: userId} + snap := DeleteUser{Id: userId} + return cmd, snap + }, + }, + { + name: "UpdateUser_BothFields", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + u := username + s := status + cmd := &UpdateUser{UserID: userId, Username: &u, Status: &s} + snap := UpdateUser{UserID: userId, Username: &u, Status: &s} + return cmd, snap + }, + }, + { + name: "UpdateUser_NilUsername", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + s := status + cmd := &UpdateUser{UserID: userId, Username: nil, Status: &s} + snap := UpdateUser{UserID: userId, Username: nil, Status: &s} + return cmd, snap + }, + }, + { + name: "UpdateUser_NilStatus", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + u := username + cmd := &UpdateUser{UserID: userId, Username: &u, Status: nil} + snap := UpdateUser{UserID: userId, Username: &u, Status: nil} + return cmd, snap + }, + }, + { + name: "UpdateUser_BothNil", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &UpdateUser{UserID: userId, Username: nil, Status: nil} + snap := UpdateUser{UserID: userId, Username: nil, Status: nil} + return cmd, snap + }, + }, + { + name: "LoginUser", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &LoginUser{Username: "user", Password: "pass"} + snap := LoginUser{Username: "user", Password: "pass"} + return cmd, snap + }, + }, + { + name: "LoginWithPersonalAccessToken", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &LoginWithPersonalAccessToken{Token: "tok"} + snap := LoginWithPersonalAccessToken{Token: "tok"} + return cmd, snap + }, + }, + { + name: "LogoutUser", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &LogoutUser{} + snap := LogoutUser{} + return cmd, snap + }, + }, + { + name: "CreateStream", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreateStream{Name: "stream"} + snap := CreateStream{Name: "stream"} + return cmd, snap + }, + }, + { + name: "GetStream", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetStream{StreamId: streamId} + snap := GetStream{StreamId: streamId} + return cmd, snap + }, + }, + { + name: "GetStreams", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetStreams{} + snap := GetStreams{} + return cmd, snap + }, + }, + { + name: "UpdateStream", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &UpdateStream{StreamId: streamId, Name: "new"} + snap := UpdateStream{StreamId: streamId, Name: "new"} + return cmd, snap + }, + }, + { + name: "DeleteStream", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteStream{StreamId: streamId} + snap := DeleteStream{StreamId: streamId} + return cmd, snap + }, + }, + { + name: "CreateTopic_NilReplicationFactor", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreateTopic{StreamId: streamId, PartitionsCount: 2, Name: "t", ReplicationFactor: nil} + snap := CreateTopic{StreamId: streamId, PartitionsCount: 2, Name: "t", ReplicationFactor: nil} + return cmd, snap + }, + }, + { + name: "CreateTopic_WithReplicationFactor", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + rf := replicationFactor + cmd := &CreateTopic{StreamId: streamId, PartitionsCount: 2, Name: "t", ReplicationFactor: &rf} + snap := CreateTopic{StreamId: streamId, PartitionsCount: 2, Name: "t", ReplicationFactor: &rf} + return cmd, snap + }, + }, + { + name: "UpdateTopic_NilReplicationFactor", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &UpdateTopic{StreamId: streamId, TopicId: topicId, Name: "t", ReplicationFactor: nil} + snap := UpdateTopic{StreamId: streamId, TopicId: topicId, Name: "t", ReplicationFactor: nil} + return cmd, snap + }, + }, + { + name: "UpdateTopic_WithReplicationFactor", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + rf := replicationFactor + cmd := &UpdateTopic{StreamId: streamId, TopicId: topicId, Name: "t", ReplicationFactor: &rf} + snap := UpdateTopic{StreamId: streamId, TopicId: topicId, Name: "t", ReplicationFactor: &rf} + return cmd, snap + }, + }, + { + name: "GetTopic", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetTopic{StreamId: streamId, TopicId: topicId} + snap := GetTopic{StreamId: streamId, TopicId: topicId} + return cmd, snap + }, + }, + { + name: "GetTopics", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetTopics{StreamId: streamId} + snap := GetTopics{StreamId: streamId} + return cmd, snap + }, + }, + { + name: "DeleteTopic", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteTopic{StreamId: streamId, TopicId: topicId} + snap := DeleteTopic{StreamId: streamId, TopicId: topicId} + return cmd, snap + }, + }, + { + name: "CreatePartitions", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreatePartitions{StreamId: streamId, TopicId: topicId, PartitionsCount: 5} + snap := CreatePartitions{StreamId: streamId, TopicId: topicId, PartitionsCount: 5} + return cmd, snap + }, + }, + { + name: "DeletePartitions", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeletePartitions{StreamId: streamId, TopicId: topicId, PartitionsCount: 2} + snap := DeletePartitions{StreamId: streamId, TopicId: topicId, PartitionsCount: 2} + return cmd, snap + }, + }, + { + name: "CreateConsumerGroup", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &CreateConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, Name: "cg"} + snap := CreateConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, Name: "cg"} + return cmd, snap + }, + }, + { + name: "GetConsumerGroup", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + snap := GetConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + return cmd, snap + }, + }, + { + name: "GetConsumerGroups", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetConsumerGroups{StreamId: streamId, TopicId: topicId} + snap := GetConsumerGroups{StreamId: streamId, TopicId: topicId} + return cmd, snap + }, + }, + { + name: "JoinConsumerGroup", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &JoinConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + snap := JoinConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + return cmd, snap + }, + }, + { + name: "LeaveConsumerGroup", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &LeaveConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + snap := LeaveConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + return cmd, snap + }, + }, + { + name: "DeleteConsumerGroup", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + snap := DeleteConsumerGroup{TopicPath: TopicPath{StreamId: streamId, TopicId: topicId}, GroupId: groupId} + return cmd, snap + }, + }, + { + name: "StoreConsumerOffsetRequest_WithPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + p := partitionId + cmd := &StoreConsumerOffsetRequest{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p, Offset: 100} + snap := StoreConsumerOffsetRequest{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p, Offset: 100} + return cmd, snap + }, + }, + { + name: "StoreConsumerOffsetRequest_NilPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &StoreConsumerOffsetRequest{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil, Offset: 100} + snap := StoreConsumerOffsetRequest{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil, Offset: 100} + return cmd, snap + }, + }, + { + name: "GetConsumerOffset_WithPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + p := partitionId + cmd := &GetConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p} + snap := GetConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p} + return cmd, snap + }, + }, + { + name: "GetConsumerOffset_NilPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil} + snap := GetConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil} + return cmd, snap + }, + }, + { + name: "DeleteConsumerOffset_WithPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + p := partitionId + cmd := &DeleteConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p} + snap := DeleteConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: &p} + return cmd, snap + }, + }, + { + name: "DeleteConsumerOffset_NilPartition", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil} + snap := DeleteConsumerOffset{StreamId: streamId, TopicId: topicId, Consumer: consumer, PartitionId: nil} + return cmd, snap + }, + }, + { + name: "DeleteSegments", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &DeleteSegments{StreamId: streamId, TopicId: topicId, PartitionId: 1, SegmentsCount: 5} + snap := DeleteSegments{StreamId: streamId, TopicId: topicId, PartitionId: 1, SegmentsCount: 5} + return cmd, snap + }, + }, + { + name: "Ping", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &Ping{} + snap := Ping{} + return cmd, snap + }, + }, + { + name: "GetStats", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetStats{} + snap := GetStats{} + return cmd, snap + }, + }, + { + name: "GetClients", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetClients{} + snap := GetClients{} + return cmd, snap + }, + }, + { + name: "GetClusterMetadata", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetClusterMetadata{} + snap := GetClusterMetadata{} + return cmd, snap + }, + }, + { + name: "GetClient", + makeCMD: func() (interface{ MarshalBinary() ([]byte, error) }, interface{}) { + cmd := &GetClient{ClientID: 99} + snap := GetClient{ClientID: 99} + return cmd, snap + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd, snapshot := tt.makeCMD() + _, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("MarshalBinary() returned error: %v", err) + } + // Dereference the pointer to compare the struct value + actual := reflect.ValueOf(cmd).Elem().Interface() + if !reflect.DeepEqual(actual, snapshot) { + t.Errorf("MarshalBinary() mutated the receiver.\nBefore:\t%+v\nAfter:\t%+v", snapshot, actual) + } + }) + } +} diff --git a/foreign/go/internal/command/offset_test.go b/foreign/go/internal/command/offset_test.go new file mode 100644 index 0000000000..f26d1b881f --- /dev/null +++ b/foreign/go/internal/command/offset_test.go @@ -0,0 +1,419 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +// Helper function to create pointer to uint32 +func uint32Ptr(v uint32) *uint32 { + return &v +} + +// TestSerialize_StoreConsumerOffsetRequest_WithPartition tests StoreConsumerOffset with partition specified +func TestSerialize_StoreConsumerOffsetRequest_WithPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(42)) + streamId, _ := iggcon.NewIdentifier("stream1") + topicId, _ := iggcon.NewIdentifier(uint32(10)) + partitionId := uint32Ptr(5) + + cmd := StoreConsumerOffsetRequest{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: partitionId, + Offset: 1000, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize StoreConsumerOffsetRequest: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x2A, 0x00, 0x00, 0x00, // ConsumerId Value = 42 + 0x02, // StreamId Kind = StringId + 0x07, // StreamId Length = 7 + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, 0x31, // "stream1" + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x0A, 0x00, 0x00, 0x00, // TopicId Value = 10 + 0x01, // hasPartition = 1 + 0x05, 0x00, 0x00, 0x00, // PartitionId = 5 + 0xE8, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset = 1000 (uint64) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_StoreConsumerOffsetRequest_WithoutPartition tests StoreConsumerOffset without partition +func TestSerialize_StoreConsumerOffsetRequest_WithoutPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier("consumer1") + streamId, _ := iggcon.NewIdentifier(uint32(1)) + topicId, _ := iggcon.NewIdentifier(uint32(2)) + + cmd := StoreConsumerOffsetRequest{ + Consumer: iggcon.NewGroupConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: nil, // No partition specified + Offset: 5000, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize StoreConsumerOffsetRequest without partition: %v", err) + } + + expected := []byte{ + 0x02, // Consumer Kind = Group + 0x02, // ConsumerId Kind = StringId + 0x09, // ConsumerId Length = 9 + 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x72, 0x31, // "consumer1" + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x01, 0x00, 0x00, 0x00, // StreamId Value = 1 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x02, 0x00, 0x00, 0x00, // TopicId Value = 2 + 0x00, // hasPartition = 0 + 0x00, 0x00, 0x00, 0x00, // PartitionId = 0 (default when not set) + 0x88, 0x13, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset = 5000 (uint64) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_StoreConsumerOffsetRequest_ZeroOffset tests with zero offset +func TestSerialize_StoreConsumerOffsetRequest_ZeroOffset(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(1)) + streamId, _ := iggcon.NewIdentifier("s") + topicId, _ := iggcon.NewIdentifier("t") + + cmd := StoreConsumerOffsetRequest{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(0), + Offset: 0, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize StoreConsumerOffsetRequest with zero offset: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x01, 0x00, 0x00, 0x00, // ConsumerId Value = 1 + 0x02, // StreamId Kind = StringId + 0x01, // StreamId Length = 1 + 0x73, // "s" + 0x02, // TopicId Kind = StringId + 0x01, // TopicId Length = 1 + 0x74, // "t" + 0x01, // hasPartition = 1 + 0x00, 0x00, 0x00, 0x00, // PartitionId = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Offset = 0 (uint64) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_StoreConsumerOffsetRequest_MaxOffset tests with maximum uint64 offset +func TestSerialize_StoreConsumerOffsetRequest_MaxOffset(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(999)) + streamId, _ := iggcon.NewIdentifier(uint32(100)) + topicId, _ := iggcon.NewIdentifier(uint32(200)) + + cmd := StoreConsumerOffsetRequest{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(10), + Offset: 18446744073709551615, // Max uint64 + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize StoreConsumerOffsetRequest with max offset: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0xE7, 0x03, 0x00, 0x00, // ConsumerId Value = 999 + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x64, 0x00, 0x00, 0x00, // StreamId Value = 100 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0xC8, 0x00, 0x00, 0x00, // TopicId Value = 200 + 0x01, // hasPartition = 1 + 0x0A, 0x00, 0x00, 0x00, // PartitionId = 10 + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, // Offset = max uint64 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerOffset_WithPartition tests GetConsumerOffset with partition +func TestSerialize_GetConsumerOffset_WithPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier("grp1") + streamId, _ := iggcon.NewIdentifier("events") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + + cmd := GetConsumerOffset{ + Consumer: iggcon.NewGroupConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(3), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerOffset: %v", err) + } + + expected := []byte{ + 0x02, // Consumer Kind = Group + 0x02, // ConsumerId Kind = StringId + 0x04, // ConsumerId Length = 4 + 0x67, 0x72, 0x70, 0x31, // "grp1" + 0x02, // StreamId Kind = StringId + 0x06, // StreamId Length = 6 + 0x65, 0x76, 0x65, 0x6E, 0x74, 0x73, // "events" + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x05, 0x00, 0x00, 0x00, // TopicId Value = 5 + 0x01, // hasPartition = 1 + 0x03, 0x00, 0x00, 0x00, // PartitionId = 3 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerOffset_WithoutPartition tests GetConsumerOffset without partition +func TestSerialize_GetConsumerOffset_WithoutPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(123)) + streamId, _ := iggcon.NewIdentifier(uint32(1)) + topicId, _ := iggcon.NewIdentifier(uint32(2)) + + cmd := GetConsumerOffset{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerOffset without partition: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x7B, 0x00, 0x00, 0x00, // ConsumerId Value = 123 + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x01, 0x00, 0x00, 0x00, // StreamId Value = 1 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x02, 0x00, 0x00, 0x00, // TopicId Value = 2 + 0x00, // hasPartition = 0 + 0x00, 0x00, 0x00, 0x00, // PartitionId = 0 (default) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetConsumerOffset_MixedIds tests with mixed identifier types +func TestSerialize_GetConsumerOffset_MixedIds(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(42)) + streamId, _ := iggcon.NewIdentifier("prod") + topicId, _ := iggcon.NewIdentifier("logs") + + cmd := GetConsumerOffset{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(7), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetConsumerOffset with mixed IDs: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x2A, 0x00, 0x00, 0x00, // ConsumerId Value = 42 + 0x02, // StreamId Kind = StringId + 0x04, // StreamId Length = 4 + 0x70, 0x72, 0x6F, 0x64, // "prod" + 0x02, // TopicId Kind = StringId + 0x04, // TopicId Length = 4 + 0x6C, 0x6F, 0x67, 0x73, // "logs" + 0x01, // hasPartition = 1 + 0x07, 0x00, 0x00, 0x00, // PartitionId = 7 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteConsumerOffset_WithPartition tests DeleteConsumerOffset with partition +func TestSerialize_DeleteConsumerOffset_WithPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier("consumer_grp") + streamId, _ := iggcon.NewIdentifier(uint32(10)) + topicId, _ := iggcon.NewIdentifier(uint32(20)) + + cmd := DeleteConsumerOffset{ + Consumer: iggcon.NewGroupConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(15), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteConsumerOffset: %v", err) + } + + expected := []byte{ + 0x02, // Consumer Kind = Group + 0x02, // ConsumerId Kind = StringId + 0x0C, // ConsumerId Length = 12 + 0x63, 0x6F, 0x6E, 0x73, 0x75, 0x6D, 0x65, 0x72, 0x5F, 0x67, 0x72, 0x70, // "consumer_grp" + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x0A, 0x00, 0x00, 0x00, // StreamId Value = 10 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x14, 0x00, 0x00, 0x00, // TopicId Value = 20 + 0x01, // hasPartition = 1 + 0x0F, 0x00, 0x00, 0x00, // PartitionId = 15 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteConsumerOffset_WithoutPartition tests DeleteConsumerOffset without partition +func TestSerialize_DeleteConsumerOffset_WithoutPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(7)) + streamId, _ := iggcon.NewIdentifier("analytics") + topicId, _ := iggcon.NewIdentifier("metrics") + + cmd := DeleteConsumerOffset{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteConsumerOffset without partition: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x07, 0x00, 0x00, 0x00, // ConsumerId Value = 7 + 0x02, // StreamId Kind = StringId + 0x09, // StreamId Length = 9 + 0x61, 0x6E, 0x61, 0x6C, 0x79, 0x74, 0x69, 0x63, 0x73, // "analytics" + 0x02, // TopicId Kind = StringId + 0x07, // TopicId Length = 7 + 0x6D, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, // "metrics" + 0x00, // hasPartition = 0 + 0x00, 0x00, 0x00, 0x00, // PartitionId = 0 (default) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteConsumerOffset_MaxPartition tests with maximum partition ID +func TestSerialize_DeleteConsumerOffset_MaxPartition(t *testing.T) { + consumerId, _ := iggcon.NewIdentifier(uint32(1)) + streamId, _ := iggcon.NewIdentifier(uint32(2)) + topicId, _ := iggcon.NewIdentifier(uint32(3)) + + cmd := DeleteConsumerOffset{ + Consumer: iggcon.NewSingleConsumer(consumerId), + StreamId: streamId, + TopicId: topicId, + PartitionId: uint32Ptr(4294967295), // Max uint32 + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteConsumerOffset with max partition: %v", err) + } + + expected := []byte{ + 0x01, // Consumer Kind = Single + 0x01, // ConsumerId Kind = NumericId + 0x04, // ConsumerId Length = 4 + 0x01, 0x00, 0x00, 0x00, // ConsumerId Value = 1 + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x02, 0x00, 0x00, 0x00, // StreamId Value = 2 + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x03, 0x00, 0x00, 0x00, // TopicId Value = 3 + 0x01, // hasPartition = 1 + 0xFF, 0xFF, 0xFF, 0xFF, // PartitionId = 4294967295 (max uint32) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/partition_test.go b/foreign/go/internal/command/partition_test.go new file mode 100644 index 0000000000..60d66bbb52 --- /dev/null +++ b/foreign/go/internal/command/partition_test.go @@ -0,0 +1,242 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +// TestSerialize_CreatePartitions_NumericIds tests serialization with numeric identifiers +func TestSerialize_CreatePartitions_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(123)) + topicId, _ := iggcon.NewIdentifier(uint32(456)) + + cmd := CreatePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 10, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePartitions: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x7B, 0x00, 0x00, 0x00, // StreamId Value = 123 (little endian) + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0xC8, 0x01, 0x00, 0x00, // TopicId Value = 456 (little endian) + 0x0A, 0x00, 0x00, 0x00, // PartitionsCount = 10 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePartitions_StringIds tests serialization with string identifiers +func TestSerialize_CreatePartitions_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("my_stream") + topicId, _ := iggcon.NewIdentifier("my_topic") + + cmd := CreatePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 5, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePartitions: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x09, // StreamId Length = 9 + 0x6D, 0x79, 0x5F, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // "my_stream" + 0x02, // TopicId Kind = StringId + 0x08, // TopicId Length = 8 + 0x6D, 0x79, 0x5F, 0x74, 0x6F, 0x70, 0x69, 0x63, // "my_topic" + 0x05, 0x00, 0x00, 0x00, // PartitionsCount = 5 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePartitions_MixedIds tests serialization with mixed identifier types +func TestSerialize_CreatePartitions_MixedIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(42)) + topicId, _ := iggcon.NewIdentifier("test") + + cmd := CreatePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 100, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePartitions with mixed IDs: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x2A, 0x00, 0x00, 0x00, // StreamId Value = 42 (little endian) + 0x02, // TopicId Kind = StringId + 0x04, // TopicId Length = 4 + 0x74, 0x65, 0x73, 0x74, // "test" + 0x64, 0x00, 0x00, 0x00, // PartitionsCount = 100 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreatePartitions_ZeroCount tests edge case with zero partitions count +func TestSerialize_CreatePartitions_ZeroCount(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier("topic") + + cmd := CreatePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 0, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreatePartitions with zero count: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x06, // StreamId Length = 6 + 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // "stream" + 0x02, // TopicId Kind = StringId + 0x05, // TopicId Length = 5 + 0x74, 0x6F, 0x70, 0x69, 0x63, // "topic" + 0x00, 0x00, 0x00, 0x00, // PartitionsCount = 0 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePartitions_NumericIds tests DeletePartitions with numeric identifiers +func TestSerialize_DeletePartitions_NumericIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(1)) + topicId, _ := iggcon.NewIdentifier(uint32(2)) + + cmd := DeletePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 3, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePartitions: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0x01, 0x00, 0x00, 0x00, // StreamId Value = 1 (little endian) + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x02, 0x00, 0x00, 0x00, // TopicId Value = 2 (little endian) + 0x03, 0x00, 0x00, 0x00, // PartitionsCount = 3 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePartitions_StringIds tests DeletePartitions with string identifiers +func TestSerialize_DeletePartitions_StringIds(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("prod_stream") + topicId, _ := iggcon.NewIdentifier("events") + + cmd := DeletePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 2, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePartitions: %v", err) + } + + expected := []byte{ + 0x02, // StreamId Kind = StringId + 0x0B, // StreamId Length = 11 + 0x70, 0x72, 0x6F, 0x64, 0x5F, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // "prod_stream" + 0x02, // TopicId Kind = StringId + 0x06, // TopicId Length = 6 + 0x65, 0x76, 0x65, 0x6E, 0x74, 0x73, // "events" + 0x02, 0x00, 0x00, 0x00, // PartitionsCount = 2 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeletePartitions_MaxCount tests edge case with maximum uint32 partitions count +func TestSerialize_DeletePartitions_MaxCount(t *testing.T) { + streamId, _ := iggcon.NewIdentifier(uint32(999)) + topicId, _ := iggcon.NewIdentifier(uint32(888)) + + cmd := DeletePartitions{ + StreamId: streamId, + TopicId: topicId, + PartitionsCount: 4294967295, // Max uint32 value + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeletePartitions with max count: %v", err) + } + + expected := []byte{ + 0x01, // StreamId Kind = NumericId + 0x04, // StreamId Length = 4 + 0xE7, 0x03, 0x00, 0x00, // StreamId Value = 999 (little endian) + 0x01, // TopicId Kind = NumericId + 0x04, // TopicId Length = 4 + 0x78, 0x03, 0x00, 0x00, // TopicId Value = 888 (little endian) + 0xFF, 0xFF, 0xFF, 0xFF, // PartitionsCount = 4294967295 (little endian) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/session_test.go b/foreign/go/internal/command/session_test.go index 6646232294..2105e3bd6e 100644 --- a/foreign/go/internal/command/session_test.go +++ b/foreign/go/internal/command/session_test.go @@ -18,12 +18,48 @@ package command import ( + "bytes" "encoding/binary" "testing" iggcon "github.com/apache/iggy/foreign/go/contracts" ) +// buildExpectedLoginUser constructs the expected byte representation for a LoginUser command. +// Format: [username_len(1)][username][password_len(1)][password][version_len(4)][version][context_len(4)][context] +func buildExpectedLoginUser(username, password string) []byte { + versionBytes := []byte(iggcon.Version) + contextBytes := []byte("") + + totalLength := 1 + len(username) + 1 + len(password) + + 4 + len(versionBytes) + 4 + len(contextBytes) + + buf := make([]byte, totalLength) + pos := 0 + + buf[pos] = byte(len(username)) + pos++ + copy(buf[pos:], username) + pos += len(username) + + buf[pos] = byte(len(password)) + pos++ + copy(buf[pos:], password) + pos += len(password) + + binary.LittleEndian.PutUint32(buf[pos:], uint32(len(versionBytes))) + pos += 4 + copy(buf[pos:], versionBytes) + pos += len(versionBytes) + + binary.LittleEndian.PutUint32(buf[pos:], uint32(len(contextBytes))) + pos += 4 + copy(buf[pos:], contextBytes) + + return buf +} + +// TestSerialize_LoginUser_ContainsVersion verifies that the SDK version is included in login serialization. func TestSerialize_LoginUser_ContainsVersion(t *testing.T) { request := LoginUser{ Username: "iggy", @@ -49,3 +85,188 @@ func TestSerialize_LoginUser_ContainsVersion(t *testing.T) { t.Errorf("Version mismatch. Expected: %q, Got: %q", iggcon.Version, version) } } + +// TestSerialize_LoginUser tests normal login with username and password +func TestSerialize_LoginUser(t *testing.T) { + cmd := LoginUser{ + Username: "admin", + Password: "secret123", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginUser: %v", err) + } + + expected := buildExpectedLoginUser("admin", "secret123") + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginUser_EmptyCredentials tests edge case with empty username and password +func TestSerialize_LoginUser_EmptyCredentials(t *testing.T) { + cmd := LoginUser{ + Username: "", + Password: "", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginUser with empty credentials: %v", err) + } + + expected := buildExpectedLoginUser("", "") + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginUser_LongCredentials tests with longer username and password +func TestSerialize_LoginUser_LongCredentials(t *testing.T) { + cmd := LoginUser{ + Username: "user@example.com", + Password: "very_secure_password_123!", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginUser with long credentials: %v", err) + } + + expected := buildExpectedLoginUser("user@example.com", "very_secure_password_123!") + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginUser_SingleCharCredentials tests edge case with single character credentials +func TestSerialize_LoginUser_SingleCharCredentials(t *testing.T) { + cmd := LoginUser{ + Username: "a", + Password: "b", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginUser with single char credentials: %v", err) + } + + expected := buildExpectedLoginUser("a", "b") + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginWithPersonalAccessToken tests login with token +func TestSerialize_LoginWithPersonalAccessToken(t *testing.T) { + cmd := LoginWithPersonalAccessToken{ + Token: "my_access_token_12345", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginWithPersonalAccessToken: %v", err) + } + + expected := []byte{ + 0x15, // Token length = 21 + // "my_access_token_12345" + 0x6D, 0x79, 0x5F, 0x61, 0x63, 0x63, 0x65, 0x73, + 0x73, 0x5F, 0x74, 0x6F, 0x6B, 0x65, 0x6E, 0x5F, + 0x31, 0x32, 0x33, 0x34, 0x35, + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginWithPersonalAccessToken_ShortToken tests with short token +func TestSerialize_LoginWithPersonalAccessToken_ShortToken(t *testing.T) { + cmd := LoginWithPersonalAccessToken{ + Token: "abc", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginWithPersonalAccessToken with short token: %v", err) + } + + expected := []byte{ + 0x03, // Token length = 3 + 0x61, 0x62, 0x63, // "abc" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginWithPersonalAccessToken_EmptyToken tests edge case with empty token +func TestSerialize_LoginWithPersonalAccessToken_EmptyToken(t *testing.T) { + cmd := LoginWithPersonalAccessToken{ + Token: "", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginWithPersonalAccessToken with empty token: %v", err) + } + + expected := []byte{ + 0x00, // Token length = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LoginWithPersonalAccessToken_LongToken tests with longer token +func TestSerialize_LoginWithPersonalAccessToken_LongToken(t *testing.T) { + cmd := LoginWithPersonalAccessToken{ + Token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LoginWithPersonalAccessToken with long token: %v", err) + } + + expected := []byte{ + 0x6F, // Token length = 111 + // "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ" + 0x65, 0x79, 0x4A, 0x68, 0x62, 0x47, 0x63, 0x69, 0x4F, 0x69, 0x4A, 0x49, 0x55, 0x7A, 0x49, 0x31, + 0x4E, 0x69, 0x49, 0x73, 0x49, 0x6E, 0x52, 0x35, 0x63, 0x43, 0x49, 0x36, 0x49, 0x6B, 0x70, 0x58, + 0x56, 0x43, 0x4A, 0x39, 0x2E, 0x65, 0x79, 0x4A, 0x7A, 0x64, 0x57, 0x49, 0x69, 0x4F, 0x69, 0x49, + 0x78, 0x4D, 0x6A, 0x4D, 0x30, 0x4E, 0x54, 0x59, 0x33, 0x4F, 0x44, 0x6B, 0x77, 0x49, 0x69, 0x77, + 0x69, 0x62, 0x6D, 0x46, 0x74, 0x5A, 0x53, 0x49, 0x36, 0x49, 0x6B, 0x70, 0x76, 0x61, 0x47, 0x34, + 0x67, 0x52, 0x47, 0x39, 0x6C, 0x49, 0x69, 0x77, 0x69, 0x61, 0x57, 0x46, 0x30, 0x49, 0x6A, 0x6F, + 0x78, 0x4E, 0x54, 0x45, 0x32, 0x4D, 0x6A, 0x4D, 0x35, 0x4D, 0x44, 0x49, 0x79, 0x66, 0x51, + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_LogoutUser tests LogoutUser serialization +func TestSerialize_LogoutUser(t *testing.T) { + cmd := LogoutUser{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize LogoutUser: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/system_test.go b/foreign/go/internal/command/system_test.go new file mode 100644 index 0000000000..547ed1a109 --- /dev/null +++ b/foreign/go/internal/command/system_test.go @@ -0,0 +1,167 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" +) + +// TestSerialize_Ping tests serialization of Ping command +func TestSerialize_Ping(t *testing.T) { + cmd := Ping{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize Ping: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetStats tests serialization of GetStats command +func TestSerialize_GetStats(t *testing.T) { + cmd := GetStats{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetStats: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClients tests serialization of GetClients command +func TestSerialize_GetClients(t *testing.T) { + cmd := GetClients{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClients: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClusterMetadata tests serialization of GetClusterMetadata command +func TestSerialize_GetClusterMetadata(t *testing.T) { + cmd := GetClusterMetadata{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClusterMetadata: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClient tests serialization of GetClient command with normal value +func TestSerialize_GetClient(t *testing.T) { + cmd := GetClient{ + ClientID: 42, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClient: %v", err) + } + + expected := []byte{ + 0x2A, 0x00, 0x00, 0x00, // ClientID = 42 (little endian uint32) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClient_Zero tests serialization with zero ClientID (edge case) +func TestSerialize_GetClient_Zero(t *testing.T) { + cmd := GetClient{ + ClientID: 0, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClient with zero ClientID: %v", err) + } + + expected := []byte{ + 0x00, 0x00, 0x00, 0x00, // ClientID = 0 (little endian uint32) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClient_MaxValue tests serialization with maximum uint32 value (edge case) +func TestSerialize_GetClient_MaxValue(t *testing.T) { + cmd := GetClient{ + ClientID: 4294967295, // Max uint32 value + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClient with max ClientID: %v", err) + } + + expected := []byte{ + 0xFF, 0xFF, 0xFF, 0xFF, // ClientID = 4294967295 (little endian uint32) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetClient_LargeValue tests serialization with a large ClientID value +func TestSerialize_GetClient_LargeValue(t *testing.T) { + cmd := GetClient{ + ClientID: 16909060, // 0x01020304 in hex + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetClient with large ClientID: %v", err) + } + + expected := []byte{ + 0x04, 0x03, 0x02, 0x01, // ClientID = 16909060 (little endian: 0x01020304) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/topic.go b/foreign/go/internal/command/topic.go index 1ce8a988d0..e5868d15fd 100644 --- a/foreign/go/internal/command/topic.go +++ b/foreign/go/internal/command/topic.go @@ -38,8 +38,9 @@ func (t *CreateTopic) Code() Code { } func (t *CreateTopic) MarshalBinary() ([]byte, error) { - if t.ReplicationFactor == nil { - t.ReplicationFactor = new(uint8) + var replicationFactor uint8 + if t.ReplicationFactor != nil { + replicationFactor = *t.ReplicationFactor } streamIdBytes, err := t.StreamId.MarshalBinary() @@ -81,7 +82,7 @@ func (t *CreateTopic) MarshalBinary() ([]byte, error) { position += 8 // ReplicationFactor - bytes[position] = *t.ReplicationFactor + bytes[position] = replicationFactor position++ // Name @@ -145,8 +146,9 @@ func (u *UpdateTopic) Code() Code { } func (u *UpdateTopic) MarshalBinary() ([]byte, error) { - if u.ReplicationFactor == nil { - u.ReplicationFactor = new(uint8) + var replicationFactor uint8 + if u.ReplicationFactor != nil { + replicationFactor = *u.ReplicationFactor } streamIdBytes, err := u.StreamId.MarshalBinary() if err != nil { @@ -173,7 +175,7 @@ func (u *UpdateTopic) MarshalBinary() ([]byte, error) { binary.LittleEndian.PutUint64(buffer[offset:], u.MaxTopicSize) offset += 8 - buffer[offset] = *u.ReplicationFactor + buffer[offset] = replicationFactor offset++ buffer[offset] = uint8(len(u.Name)) diff --git a/foreign/go/internal/command/update_user.go b/foreign/go/internal/command/update_user.go index a20d647fb7..76401e0293 100644 --- a/foreign/go/internal/command/update_user.go +++ b/foreign/go/internal/command/update_user.go @@ -34,23 +34,22 @@ func (u *UpdateUser) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } - length := len(userIdBytes) + length := len(userIdBytes) + 2 - if u.Username == nil { - u.Username = new(string) + var username string + if u.Username != nil { + username = *u.Username } - username := *u.Username - if len(username) != 0 { - length += 2 + len(username) + length += 1 + len(username) } if u.Status != nil { - length += 2 + length += 1 } - bytes := make([]byte, length+1) + bytes := make([]byte, length) position := 0 copy(bytes[position:position+len(userIdBytes)], userIdBytes) diff --git a/foreign/go/internal/command/update_user_test.go b/foreign/go/internal/command/update_user_test.go new file mode 100644 index 0000000000..71cbbc6a0d --- /dev/null +++ b/foreign/go/internal/command/update_user_test.go @@ -0,0 +1,248 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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 command + +import ( + "bytes" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +// Helper functions for creating pointers +func stringPtr(s string) *string { + return &s +} + +func userStatusPtr(s iggcon.UserStatus) *iggcon.UserStatus { + return &s +} + +// TestSerialize_UpdateUser_BothFields tests UpdateUser with both username and status +func TestSerialize_UpdateUser_BothFields(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(42)) + username := "new_admin" + status := iggcon.Active + + cmd := UpdateUser{ + UserID: userId, + Username: &username, + Status: &status, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser: %v", err) + } + + expected := []byte{ + 0x01, // UserId Kind = NumericId + 0x04, // UserId Length = 4 + 0x2A, 0x00, 0x00, 0x00, // UserId Value = 42 + 0x01, // Has username = 1 + 0x09, // Username length = 9 + 0x6E, 0x65, 0x77, 0x5F, 0x61, 0x64, 0x6D, 0x69, 0x6E, // "new_admin" + 0x01, // Has status = 1 + 0x01, // Status = Active (1) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_OnlyUsername tests UpdateUser with only username +func TestSerialize_UpdateUser_OnlyUsername(t *testing.T) { + userId, _ := iggcon.NewIdentifier("user123") + + cmd := UpdateUser{ + UserID: userId, + Username: stringPtr("updated_name"), + Status: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with only username: %v", err) + } + + expected := []byte{ + 0x02, // UserId Kind = StringId + 0x07, // UserId Length = 7 + 0x75, 0x73, 0x65, 0x72, 0x31, 0x32, 0x33, // "user123" + 0x01, // Has username = 1 + 0x0C, // Username length = 12 + 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x64, 0x5F, 0x6E, 0x61, 0x6D, 0x65, // "updated_name" + 0x00, // Has status = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_OnlyStatus tests UpdateUser with only status +func TestSerialize_UpdateUser_OnlyStatus(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + + cmd := UpdateUser{ + UserID: userId, + Username: nil, + Status: userStatusPtr(iggcon.Inactive), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with only status: %v", err) + } + + expected := []byte{ + 0x01, // UserId Kind = NumericId + 0x04, // UserId Length = 4 + 0x01, 0x00, 0x00, 0x00, // UserId Value = 1 + 0x00, // Has username = 0 + 0x01, // Has status = 1 + 0x02, // Status = Inactive (2) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_NeitherField tests UpdateUser with no updates (both nil) +func TestSerialize_UpdateUser_NeitherField(t *testing.T) { + userId, _ := iggcon.NewIdentifier("admin") + + cmd := UpdateUser{ + UserID: userId, + Username: nil, + Status: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with no fields: %v", err) + } + + expected := []byte{ + 0x02, // UserId Kind = StringId + 0x05, // UserId Length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + 0x00, // Has username = 0 + 0x00, // Has status = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_EmptyUsername tests UpdateUser with empty username string +func TestSerialize_UpdateUser_EmptyUsername(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(99)) + + cmd := UpdateUser{ + UserID: userId, + Username: stringPtr(""), + Status: userStatusPtr(iggcon.Active), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with empty username: %v", err) + } + + expected := []byte{ + 0x01, // UserId Kind = NumericId + 0x04, // UserId Length = 4 + 0x63, 0x00, 0x00, 0x00, // UserId Value = 99 + 0x00, // Has username = 0 (empty string treated as no username) + 0x01, // Has status = 1 + 0x01, // Status = Active (1) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_SingleCharUsername tests with single character username +func TestSerialize_UpdateUser_SingleCharUsername(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(5)) + + cmd := UpdateUser{ + UserID: userId, + Username: stringPtr("a"), + Status: userStatusPtr(iggcon.Inactive), + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with single char username: %v", err) + } + + expected := []byte{ + 0x01, // UserId Kind = NumericId + 0x04, // UserId Length = 4 + 0x05, 0x00, 0x00, 0x00, // UserId Value = 5 + 0x01, // Has username = 1 + 0x01, // Username length = 1 + 0x61, // "a" + 0x01, // Has status = 1 + 0x02, // Status = Inactive (2) + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_UpdateUser_LongUsername tests with a longer username +func TestSerialize_UpdateUser_LongUsername(t *testing.T) { + userId, _ := iggcon.NewIdentifier("test") + + cmd := UpdateUser{ + UserID: userId, + Username: stringPtr("very_long_username_for_testing"), + Status: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize UpdateUser with long username: %v", err) + } + + expected := []byte{ + 0x02, // UserId Kind = StringId + 0x04, // UserId Length = 4 + 0x74, 0x65, 0x73, 0x74, // "test" + 0x01, // Has username = 1 + 0x1E, // Username length = 30 + // "very_long_username_for_testing" + 0x76, 0x65, 0x72, 0x79, 0x5F, 0x6C, 0x6F, 0x6E, + 0x67, 0x5F, 0x75, 0x73, 0x65, 0x72, 0x6E, 0x61, + 0x6D, 0x65, 0x5F, 0x66, 0x6F, 0x72, 0x5F, 0x74, + 0x65, 0x73, 0x74, 0x69, 0x6E, 0x67, + 0x00, // Has status = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/user.go b/foreign/go/internal/command/user.go index a0470d9ab3..9896ca4697 100644 --- a/foreign/go/internal/command/user.go +++ b/foreign/go/internal/command/user.go @@ -35,7 +35,7 @@ func (c *CreateUser) Code() Code { } func (c *CreateUser) MarshalBinary() ([]byte, error) { - capacity := 4 + len(c.Username) + len(c.Password) + capacity := 1 + len(c.Username) + 1 + len(c.Password) + 1 + 1 if c.Permissions != nil { capacity += 4 + c.Permissions.Size() } diff --git a/foreign/go/internal/command/user_test.go b/foreign/go/internal/command/user_test.go index c1d9cd4c6e..7cd043233e 100644 --- a/foreign/go/internal/command/user_test.go +++ b/foreign/go/internal/command/user_test.go @@ -25,6 +25,29 @@ import ( iggcon "github.com/apache/iggy/foreign/go/contracts" ) +// Helper to create test permissions with only global permissions +func createTestGlobalPermissions(all bool) iggcon.GlobalPermissions { + return iggcon.GlobalPermissions{ + ManageServers: all, + ReadServers: all, + ManageUsers: all, + ReadUsers: all, + ManageStreams: all, + ReadStreams: all, + ManageTopics: all, + ReadTopics: all, + PollMessages: all, + SendMessages: all, + } +} + +func createTestPermissions(global iggcon.GlobalPermissions) *iggcon.Permissions { + return &iggcon.Permissions{ + Global: global, + Streams: nil, // Simple case: no stream-specific permissions + } +} + func TestSerialize_CreateUser_NilPermissions(t *testing.T) { request := CreateUser{ Username: "u", @@ -207,3 +230,308 @@ func TestSerialize_UpdatePermissions_WithPermissions(t *testing.T) { t.Errorf("permissions payload mismatch") } } + +// TestSerialize_CreateUser_WithPermissions_ActiveStatus tests CreateUser with permissions and Active status +func TestSerialize_CreateUser_WithPermissions_ActiveStatus(t *testing.T) { + globalPerms := createTestGlobalPermissions(true) + permissions := createTestPermissions(globalPerms) + + cmd := CreateUser{ + Username: "admin", + Password: "secret", + Status: iggcon.Active, + Permissions: permissions, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateUser: %v", err) + } + + expected := []byte{ + 0x05, // Username length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + 0x06, // Password length = 6 + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, // "secret" + 0x01, // Status = Active (1) + 0x01, // Has permissions = 1 + 0x0B, 0x00, 0x00, 0x00, // Permissions length = 11 + // Global permissions (10 bytes) - all true + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x00, // No stream-specific permissions + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreateUser_InactiveStatus tests CreateUser with Inactive status +func TestSerialize_CreateUser_InactiveStatus(t *testing.T) { + cmd := CreateUser{ + Username: "inactive_user", + Password: "pwd", + Status: iggcon.Inactive, + Permissions: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateUser with inactive status: %v", err) + } + + expected := []byte{ + 0x0D, // Username length = 13 + 0x69, 0x6E, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5F, 0x75, 0x73, 0x65, 0x72, // "inactive_user" + 0x03, // Password length = 3 + 0x70, 0x77, 0x64, // "pwd" + 0x02, // Status = Inactive (2) + 0x00, // Has permissions = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreateUser_EmptyCredentials tests edge case with empty username/password +func TestSerialize_CreateUser_EmptyCredentials(t *testing.T) { + cmd := CreateUser{ + Username: "", + Password: "", + Status: iggcon.Active, + Permissions: nil, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateUser with empty credentials: %v", err) + } + + expected := []byte{ + 0x00, // Username length = 0 + 0x00, // Password length = 0 + 0x01, // Status = Active (1) + 0x00, // Has permissions = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_CreateUser_PartialPermissions tests with partial global permissions +func TestSerialize_CreateUser_PartialPermissions(t *testing.T) { + globalPerms := iggcon.GlobalPermissions{ + ManageServers: true, + ReadServers: true, + ManageUsers: false, + ReadUsers: true, + ManageStreams: false, + ReadStreams: true, + ManageTopics: false, + ReadTopics: true, + PollMessages: true, + SendMessages: false, + } + permissions := createTestPermissions(globalPerms) + + cmd := CreateUser{ + Username: "user", + Password: "pass", + Status: iggcon.Active, + Permissions: permissions, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize CreateUser with partial permissions: %v", err) + } + + expected := []byte{ + 0x04, // Username length = 4 + 0x75, 0x73, 0x65, 0x72, // "user" + 0x04, // Password length = 4 + 0x70, 0x61, 0x73, 0x73, // "pass" + 0x01, // Status = Active (1) + 0x01, // Has permissions = 1 + 0x0B, 0x00, 0x00, 0x00, // Permissions length = 11 + // Global permissions: 1,1,0,1,0,1,0,1,1,0 + 0x01, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, + 0x00, // No stream-specific permissions + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetUsers tests GetUsers serialization (empty) +func TestSerialize_GetUsers(t *testing.T) { + cmd := GetUsers{} + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetUsers: %v", err) + } + + expected := []byte{} // Empty byte array + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetUser_NumericId tests GetUser with numeric identifier +func TestSerialize_GetUser_NumericId(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(123)) + + cmd := GetUser{ + Id: userId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetUser with numeric ID: %v", err) + } + + expected := []byte{ + 0x01, // Kind = NumericId + 0x04, // Length = 4 + 0x7B, 0x00, 0x00, 0x00, // Value = 123 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_GetUser_StringId tests GetUser with string identifier +func TestSerialize_GetUser_StringId(t *testing.T) { + userId, _ := iggcon.NewIdentifier("admin") + + cmd := GetUser{ + Id: userId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize GetUser with string ID: %v", err) + } + + expected := []byte{ + 0x02, // Kind = StringId + 0x05, // Length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_ChangePassword tests ChangePassword serialization +func TestSerialize_ChangePassword(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + + cmd := ChangePassword{ + UserID: userId, + CurrentPassword: "old_pass", + NewPassword: "new_pass", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize ChangePassword: %v", err) + } + + expected := []byte{ + 0x01, // UserId Kind = NumericId + 0x04, // UserId Length = 4 + 0x01, 0x00, 0x00, 0x00, // UserId Value = 1 + 0x08, // CurrentPassword length = 8 + 0x6F, 0x6C, 0x64, 0x5F, 0x70, 0x61, 0x73, 0x73, // "old_pass" + 0x08, // NewPassword length = 8 + 0x6E, 0x65, 0x77, 0x5F, 0x70, 0x61, 0x73, 0x73, // "new_pass" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_ChangePassword_EmptyPasswords tests edge case with empty passwords +func TestSerialize_ChangePassword_EmptyPasswords(t *testing.T) { + userId, _ := iggcon.NewIdentifier("admin") + + cmd := ChangePassword{ + UserID: userId, + CurrentPassword: "", + NewPassword: "", + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize ChangePassword with empty passwords: %v", err) + } + + expected := []byte{ + 0x02, // UserId Kind = StringId + 0x05, // UserId Length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + 0x00, // CurrentPassword length = 0 + 0x00, // NewPassword length = 0 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteUser_NumericId tests DeleteUser with numeric identifier +func TestSerialize_DeleteUser_NumericId(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(999)) + + cmd := DeleteUser{ + Id: userId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteUser with numeric ID: %v", err) + } + + expected := []byte{ + 0x01, // Kind = NumericId + 0x04, // Length = 4 + 0xE7, 0x03, 0x00, 0x00, // Value = 999 + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +} + +// TestSerialize_DeleteUser_StringId tests DeleteUser with string identifier +func TestSerialize_DeleteUser_StringId(t *testing.T) { + userId, _ := iggcon.NewIdentifier("test_user") + + cmd := DeleteUser{ + Id: userId, + } + + serialized, err := cmd.MarshalBinary() + if err != nil { + t.Fatalf("Failed to serialize DeleteUser with string ID: %v", err) + } + + expected := []byte{ + 0x02, // Kind = StringId + 0x09, // Length = 9 + 0x74, 0x65, 0x73, 0x74, 0x5F, 0x75, 0x73, 0x65, 0x72, // "test_user" + } + + if !bytes.Equal(serialized, expected) { + t.Errorf("Serialized bytes are incorrect.\nExpected:\t%v\nGot:\t\t%v", expected, serialized) + } +}