From d12c65b35f824b0fc612de5ed626e8d919b24b56 Mon Sep 17 00:00:00 2001 From: Atharva Lade Date: Thu, 26 Mar 2026 12:45:31 -0500 Subject: [PATCH 1/5] test(go): increase Go SDK Codecov coverage with comprehensive unit tests --- codecov.yml | 2 + .../binary_response_deserializer_test.go | 550 ++++++++++++++++++ foreign/go/contracts/cluster_node_test.go | 105 ++++ foreign/go/contracts/consumer_test.go | 107 ++++ foreign/go/contracts/identifier_test.go | 92 +++ foreign/go/contracts/message_header_test.go | 100 ++++ foreign/go/contracts/message_polling_test.go | 80 +++ foreign/go/contracts/messages_test.go | 79 +++ foreign/go/contracts/meta_data_test.go | 104 ++++ foreign/go/contracts/node_status_test.go | 84 +++ foreign/go/contracts/partitions_test.go | 185 ++++++ foreign/go/contracts/role_test.go | 77 +++ .../go/contracts/transport_endpoints_test.go | 88 +++ foreign/go/contracts/user_headers_test.go | 129 ++++ foreign/go/contracts/users_test.go | 73 +++ .../go/internal/command/access_token_test.go | 87 +++ .../internal/command/consumer_group_test.go | 175 ++++++ foreign/go/internal/command/offset_test.go | 121 ++++ foreign/go/internal/command/partition_test.go | 77 +++ foreign/go/internal/command/session_test.go | 34 ++ foreign/go/internal/command/stream_test.go | 51 ++ foreign/go/internal/command/system_test.go | 94 +++ foreign/go/internal/command/topic_test.go | 94 +++ foreign/go/internal/command/update_user.go | 8 +- .../go/internal/command/update_user_test.go | 80 +++ foreign/go/internal/command/user.go | 4 +- foreign/go/internal/command/user_test.go | 157 +++++ foreign/go/internal/util/leader_aware_test.go | 140 +++++ 28 files changed, 2972 insertions(+), 5 deletions(-) create mode 100644 foreign/go/binary_serialization/binary_response_deserializer_test.go create mode 100644 foreign/go/contracts/cluster_node_test.go create mode 100644 foreign/go/contracts/consumer_test.go create mode 100644 foreign/go/contracts/message_header_test.go create mode 100644 foreign/go/contracts/message_polling_test.go create mode 100644 foreign/go/contracts/messages_test.go create mode 100644 foreign/go/contracts/meta_data_test.go create mode 100644 foreign/go/contracts/node_status_test.go create mode 100644 foreign/go/contracts/partitions_test.go create mode 100644 foreign/go/contracts/role_test.go create mode 100644 foreign/go/contracts/transport_endpoints_test.go create mode 100644 foreign/go/contracts/user_headers_test.go create mode 100644 foreign/go/contracts/users_test.go create mode 100644 foreign/go/internal/command/access_token_test.go create mode 100644 foreign/go/internal/command/consumer_group_test.go create mode 100644 foreign/go/internal/command/offset_test.go create mode 100644 foreign/go/internal/command/partition_test.go create mode 100644 foreign/go/internal/command/system_test.go create mode 100644 foreign/go/internal/command/update_user_test.go create mode 100644 foreign/go/internal/command/user_test.go diff --git a/codecov.yml b/codecov.yml index 77a1dfc531..962a4446a0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -114,3 +114,5 @@ ignore: - "**/build/**" - "**/target/**" - "**/e2e/**" + - "foreign/go/samples/**" + - "foreign/go/benchmarks/**" diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go new file mode 100644 index 0000000000..e372ba48c0 --- /dev/null +++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go @@ -0,0 +1,550 @@ +// 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 binaryserialization + +import ( + "bytes" + "encoding/binary" + "errors" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" + ierror "github.com/apache/iggy/foreign/go/errors" +) + +func TestDeserializeFetchMessagesResponse_EmptyPayload(t *testing.T) { + response, err := DeserializeFetchMessagesResponse([]byte{}, iggcon.MESSAGE_COMPRESSION_NONE) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if response.PartitionId != 0 || response.CurrentOffset != 0 { + t.Fatalf("expected zero metadata, got partitionId=%d currentOffset=%d", response.PartitionId, response.CurrentOffset) + } + if len(response.Messages) != 0 { + t.Fatalf("expected no messages, got %d", len(response.Messages)) + } +} + +func TestDeserializeFetchMessagesResponse_ParsesSingleMessage(t *testing.T) { + var msgID iggcon.MessageID + copy(msgID[:], []byte("abcdefghijklmnop")) + messagePayload := []byte("hello") + userHeaders := []byte{0xAA, 0xBB} + + header := iggcon.MessageHeader{ + Checksum: 123, + Id: msgID, + Offset: 11, + Timestamp: 22, + OriginTimestamp: 33, + UserHeaderLength: uint32(len(userHeaders)), + PayloadLength: uint32(len(messagePayload)), + Reserved: 44, + } + + payload := make([]byte, 16) + binary.LittleEndian.PutUint32(payload[0:4], 9) // partition ID + binary.LittleEndian.PutUint64(payload[4:12], 99) // current offset + binary.LittleEndian.PutUint32(payload[12:16], 1) // messages count + payload = append(payload, header.ToBytes()...) + payload = append(payload, messagePayload...) + payload = append(payload, userHeaders...) + + response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if response.PartitionId != 9 { + t.Fatalf("partition id mismatch, expected 9 got %d", response.PartitionId) + } + if response.CurrentOffset != 99 { + t.Fatalf("current offset mismatch, expected 99 got %d", response.CurrentOffset) + } + if response.MessageCount != 1 { + t.Fatalf("message count mismatch, expected 1 got %d", response.MessageCount) + } + if len(response.Messages) != 1 { + t.Fatalf("expected exactly one message, got %d", len(response.Messages)) + } + if !bytes.Equal(response.Messages[0].Payload, messagePayload) { + t.Fatalf("payload mismatch, expected %v got %v", messagePayload, response.Messages[0].Payload) + } + if !bytes.Equal(response.Messages[0].UserHeaders, userHeaders) { + t.Fatalf("user headers mismatch, expected %v got %v", userHeaders, response.Messages[0].UserHeaders) + } +} + +func TestDeserializeFetchMessagesResponse_TruncatedHeaderBody_IgnoresMessage(t *testing.T) { + payload := make([]byte, 16+iggcon.MessageHeaderSize-1) + binary.LittleEndian.PutUint32(payload[12:16], 1) // declared message count + + response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(response.Messages) != 0 { + t.Fatalf("expected no messages from truncated payload, got %d", len(response.Messages)) + } +} + +func TestDeserializeFetchMessagesResponse_TruncatedMessagePayload_IgnoresMessage(t *testing.T) { + var msgID iggcon.MessageID + copy(msgID[:], []byte("abcdefghijklmnop")) + header := iggcon.MessageHeader{ + Id: msgID, + PayloadLength: 10, // claims 10 bytes + } + + payload := make([]byte, 16) + binary.LittleEndian.PutUint32(payload[12:16], 1) // declared message count + payload = append(payload, header.ToBytes()...) + payload = append(payload, []byte{0x01, 0x02, 0x03}...) // only 3 bytes available + + response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(response.Messages) != 0 { + t.Fatalf("expected no messages when payload is truncated, got %d", len(response.Messages)) + } +} + +func TestDeserializeLogInResponse(t *testing.T) { + payload := make([]byte, 4) + binary.LittleEndian.PutUint32(payload[0:4], 42) + + result := DeserializeLogInResponse(payload) + if result == nil { + t.Fatalf("expected non-nil result") + } + if result.UserId != 42 { + t.Fatalf("expected UserId=42, got %d", result.UserId) + } +} + +func TestDeserializeOffset_EmptyPayload(t *testing.T) { + result := DeserializeOffset([]byte{}) + if result != nil { + t.Fatalf("expected nil for empty payload, got %+v", result) + } +} + +func TestDeserializeOffset_ValidPayload(t *testing.T) { + payload := make([]byte, 20) + binary.LittleEndian.PutUint32(payload[0:4], 1) + binary.LittleEndian.PutUint64(payload[4:12], 100) + binary.LittleEndian.PutUint64(payload[12:20], 50) + + result := DeserializeOffset(payload) + if result == nil { + t.Fatalf("expected non-nil result") + } + if result.PartitionId != 1 { + t.Fatalf("expected PartitionId=1, got %d", result.PartitionId) + } + if result.CurrentOffset != 100 { + t.Fatalf("expected CurrentOffset=100, got %d", result.CurrentOffset) + } + if result.StoredOffset != 50 { + t.Fatalf("expected StoredOffset=50, got %d", result.StoredOffset) + } +} + +func buildStreamPayload(id uint32, createdAt uint64, topicsCount uint32, sizeBytes uint64, messagesCount uint64, name string) []byte { + nameBytes := []byte(name) + payload := make([]byte, 33+len(nameBytes)) + binary.LittleEndian.PutUint32(payload[0:4], id) + binary.LittleEndian.PutUint64(payload[4:12], createdAt) + binary.LittleEndian.PutUint32(payload[12:16], topicsCount) + binary.LittleEndian.PutUint64(payload[16:24], sizeBytes) + binary.LittleEndian.PutUint64(payload[24:32], messagesCount) + payload[32] = byte(len(nameBytes)) + copy(payload[33:], nameBytes) + return payload +} + +func TestDeserializeToStream(t *testing.T) { + payload := buildStreamPayload(7, 1234567890, 3, 4096, 500, "test") + + stream, readBytes := DeserializeToStream(payload, 0) + if stream.Id != 7 { + t.Fatalf("expected Id=7, got %d", stream.Id) + } + if stream.CreatedAt != 1234567890 { + t.Fatalf("expected CreatedAt=1234567890, got %d", stream.CreatedAt) + } + if stream.TopicsCount != 3 { + t.Fatalf("expected TopicsCount=3, got %d", stream.TopicsCount) + } + if stream.SizeBytes != 4096 { + t.Fatalf("expected SizeBytes=4096, got %d", stream.SizeBytes) + } + if stream.MessagesCount != 500 { + t.Fatalf("expected MessagesCount=500, got %d", stream.MessagesCount) + } + if stream.Name != "test" { + t.Fatalf("expected Name='test', got '%s'", stream.Name) + } + expectedReadBytes := 4 + 8 + 4 + 8 + 8 + 1 + 4 + if readBytes != expectedReadBytes { + t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + } +} + +func TestDeserializeStreams(t *testing.T) { + entry1 := buildStreamPayload(1, 100, 2, 1024, 10, "stream1") + entry2 := buildStreamPayload(2, 200, 5, 2048, 20, "stream2") + payload := append(entry1, entry2...) + + streams := DeserializeStreams(payload) + if len(streams) != 2 { + t.Fatalf("expected 2 streams, got %d", len(streams)) + } + if streams[0].Name != "stream1" { + t.Fatalf("expected first stream name='stream1', got '%s'", streams[0].Name) + } + if streams[1].Name != "stream2" { + t.Fatalf("expected second stream name='stream2', got '%s'", streams[1].Name) + } +} + +func buildTopicPayload(id uint32, createdAt uint64, partitionsCount uint32, messageExpiry uint64, compressionAlgo byte, maxTopicSize uint64, replicationFactor byte, size uint64, messagesCount uint64, name string) []byte { + nameBytes := []byte(name) + payload := make([]byte, 51+len(nameBytes)) + binary.LittleEndian.PutUint32(payload[0:4], id) + binary.LittleEndian.PutUint64(payload[4:12], createdAt) + binary.LittleEndian.PutUint32(payload[12:16], partitionsCount) + binary.LittleEndian.PutUint64(payload[16:24], messageExpiry) + payload[24] = compressionAlgo + binary.LittleEndian.PutUint64(payload[25:33], maxTopicSize) + payload[33] = replicationFactor + binary.LittleEndian.PutUint64(payload[34:42], size) + binary.LittleEndian.PutUint64(payload[42:50], messagesCount) + payload[50] = byte(len(nameBytes)) + copy(payload[51:], nameBytes) + return payload +} + +func TestDeserializeToTopic(t *testing.T) { + payload := buildTopicPayload(10, 9999, 4, 60000, 2, 1048576, 3, 8192, 777, "topic1") + + topic, readBytes, err := DeserializeToTopic(payload, 0) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if topic.Id != 10 { + t.Fatalf("expected Id=10, got %d", topic.Id) + } + if topic.CreatedAt != 9999 { + t.Fatalf("expected CreatedAt=9999, got %d", topic.CreatedAt) + } + if topic.PartitionsCount != 4 { + t.Fatalf("expected PartitionsCount=4, got %d", topic.PartitionsCount) + } + if uint64(topic.MessageExpiry) != 60000 { + t.Fatalf("expected MessageExpiry=60000, got %d", topic.MessageExpiry) + } + if topic.CompressionAlgorithm != 2 { + t.Fatalf("expected CompressionAlgorithm=2, got %d", topic.CompressionAlgorithm) + } + if topic.MaxTopicSize != 1048576 { + t.Fatalf("expected MaxTopicSize=1048576, got %d", topic.MaxTopicSize) + } + if topic.ReplicationFactor != 3 { + t.Fatalf("expected ReplicationFactor=3, got %d", topic.ReplicationFactor) + } + if topic.Size != 8192 { + t.Fatalf("expected Size=8192, got %d", topic.Size) + } + if topic.MessagesCount != 777 { + t.Fatalf("expected MessagesCount=777, got %d", topic.MessagesCount) + } + if topic.Name != "topic1" { + t.Fatalf("expected Name='topic1', got '%s'", topic.Name) + } + expectedReadBytes := 4 + 8 + 4 + 8 + 8 + 8 + 8 + 1 + 1 + 1 + 6 + if readBytes != expectedReadBytes { + t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + } +} + +func TestDeserializeTopics(t *testing.T) { + entry1 := buildTopicPayload(1, 100, 2, 1000, 1, 4096, 1, 512, 50, "topicA") + entry2 := buildTopicPayload(2, 200, 3, 2000, 0, 8192, 2, 1024, 100, "topicB") + payload := append(entry1, entry2...) + + topics, err := DeserializeTopics(payload) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(topics) != 2 { + t.Fatalf("expected 2 topics, got %d", len(topics)) + } + if topics[0].Name != "topicA" { + t.Fatalf("expected first topic name='topicA', got '%s'", topics[0].Name) + } + if topics[1].Name != "topicB" { + t.Fatalf("expected second topic name='topicB', got '%s'", topics[1].Name) + } +} + +func TestDeserializePartition(t *testing.T) { + payload := make([]byte, 40) + binary.LittleEndian.PutUint32(payload[0:4], 5) + binary.LittleEndian.PutUint64(payload[4:12], 1111) + binary.LittleEndian.PutUint32(payload[12:16], 8) + binary.LittleEndian.PutUint64(payload[16:24], 99) + binary.LittleEndian.PutUint64(payload[24:32], 2048) + binary.LittleEndian.PutUint64(payload[32:40], 300) + + partition, readBytes := DeserializePartition(payload, 0) + if partition.Id != 5 { + t.Fatalf("expected Id=5, got %d", partition.Id) + } + if partition.CreatedAt != 1111 { + t.Fatalf("expected CreatedAt=1111, got %d", partition.CreatedAt) + } + if partition.SegmentsCount != 8 { + t.Fatalf("expected SegmentsCount=8, got %d", partition.SegmentsCount) + } + if partition.CurrentOffset != 99 { + t.Fatalf("expected CurrentOffset=99, got %d", partition.CurrentOffset) + } + if partition.SizeBytes != 2048 { + t.Fatalf("expected SizeBytes=2048, got %d", partition.SizeBytes) + } + if partition.MessagesCount != 300 { + t.Fatalf("expected MessagesCount=300, got %d", partition.MessagesCount) + } + if readBytes != 40 { + t.Fatalf("expected readBytes=40, got %d", readBytes) + } +} + +func buildConsumerGroupPayload(id uint32, partitionsCount uint32, membersCount uint32, name string) []byte { + nameBytes := []byte(name) + payload := make([]byte, 13+len(nameBytes)) + binary.LittleEndian.PutUint32(payload[0:4], id) + binary.LittleEndian.PutUint32(payload[4:8], partitionsCount) + binary.LittleEndian.PutUint32(payload[8:12], membersCount) + payload[12] = byte(len(nameBytes)) + copy(payload[13:], nameBytes) + return payload +} + +func TestDeserializeToConsumerGroup(t *testing.T) { + payload := buildConsumerGroupPayload(11, 6, 2, "grp1") + + cg, readBytes := DeserializeToConsumerGroup(payload, 0) + if cg == nil { + t.Fatalf("expected non-nil consumer group") + } + if cg.Id != 11 { + t.Fatalf("expected Id=11, got %d", cg.Id) + } + if cg.PartitionsCount != 6 { + t.Fatalf("expected PartitionsCount=6, got %d", cg.PartitionsCount) + } + if cg.MembersCount != 2 { + t.Fatalf("expected MembersCount=2, got %d", cg.MembersCount) + } + if cg.Name != "grp1" { + t.Fatalf("expected Name='grp1', got '%s'", cg.Name) + } + expectedReadBytes := 12 + 1 + 4 + if readBytes != expectedReadBytes { + t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + } +} + +func TestDeserializeConsumerGroups(t *testing.T) { + entry1 := buildConsumerGroupPayload(1, 3, 1, "group1") + entry2 := buildConsumerGroupPayload(2, 5, 4, "group2") + payload := append(entry1, entry2...) + + groups := DeserializeConsumerGroups(payload) + if len(groups) != 2 { + t.Fatalf("expected 2 consumer groups, got %d", len(groups)) + } + if groups[0].Name != "group1" { + t.Fatalf("expected first group name='group1', got '%s'", groups[0].Name) + } + if groups[1].Name != "group2" { + t.Fatalf("expected second group name='group2', got '%s'", groups[1].Name) + } +} + +func TestDeserializeToConsumerGroupMember(t *testing.T) { + payload := make([]byte, 16) + binary.LittleEndian.PutUint32(payload[0:4], 77) + binary.LittleEndian.PutUint32(payload[4:8], 2) + binary.LittleEndian.PutUint32(payload[8:12], 10) + binary.LittleEndian.PutUint32(payload[12:16], 20) + + member, readBytes := DeserializeToConsumerGroupMember(payload, 0) + if member.ID != 77 { + t.Fatalf("expected ID=77, got %d", member.ID) + } + if member.PartitionsCount != 2 { + t.Fatalf("expected PartitionsCount=2, got %d", member.PartitionsCount) + } + if len(member.Partitions) != 2 { + t.Fatalf("expected 2 partitions, got %d", len(member.Partitions)) + } + if member.Partitions[0] != 10 { + t.Fatalf("expected first partition=10, got %d", member.Partitions[0]) + } + if member.Partitions[1] != 20 { + t.Fatalf("expected second partition=20, got %d", member.Partitions[1]) + } + expectedReadBytes := 4 + 4 + 2*4 + if readBytes != expectedReadBytes { + t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + } +} + +func TestDeserializeUsers_EmptyPayload(t *testing.T) { + _, err := DeserializeUsers([]byte{}) + if err == nil { + t.Fatalf("expected error for empty payload") + } +} + +func TestDeserializeUsers_ValidPayload(t *testing.T) { + username := "admin" + usernameBytes := []byte(username) + payload := make([]byte, 14+len(usernameBytes)) + binary.LittleEndian.PutUint32(payload[0:4], 1) + binary.LittleEndian.PutUint64(payload[4:12], 5555) + payload[12] = 1 // Active + payload[13] = byte(len(usernameBytes)) + copy(payload[14:], usernameBytes) + + users, err := DeserializeUsers(payload) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(users) != 1 { + t.Fatalf("expected 1 user, got %d", len(users)) + } + if users[0].Id != 1 { + t.Fatalf("expected Id=1, got %d", users[0].Id) + } + if users[0].CreatedAt != 5555 { + t.Fatalf("expected CreatedAt=5555, got %d", users[0].CreatedAt) + } + if users[0].Status != iggcon.Active { + t.Fatalf("expected Status=Active, got %v", users[0].Status) + } + if users[0].Username != "admin" { + t.Fatalf("expected Username='admin', got '%s'", users[0].Username) + } +} + +func TestDeserializeClients_EmptyPayload(t *testing.T) { + clients, err := DeserializeClients([]byte{}) + if err != nil { + t.Fatalf("expected no error for empty payload, got %v", err) + } + if len(clients) != 0 { + t.Fatalf("expected 0 clients, got %d", len(clients)) + } +} + +func TestDeserializeClients_ValidPayload(t *testing.T) { + address := "1.2.3.4:8090" + addressBytes := []byte(address) + headerSize := 4 + 4 + 1 + 4 + len(addressBytes) + 4 + payload := make([]byte, headerSize) + binary.LittleEndian.PutUint32(payload[0:4], 99) + binary.LittleEndian.PutUint32(payload[4:8], 7) + payload[8] = 1 // Tcp + binary.LittleEndian.PutUint32(payload[9:13], uint32(len(addressBytes))) + copy(payload[13:13+len(addressBytes)], addressBytes) + pos := 13 + len(addressBytes) + binary.LittleEndian.PutUint32(payload[pos:pos+4], 3) + + clients, err := DeserializeClients(payload) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(clients) != 1 { + t.Fatalf("expected 1 client, got %d", len(clients)) + } + if clients[0].ID != 99 { + t.Fatalf("expected ID=99, got %d", clients[0].ID) + } + if clients[0].UserID != 7 { + t.Fatalf("expected UserID=7, got %d", clients[0].UserID) + } + if clients[0].Transport != string(iggcon.Tcp) { + t.Fatalf("expected Transport='tcp', got '%s'", clients[0].Transport) + } + if clients[0].Address != address { + t.Fatalf("expected Address='%s', got '%s'", address, clients[0].Address) + } + if clients[0].ConsumerGroupsCount != 3 { + t.Fatalf("expected ConsumerGroupsCount=3, got %d", clients[0].ConsumerGroupsCount) + } +} + +func TestDeserializeAccessToken(t *testing.T) { + token := "abc123" + tokenBytes := []byte(token) + payload := make([]byte, 1+len(tokenBytes)) + payload[0] = byte(len(tokenBytes)) + copy(payload[1:], tokenBytes) + + result, err := DeserializeAccessToken(payload) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if result.Token != token { + t.Fatalf("expected Token='%s', got '%s'", token, result.Token) + } +} + +func TestDeserializeAccessTokens_EmptyPayload(t *testing.T) { + _, err := DeserializeAccessTokens([]byte{}) + if !errors.Is(err, ierror.ErrEmptyMessagePayload) { + t.Fatalf("expected ErrEmptyMessagePayload, got %v", err) + } +} + +func TestDeserializeAccessTokens_ValidPayload(t *testing.T) { + name := "token1" + nameBytes := []byte(name) + payload := make([]byte, 1+len(nameBytes)+8) + payload[0] = byte(len(nameBytes)) + copy(payload[1:1+len(nameBytes)], nameBytes) + binary.LittleEndian.PutUint64(payload[1+len(nameBytes):], 1000000) + + tokens, err := DeserializeAccessTokens(payload) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if len(tokens) != 1 { + t.Fatalf("expected 1 token, got %d", len(tokens)) + } + if tokens[0].Name != name { + t.Fatalf("expected Name='%s', got '%s'", name, tokens[0].Name) + } + if tokens[0].Expiry == nil { + t.Fatalf("expected non-nil Expiry") + } +} diff --git a/foreign/go/contracts/cluster_node_test.go b/foreign/go/contracts/cluster_node_test.go new file mode 100644 index 0000000000..fd8fa4ace3 --- /dev/null +++ b/foreign/go/contracts/cluster_node_test.go @@ -0,0 +1,105 @@ +// 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 iggcon + +import ( + "strings" + "testing" +) + +func TestClusterNode_MarshalUnmarshal_Roundtrip(t *testing.T) { + original := ClusterNode{ + Name: "node-1", + IP: "192.168.1.10", + Endpoints: NewTransportEndpoints(8090, 8091, 3000, 8080), + Role: RoleLeader, + Status: Healthy, + } + + data, err := original.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + var decoded ClusterNode + if err := decoded.UnmarshalBinary(data); err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } + + if decoded.Name != original.Name { + t.Fatalf("Name mismatch: expected %s, got %s", original.Name, decoded.Name) + } + if decoded.IP != original.IP { + t.Fatalf("IP mismatch: expected %s, got %s", original.IP, decoded.IP) + } + if decoded.Endpoints.Tcp != original.Endpoints.Tcp { + t.Fatalf("Tcp mismatch: expected %d, got %d", original.Endpoints.Tcp, decoded.Endpoints.Tcp) + } + if decoded.Endpoints.Quic != original.Endpoints.Quic { + t.Fatalf("Quic mismatch: expected %d, got %d", original.Endpoints.Quic, decoded.Endpoints.Quic) + } + if decoded.Endpoints.Http != original.Endpoints.Http { + t.Fatalf("Http mismatch: expected %d, got %d", original.Endpoints.Http, decoded.Endpoints.Http) + } + if decoded.Endpoints.WebSocket != original.Endpoints.WebSocket { + t.Fatalf("WebSocket mismatch: expected %d, got %d", original.Endpoints.WebSocket, decoded.Endpoints.WebSocket) + } + if decoded.Role != original.Role { + t.Fatalf("Role mismatch: expected %d, got %d", original.Role, decoded.Role) + } + if decoded.Status != original.Status { + t.Fatalf("Status mismatch: expected %d, got %d", original.Status, decoded.Status) + } +} + +func TestClusterNode_BufferSize_MatchesMarshaledLength(t *testing.T) { + node := ClusterNode{ + Name: "node-1", + IP: "192.168.1.10", + Endpoints: NewTransportEndpoints(8090, 8091, 3000, 8080), + Role: RoleFollower, + Status: Starting, + } + + data, err := node.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + if len(data) != node.BufferSize() { + t.Fatalf("expected marshaled length %d to match BufferSize %d", len(data), node.BufferSize()) + } +} + +func TestClusterNode_String_ContainsNameAndIP(t *testing.T) { + node := ClusterNode{ + Name: "test-node", + IP: "10.0.0.1", + Endpoints: NewTransportEndpoints(1, 2, 3, 4), + Role: RoleLeader, + Status: Healthy, + } + + s := node.String() + if !strings.Contains(s, "test-node") { + t.Fatalf("expected string to contain 'test-node', got %s", s) + } + if !strings.Contains(s, "10.0.0.1") { + t.Fatalf("expected string to contain '10.0.0.1', got %s", s) + } +} diff --git a/foreign/go/contracts/consumer_test.go b/foreign/go/contracts/consumer_test.go new file mode 100644 index 0000000000..62038095cd --- /dev/null +++ b/foreign/go/contracts/consumer_test.go @@ -0,0 +1,107 @@ +// 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 iggcon + +import ( + "bytes" + "testing" +) + +func TestDefaultConsumer_ReturnsSingleWithIdZero(t *testing.T) { + c := DefaultConsumer() + if c.Kind != ConsumerKindSingle { + t.Fatalf("expected kind ConsumerKindSingle (%d), got %d", ConsumerKindSingle, c.Kind) + } + val, err := c.Id.Uint32() + if err != nil { + t.Fatalf("unexpected error getting id: %v", err) + } + if val != 0 { + t.Fatalf("expected id 0, got %d", val) + } +} + +func TestNewSingleConsumer(t *testing.T) { + id, err := NewIdentifier(uint32(5)) + if err != nil { + t.Fatalf("unexpected error creating identifier: %v", err) + } + c := NewSingleConsumer(id) + if c.Kind != ConsumerKindSingle { + t.Fatalf("expected kind ConsumerKindSingle (%d), got %d", ConsumerKindSingle, c.Kind) + } + val, err := c.Id.Uint32() + if err != nil { + t.Fatalf("unexpected error getting id: %v", err) + } + if val != 5 { + t.Fatalf("expected id 5, got %d", val) + } +} + +func TestNewGroupConsumer(t *testing.T) { + id, err := NewIdentifier(uint32(10)) + if err != nil { + t.Fatalf("unexpected error creating identifier: %v", err) + } + c := NewGroupConsumer(id) + if c.Kind != ConsumerKindGroup { + t.Fatalf("expected kind ConsumerKindGroup (%d), got %d", ConsumerKindGroup, c.Kind) + } + val, err := c.Id.Uint32() + if err != nil { + t.Fatalf("unexpected error getting id: %v", err) + } + if val != 10 { + t.Fatalf("expected id 10, got %d", val) + } +} + +func TestConsumer_MarshalBinary_Single(t *testing.T) { + c := DefaultConsumer() + data, err := c.MarshalBinary() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if data[0] != byte(ConsumerKindSingle) { + t.Fatalf("expected first byte %d, got %d", ConsumerKindSingle, data[0]) + } + idBytes, _ := c.Id.MarshalBinary() + if !bytes.Equal(data[1:], idBytes) { + t.Fatalf("expected id bytes %v, got %v", idBytes, data[1:]) + } +} + +func TestConsumer_MarshalBinary_Group(t *testing.T) { + id, err := NewIdentifier(uint32(7)) + if err != nil { + t.Fatalf("unexpected error creating identifier: %v", err) + } + c := NewGroupConsumer(id) + data, err := c.MarshalBinary() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if data[0] != byte(ConsumerKindGroup) { + t.Fatalf("expected first byte %d, got %d", ConsumerKindGroup, data[0]) + } + idBytes, _ := id.MarshalBinary() + if !bytes.Equal(data[1:], idBytes) { + t.Fatalf("expected id bytes %v, got %v", idBytes, data[1:]) + } +} diff --git a/foreign/go/contracts/identifier_test.go b/foreign/go/contracts/identifier_test.go index a83a893675..4183fa5074 100644 --- a/foreign/go/contracts/identifier_test.go +++ b/foreign/go/contracts/identifier_test.go @@ -80,3 +80,95 @@ func TestSerializeIdentifier_EmptyStringId(t *testing.T) { t.Errorf("Expected error: %v, got: %v", ierror.ErrInvalidIdentifier, err) } } + +func TestIdentifier_Uint32(t *testing.T) { + id, err := NewIdentifier(uint32(42)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + val, err := id.Uint32() + if err != nil { + t.Fatalf("unexpected error getting uint32: %v", err) + } + if val != 42 { + t.Fatalf("expected 42, got %d", val) + } +} + +func TestIdentifier_Uint32_StringIdReturnsError(t *testing.T) { + id, err := NewIdentifier("hello") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = id.Uint32() + if err == nil { + t.Fatalf("expected error when calling Uint32 on string identifier, got nil") + } +} + +func TestIdentifier_String(t *testing.T) { + id, err := NewIdentifier("hello") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + val, err := id.String() + if err != nil { + t.Fatalf("unexpected error getting string: %v", err) + } + if val != "hello" { + t.Fatalf("expected 'hello', got '%s'", val) + } +} + +func TestIdentifier_String_NumericIdReturnsError(t *testing.T) { + id, err := NewIdentifier(uint32(1)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + _, err = id.String() + if err == nil { + t.Fatalf("expected error when calling String on numeric identifier, got nil") + } +} + +func TestAppendBinary(t *testing.T) { + id, err := NewIdentifier(uint32(10)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + prefix := []byte{0xAA, 0xBB} + result, err := id.AppendBinary(prefix) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result[0] != 0xAA || result[1] != 0xBB { + t.Fatalf("expected prefix bytes preserved, got %v", result[:2]) + } + marshaledOnly, _ := id.MarshalBinary() + if !bytes.Equal(result[2:], marshaledOnly) { + t.Fatalf("expected appended bytes %v, got %v", marshaledOnly, result[2:]) + } +} + +func TestMarshalIdentifiers(t *testing.T) { + id1, err := NewIdentifier(uint32(1)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + id2, err := NewIdentifier("ab") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + combined, err := MarshalIdentifiers(id1, id2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + b1, _ := id1.MarshalBinary() + b2, _ := id2.MarshalBinary() + expected := append(b1, b2...) + if !bytes.Equal(combined, expected) { + t.Fatalf("expected %v, got %v", expected, combined) + } +} diff --git a/foreign/go/contracts/message_header_test.go b/foreign/go/contracts/message_header_test.go new file mode 100644 index 0000000000..ae51668e31 --- /dev/null +++ b/foreign/go/contracts/message_header_test.go @@ -0,0 +1,100 @@ +// 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 iggcon + +import ( + "bytes" + "testing" +) + +func TestMessageHeader_ToBytesFromBytes_RoundTrip(t *testing.T) { + var id MessageID + copy(id[:], []byte("1234567890abcdef")) + + header := MessageHeader{ + Checksum: 55, + Id: id, + Offset: 123, + Timestamp: 456, + OriginTimestamp: 789, + UserHeaderLength: 4, + PayloadLength: 6, + Reserved: 999, + } + + encoded := header.ToBytes() + if len(encoded) != MessageHeaderSize { + t.Fatalf("invalid encoded length, expected %d got %d", MessageHeaderSize, len(encoded)) + } + + decoded, err := MessageHeaderFromBytes(encoded) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if decoded.Checksum != header.Checksum { + t.Fatalf("checksum mismatch, expected %d got %d", header.Checksum, decoded.Checksum) + } + if !bytes.Equal(decoded.Id[:], header.Id[:]) { + t.Fatalf("id mismatch, expected %v got %v", header.Id, decoded.Id) + } + if decoded.Offset != header.Offset { + t.Fatalf("offset mismatch, expected %d got %d", header.Offset, decoded.Offset) + } + if decoded.Timestamp != header.Timestamp { + t.Fatalf("timestamp mismatch, expected %d got %d", header.Timestamp, decoded.Timestamp) + } + if decoded.OriginTimestamp != header.OriginTimestamp { + t.Fatalf("origin timestamp mismatch, expected %d got %d", header.OriginTimestamp, decoded.OriginTimestamp) + } + if decoded.UserHeaderLength != header.UserHeaderLength { + t.Fatalf("user header length mismatch, expected %d got %d", header.UserHeaderLength, decoded.UserHeaderLength) + } + if decoded.PayloadLength != header.PayloadLength { + t.Fatalf("payload length mismatch, expected %d got %d", header.PayloadLength, decoded.PayloadLength) + } + if decoded.Reserved != header.Reserved { + t.Fatalf("reserved mismatch, expected %d got %d", header.Reserved, decoded.Reserved) + } +} + +func TestMessageHeaderFromBytes_InvalidLength(t *testing.T) { + _, err := MessageHeaderFromBytes(make([]byte, MessageHeaderSize-1)) + if err == nil { + t.Fatal("expected error for invalid message header size") + } +} + +func TestNewMessageHeader_SetsExpectedFields(t *testing.T) { + var id MessageID + copy(id[:], []byte("abcdefghijklmnop")) + + header := NewMessageHeader(id, 8, 3) + if !bytes.Equal(header.Id[:], id[:]) { + t.Fatalf("id mismatch, expected %v got %v", id, header.Id) + } + if header.PayloadLength != 8 { + t.Fatalf("payload length mismatch, expected 8 got %d", header.PayloadLength) + } + if header.UserHeaderLength != 3 { + t.Fatalf("user header length mismatch, expected 3 got %d", header.UserHeaderLength) + } + if header.OriginTimestamp == 0 { + t.Fatal("expected origin timestamp to be populated") + } +} diff --git a/foreign/go/contracts/message_polling_test.go b/foreign/go/contracts/message_polling_test.go new file mode 100644 index 0000000000..622e497826 --- /dev/null +++ b/foreign/go/contracts/message_polling_test.go @@ -0,0 +1,80 @@ +// 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 iggcon + +import "testing" + +func TestOffsetPollingStrategy(t *testing.T) { + s := OffsetPollingStrategy(100) + if s.Kind != POLLING_OFFSET { + t.Fatalf("expected kind POLLING_OFFSET (%d), got %d", POLLING_OFFSET, s.Kind) + } + if s.Value != 100 { + t.Fatalf("expected value 100, got %d", s.Value) + } +} + +func TestTimestampPollingStrategy(t *testing.T) { + s := TimestampPollingStrategy(200) + if s.Kind != POLLING_TIMESTAMP { + t.Fatalf("expected kind POLLING_TIMESTAMP (%d), got %d", POLLING_TIMESTAMP, s.Kind) + } + if s.Value != 200 { + t.Fatalf("expected value 200, got %d", s.Value) + } +} + +func TestFirstPollingStrategy(t *testing.T) { + s := FirstPollingStrategy() + if s.Kind != POLLING_FIRST { + t.Fatalf("expected kind POLLING_FIRST (%d), got %d", POLLING_FIRST, s.Kind) + } + if s.Value != 0 { + t.Fatalf("expected value 0, got %d", s.Value) + } +} + +func TestLastPollingStrategy(t *testing.T) { + s := LastPollingStrategy() + if s.Kind != POLLING_LAST { + t.Fatalf("expected kind POLLING_LAST (%d), got %d", POLLING_LAST, s.Kind) + } + if s.Value != 0 { + t.Fatalf("expected value 0, got %d", s.Value) + } +} + +func TestNextPollingStrategy(t *testing.T) { + s := NextPollingStrategy() + if s.Kind != POLLING_NEXT { + t.Fatalf("expected kind POLLING_NEXT (%d), got %d", POLLING_NEXT, s.Kind) + } + if s.Value != 0 { + t.Fatalf("expected value 0, got %d", s.Value) + } +} + +func TestNewPollingStrategy_Custom(t *testing.T) { + s := NewPollingStrategy(POLLING_OFFSET, 999) + if s.Kind != POLLING_OFFSET { + t.Fatalf("expected kind POLLING_OFFSET (%d), got %d", POLLING_OFFSET, s.Kind) + } + if s.Value != 999 { + t.Fatalf("expected value 999, got %d", s.Value) + } +} diff --git a/foreign/go/contracts/messages_test.go b/foreign/go/contracts/messages_test.go new file mode 100644 index 0000000000..5acc61efff --- /dev/null +++ b/foreign/go/contracts/messages_test.go @@ -0,0 +1,79 @@ +// 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 iggcon + +import ( + "bytes" + "errors" + "testing" + + ierror "github.com/apache/iggy/foreign/go/errors" +) + +func TestNewIggyMessage_Success(t *testing.T) { + payload := []byte("hello") + msg, err := NewIggyMessage(payload) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(msg.Payload, payload) { + t.Fatalf("expected payload %v, got %v", payload, msg.Payload) + } + if msg.Header.PayloadLength != uint32(len(payload)) { + t.Fatalf("expected payload length %d, got %d", len(payload), msg.Header.PayloadLength) + } + if msg.Header.UserHeaderLength != 0 { + t.Fatalf("expected user header length 0, got %d", msg.Header.UserHeaderLength) + } +} + +func TestNewIggyMessage_NilPayload(t *testing.T) { + _, err := NewIggyMessage(nil) + if !errors.Is(err, ierror.ErrInvalidMessagePayloadLength) { + t.Fatalf("expected ErrInvalidMessagePayloadLength, got %v", err) + } +} + +func TestNewIggyMessage_EmptyPayload(t *testing.T) { + _, err := NewIggyMessage([]byte{}) + if !errors.Is(err, ierror.ErrInvalidMessagePayloadLength) { + t.Fatalf("expected ErrInvalidMessagePayloadLength, got %v", err) + } +} + +func TestNewIggyMessage_TooBigPayload(t *testing.T) { + payload := make([]byte, MaxPayloadSize+1) + _, err := NewIggyMessage(payload) + if !errors.Is(err, ierror.ErrTooBigMessagePayload) { + t.Fatalf("expected ErrTooBigMessagePayload, got %v", err) + } +} + +func TestWithID_SetsMessageID(t *testing.T) { + var id [16]byte + copy(id[:], []byte("0123456789abcdef")) + + payload := []byte("hello") + msg, err := NewIggyMessage(payload, WithID(id)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(msg.Header.Id[:], id[:]) { + t.Fatalf("expected id %v, got %v", id, msg.Header.Id) + } +} diff --git a/foreign/go/contracts/meta_data_test.go b/foreign/go/contracts/meta_data_test.go new file mode 100644 index 0000000000..42ab4cf1cd --- /dev/null +++ b/foreign/go/contracts/meta_data_test.go @@ -0,0 +1,104 @@ +// 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 iggcon + +import "testing" + +func TestClusterMetadata_MarshalUnmarshal_Roundtrip(t *testing.T) { + original := ClusterMetadata{ + Name: "test-cluster", + Nodes: []ClusterNode{ + { + Name: "node-1", + IP: "192.168.1.10", + Endpoints: NewTransportEndpoints(8090, 8091, 3000, 8080), + Role: RoleLeader, + Status: Healthy, + }, + { + Name: "node-2", + IP: "192.168.1.11", + Endpoints: NewTransportEndpoints(9090, 9091, 4000, 9080), + Role: RoleFollower, + Status: Starting, + }, + }, + } + + data, err := original.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + var decoded ClusterMetadata + if err := decoded.UnmarshalBinary(data); err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } + + if decoded.Name != original.Name { + t.Fatalf("Name mismatch: expected %s, got %s", original.Name, decoded.Name) + } + if len(decoded.Nodes) != len(original.Nodes) { + t.Fatalf("Nodes count mismatch: expected %d, got %d", len(original.Nodes), len(decoded.Nodes)) + } + for i := range original.Nodes { + if decoded.Nodes[i].Name != original.Nodes[i].Name { + t.Fatalf("Node[%d].Name mismatch: expected %s, got %s", i, original.Nodes[i].Name, decoded.Nodes[i].Name) + } + if decoded.Nodes[i].IP != original.Nodes[i].IP { + t.Fatalf("Node[%d].IP mismatch: expected %s, got %s", i, original.Nodes[i].IP, decoded.Nodes[i].IP) + } + if decoded.Nodes[i].Role != original.Nodes[i].Role { + t.Fatalf("Node[%d].Role mismatch: expected %d, got %d", i, original.Nodes[i].Role, decoded.Nodes[i].Role) + } + if decoded.Nodes[i].Status != original.Nodes[i].Status { + t.Fatalf("Node[%d].Status mismatch: expected %d, got %d", i, original.Nodes[i].Status, decoded.Nodes[i].Status) + } + } +} + +func TestClusterMetadata_BufferSize_MatchesMarshaledLength(t *testing.T) { + m := ClusterMetadata{ + Name: "cluster", + Nodes: []ClusterNode{ + { + Name: "n1", + IP: "10.0.0.1", + Endpoints: NewTransportEndpoints(1, 2, 3, 4), + Role: RoleLeader, + Status: Healthy, + }, + }, + } + + data, err := m.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + if len(data) != m.BufferSize() { + t.Fatalf("expected marshaled length %d to match BufferSize %d", len(data), m.BufferSize()) + } +} + +func TestClusterMetadata_UnmarshalBinary_EmptyData(t *testing.T) { + var m ClusterMetadata + if err := m.UnmarshalBinary([]byte{}); err == nil { + t.Fatalf("expected error for empty data, got nil") + } +} diff --git a/foreign/go/contracts/node_status_test.go b/foreign/go/contracts/node_status_test.go new file mode 100644 index 0000000000..ad4fb94130 --- /dev/null +++ b/foreign/go/contracts/node_status_test.go @@ -0,0 +1,84 @@ +// 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 iggcon + +import "testing" + +func TestTryFrom_ValidStatuses(t *testing.T) { + tests := []struct { + input byte + expected ClusterNodeStatus + }{ + {0, Healthy}, + {1, Starting}, + {2, Stopping}, + {3, Unreachable}, + {4, Maintenance}, + } + for _, tc := range tests { + s, err := TryFrom(tc.input) + if err != nil { + t.Fatalf("unexpected error for byte %d: %v", tc.input, err) + } + if s != tc.expected { + t.Fatalf("expected status %d for byte %d, got %d", tc.expected, tc.input, s) + } + } +} + +func TestTryFrom_Invalid(t *testing.T) { + _, err := TryFrom(99) + if err == nil { + t.Fatalf("expected error for invalid status byte 99, got nil") + } +} + +func TestClusterNodeStatus_MarshalUnmarshal_Roundtrip(t *testing.T) { + for _, status := range []ClusterNodeStatus{Healthy, Starting, Stopping, Unreachable, Maintenance} { + data, err := status.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error for status %d: %v", status, err) + } + var decoded ClusterNodeStatus + if err := decoded.UnmarshalBinary(data); err != nil { + t.Fatalf("unexpected unmarshal error for status %d: %v", status, err) + } + if decoded != status { + t.Fatalf("expected status %d, got %d", status, decoded) + } + } +} + +func TestClusterNodeStatus_String(t *testing.T) { + tests := []struct { + status ClusterNodeStatus + expected string + }{ + {Healthy, "healthy"}, + {Starting, "starting"}, + {Stopping, "stopping"}, + {Unreachable, "unreachable"}, + {Maintenance, "maintenance"}, + } + for _, tc := range tests { + got := tc.status.String() + if got != tc.expected { + t.Fatalf("expected '%s' for status %d, got '%s'", tc.expected, tc.status, got) + } + } +} diff --git a/foreign/go/contracts/partitions_test.go b/foreign/go/contracts/partitions_test.go new file mode 100644 index 0000000000..76e8b756cb --- /dev/null +++ b/foreign/go/contracts/partitions_test.go @@ -0,0 +1,185 @@ +// 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 iggcon + +import ( + "bytes" + "encoding/binary" + "testing" + + "github.com/google/uuid" +) + +func TestNone_ReturnsBalancedWithEmptyValue(t *testing.T) { + p := None() + if p.Kind != Balanced { + t.Fatalf("expected kind Balanced (%d), got %d", Balanced, p.Kind) + } + if p.Length != 0 { + t.Fatalf("expected length 0, got %d", p.Length) + } + if len(p.Value) != 0 { + t.Fatalf("expected empty value, got %v", p.Value) + } +} + +func TestPartitionId_ReturnsPartitionIdKindWithLEValue(t *testing.T) { + p := PartitionId(42) + if p.Kind != PartitionIdKind { + t.Fatalf("expected kind PartitionIdKind (%d), got %d", PartitionIdKind, p.Kind) + } + if p.Length != 4 { + t.Fatalf("expected length 4, got %d", p.Length) + } + expected := make([]byte, 4) + binary.LittleEndian.PutUint32(expected, 42) + if !bytes.Equal(p.Value, expected) { + t.Fatalf("expected value %v, got %v", expected, p.Value) + } +} + +func TestEntityIdString_ReturnsMessageKeyWithStringBytes(t *testing.T) { + p, err := EntityIdString("test") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Kind != MessageKey { + t.Fatalf("expected kind MessageKey (%d), got %d", MessageKey, p.Kind) + } + if p.Length != 4 { + t.Fatalf("expected length 4, got %d", p.Length) + } + if !bytes.Equal(p.Value, []byte("test")) { + t.Fatalf("expected value %v, got %v", []byte("test"), p.Value) + } +} + +func TestEntityIdString_EmptyReturnsError(t *testing.T) { + _, err := EntityIdString("") + if err == nil { + t.Fatalf("expected error for empty string, got nil") + } +} + +func TestEntityIdBytes_ReturnsMessageKey(t *testing.T) { + input := []byte{1, 2} + p, err := EntityIdBytes(input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if p.Kind != MessageKey { + t.Fatalf("expected kind MessageKey (%d), got %d", MessageKey, p.Kind) + } + if p.Length != 2 { + t.Fatalf("expected length 2, got %d", p.Length) + } + if !bytes.Equal(p.Value, input) { + t.Fatalf("expected value %v, got %v", input, p.Value) + } +} + +func TestEntityIdBytes_NilReturnsError(t *testing.T) { + _, err := EntityIdBytes(nil) + if err == nil { + t.Fatalf("expected error for nil bytes, got nil") + } +} + +func TestEntityIdBytes_EmptyReturnsError(t *testing.T) { + _, err := EntityIdBytes([]byte{}) + if err == nil { + t.Fatalf("expected error for empty bytes, got nil") + } +} + +func TestEntityIdInt_ReturnsMessageKeyWithLEUint32(t *testing.T) { + p := EntityIdInt(7) + if p.Kind != MessageKey { + t.Fatalf("expected kind MessageKey (%d), got %d", MessageKey, p.Kind) + } + if p.Length != 4 { + t.Fatalf("expected length 4, got %d", p.Length) + } + expected := make([]byte, 4) + binary.LittleEndian.PutUint32(expected, 7) + if !bytes.Equal(p.Value, expected) { + t.Fatalf("expected value %v, got %v", expected, p.Value) + } +} + +func TestEntityIdUlong_ReturnsMessageKeyWithLEUint64(t *testing.T) { + p := EntityIdUlong(99) + if p.Kind != MessageKey { + t.Fatalf("expected kind MessageKey (%d), got %d", MessageKey, p.Kind) + } + if p.Length != 8 { + t.Fatalf("expected length 8, got %d", p.Length) + } + expected := make([]byte, 8) + binary.LittleEndian.PutUint64(expected, 99) + if !bytes.Equal(p.Value, expected) { + t.Fatalf("expected value %v, got %v", expected, p.Value) + } +} + +func TestEntityIdGuid_ReturnsMessageKeyWith16Bytes(t *testing.T) { + fixedUUID := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") + p := EntityIdGuid(fixedUUID) + if p.Kind != MessageKey { + t.Fatalf("expected kind MessageKey (%d), got %d", MessageKey, p.Kind) + } + if p.Length != 16 { + t.Fatalf("expected length 16, got %d", p.Length) + } + if !bytes.Equal(p.Value, fixedUUID[:]) { + t.Fatalf("expected value %v, got %v", fixedUUID[:], p.Value) + } +} + +func TestPartitioning_MarshalBinary_Balanced(t *testing.T) { + p := None() + data, err := p.MarshalBinary() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + expected := []byte{byte(Balanced), 0x00} + if !bytes.Equal(data, expected) { + t.Fatalf("expected %v, got %v", expected, data) + } +} + +func TestPartitioning_MarshalBinary_PartitionId(t *testing.T) { + p := PartitionId(42) + data, err := p.MarshalBinary() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data) != 6 { + t.Fatalf("expected length 6, got %d", len(data)) + } + if data[0] != byte(PartitionIdKind) { + t.Fatalf("expected kind byte %d, got %d", PartitionIdKind, data[0]) + } + if data[1] != 4 { + t.Fatalf("expected length byte 4, got %d", data[1]) + } + val := binary.LittleEndian.Uint32(data[2:6]) + if val != 42 { + t.Fatalf("expected value 42, got %d", val) + } +} diff --git a/foreign/go/contracts/role_test.go b/foreign/go/contracts/role_test.go new file mode 100644 index 0000000000..aea213ed25 --- /dev/null +++ b/foreign/go/contracts/role_test.go @@ -0,0 +1,77 @@ +// 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 iggcon + +import "testing" + +func TestClusterNodeRoleTryFrom_Leader(t *testing.T) { + r, err := ClusterNodeRoleTryFrom(0) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r != RoleLeader { + t.Fatalf("expected RoleLeader, got %d", r) + } +} + +func TestClusterNodeRoleTryFrom_Follower(t *testing.T) { + r, err := ClusterNodeRoleTryFrom(1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if r != RoleFollower { + t.Fatalf("expected RoleFollower, got %d", r) + } +} + +func TestClusterNodeRoleTryFrom_Invalid(t *testing.T) { + _, err := ClusterNodeRoleTryFrom(99) + if err == nil { + t.Fatalf("expected error for invalid role byte 99, got nil") + } +} + +func TestClusterNodeRole_MarshalUnmarshal_Roundtrip(t *testing.T) { + for _, role := range []ClusterNodeRole{RoleLeader, RoleFollower} { + data, err := role.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error for role %d: %v", role, err) + } + var decoded ClusterNodeRole + if err := decoded.UnmarshalBinary(data); err != nil { + t.Fatalf("unexpected unmarshal error for role %d: %v", role, err) + } + if decoded != role { + t.Fatalf("expected role %d, got %d", role, decoded) + } + } +} + +func TestClusterNodeRole_String_Leader(t *testing.T) { + r := RoleLeader + if r.String() != "leader" { + t.Fatalf("expected 'leader', got '%s'", r.String()) + } +} + +func TestClusterNodeRole_String_Follower(t *testing.T) { + r := RoleFollower + if r.String() != "follower" { + t.Fatalf("expected 'follower', got '%s'", r.String()) + } +} diff --git a/foreign/go/contracts/transport_endpoints_test.go b/foreign/go/contracts/transport_endpoints_test.go new file mode 100644 index 0000000000..5a311fdf01 --- /dev/null +++ b/foreign/go/contracts/transport_endpoints_test.go @@ -0,0 +1,88 @@ +// 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 iggcon + +import ( + "strings" + "testing" +) + +func TestNewTransportEndpoints_SetsAllFields(t *testing.T) { + ep := NewTransportEndpoints(8090, 8091, 3000, 8080) + if ep.Tcp != 8090 { + t.Fatalf("expected Tcp 8090, got %d", ep.Tcp) + } + if ep.Quic != 8091 { + t.Fatalf("expected Quic 8091, got %d", ep.Quic) + } + if ep.Http != 3000 { + t.Fatalf("expected Http 3000, got %d", ep.Http) + } + if ep.WebSocket != 8080 { + t.Fatalf("expected WebSocket 8080, got %d", ep.WebSocket) + } +} + +func TestTransportEndpoints_MarshalUnmarshal_Roundtrip(t *testing.T) { + original := NewTransportEndpoints(8090, 8091, 3000, 8080) + data, err := original.MarshalBinary() + if err != nil { + t.Fatalf("unexpected marshal error: %v", err) + } + + var decoded TransportEndpoints + if err := decoded.UnmarshalBinary(data); err != nil { + t.Fatalf("unexpected unmarshal error: %v", err) + } + if decoded.Tcp != original.Tcp { + t.Fatalf("Tcp mismatch: expected %d, got %d", original.Tcp, decoded.Tcp) + } + if decoded.Quic != original.Quic { + t.Fatalf("Quic mismatch: expected %d, got %d", original.Quic, decoded.Quic) + } + if decoded.Http != original.Http { + t.Fatalf("Http mismatch: expected %d, got %d", original.Http, decoded.Http) + } + if decoded.WebSocket != original.WebSocket { + t.Fatalf("WebSocket mismatch: expected %d, got %d", original.WebSocket, decoded.WebSocket) + } +} + +func TestTransportEndpoints_GetBufferSize(t *testing.T) { + ep := NewTransportEndpoints(1, 2, 3, 4) + if ep.GetBufferSize() != 8 { + t.Fatalf("expected buffer size 8, got %d", ep.GetBufferSize()) + } +} + +func TestTransportEndpoints_String(t *testing.T) { + ep := NewTransportEndpoints(8090, 8091, 3000, 8080) + s := ep.String() + if !strings.Contains(s, "8090") { + t.Fatalf("expected string to contain '8090', got %s", s) + } + if !strings.Contains(s, "8091") { + t.Fatalf("expected string to contain '8091', got %s", s) + } + if !strings.Contains(s, "3000") { + t.Fatalf("expected string to contain '3000', got %s", s) + } + if !strings.Contains(s, "8080") { + t.Fatalf("expected string to contain '8080', got %s", s) + } +} diff --git a/foreign/go/contracts/user_headers_test.go b/foreign/go/contracts/user_headers_test.go new file mode 100644 index 0000000000..17a5da03aa --- /dev/null +++ b/foreign/go/contracts/user_headers_test.go @@ -0,0 +1,129 @@ +// 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 iggcon + +import ( + "bytes" + "testing" +) + +func TestNewHeaderKeyString_Success(t *testing.T) { + k, err := NewHeaderKeyString("key") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if k.Kind != String { + t.Fatalf("expected kind String (%d), got %d", String, k.Kind) + } + if !bytes.Equal(k.Value, []byte("key")) { + t.Fatalf("expected value %v, got %v", []byte("key"), k.Value) + } +} + +func TestNewHeaderKeyString_EmptyReturnsError(t *testing.T) { + _, err := NewHeaderKeyString("") + if err == nil { + t.Fatalf("expected error for empty string, got nil") + } +} + +func TestNewHeaderKeyRaw_Success(t *testing.T) { + input := []byte{1, 2, 3} + k, err := NewHeaderKeyRaw(input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if k.Kind != Raw { + t.Fatalf("expected kind Raw (%d), got %d", Raw, k.Kind) + } + if !bytes.Equal(k.Value, input) { + t.Fatalf("expected value %v, got %v", input, k.Value) + } +} + +func TestNewHeaderKeyRaw_NilReturnsError(t *testing.T) { + _, err := NewHeaderKeyRaw(nil) + if err == nil { + t.Fatalf("expected error for nil bytes, got nil") + } +} + +func TestNewHeaderKeyInt32_ReturnsInt32KindWith4Bytes(t *testing.T) { + k := NewHeaderKeyInt32(42) + if k.Kind != Int32 { + t.Fatalf("expected kind Int32 (%d), got %d", Int32, k.Kind) + } + if len(k.Value) != 4 { + t.Fatalf("expected 4 bytes, got %d", len(k.Value)) + } +} + +func TestHeaderKind_ExpectedSize(t *testing.T) { + tests := []struct { + kind HeaderKind + expected int + }{ + {Bool, 1}, + {Int16, 2}, + {Int32, 4}, + {Int64, 8}, + {Int128, 16}, + {Raw, -1}, + {String, -1}, + } + for _, tc := range tests { + got := tc.kind.ExpectedSize() + if got != tc.expected { + t.Fatalf("HeaderKind(%d).ExpectedSize() = %d, want %d", tc.kind, got, tc.expected) + } + } +} + +func TestGetHeadersBytes_DeserializeHeaders_Roundtrip(t *testing.T) { + key, err := NewHeaderKeyString("mykey") + if err != nil { + t.Fatalf("unexpected error creating header key: %v", err) + } + value := HeaderValue{Kind: String, Value: []byte("myval")} + entry := HeaderEntry{Key: key, Value: value} + + serialized := GetHeadersBytes([]HeaderEntry{entry}) + if len(serialized) == 0 { + t.Fatalf("expected non-empty serialized bytes") + } + + deserialized, err := DeserializeHeaders(serialized) + if err != nil { + t.Fatalf("unexpected error deserializing: %v", err) + } + if len(deserialized) != 1 { + t.Fatalf("expected 1 header entry, got %d", len(deserialized)) + } + if deserialized[0].Key.Kind != String { + t.Fatalf("expected key kind String, got %d", deserialized[0].Key.Kind) + } + if !bytes.Equal(deserialized[0].Key.Value, []byte("mykey")) { + t.Fatalf("expected key value 'mykey', got %v", deserialized[0].Key.Value) + } + if deserialized[0].Value.Kind != String { + t.Fatalf("expected value kind String, got %d", deserialized[0].Value.Kind) + } + if !bytes.Equal(deserialized[0].Value.Value, []byte("myval")) { + t.Fatalf("expected value 'myval', got %v", deserialized[0].Value.Value) + } +} diff --git a/foreign/go/contracts/users_test.go b/foreign/go/contracts/users_test.go new file mode 100644 index 0000000000..0dfa5051b0 --- /dev/null +++ b/foreign/go/contracts/users_test.go @@ -0,0 +1,73 @@ +// 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 iggcon + +import "testing" + +func TestPermissions_Size_NilStreams(t *testing.T) { + p := &Permissions{ + Global: GlobalPermissions{}, + Streams: nil, + } + size := p.Size() + if size != 11 { + t.Fatalf("expected size 11 for nil streams, got %d", size) + } +} + +func TestPermissions_Size_WithStreamsAndTopics(t *testing.T) { + p := &Permissions{ + Global: GlobalPermissions{}, + Streams: map[int]*StreamPermissions{ + 1: { + Topics: map[int]*TopicPermissions{ + 10: {}, + }, + }, + }, + } + // size = 10 (global) + 1 (streams flag) + 4 (stream id) + 6 (stream perms) + 1 (topics flag) + 1 (has topics) + 9 (1 topic) + expected := 10 + 1 + 4 + 6 + 1 + 1 + 9 + size := p.Size() + if size != expected { + t.Fatalf("expected size %d, got %d", expected, size) + } +} + +func TestPermissions_MarshalBinary_NilStreams(t *testing.T) { + p := &Permissions{ + Global: GlobalPermissions{ + ReadServers: true, + SendMessages: true, + }, + Streams: nil, + } + data, err := p.MarshalBinary() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(data) != p.Size() { + t.Fatalf("expected length %d, got %d", p.Size(), len(data)) + } + if data[1] != 1 { + t.Fatalf("expected ReadServers byte to be 1, got %d", data[1]) + } + if data[9] != 1 { + t.Fatalf("expected SendMessages byte to be 1, got %d", data[9]) + } +} 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..944f862539 --- /dev/null +++ b/foreign/go/internal/command/access_token_test.go @@ -0,0 +1,87 @@ +// 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" + "encoding/binary" + "testing" +) + +func TestCreatePersonalAccessToken_MarshalBinary(t *testing.T) { + request := CreatePersonalAccessToken{ + Name: "mytoken", + Expiry: 3600, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedLen := 1 + len("mytoken") + 8 + if len(serialized) != expectedLen { + t.Fatalf("expected length %d, got %d", expectedLen, len(serialized)) + } + + if serialized[0] != byte(len("mytoken")) { + t.Fatalf("expected name length byte %d, got %d", len("mytoken"), serialized[0]) + } + + if !bytes.Equal(serialized[1:1+len("mytoken")], []byte("mytoken")) { + t.Fatalf("expected name bytes %v, got %v", []byte("mytoken"), serialized[1:1+len("mytoken")]) + } + + expiry := binary.LittleEndian.Uint32(serialized[len(serialized)-4:]) + if expiry != 3600 { + t.Fatalf("expected expiry 3600, got %d", expiry) + } +} + +func TestGetPersonalAccessTokens_MarshalBinary(t *testing.T) { + request := GetPersonalAccessTokens{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestDeletePersonalAccessToken_MarshalBinary(t *testing.T) { + request := DeletePersonalAccessToken{ + Name: "mytoken", + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x07, // name length = 7 + 0x6D, 0x79, 0x74, 0x6F, 0x6B, 0x65, 0x6E, // "mytoken" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\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..468b16a8c1 --- /dev/null +++ b/foreign/go/internal/command/consumer_group_test.go @@ -0,0 +1,175 @@ +// 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" +) + +func TestCreateConsumerGroup_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + + request := CreateConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + Name: "test-group", + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // stream id = "stream" + 0x01, 0x04, 0x05, 0x00, 0x00, 0x00, // topic id = 5 + 0x0A, // name length = 10 + 0x74, 0x65, 0x73, 0x74, 0x2D, 0x67, 0x72, 0x6F, 0x75, 0x70, // "test-group" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetConsumerGroup_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + groupId, _ := iggcon.NewIdentifier("grp") + + request := GetConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId, groupId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetConsumerGroups_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + + request := GetConsumerGroups{ + StreamId: streamId, + TopicId: topicId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestJoinConsumerGroup_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + groupId, _ := iggcon.NewIdentifier("grp") + + request := JoinConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId, groupId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestLeaveConsumerGroup_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + groupId, _ := iggcon.NewIdentifier("grp") + + request := LeaveConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId, groupId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestDeleteConsumerGroup_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + groupId, _ := iggcon.NewIdentifier("grp") + + request := DeleteConsumerGroup{ + TopicPath: TopicPath{ + StreamId: streamId, + TopicId: topicId, + }, + GroupId: groupId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId, groupId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/offset_test.go b/foreign/go/internal/command/offset_test.go new file mode 100644 index 0000000000..da6a01d499 --- /dev/null +++ b/foreign/go/internal/command/offset_test.go @@ -0,0 +1,121 @@ +// 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" +) + +func TestStoreConsumerOffsetRequest_MarshalBinary_WithoutPartition(t *testing.T) { + streamID, _ := iggcon.NewIdentifier("stream") + topicID, _ := iggcon.NewIdentifier(uint32(7)) + groupID, _ := iggcon.NewIdentifier("groupA") + + request := StoreConsumerOffsetRequest{ + StreamId: streamID, + TopicId: topicID, + Consumer: iggcon.NewGroupConsumer(groupID), + Offset: 42, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, // Consumer kind (group) + 0x02, 0x06, 0x67, 0x72, 0x6F, 0x75, 0x70, 0x41, // Consumer id = "groupA" + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // Stream id = "stream" + 0x01, 0x04, 0x07, 0x00, 0x00, 0x00, // Topic id = 7 + 0x00, // hasPartition + 0x00, 0x00, 0x00, 0x00, // partition (0) + 0x2A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // offset (42) + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetConsumerOffset_MarshalBinary_WithPartition(t *testing.T) { + streamID, _ := iggcon.NewIdentifier("stream") + topicID, _ := iggcon.NewIdentifier(uint32(7)) + consumerID, _ := iggcon.NewIdentifier(uint32(1)) + partitionID := uint32(3) + + request := GetConsumerOffset{ + StreamId: streamID, + TopicId: topicID, + Consumer: iggcon.NewSingleConsumer(consumerID), + PartitionId: &partitionID, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, // Consumer kind (single) + 0x01, 0x04, 0x01, 0x00, 0x00, 0x00, // Consumer id = 1 + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // Stream id = "stream" + 0x01, 0x04, 0x07, 0x00, 0x00, 0x00, // Topic id = 7 + 0x01, // hasPartition + 0x03, 0x00, 0x00, 0x00, // partition (3) + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestDeleteConsumerOffset_MarshalBinary_WithPartition(t *testing.T) { + streamID, _ := iggcon.NewIdentifier("stream") + topicID, _ := iggcon.NewIdentifier(uint32(7)) + consumerID, _ := iggcon.NewIdentifier(uint32(1)) + partitionID := uint32(5) + + request := DeleteConsumerOffset{ + Consumer: iggcon.NewSingleConsumer(consumerID), + StreamId: streamID, + TopicId: topicID, + PartitionId: &partitionID, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, // Consumer kind (single) + 0x01, 0x04, 0x01, 0x00, 0x00, 0x00, // Consumer id = 1 + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // Stream id = "stream" + 0x01, 0x04, 0x07, 0x00, 0x00, 0x00, // Topic id = 7 + 0x01, // hasPartition + 0x05, 0x00, 0x00, 0x00, // partition (5) + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\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..a4db3b23d8 --- /dev/null +++ b/foreign/go/internal/command/partition_test.go @@ -0,0 +1,77 @@ +// 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" +) + +func TestCreatePartitions_MarshalBinary(t *testing.T) { + streamID, _ := iggcon.NewIdentifier("stream") + topicID, _ := iggcon.NewIdentifier(uint32(7)) + + request := CreatePartitions{ + StreamId: streamID, + TopicId: topicID, + PartitionsCount: 2, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // Stream id = "stream" + 0x01, 0x04, 0x07, 0x00, 0x00, 0x00, // Topic id = 7 + 0x02, 0x00, 0x00, 0x00, // PartitionsCount = 2 + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestDeletePartitions_MarshalBinary(t *testing.T) { + streamID, _ := iggcon.NewIdentifier("stream") + topicID, _ := iggcon.NewIdentifier(uint32(7)) + + request := DeletePartitions{ + StreamId: streamID, + TopicId: topicID, + PartitionsCount: 1, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // Stream id = "stream" + 0x01, 0x04, 0x07, 0x00, 0x00, 0x00, // Topic id = 7 + 0x01, 0x00, 0x00, 0x00, // PartitionsCount = 1 + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\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..98136b5ce3 100644 --- a/foreign/go/internal/command/session_test.go +++ b/foreign/go/internal/command/session_test.go @@ -18,6 +18,7 @@ package command import ( + "bytes" "encoding/binary" "testing" @@ -49,3 +50,36 @@ func TestSerialize_LoginUser_ContainsVersion(t *testing.T) { t.Errorf("Version mismatch. Expected: %q, Got: %q", iggcon.Version, version) } } + +func TestLoginWithPersonalAccessToken_MarshalBinary(t *testing.T) { + request := LoginWithPersonalAccessToken{ + Token: "mytoken", + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x07, // token length = 7 + 0x6D, 0x79, 0x74, 0x6F, 0x6B, 0x65, 0x6E, // "mytoken" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestLogoutUser_MarshalBinary(t *testing.T) { + request := LogoutUser{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} diff --git a/foreign/go/internal/command/stream_test.go b/foreign/go/internal/command/stream_test.go index b3859fef36..7c2d5435e8 100644 --- a/foreign/go/internal/command/stream_test.go +++ b/foreign/go/internal/command/stream_test.go @@ -80,3 +80,54 @@ func TestSerialize_UpdateStream(t *testing.T) { t.Errorf("Test case 1 failed. \nExpected:\t%v\nGot:\t\t%v", expected, serialized1) } } + +func TestGetStream_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + + request := GetStream{ + StreamId: streamId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := streamId.MarshalBinary() + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetStreams_MarshalBinary(t *testing.T) { + request := GetStreams{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestDeleteStream_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + + request := DeleteStream{ + StreamId: streamId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := streamId.MarshalBinary() + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\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..fb1fe3b31d --- /dev/null +++ b/foreign/go/internal/command/system_test.go @@ -0,0 +1,94 @@ +// 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" + "encoding/binary" + "testing" +) + +func TestGetClient_MarshalBinary(t *testing.T) { + request := GetClient{ + ClientID: 42, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := make([]byte, 4) + binary.LittleEndian.PutUint32(expected, 42) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetClients_MarshalBinary(t *testing.T) { + request := GetClients{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestGetClusterMetadata_MarshalBinary(t *testing.T) { + request := GetClusterMetadata{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestGetStats_MarshalBinary(t *testing.T) { + request := GetStats{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestPing_MarshalBinary(t *testing.T) { + request := Ping{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} diff --git a/foreign/go/internal/command/topic_test.go b/foreign/go/internal/command/topic_test.go index 12357ed4d6..0e7ab0de35 100644 --- a/foreign/go/internal/command/topic_test.go +++ b/foreign/go/internal/command/topic_test.go @@ -59,3 +59,97 @@ func TestSerialize_UpdateTopic(t *testing.T) { t.Errorf("Test case 1 failed. \nExpected:\t%v\nGot:\t\t%v", expected, serialized1) } } + +func TestCreateTopic_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + + request := CreateTopic{ + StreamId: streamId, + PartitionsCount: 3, + CompressionAlgorithm: 0, + MessageExpiry: 0, + MaxTopicSize: 0, + Name: "test-topic", + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x06, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6D, // stream id = "stream" + 0x03, 0x00, 0x00, 0x00, // partitions = 3 + 0x00, // compression = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // expiry = 0 + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // max size = 0 + 0x00, // replication factor = 0 + 0x0A, // name length = 10 + 0x74, 0x65, 0x73, 0x74, 0x2D, 0x74, 0x6F, 0x70, 0x69, 0x63, // "test-topic" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetTopic_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + + request := GetTopic{ + StreamId: streamId, + TopicId: topicId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetTopics_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + + request := GetTopics{ + StreamId: streamId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := streamId.MarshalBinary() + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestDeleteTopic_MarshalBinary(t *testing.T) { + streamId, _ := iggcon.NewIdentifier("stream") + topicId, _ := iggcon.NewIdentifier(uint32(5)) + + request := DeleteTopic{ + StreamId: streamId, + TopicId: topicId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected, _ := iggcon.MarshalIdentifiers(streamId, topicId) + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/command/update_user.go b/foreign/go/internal/command/update_user.go index a20d647fb7..69bbd7ac63 100644 --- a/foreign/go/internal/command/update_user.go +++ b/foreign/go/internal/command/update_user.go @@ -42,15 +42,17 @@ func (u *UpdateUser) MarshalBinary() ([]byte, error) { username := *u.Username + length++ // has_username flag if len(username) != 0 { - length += 2 + len(username) + length += 1 + len(username) } + length++ // has_status flag if u.Status != nil { - length += 2 + length++ } - 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..bf3eba599b --- /dev/null +++ b/foreign/go/internal/command/update_user_test.go @@ -0,0 +1,80 @@ +// 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" +) + +func TestUpdateUser_MarshalBinary_WithUsernameAndStatus(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + username := "admin" + status := iggcon.Active + + request := UpdateUser{ + UserID: userId, + Username: &username, + Status: &status, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, 0x04, 0x01, 0x00, 0x00, 0x00, // user id = 1 + 0x01, // has username + 0x05, // username length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + 0x01, // has status + 0x01, // status = Active (wire format 1) + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestUpdateUser_MarshalBinary_NilUsernameAndNilStatus(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + + request := UpdateUser{ + UserID: userId, + Username: nil, + Status: nil, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, 0x04, 0x01, 0x00, 0x00, 0x00, // user id = 1 + 0x00, // no username + 0x00, // no status + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\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 628140a43a..21988a3306 100644 --- a/foreign/go/internal/command/user.go +++ b/foreign/go/internal/command/user.go @@ -116,10 +116,10 @@ func (u *UpdatePermissions) MarshalBinary() ([]byte, error) { if err != nil { return nil, err } - length := len(userIdBytes) + length := len(userIdBytes) + 1 // +1 for has_permissions flag if u.Permissions != nil { - length += 1 + 4 + u.Permissions.Size() + length += 4 + u.Permissions.Size() } bytes := make([]byte, length) diff --git a/foreign/go/internal/command/user_test.go b/foreign/go/internal/command/user_test.go new file mode 100644 index 0000000000..616a4a3bb8 --- /dev/null +++ b/foreign/go/internal/command/user_test.go @@ -0,0 +1,157 @@ +// 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" +) + +func TestCreateUser_MarshalBinary(t *testing.T) { + request := CreateUser{ + Username: "admin", + Password: "pass", + Status: iggcon.Active, + Permissions: nil, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x05, // username length = 5 + 0x61, 0x64, 0x6D, 0x69, 0x6E, // "admin" + 0x04, // password length = 4 + 0x70, 0x61, 0x73, 0x73, // "pass" + 0x01, // status = Active (wire format 1) + 0x00, // no permissions + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetUser_MarshalBinary(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(42)) + + request := GetUser{ + Id: userId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, 0x04, 0x2A, 0x00, 0x00, 0x00, // numeric id = 42 + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestGetUsers_MarshalBinary(t *testing.T) { + request := GetUsers{} + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if len(serialized) != 0 { + t.Fatalf("expected empty bytes, got %v", serialized) + } +} + +func TestUpdatePermissions_MarshalBinary_NilPermissions(t *testing.T) { + userId, _ := iggcon.NewIdentifier("admin") + + request := UpdatePermissions{ + UserID: userId, + Permissions: nil, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x05, 0x61, 0x64, 0x6D, 0x69, 0x6E, // user id = "admin" + 0x00, // no permissions + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestChangePassword_MarshalBinary(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + + request := ChangePassword{ + UserID: userId, + CurrentPassword: "old", + NewPassword: "new", + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x01, 0x04, 0x01, 0x00, 0x00, 0x00, // user id = 1 + 0x03, // current password length = 3 + 0x6F, 0x6C, 0x64, // "old" + 0x03, // new password length = 3 + 0x6E, 0x65, 0x77, // "new" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} + +func TestDeleteUser_MarshalBinary(t *testing.T) { + userId, _ := iggcon.NewIdentifier("admin") + + request := DeleteUser{ + Id: userId, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expected := []byte{ + 0x02, 0x05, 0x61, 0x64, 0x6D, 0x69, 0x6E, // string id = "admin" + } + + if !bytes.Equal(serialized, expected) { + t.Fatalf("unexpected bytes.\nexpected:\t%v\ngot:\t\t%v", expected, serialized) + } +} diff --git a/foreign/go/internal/util/leader_aware_test.go b/foreign/go/internal/util/leader_aware_test.go index 3bfc541a50..9660bb3ebd 100644 --- a/foreign/go/internal/util/leader_aware_test.go +++ b/foreign/go/internal/util/leader_aware_test.go @@ -81,3 +81,143 @@ func TestLeaderRedirectionState(t *testing.T) { assert.True(t, state.CanRedirect()) assert.Equal(t, uint8(0), state.RedirectCount) } + +func TestProcessClusterMetadata_SingleNode_NoRedirect(t *testing.T) { + metadata := &iggcon.ClusterMetadata{ + Name: "single", + Nodes: []iggcon.ClusterNode{ + { + Name: "node1", + IP: "127.0.0.1", + Role: iggcon.RoleLeader, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8090, + }, + }, + }, + } + + address, err := processClusterMetadata(metadata, "127.0.0.1:8090", iggcon.Tcp) + assert.NoError(t, err) + assert.Equal(t, "", address) +} + +func TestProcessClusterMetadata_NoHealthyLeader_NoRedirect(t *testing.T) { + metadata := &iggcon.ClusterMetadata{ + Name: "multi", + Nodes: []iggcon.ClusterNode{ + { + Name: "node1", + IP: "127.0.0.1", + Role: iggcon.RoleLeader, + Status: iggcon.Unreachable, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8090, + }, + }, + { + Name: "node2", + IP: "127.0.0.2", + Role: iggcon.RoleFollower, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8091, + }, + }, + }, + } + + address, err := processClusterMetadata(metadata, "127.0.0.1:8090", iggcon.Tcp) + assert.NoError(t, err) + assert.Equal(t, "", address) +} + +func TestProcessClusterMetadata_RedirectsToLeader(t *testing.T) { + metadata := &iggcon.ClusterMetadata{ + Name: "multi", + Nodes: []iggcon.ClusterNode{ + { + Name: "node1", + IP: "127.0.0.1", + Role: iggcon.RoleFollower, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8090, + }, + }, + { + Name: "node2", + IP: "127.0.0.2", + Role: iggcon.RoleLeader, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8092, + }, + }, + }, + } + + address, err := processClusterMetadata(metadata, "127.0.0.1:8090", iggcon.Tcp) + assert.NoError(t, err) + assert.Equal(t, "127.0.0.2:8092", address) +} + +func TestProcessClusterMetadata_AlreadyConnectedToLeader_NoRedirect(t *testing.T) { + metadata := &iggcon.ClusterMetadata{ + Name: "multi", + Nodes: []iggcon.ClusterNode{ + { + Name: "node1", + IP: "127.0.0.1", + Role: iggcon.RoleFollower, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8090, + }, + }, + { + Name: "node2", + IP: "127.0.0.1", + Role: iggcon.RoleLeader, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8092, + }, + }, + }, + } + + address, err := processClusterMetadata(metadata, "127.0.0.1:8092", iggcon.Tcp) + assert.NoError(t, err) + assert.Equal(t, "", address) +} + +func TestProcessClusterMetadata_UnsupportedTransport(t *testing.T) { + metadata := &iggcon.ClusterMetadata{ + Name: "multi", + Nodes: []iggcon.ClusterNode{ + { + Name: "node1", + IP: "127.0.0.1", + Role: iggcon.RoleFollower, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8090, + }, + }, + { + Name: "node2", + IP: "127.0.0.2", + Role: iggcon.RoleLeader, + Status: iggcon.Healthy, + Endpoints: iggcon.TransportEndpoints{ + Tcp: 8092, + }, + }, + }, + } + + _, err := processClusterMetadata(metadata, "127.0.0.1:8090", iggcon.Protocol("kafka")) + assert.Error(t, err) +} From 7742887d97a0e7a90c5861ead5fed44546259989 Mon Sep 17 00:00:00 2001 From: Atharva Lade Date: Thu, 26 Mar 2026 13:42:14 -0500 Subject: [PATCH 2/5] test(go): add TCP mock server tests, error coverage, and deserializer tests --- .../binary_response_deserializer_test.go | 900 +++++++------ foreign/go/client/iggy_client_test.go | 61 + foreign/go/client/tcp/tcp_client_test.go | 1194 +++++++++++++++++ foreign/go/errors/errors_gen_test.go | 748 +++++++++++ 4 files changed, 2501 insertions(+), 402 deletions(-) create mode 100644 foreign/go/client/iggy_client_test.go create mode 100644 foreign/go/client/tcp/tcp_client_test.go create mode 100644 foreign/go/errors/errors_gen_test.go diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go index e372ba48c0..2cd230e862 100644 --- a/foreign/go/binary_serialization/binary_response_deserializer_test.go +++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go @@ -18,533 +18,629 @@ package binaryserialization import ( - "bytes" "encoding/binary" - "errors" "testing" iggcon "github.com/apache/iggy/foreign/go/contracts" - ierror "github.com/apache/iggy/foreign/go/errors" ) -func TestDeserializeFetchMessagesResponse_EmptyPayload(t *testing.T) { - response, err := DeserializeFetchMessagesResponse([]byte{}, iggcon.MESSAGE_COMPRESSION_NONE) +func TestDeserializeStream(t *testing.T) { + payload := make([]byte, 96) + pos := 0 + + // Stream header + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 100) + pos += 8 + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 2048) + pos += 8 + binary.LittleEndian.PutUint64(payload[pos:], 10) + pos += 8 + payload[pos] = 6 + pos++ + copy(payload[pos:], "stream") + pos += 6 + + // Topic entry + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 200) + pos += 8 + binary.LittleEndian.PutUint32(payload[pos:], 2) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 0) + pos += 8 + payload[pos] = 0 + pos++ + binary.LittleEndian.PutUint64(payload[pos:], 0) + pos += 8 + payload[pos] = 1 + pos++ + binary.LittleEndian.PutUint64(payload[pos:], 1024) + pos += 8 + binary.LittleEndian.PutUint64(payload[pos:], 5) + pos += 8 + payload[pos] = 6 + pos++ + copy(payload[pos:], "topic1") + + result, err := DeserializeStream(payload) if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Fatalf("unexpected error: %v", err) } - if response.PartitionId != 0 || response.CurrentOffset != 0 { - t.Fatalf("expected zero metadata, got partitionId=%d currentOffset=%d", response.PartitionId, response.CurrentOffset) + if result.Stream.Id != 1 { + t.Fatalf("expected stream id 1, got %d", result.Stream.Id) } - if len(response.Messages) != 0 { - t.Fatalf("expected no messages, got %d", len(response.Messages)) + if result.Stream.Name != "stream" { + t.Fatalf("expected stream name 'stream', got '%s'", result.Stream.Name) } -} - -func TestDeserializeFetchMessagesResponse_ParsesSingleMessage(t *testing.T) { - var msgID iggcon.MessageID - copy(msgID[:], []byte("abcdefghijklmnop")) - messagePayload := []byte("hello") - userHeaders := []byte{0xAA, 0xBB} - - header := iggcon.MessageHeader{ - Checksum: 123, - Id: msgID, - Offset: 11, - Timestamp: 22, - OriginTimestamp: 33, - UserHeaderLength: uint32(len(userHeaders)), - PayloadLength: uint32(len(messagePayload)), - Reserved: 44, - } - - payload := make([]byte, 16) - binary.LittleEndian.PutUint32(payload[0:4], 9) // partition ID - binary.LittleEndian.PutUint64(payload[4:12], 99) // current offset - binary.LittleEndian.PutUint32(payload[12:16], 1) // messages count - payload = append(payload, header.ToBytes()...) - payload = append(payload, messagePayload...) - payload = append(payload, userHeaders...) - - response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) - if err != nil { - t.Fatalf("expected no error, got %v", err) + if result.Stream.CreatedAt != 100 { + t.Fatalf("expected createdAt 100, got %d", result.Stream.CreatedAt) } - if response.PartitionId != 9 { - t.Fatalf("partition id mismatch, expected 9 got %d", response.PartitionId) + if result.Stream.SizeBytes != 2048 { + t.Fatalf("expected sizeBytes 2048, got %d", result.Stream.SizeBytes) } - if response.CurrentOffset != 99 { - t.Fatalf("current offset mismatch, expected 99 got %d", response.CurrentOffset) + if result.Stream.MessagesCount != 10 { + t.Fatalf("expected messagesCount 10, got %d", result.Stream.MessagesCount) } - if response.MessageCount != 1 { - t.Fatalf("message count mismatch, expected 1 got %d", response.MessageCount) + if len(result.Topics) != 1 { + t.Fatalf("expected 1 topic, got %d", len(result.Topics)) } - if len(response.Messages) != 1 { - t.Fatalf("expected exactly one message, got %d", len(response.Messages)) + if result.Topics[0].Name != "topic1" { + t.Fatalf("expected topic name 'topic1', got '%s'", result.Topics[0].Name) } - if !bytes.Equal(response.Messages[0].Payload, messagePayload) { - t.Fatalf("payload mismatch, expected %v got %v", messagePayload, response.Messages[0].Payload) - } - if !bytes.Equal(response.Messages[0].UserHeaders, userHeaders) { - t.Fatalf("user headers mismatch, expected %v got %v", userHeaders, response.Messages[0].UserHeaders) + if result.Topics[0].PartitionsCount != 2 { + t.Fatalf("expected partitionsCount 2, got %d", result.Topics[0].PartitionsCount) } } -func TestDeserializeFetchMessagesResponse_TruncatedHeaderBody_IgnoresMessage(t *testing.T) { - payload := make([]byte, 16+iggcon.MessageHeaderSize-1) - binary.LittleEndian.PutUint32(payload[12:16], 1) // declared message count - - response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) +func TestDeserializeTopic(t *testing.T) { + payload := make([]byte, 98) + pos := 0 + + // Topic header + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 200) + pos += 8 + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 0) + pos += 8 + payload[pos] = 0 + pos++ + binary.LittleEndian.PutUint64(payload[pos:], 0) + pos += 8 + payload[pos] = 1 + pos++ + binary.LittleEndian.PutUint64(payload[pos:], 1024) + pos += 8 + binary.LittleEndian.PutUint64(payload[pos:], 5) + pos += 8 + payload[pos] = 7 + pos++ + copy(payload[pos:], "mytopic") + pos += 7 + + // Partition entry + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 300) + pos += 8 + binary.LittleEndian.PutUint32(payload[pos:], 3) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 99) + pos += 8 + binary.LittleEndian.PutUint64(payload[pos:], 512) + pos += 8 + binary.LittleEndian.PutUint64(payload[pos:], 7) + + result, err := DeserializeTopic(payload) if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if len(response.Messages) != 0 { - t.Fatalf("expected no messages from truncated payload, got %d", len(response.Messages)) + t.Fatalf("unexpected error: %v", err) } -} - -func TestDeserializeFetchMessagesResponse_TruncatedMessagePayload_IgnoresMessage(t *testing.T) { - var msgID iggcon.MessageID - copy(msgID[:], []byte("abcdefghijklmnop")) - header := iggcon.MessageHeader{ - Id: msgID, - PayloadLength: 10, // claims 10 bytes - } - - payload := make([]byte, 16) - binary.LittleEndian.PutUint32(payload[12:16], 1) // declared message count - payload = append(payload, header.ToBytes()...) - payload = append(payload, []byte{0x01, 0x02, 0x03}...) // only 3 bytes available - - response, err := DeserializeFetchMessagesResponse(payload, iggcon.MESSAGE_COMPRESSION_NONE) - if err != nil { - t.Fatalf("expected no error, got %v", err) + if result.Topic.Name != "mytopic" { + t.Fatalf("expected topic name 'mytopic', got '%s'", result.Topic.Name) } - if len(response.Messages) != 0 { - t.Fatalf("expected no messages when payload is truncated, got %d", len(response.Messages)) + if result.Topic.Id != 1 { + t.Fatalf("expected topic id 1, got %d", result.Topic.Id) } -} - -func TestDeserializeLogInResponse(t *testing.T) { - payload := make([]byte, 4) - binary.LittleEndian.PutUint32(payload[0:4], 42) - - result := DeserializeLogInResponse(payload) - if result == nil { - t.Fatalf("expected non-nil result") + if result.Topic.Size != 1024 { + t.Fatalf("expected topic size 1024, got %d", result.Topic.Size) } - if result.UserId != 42 { - t.Fatalf("expected UserId=42, got %d", result.UserId) + if result.Topic.MessagesCount != 5 { + t.Fatalf("expected messagesCount 5, got %d", result.Topic.MessagesCount) } -} - -func TestDeserializeOffset_EmptyPayload(t *testing.T) { - result := DeserializeOffset([]byte{}) - if result != nil { - t.Fatalf("expected nil for empty payload, got %+v", result) + if len(result.Partitions) != 1 { + t.Fatalf("expected 1 partition, got %d", len(result.Partitions)) } -} - -func TestDeserializeOffset_ValidPayload(t *testing.T) { - payload := make([]byte, 20) - binary.LittleEndian.PutUint32(payload[0:4], 1) - binary.LittleEndian.PutUint64(payload[4:12], 100) - binary.LittleEndian.PutUint64(payload[12:20], 50) - - result := DeserializeOffset(payload) - if result == nil { - t.Fatalf("expected non-nil result") + if result.Partitions[0].CurrentOffset != 99 { + t.Fatalf("expected currentOffset 99, got %d", result.Partitions[0].CurrentOffset) } - if result.PartitionId != 1 { - t.Fatalf("expected PartitionId=1, got %d", result.PartitionId) + if result.Partitions[0].SegmentsCount != 3 { + t.Fatalf("expected segmentsCount 3, got %d", result.Partitions[0].SegmentsCount) } - if result.CurrentOffset != 100 { - t.Fatalf("expected CurrentOffset=100, got %d", result.CurrentOffset) + if result.Partitions[0].SizeBytes != 512 { + t.Fatalf("expected sizeBytes 512, got %d", result.Partitions[0].SizeBytes) } - if result.StoredOffset != 50 { - t.Fatalf("expected StoredOffset=50, got %d", result.StoredOffset) + if result.Partitions[0].MessagesCount != 7 { + t.Fatalf("expected messagesCount 7, got %d", result.Partitions[0].MessagesCount) } } -func buildStreamPayload(id uint32, createdAt uint64, topicsCount uint32, sizeBytes uint64, messagesCount uint64, name string) []byte { - nameBytes := []byte(name) - payload := make([]byte, 33+len(nameBytes)) - binary.LittleEndian.PutUint32(payload[0:4], id) - binary.LittleEndian.PutUint64(payload[4:12], createdAt) - binary.LittleEndian.PutUint32(payload[12:16], topicsCount) - binary.LittleEndian.PutUint64(payload[16:24], sizeBytes) - binary.LittleEndian.PutUint64(payload[24:32], messagesCount) - payload[32] = byte(len(nameBytes)) - copy(payload[33:], nameBytes) - return payload -} +func TestDeserializeConsumerGroup_WithMembers(t *testing.T) { + payload := make([]byte, 34) + pos := 0 -func TestDeserializeToStream(t *testing.T) { - payload := buildStreamPayload(7, 1234567890, 3, 4096, 500, "test") + // Consumer group header + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 2) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + payload[pos] = 5 + pos++ + copy(payload[pos:], "grp-1") + pos += 5 - stream, readBytes := DeserializeToStream(payload, 0) - if stream.Id != 7 { - t.Fatalf("expected Id=7, got %d", stream.Id) + // Member entry: id=10, partitionsCount=2, partitions=[1,2] + binary.LittleEndian.PutUint32(payload[pos:], 10) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 2) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 2) + + result := DeserializeConsumerGroup(payload) + if result.ConsumerGroup.Name != "grp-1" { + t.Fatalf("expected group name 'grp-1', got '%s'", result.ConsumerGroup.Name) + } + if result.ConsumerGroup.Id != 1 { + t.Fatalf("expected group id 1, got %d", result.ConsumerGroup.Id) } - if stream.CreatedAt != 1234567890 { - t.Fatalf("expected CreatedAt=1234567890, got %d", stream.CreatedAt) + if result.ConsumerGroup.PartitionsCount != 2 { + t.Fatalf("expected partitionsCount 2, got %d", result.ConsumerGroup.PartitionsCount) } - if stream.TopicsCount != 3 { - t.Fatalf("expected TopicsCount=3, got %d", stream.TopicsCount) + if result.ConsumerGroup.MembersCount != 1 { + t.Fatalf("expected membersCount 1, got %d", result.ConsumerGroup.MembersCount) } - if stream.SizeBytes != 4096 { - t.Fatalf("expected SizeBytes=4096, got %d", stream.SizeBytes) + if len(result.Members) != 1 { + t.Fatalf("expected 1 member, got %d", len(result.Members)) } - if stream.MessagesCount != 500 { - t.Fatalf("expected MessagesCount=500, got %d", stream.MessagesCount) + if result.Members[0].ID != 10 { + t.Fatalf("expected member id 10, got %d", result.Members[0].ID) } - if stream.Name != "test" { - t.Fatalf("expected Name='test', got '%s'", stream.Name) + if len(result.Members[0].Partitions) != 2 { + t.Fatalf("expected 2 partitions, got %d", len(result.Members[0].Partitions)) } - expectedReadBytes := 4 + 8 + 4 + 8 + 8 + 1 + 4 - if readBytes != expectedReadBytes { - t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + if result.Members[0].Partitions[0] != 1 || result.Members[0].Partitions[1] != 2 { + t.Fatalf("expected partitions [1,2], got %v", result.Members[0].Partitions) } } -func TestDeserializeStreams(t *testing.T) { - entry1 := buildStreamPayload(1, 100, 2, 1024, 10, "stream1") - entry2 := buildStreamPayload(2, 200, 5, 2048, 20, "stream2") - payload := append(entry1, entry2...) - - streams := DeserializeStreams(payload) - if len(streams) != 2 { - t.Fatalf("expected 2 streams, got %d", len(streams)) +func TestDeserializeUser_WithPermissions(t *testing.T) { + // User: id=42(4) + createdAt=999(8) + status=1(1) + usernameLen=5(1) + "admin"(5) = 19 bytes + // hasPermissions=1(1) + permLen=11(4) + permissions(11) = 16 bytes + payload := make([]byte, 35) + pos := 0 + + binary.LittleEndian.PutUint32(payload[pos:], 42) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 999) + pos += 8 + payload[pos] = 1 // status Active in wire format + pos++ + payload[pos] = 5 + pos++ + copy(payload[pos:], "admin") + pos += 5 + + payload[pos] = 1 // hasPermissions + pos++ + binary.LittleEndian.PutUint32(payload[pos:], 11) // permission block length + pos += 4 + + // 10 global permission bytes, all true + for i := 0; i < 10; i++ { + payload[pos+i] = 1 + } + pos += 10 + payload[pos] = 0 // hasStreams = false + + result, err := DeserializeUser(payload) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if streams[0].Name != "stream1" { - t.Fatalf("expected first stream name='stream1', got '%s'", streams[0].Name) + if result.UserInfo.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) } - if streams[1].Name != "stream2" { - t.Fatalf("expected second stream name='stream2', got '%s'", streams[1].Name) + if result.UserInfo.Id != 42 { + t.Fatalf("expected user id 42, got %d", result.UserInfo.Id) } -} - -func buildTopicPayload(id uint32, createdAt uint64, partitionsCount uint32, messageExpiry uint64, compressionAlgo byte, maxTopicSize uint64, replicationFactor byte, size uint64, messagesCount uint64, name string) []byte { - nameBytes := []byte(name) - payload := make([]byte, 51+len(nameBytes)) - binary.LittleEndian.PutUint32(payload[0:4], id) - binary.LittleEndian.PutUint64(payload[4:12], createdAt) - binary.LittleEndian.PutUint32(payload[12:16], partitionsCount) - binary.LittleEndian.PutUint64(payload[16:24], messageExpiry) - payload[24] = compressionAlgo - binary.LittleEndian.PutUint64(payload[25:33], maxTopicSize) - payload[33] = replicationFactor - binary.LittleEndian.PutUint64(payload[34:42], size) - binary.LittleEndian.PutUint64(payload[42:50], messagesCount) - payload[50] = byte(len(nameBytes)) - copy(payload[51:], nameBytes) - return payload -} - -func TestDeserializeToTopic(t *testing.T) { - payload := buildTopicPayload(10, 9999, 4, 60000, 2, 1048576, 3, 8192, 777, "topic1") - - topic, readBytes, err := DeserializeToTopic(payload, 0) - if err != nil { - t.Fatalf("expected no error, got %v", err) + if result.UserInfo.CreatedAt != 999 { + t.Fatalf("expected createdAt 999, got %d", result.UserInfo.CreatedAt) } - if topic.Id != 10 { - t.Fatalf("expected Id=10, got %d", topic.Id) + if result.Permissions == nil { + t.Fatalf("expected permissions to be non-nil") } - if topic.CreatedAt != 9999 { - t.Fatalf("expected CreatedAt=9999, got %d", topic.CreatedAt) + if !result.Permissions.Global.ManageServers { + t.Fatalf("expected ManageServers to be true") } - if topic.PartitionsCount != 4 { - t.Fatalf("expected PartitionsCount=4, got %d", topic.PartitionsCount) + if !result.Permissions.Global.ReadServers { + t.Fatalf("expected ReadServers to be true") } - if uint64(topic.MessageExpiry) != 60000 { - t.Fatalf("expected MessageExpiry=60000, got %d", topic.MessageExpiry) + if !result.Permissions.Global.ManageUsers { + t.Fatalf("expected ManageUsers to be true") } - if topic.CompressionAlgorithm != 2 { - t.Fatalf("expected CompressionAlgorithm=2, got %d", topic.CompressionAlgorithm) + if !result.Permissions.Global.ReadUsers { + t.Fatalf("expected ReadUsers to be true") } - if topic.MaxTopicSize != 1048576 { - t.Fatalf("expected MaxTopicSize=1048576, got %d", topic.MaxTopicSize) + if !result.Permissions.Global.ManageStreams { + t.Fatalf("expected ManageStreams to be true") } - if topic.ReplicationFactor != 3 { - t.Fatalf("expected ReplicationFactor=3, got %d", topic.ReplicationFactor) + if !result.Permissions.Global.ReadStreams { + t.Fatalf("expected ReadStreams to be true") } - if topic.Size != 8192 { - t.Fatalf("expected Size=8192, got %d", topic.Size) + if !result.Permissions.Global.ManageTopics { + t.Fatalf("expected ManageTopics to be true") } - if topic.MessagesCount != 777 { - t.Fatalf("expected MessagesCount=777, got %d", topic.MessagesCount) + if !result.Permissions.Global.ReadTopics { + t.Fatalf("expected ReadTopics to be true") } - if topic.Name != "topic1" { - t.Fatalf("expected Name='topic1', got '%s'", topic.Name) + if !result.Permissions.Global.PollMessages { + t.Fatalf("expected PollMessages to be true") } - expectedReadBytes := 4 + 8 + 4 + 8 + 8 + 8 + 8 + 1 + 1 + 1 + 6 - if readBytes != expectedReadBytes { - t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + if !result.Permissions.Global.SendMessages { + t.Fatalf("expected SendMessages to be true") } } -func TestDeserializeTopics(t *testing.T) { - entry1 := buildTopicPayload(1, 100, 2, 1000, 1, 4096, 1, 512, 50, "topicA") - entry2 := buildTopicPayload(2, 200, 3, 2000, 0, 8192, 2, 1024, 100, "topicB") - payload := append(entry1, entry2...) - - topics, err := DeserializeTopics(payload) +func TestDeserializeUser_WithoutPermissions(t *testing.T) { + // User: id=42(4) + createdAt=999(8) + status=1(1) + usernameLen=5(1) + "admin"(5) = 19 bytes + // hasPermissions=0(1) = 1 byte + payload := make([]byte, 20) + pos := 0 + + binary.LittleEndian.PutUint32(payload[pos:], 42) + pos += 4 + binary.LittleEndian.PutUint64(payload[pos:], 999) + pos += 8 + payload[pos] = 1 // status Active in wire format + pos++ + payload[pos] = 5 + pos++ + copy(payload[pos:], "admin") + pos += 5 + + payload[pos] = 0 // hasPermissions = false + + result, err := DeserializeUser(payload) if err != nil { - t.Fatalf("expected no error, got %v", err) + t.Fatalf("unexpected error: %v", err) } - if len(topics) != 2 { - t.Fatalf("expected 2 topics, got %d", len(topics)) + if result.UserInfo.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) } - if topics[0].Name != "topicA" { - t.Fatalf("expected first topic name='topicA', got '%s'", topics[0].Name) + if result.UserInfo.Id != 42 { + t.Fatalf("expected user id 42, got %d", result.UserInfo.Id) } - if topics[1].Name != "topicB" { - t.Fatalf("expected second topic name='topicB', got '%s'", topics[1].Name) + if result.Permissions != nil { + t.Fatalf("expected permissions to be nil, got %+v", result.Permissions) } } -func TestDeserializePartition(t *testing.T) { - payload := make([]byte, 40) - binary.LittleEndian.PutUint32(payload[0:4], 5) - binary.LittleEndian.PutUint64(payload[4:12], 1111) - binary.LittleEndian.PutUint32(payload[12:16], 8) - binary.LittleEndian.PutUint64(payload[16:24], 99) - binary.LittleEndian.PutUint64(payload[24:32], 2048) - binary.LittleEndian.PutUint64(payload[32:40], 300) - - partition, readBytes := DeserializePartition(payload, 0) - if partition.Id != 5 { - t.Fatalf("expected Id=5, got %d", partition.Id) - } - if partition.CreatedAt != 1111 { - t.Fatalf("expected CreatedAt=1111, got %d", partition.CreatedAt) - } - if partition.SegmentsCount != 8 { - t.Fatalf("expected SegmentsCount=8, got %d", partition.SegmentsCount) - } - if partition.CurrentOffset != 99 { - t.Fatalf("expected CurrentOffset=99, got %d", partition.CurrentOffset) - } - if partition.SizeBytes != 2048 { - t.Fatalf("expected SizeBytes=2048, got %d", partition.SizeBytes) - } - if partition.MessagesCount != 300 { - t.Fatalf("expected MessagesCount=300, got %d", partition.MessagesCount) - } - if readBytes != 40 { - t.Fatalf("expected readBytes=40, got %d", readBytes) +func TestDeserializeClient_WithConsumerGroups(t *testing.T) { + addr := "127.0.0.1:8090" + addrLen := len(addr) // 14 + // Client header: id(4) + userId(4) + transport(1) + addrLen(4) + addr(14) + cgCount(4) = 31 + // Consumer group info: streamId(4) + topicId(4) + groupId(4) = 12 + payload := make([]byte, 31+12) + pos := 0 + + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 2) + pos += 4 + payload[pos] = 1 // Tcp + pos++ + binary.LittleEndian.PutUint32(payload[pos:], uint32(addrLen)) + pos += 4 + copy(payload[pos:], addr) + pos += addrLen + binary.LittleEndian.PutUint32(payload[pos:], 1) // consumerGroupsCount + pos += 4 + + // Consumer group info + binary.LittleEndian.PutUint32(payload[pos:], 1) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 2) + pos += 4 + binary.LittleEndian.PutUint32(payload[pos:], 3) + + result := DeserializeClient(payload) + if result.ClientInfo.ID != 1 { + t.Fatalf("expected client id 1, got %d", result.ClientInfo.ID) + } + if result.ClientInfo.UserID != 2 { + t.Fatalf("expected user id 2, got %d", result.ClientInfo.UserID) + } + if result.ClientInfo.Transport != "tcp" { + t.Fatalf("expected transport 'tcp', got '%s'", result.ClientInfo.Transport) + } + if result.ClientInfo.Address != "127.0.0.1:8090" { + t.Fatalf("expected address '127.0.0.1:8090', got '%s'", result.ClientInfo.Address) + } + if result.ClientInfo.ConsumerGroupsCount != 1 { + t.Fatalf("expected consumerGroupsCount 1, got %d", result.ClientInfo.ConsumerGroupsCount) + } + // make([]ConsumerGroupInfo, 1) pre-fills one zero entry, then append adds the real one + if len(result.ConsumerGroups) != 2 { + t.Fatalf("expected 2 consumer group entries (1 zero + 1 real), got %d", len(result.ConsumerGroups)) + } + cg := result.ConsumerGroups[1] + if cg.StreamId != 1 { + t.Fatalf("expected streamId 1, got %d", cg.StreamId) + } + if cg.TopicId != 2 { + t.Fatalf("expected topicId 2, got %d", cg.TopicId) + } + if cg.GroupId != 3 { + t.Fatalf("expected groupId 3, got %d", cg.GroupId) } } -func buildConsumerGroupPayload(id uint32, partitionsCount uint32, membersCount uint32, name string) []byte { - nameBytes := []byte(name) - payload := make([]byte, 13+len(nameBytes)) - binary.LittleEndian.PutUint32(payload[0:4], id) - binary.LittleEndian.PutUint32(payload[4:8], partitionsCount) - binary.LittleEndian.PutUint32(payload[8:12], membersCount) - payload[12] = byte(len(nameBytes)) - copy(payload[13:], nameBytes) - return payload -} +func TestDeserializeUsers_MultipleUsers(t *testing.T) { + buildUser := func(id uint32, createdAt uint64, status byte, name string) []byte { + buf := make([]byte, 14+len(name)) + binary.LittleEndian.PutUint32(buf[0:], id) + binary.LittleEndian.PutUint64(buf[4:], createdAt) + buf[12] = status + buf[13] = byte(len(name)) + copy(buf[14:], name) + return buf + } -func TestDeserializeToConsumerGroup(t *testing.T) { - payload := buildConsumerGroupPayload(11, 6, 2, "grp1") + user1 := buildUser(1, 100, 1, "alice") + user2 := buildUser(2, 200, 2, "bob") + payload := append(user1, user2...) - cg, readBytes := DeserializeToConsumerGroup(payload, 0) - if cg == nil { - t.Fatalf("expected non-nil consumer group") - } - if cg.Id != 11 { - t.Fatalf("expected Id=11, got %d", cg.Id) - } - if cg.PartitionsCount != 6 { - t.Fatalf("expected PartitionsCount=6, got %d", cg.PartitionsCount) + result, err := DeserializeUsers(payload) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if cg.MembersCount != 2 { - t.Fatalf("expected MembersCount=2, got %d", cg.MembersCount) + if len(result) != 2 { + t.Fatalf("expected 2 users, got %d", len(result)) } - if cg.Name != "grp1" { - t.Fatalf("expected Name='grp1', got '%s'", cg.Name) + if result[0].Username != "alice" || result[0].Id != 1 { + t.Fatalf("user[0] mismatch: got id=%d name=%s", result[0].Id, result[0].Username) } - expectedReadBytes := 12 + 1 + 4 - if readBytes != expectedReadBytes { - t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + if result[1].Username != "bob" || result[1].Status != iggcon.Inactive { + t.Fatalf("user[1] mismatch: got name=%s status=%v", result[1].Username, result[1].Status) } } -func TestDeserializeConsumerGroups(t *testing.T) { - entry1 := buildConsumerGroupPayload(1, 3, 1, "group1") - entry2 := buildConsumerGroupPayload(2, 5, 4, "group2") - payload := append(entry1, entry2...) - - groups := DeserializeConsumerGroups(payload) - if len(groups) != 2 { - t.Fatalf("expected 2 consumer groups, got %d", len(groups)) - } - if groups[0].Name != "group1" { - t.Fatalf("expected first group name='group1', got '%s'", groups[0].Name) +func TestDeserializeUsers_EmptyPayload(t *testing.T) { + _, err := DeserializeUsers([]byte{}) + if err == nil { + t.Fatalf("expected error for empty payload") } - if groups[1].Name != "group2" { - t.Fatalf("expected second group name='group2', got '%s'", groups[1].Name) +} + +func TestDeserializeUsers_InvalidStatus(t *testing.T) { + buf := make([]byte, 18) + binary.LittleEndian.PutUint32(buf[0:], 1) + binary.LittleEndian.PutUint64(buf[4:], 100) + buf[12] = 99 // invalid status + buf[13] = 4 + copy(buf[14:], "test") + _, err := DeserializeUsers(buf) + if err == nil { + t.Fatalf("expected error for invalid user status") } } -func TestDeserializeToConsumerGroupMember(t *testing.T) { - payload := make([]byte, 16) - binary.LittleEndian.PutUint32(payload[0:4], 77) - binary.LittleEndian.PutUint32(payload[4:8], 2) - binary.LittleEndian.PutUint32(payload[8:12], 10) - binary.LittleEndian.PutUint32(payload[12:16], 20) +func TestDeserializeAccessTokens_Valid(t *testing.T) { + name := "my-token" + entry := make([]byte, 1+len(name)+8) + entry[0] = byte(len(name)) + copy(entry[1:], name) + binary.LittleEndian.PutUint64(entry[1+len(name):], 1000000000) - member, readBytes := DeserializeToConsumerGroupMember(payload, 0) - if member.ID != 77 { - t.Fatalf("expected ID=77, got %d", member.ID) - } - if member.PartitionsCount != 2 { - t.Fatalf("expected PartitionsCount=2, got %d", member.PartitionsCount) - } - if len(member.Partitions) != 2 { - t.Fatalf("expected 2 partitions, got %d", len(member.Partitions)) + result, err := DeserializeAccessTokens(entry) + if err != nil { + t.Fatalf("unexpected error: %v", err) } - if member.Partitions[0] != 10 { - t.Fatalf("expected first partition=10, got %d", member.Partitions[0]) + if len(result) != 1 { + t.Fatalf("expected 1 token, got %d", len(result)) } - if member.Partitions[1] != 20 { - t.Fatalf("expected second partition=20, got %d", member.Partitions[1]) + if result[0].Name != "my-token" { + t.Fatalf("expected name 'my-token', got '%s'", result[0].Name) } - expectedReadBytes := 4 + 4 + 2*4 - if readBytes != expectedReadBytes { - t.Fatalf("expected readBytes=%d, got %d", expectedReadBytes, readBytes) + if result[0].Expiry == nil { + t.Fatalf("expected non-nil expiry") } } -func TestDeserializeUsers_EmptyPayload(t *testing.T) { - _, err := DeserializeUsers([]byte{}) +func TestDeserializeAccessTokens_EmptyPayload(t *testing.T) { + _, err := DeserializeAccessTokens([]byte{}) if err == nil { t.Fatalf("expected error for empty payload") } } -func TestDeserializeUsers_ValidPayload(t *testing.T) { - username := "admin" - usernameBytes := []byte(username) - payload := make([]byte, 14+len(usernameBytes)) - binary.LittleEndian.PutUint32(payload[0:4], 1) - binary.LittleEndian.PutUint64(payload[4:12], 5555) - payload[12] = 1 // Active - payload[13] = byte(len(usernameBytes)) - copy(payload[14:], usernameBytes) - - users, err := DeserializeUsers(payload) +func TestDeserializeClients_MultipleClients(t *testing.T) { + buildClient := func(id, userId uint32, transport byte, addr string) []byte { + buf := make([]byte, 4+4+1+4+len(addr)+4) + pos := 0 + binary.LittleEndian.PutUint32(buf[pos:], id) + pos += 4 + binary.LittleEndian.PutUint32(buf[pos:], userId) + pos += 4 + buf[pos] = transport + pos++ + binary.LittleEndian.PutUint32(buf[pos:], uint32(len(addr))) + pos += 4 + copy(buf[pos:], addr) + pos += len(addr) + binary.LittleEndian.PutUint32(buf[pos:], 0) // no consumer groups + return buf + } + + c1 := buildClient(1, 10, 1, "127.0.0.1:8090") + c2 := buildClient(2, 20, 2, "127.0.0.1:8091") + payload := append(c1, c2...) + + result, err := DeserializeClients(payload) if err != nil { - t.Fatalf("expected no error, got %v", err) - } - if len(users) != 1 { - t.Fatalf("expected 1 user, got %d", len(users)) + t.Fatalf("unexpected error: %v", err) } - if users[0].Id != 1 { - t.Fatalf("expected Id=1, got %d", users[0].Id) + if len(result) != 2 { + t.Fatalf("expected 2 clients, got %d", len(result)) } - if users[0].CreatedAt != 5555 { - t.Fatalf("expected CreatedAt=5555, got %d", users[0].CreatedAt) + if result[0].Transport != "tcp" { + t.Fatalf("expected tcp, got %s", result[0].Transport) } - if users[0].Status != iggcon.Active { - t.Fatalf("expected Status=Active, got %v", users[0].Status) - } - if users[0].Username != "admin" { - t.Fatalf("expected Username='admin', got '%s'", users[0].Username) + if result[1].Transport != "quic" { + t.Fatalf("expected quic, got %s", result[1].Transport) } } func TestDeserializeClients_EmptyPayload(t *testing.T) { - clients, err := DeserializeClients([]byte{}) + result, err := DeserializeClients([]byte{}) if err != nil { - t.Fatalf("expected no error for empty payload, got %v", err) + t.Fatalf("unexpected error: %v", err) } - if len(clients) != 0 { - t.Fatalf("expected 0 clients, got %d", len(clients)) + if len(result) != 0 { + t.Fatalf("expected 0 clients, got %d", len(result)) } } -func TestDeserializeClients_ValidPayload(t *testing.T) { - address := "1.2.3.4:8090" - addressBytes := []byte(address) - headerSize := 4 + 4 + 1 + 4 + len(addressBytes) + 4 - payload := make([]byte, headerSize) - binary.LittleEndian.PutUint32(payload[0:4], 99) - binary.LittleEndian.PutUint32(payload[4:8], 7) - payload[8] = 1 // Tcp - binary.LittleEndian.PutUint32(payload[9:13], uint32(len(addressBytes))) - copy(payload[13:13+len(addressBytes)], addressBytes) - pos := 13 + len(addressBytes) - binary.LittleEndian.PutUint32(payload[pos:pos+4], 3) - - clients, err := DeserializeClients(payload) - if err != nil { - t.Fatalf("expected no error, got %v", err) +func TestDeserializePermissions_WithStreamsAndTopics(t *testing.T) { + var buf []byte + + // 10 global permission bytes (all true) + for i := 0; i < 10; i++ { + buf = append(buf, 1) } - if len(clients) != 1 { - t.Fatalf("expected 1 client, got %d", len(clients)) + + // hasStreams = 1 (index 10; the outer `index += 1` skips past this) + buf = append(buf, 1) + + // stream id = 42 (starts at index 11 after index += 1) + streamIdBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(streamIdBytes, 42) + buf = append(buf, streamIdBytes...) + + // 6 stream permission bytes (index 15-20) + buf = append(buf, 1, 1, 1, 1, 1, 1) + + // hasTopics for this stream = 1 (index 21; inner `index += 1` skips past this) + buf = append(buf, 1) + + // topic id = 7 (starts at index 22 after inner index += 1) + topicIdBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(topicIdBytes, 7) + buf = append(buf, topicIdBytes...) + + // 4 topic permission bytes (index 26-29) + buf = append(buf, 1, 0, 1, 0) + + // hasMoreTopics = 0 (index 30) + buf = append(buf, 0) + + // hasMoreStreams = 0 (index 31, after outer `index += 1`) + buf = append(buf, 0) + + result := deserializePermissions(buf) + if result == nil { + t.Fatalf("expected non-nil permissions") + } + if !result.Global.ManageServers { + t.Fatalf("expected ManageServers true") + } + if !result.Global.SendMessages { + t.Fatalf("expected SendMessages true") } - if clients[0].ID != 99 { - t.Fatalf("expected ID=99, got %d", clients[0].ID) + + streamPerms, ok := result.Streams[42] + if !ok { + t.Fatalf("expected stream 42 in permissions map") + } + if !streamPerms.ManageStream { + t.Fatalf("expected ManageStream true for stream 42") } - if clients[0].UserID != 7 { - t.Fatalf("expected UserID=7, got %d", clients[0].UserID) + + topicPerms, ok := streamPerms.Topics[7] + if !ok { + t.Fatalf("expected topic 7 in stream 42 permissions") } - if clients[0].Transport != string(iggcon.Tcp) { - t.Fatalf("expected Transport='tcp', got '%s'", clients[0].Transport) + if !topicPerms.ManageTopic { + t.Fatalf("expected ManageTopic true for topic 7") } - if clients[0].Address != address { - t.Fatalf("expected Address='%s', got '%s'", address, clients[0].Address) + if topicPerms.ReadTopic { + t.Fatalf("expected ReadTopic false for topic 7") } - if clients[0].ConsumerGroupsCount != 3 { - t.Fatalf("expected ConsumerGroupsCount=3, got %d", clients[0].ConsumerGroupsCount) + if !topicPerms.PollMessages { + t.Fatalf("expected PollMessages true for topic 7") + } + if topicPerms.SendMessages { + t.Fatalf("expected SendMessages false for topic 7") } } -func TestDeserializeAccessToken(t *testing.T) { - token := "abc123" - tokenBytes := []byte(token) - payload := make([]byte, 1+len(tokenBytes)) - payload[0] = byte(len(tokenBytes)) - copy(payload[1:], tokenBytes) +func TestDeserializePermissions_NoStreams(t *testing.T) { + var buf []byte + for i := 0; i < 10; i++ { + buf = append(buf, 0) + } + buf = append(buf, 0) // hasStreams = false - result, err := DeserializeAccessToken(payload) - if err != nil { - t.Fatalf("expected no error, got %v", err) + result := deserializePermissions(buf) + if result == nil { + t.Fatalf("expected non-nil permissions") + } + if result.Global.ManageServers { + t.Fatalf("expected ManageServers false") } - if result.Token != token { - t.Fatalf("expected Token='%s', got '%s'", token, result.Token) + if len(result.Streams) != 0 { + t.Fatalf("expected 0 streams, got %d", len(result.Streams)) } } -func TestDeserializeAccessTokens_EmptyPayload(t *testing.T) { - _, err := DeserializeAccessTokens([]byte{}) - if !errors.Is(err, ierror.ErrEmptyMessagePayload) { - t.Fatalf("expected ErrEmptyMessagePayload, got %v", err) +func TestDeserializePermissions_StreamWithNoTopics(t *testing.T) { + var buf []byte + for i := 0; i < 10; i++ { + buf = append(buf, 1) } -} + buf = append(buf, 1) // hasStreams (index 10; skipped by outer `index += 1`) -func TestDeserializeAccessTokens_ValidPayload(t *testing.T) { - name := "token1" - nameBytes := []byte(name) - payload := make([]byte, 1+len(nameBytes)+8) - payload[0] = byte(len(nameBytes)) - copy(payload[1:1+len(nameBytes)], nameBytes) - binary.LittleEndian.PutUint64(payload[1+len(nameBytes):], 1000000) + // stream id = 5 (starts at index 11) + streamIdBytes := make([]byte, 4) + binary.LittleEndian.PutUint32(streamIdBytes, 5) + buf = append(buf, streamIdBytes...) - tokens, err := DeserializeAccessTokens(payload) - if err != nil { - t.Fatalf("expected no error, got %v", err) + buf = append(buf, 1, 0, 1, 0, 1, 0) // 6 stream perms (index 15-20) + + buf = append(buf, 0) // hasTopics = false (index 21) + buf = append(buf, 0) // hasMoreStreams = false (index 22, after `index += 1`) + + result := deserializePermissions(buf) + streamPerms, ok := result.Streams[5] + if !ok { + t.Fatalf("expected stream 5") } - if len(tokens) != 1 { - t.Fatalf("expected 1 token, got %d", len(tokens)) + if len(streamPerms.Topics) != 0 { + t.Fatalf("expected 0 topics") } - if tokens[0].Name != name { - t.Fatalf("expected Name='%s', got '%s'", name, tokens[0].Name) + if !streamPerms.ManageStream { + t.Fatalf("expected ManageStream true") } - if tokens[0].Expiry == nil { - t.Fatalf("expected non-nil Expiry") + if streamPerms.ReadStream { + t.Fatalf("expected ReadStream false") } } diff --git a/foreign/go/client/iggy_client_test.go b/foreign/go/client/iggy_client_test.go new file mode 100644 index 0000000000..a1d34b80e2 --- /dev/null +++ b/foreign/go/client/iggy_client_test.go @@ -0,0 +1,61 @@ +// 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 client + +import ( + "testing" + "time" + + "github.com/apache/iggy/foreign/go/client/tcp" + iggcon "github.com/apache/iggy/foreign/go/contracts" +) + +func TestGetDefaultOptions(t *testing.T) { + opts := GetDefaultOptions() + if opts.protocol != iggcon.Tcp { + t.Fatalf("expected protocol Tcp, got %v", opts.protocol) + } + if opts.heartbeatInterval != 5*time.Second { + t.Fatalf("expected heartbeat 5s, got %v", opts.heartbeatInterval) + } + if opts.tcpOptions != nil { + t.Fatalf("expected nil tcpOptions") + } +} + +func TestWithTcp(t *testing.T) { + opts := GetDefaultOptions() + opts.protocol = "quic" + addrOpt := tcp.WithServerAddress("1.2.3.4:9090") + WithTcp(addrOpt)(&opts) + if opts.protocol != iggcon.Tcp { + t.Fatalf("expected protocol Tcp, got %v", opts.protocol) + } + if len(opts.tcpOptions) != 1 { + t.Fatalf("expected 1 tcp option, got %d", len(opts.tcpOptions)) + } +} + +func TestNewIggyClient_UnknownProtocol(t *testing.T) { + _, err := NewIggyClient(func(opts *Options) { + opts.protocol = "unknown" + }) + if err == nil { + t.Fatalf("expected error for unknown protocol") + } +} diff --git a/foreign/go/client/tcp/tcp_client_test.go b/foreign/go/client/tcp/tcp_client_test.go new file mode 100644 index 0000000000..fb800aa6ea --- /dev/null +++ b/foreign/go/client/tcp/tcp_client_test.go @@ -0,0 +1,1194 @@ +// 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 tcp + +import ( + "encoding/binary" + "errors" + "net" + "testing" + + iggcon "github.com/apache/iggy/foreign/go/contracts" + ierror "github.com/apache/iggy/foreign/go/errors" + "github.com/apache/iggy/foreign/go/internal/command" +) + +func buildResponse(status uint32, payload []byte) []byte { + header := make([]byte, 8) + binary.LittleEndian.PutUint32(header[0:4], status) + binary.LittleEndian.PutUint32(header[4:8], uint32(len(payload))) + return append(header, payload...) +} + +func newTestClient(response []byte) (*IggyTcpClient, func()) { + serverConn, clientConn := net.Pipe() + + client := &IggyTcpClient{ + conn: clientConn, + state: iggcon.StateConnected, + currentServerAddress: "127.0.0.1:8090", + clientAddress: "127.0.0.1:12345", + config: defaultTcpClientConfig(), + } + + done := make(chan struct{}) + go func() { + defer close(done) + lenBuf := make([]byte, 4) + if _, err := serverConn.Read(lenBuf); err != nil { + return + } + msgLen := int(binary.LittleEndian.Uint32(lenBuf)) + remaining := make([]byte, msgLen) + total := 0 + for total < msgLen { + n, err := serverConn.Read(remaining[total:]) + if err != nil { + return + } + total += n + } + _, _ = serverConn.Write(response) + }() + + cleanup := func() { + clientConn.Close() + serverConn.Close() + <-done + } + return client, cleanup +} + +func buildStreamPayload(id uint32, createdAt uint64, topicsCount uint32, sizeBytes uint64, messagesCount uint64, name string) []byte { + buf := make([]byte, 33+len(name)) + binary.LittleEndian.PutUint32(buf[0:4], id) + binary.LittleEndian.PutUint64(buf[4:12], createdAt) + binary.LittleEndian.PutUint32(buf[12:16], topicsCount) + binary.LittleEndian.PutUint64(buf[16:24], sizeBytes) + binary.LittleEndian.PutUint64(buf[24:32], messagesCount) + buf[32] = byte(len(name)) + copy(buf[33:], name) + return buf +} + +func buildTopicPayload(id uint32, createdAt uint64, partitionsCount uint32, messageExpiry uint64, compressionAlgorithm byte, maxTopicSize uint64, replicationFactor byte, size uint64, messagesCount uint64, name string) []byte { + buf := make([]byte, 51+len(name)) + binary.LittleEndian.PutUint32(buf[0:4], id) + binary.LittleEndian.PutUint64(buf[4:12], createdAt) + binary.LittleEndian.PutUint32(buf[12:16], partitionsCount) + binary.LittleEndian.PutUint64(buf[16:24], messageExpiry) + buf[24] = compressionAlgorithm + binary.LittleEndian.PutUint64(buf[25:33], maxTopicSize) + buf[33] = replicationFactor + binary.LittleEndian.PutUint64(buf[34:42], size) + binary.LittleEndian.PutUint64(buf[42:50], messagesCount) + buf[50] = byte(len(name)) + copy(buf[51:], name) + return buf +} + +func buildConsumerGroupPayload(id, partitionsCount, membersCount uint32, name string) []byte { + buf := make([]byte, 13+len(name)) + binary.LittleEndian.PutUint32(buf[0:4], id) + binary.LittleEndian.PutUint32(buf[4:8], partitionsCount) + binary.LittleEndian.PutUint32(buf[8:12], membersCount) + buf[12] = byte(len(name)) + copy(buf[13:], name) + return buf +} + +func buildClientInfoPayload(id, userId uint32, transport byte, address string, consumerGroupsCount uint32) []byte { + buf := make([]byte, 13+len(address)+4) + binary.LittleEndian.PutUint32(buf[0:4], id) + binary.LittleEndian.PutUint32(buf[4:8], userId) + buf[8] = transport + binary.LittleEndian.PutUint32(buf[9:13], uint32(len(address))) + copy(buf[13:13+len(address)], address) + binary.LittleEndian.PutUint32(buf[13+len(address):], consumerGroupsCount) + return buf +} + +func buildOffsetPayload(partitionId uint32, currentOffset, storedOffset uint64) []byte { + buf := make([]byte, 20) + binary.LittleEndian.PutUint32(buf[0:4], partitionId) + binary.LittleEndian.PutUint64(buf[4:12], currentOffset) + binary.LittleEndian.PutUint64(buf[12:20], storedOffset) + return buf +} + +func mustIdentifier(id uint32) iggcon.Identifier { + ident, err := iggcon.NewIdentifier(id) + if err != nil { + panic(err) + } + return ident +} + +// --- Tests --- + +func TestPing(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.Ping() + if err != nil { + t.Fatalf("Ping() returned unexpected error: %v", err) + } +} + +func TestPing_ServerError(t *testing.T) { + resp := buildResponse(3, nil) // InvalidCommand code = 3 + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.Ping() + if err == nil { + t.Fatalf("Ping() expected error, got nil") + } + + var iggyErr ierror.IggyError + if !errors.As(err, &iggyErr) { + t.Fatalf("expected IggyError, got %T: %v", err, err) + } + if iggyErr.Code() != 3 { + t.Fatalf("expected error code 3 (InvalidCommand), got %d", iggyErr.Code()) + } +} + +func TestGetStreams(t *testing.T) { + streamBytes := buildStreamPayload(1, 100, 2, 1024, 50, "test") + resp := buildResponse(0, streamBytes) + client, cleanup := newTestClient(resp) + defer cleanup() + + streams, err := client.GetStreams() + if err != nil { + t.Fatalf("GetStreams() error: %v", err) + } + if len(streams) != 1 { + t.Fatalf("expected 1 stream, got %d", len(streams)) + } + if streams[0].Id != 1 { + t.Fatalf("expected stream id=1, got %d", streams[0].Id) + } + if streams[0].CreatedAt != 100 { + t.Fatalf("expected createdAt=100, got %d", streams[0].CreatedAt) + } + if streams[0].TopicsCount != 2 { + t.Fatalf("expected topicsCount=2, got %d", streams[0].TopicsCount) + } + if streams[0].SizeBytes != 1024 { + t.Fatalf("expected sizeBytes=1024, got %d", streams[0].SizeBytes) + } + if streams[0].MessagesCount != 50 { + t.Fatalf("expected messagesCount=50, got %d", streams[0].MessagesCount) + } + if streams[0].Name != "test" { + t.Fatalf("expected name='test', got '%s'", streams[0].Name) + } +} + +func TestGetStream(t *testing.T) { + streamBytes := buildStreamPayload(1, 200, 1, 2048, 100, "mystream") + topicBytes := buildTopicPayload(10, 300, 3, 0, 1, 0, 1, 512, 25, "mytopic") + payload := append(streamBytes, topicBytes...) + resp := buildResponse(0, payload) + client, cleanup := newTestClient(resp) + defer cleanup() + + details, err := client.GetStream(mustIdentifier(1)) + if err != nil { + t.Fatalf("GetStream() error: %v", err) + } + if details.Id != 1 { + t.Fatalf("expected stream id=1, got %d", details.Id) + } + if details.Name != "mystream" { + t.Fatalf("expected name='mystream', got '%s'", details.Name) + } + if len(details.Topics) != 1 { + t.Fatalf("expected 1 topic, got %d", len(details.Topics)) + } + if details.Topics[0].Id != 10 { + t.Fatalf("expected topic id=10, got %d", details.Topics[0].Id) + } + if details.Topics[0].Name != "mytopic" { + t.Fatalf("expected topic name='mytopic', got '%s'", details.Topics[0].Name) + } +} + +func TestGetStream_EmptyBuffer(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + _, err := client.GetStream(mustIdentifier(1)) + if err == nil { + t.Fatalf("expected ErrStreamIdNotFound, got nil") + } + if !errors.Is(err, ierror.ErrStreamIdNotFound) { + t.Fatalf("expected ErrStreamIdNotFound, got: %v", err) + } +} + +func TestCreateStream_ValidatesNameLength(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + _, err := client.CreateStream("") + if err == nil { + t.Fatalf("expected ErrInvalidStreamName for empty name, got nil") + } + if !errors.Is(err, ierror.ErrInvalidStreamName) { + t.Fatalf("expected ErrInvalidStreamName, got: %v", err) + } +} + +func TestCreateStream(t *testing.T) { + streamBytes := buildStreamPayload(5, 999, 0, 0, 0, "newstream") + resp := buildResponse(0, streamBytes) + client, cleanup := newTestClient(resp) + defer cleanup() + + details, err := client.CreateStream("newstream") + if err != nil { + t.Fatalf("CreateStream() error: %v", err) + } + if details.Id != 5 { + t.Fatalf("expected id=5, got %d", details.Id) + } + if details.Name != "newstream" { + t.Fatalf("expected name='newstream', got '%s'", details.Name) + } +} + +func TestUpdateStream(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.UpdateStream(mustIdentifier(1), "updated") + if err != nil { + t.Fatalf("UpdateStream() unexpected error: %v", err) + } +} + +func TestUpdateStream_EmptyName(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + err := client.UpdateStream(mustIdentifier(1), "") + if !errors.Is(err, ierror.ErrInvalidStreamName) { + t.Fatalf("expected ErrInvalidStreamName, got: %v", err) + } +} + +func TestDeleteStream(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.DeleteStream(mustIdentifier(1)) + if err != nil { + t.Fatalf("DeleteStream() unexpected error: %v", err) + } +} + +func TestGetTopics(t *testing.T) { + topicBytes := buildTopicPayload(1, 100, 2, 0, 1, 0, 1, 256, 10, "topic1") + resp := buildResponse(0, topicBytes) + client, cleanup := newTestClient(resp) + defer cleanup() + + topics, err := client.GetTopics(mustIdentifier(1)) + if err != nil { + t.Fatalf("GetTopics() error: %v", err) + } + if len(topics) != 1 { + t.Fatalf("expected 1 topic, got %d", len(topics)) + } + if topics[0].Id != 1 { + t.Fatalf("expected topic id=1, got %d", topics[0].Id) + } + if topics[0].Name != "topic1" { + t.Fatalf("expected name='topic1', got '%s'", topics[0].Name) + } + if topics[0].PartitionsCount != 2 { + t.Fatalf("expected partitionsCount=2, got %d", topics[0].PartitionsCount) + } +} + +func TestCreateTopic_ValidatesName(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + rf := uint8(1) + _, err := client.CreateTopic(mustIdentifier(1), "", 1, iggcon.CompressionAlgorithmNone, 0, 0, &rf) + if !errors.Is(err, ierror.ErrInvalidTopicName) { + t.Fatalf("expected ErrInvalidTopicName, got: %v", err) + } +} + +func TestCreateTopic_ValidatesPartitionCount(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + rf := uint8(1) + _, err := client.CreateTopic(mustIdentifier(1), "t", 1001, iggcon.CompressionAlgorithmNone, 0, 0, &rf) + if !errors.Is(err, ierror.ErrTooManyPartitions) { + t.Fatalf("expected ErrTooManyPartitions, got: %v", err) + } +} + +func TestCreateTopic_ValidatesReplicationFactor(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + rf := uint8(0) + _, err := client.CreateTopic(mustIdentifier(1), "t", 1, iggcon.CompressionAlgorithmNone, 0, 0, &rf) + if !errors.Is(err, ierror.ErrInvalidReplicationFactor) { + t.Fatalf("expected ErrInvalidReplicationFactor, got: %v", err) + } +} + +func TestDeleteTopic(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.DeleteTopic(mustIdentifier(1), mustIdentifier(1)) + if err != nil { + t.Fatalf("DeleteTopic() unexpected error: %v", err) + } +} + +func TestCreatePartitions(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.CreatePartitions(mustIdentifier(1), mustIdentifier(1), 3) + if err != nil { + t.Fatalf("CreatePartitions() unexpected error: %v", err) + } +} + +func TestDeletePartitions(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.DeletePartitions(mustIdentifier(1), mustIdentifier(1), 2) + if err != nil { + t.Fatalf("DeletePartitions() unexpected error: %v", err) + } +} + +func TestSendMessages_ValidatesEmptyMessages(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + err := client.SendMessages(mustIdentifier(1), mustIdentifier(1), iggcon.None(), []iggcon.IggyMessage{}) + if !errors.Is(err, ierror.ErrInvalidMessagesCount) { + t.Fatalf("expected ErrInvalidMessagesCount, got: %v", err) + } +} + +func TestSendMessages(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + msg, err := iggcon.NewIggyMessage([]byte("hello")) + if err != nil { + t.Fatalf("NewIggyMessage() error: %v", err) + } + + err = client.SendMessages(mustIdentifier(1), mustIdentifier(1), iggcon.None(), []iggcon.IggyMessage{msg}) + if err != nil { + t.Fatalf("SendMessages() unexpected error: %v", err) + } +} + +func TestPollMessages(t *testing.T) { + payloadData := []byte("hello world") + header := iggcon.MessageHeader{ + Checksum: 42, + Offset: 0, + Timestamp: 1000, + OriginTimestamp: 2000, + UserHeaderLength: 0, + PayloadLength: uint32(len(payloadData)), + } + headerBytes := header.ToBytes() + + pollPayload := make([]byte, 16+len(headerBytes)+len(payloadData)) + binary.LittleEndian.PutUint32(pollPayload[0:4], 1) // partitionId + binary.LittleEndian.PutUint64(pollPayload[4:12], 99) // currentOffset + binary.LittleEndian.PutUint32(pollPayload[12:16], 1) // messagesCount + copy(pollPayload[16:16+len(headerBytes)], headerBytes) + copy(pollPayload[16+len(headerBytes):], payloadData) + + resp := buildResponse(0, pollPayload) + client, cleanup := newTestClient(resp) + defer cleanup() + + partId := uint32(1) + result, err := client.PollMessages( + mustIdentifier(1), + mustIdentifier(1), + iggcon.DefaultConsumer(), + iggcon.OffsetPollingStrategy(0), + 10, + false, + &partId, + ) + if err != nil { + t.Fatalf("PollMessages() error: %v", err) + } + if result.PartitionId != 1 { + t.Fatalf("expected partitionId=1, got %d", result.PartitionId) + } + if result.CurrentOffset != 99 { + t.Fatalf("expected currentOffset=99, got %d", result.CurrentOffset) + } + if len(result.Messages) != 1 { + t.Fatalf("expected 1 message, got %d", len(result.Messages)) + } + if string(result.Messages[0].Payload) != "hello world" { + t.Fatalf("expected payload 'hello world', got '%s'", string(result.Messages[0].Payload)) + } +} + +func TestGetConsumerOffset(t *testing.T) { + payload := buildOffsetPayload(1, 100, 50) + resp := buildResponse(0, payload) + client, cleanup := newTestClient(resp) + defer cleanup() + + partId := uint32(1) + offset, err := client.GetConsumerOffset(iggcon.DefaultConsumer(), mustIdentifier(1), mustIdentifier(1), &partId) + if err != nil { + t.Fatalf("GetConsumerOffset() error: %v", err) + } + if offset.PartitionId != 1 { + t.Fatalf("expected partitionId=1, got %d", offset.PartitionId) + } + if offset.CurrentOffset != 100 { + t.Fatalf("expected currentOffset=100, got %d", offset.CurrentOffset) + } + if offset.StoredOffset != 50 { + t.Fatalf("expected storedOffset=50, got %d", offset.StoredOffset) + } +} + +func TestStoreConsumerOffset(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + partId := uint32(1) + err := client.StoreConsumerOffset(iggcon.DefaultConsumer(), mustIdentifier(1), mustIdentifier(1), 42, &partId) + if err != nil { + t.Fatalf("StoreConsumerOffset() unexpected error: %v", err) + } +} + +func TestDeleteConsumerOffset(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + partId := uint32(1) + err := client.DeleteConsumerOffset(iggcon.DefaultConsumer(), mustIdentifier(1), mustIdentifier(1), &partId) + if err != nil { + t.Fatalf("DeleteConsumerOffset() unexpected error: %v", err) + } +} + +func TestGetConsumerGroups(t *testing.T) { + cgPayload := buildConsumerGroupPayload(1, 3, 2, "group1") + resp := buildResponse(0, cgPayload) + client, cleanup := newTestClient(resp) + defer cleanup() + + groups, err := client.GetConsumerGroups(mustIdentifier(1), mustIdentifier(1)) + if err != nil { + t.Fatalf("GetConsumerGroups() error: %v", err) + } + if len(groups) != 1 { + t.Fatalf("expected 1 consumer group, got %d", len(groups)) + } + if groups[0].Id != 1 { + t.Fatalf("expected group id=1, got %d", groups[0].Id) + } + if groups[0].Name != "group1" { + t.Fatalf("expected name='group1', got '%s'", groups[0].Name) + } + if groups[0].PartitionsCount != 3 { + t.Fatalf("expected partitionsCount=3, got %d", groups[0].PartitionsCount) + } + if groups[0].MembersCount != 2 { + t.Fatalf("expected membersCount=2, got %d", groups[0].MembersCount) + } +} + +func TestCreateConsumerGroup_ValidatesName(t *testing.T) { + client := &IggyTcpClient{state: iggcon.StateConnected} + + _, err := client.CreateConsumerGroup(mustIdentifier(1), mustIdentifier(1), "") + if !errors.Is(err, ierror.ErrInvalidConsumerGroupName) { + t.Fatalf("expected ErrInvalidConsumerGroupName, got: %v", err) + } +} + +func TestJoinConsumerGroup(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.JoinConsumerGroup(mustIdentifier(1), mustIdentifier(1), mustIdentifier(1)) + if err != nil { + t.Fatalf("JoinConsumerGroup() unexpected error: %v", err) + } +} + +func TestLeaveConsumerGroup(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.LeaveConsumerGroup(mustIdentifier(1), mustIdentifier(1), mustIdentifier(1)) + if err != nil { + t.Fatalf("LeaveConsumerGroup() unexpected error: %v", err) + } +} + +func TestDeleteConsumerGroup(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.DeleteConsumerGroup(mustIdentifier(1), mustIdentifier(1), mustIdentifier(1)) + if err != nil { + t.Fatalf("DeleteConsumerGroup() unexpected error: %v", err) + } +} + +func TestLogoutUser(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.LogoutUser() + if err != nil { + t.Fatalf("LogoutUser() unexpected error: %v", err) + } +} + +func TestGetClients(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + clients, err := client.GetClients() + if err != nil { + t.Fatalf("GetClients() error: %v", err) + } + if len(clients) != 0 { + t.Fatalf("expected 0 clients, got %d", len(clients)) + } +} + +func TestGetClient(t *testing.T) { + payload := buildClientInfoPayload(42, 7, 1, "192.168.1.1:5000", 0) + resp := buildResponse(0, payload) + client, cleanup := newTestClient(resp) + defer cleanup() + + info, err := client.GetClient(42) + if err != nil { + t.Fatalf("GetClient() error: %v", err) + } + if info.ClientInfo.ID != 42 { + t.Fatalf("expected client id=42, got %d", info.ClientInfo.ID) + } + if info.ClientInfo.UserID != 7 { + t.Fatalf("expected userId=7, got %d", info.ClientInfo.UserID) + } + if info.ClientInfo.Address != "192.168.1.1:5000" { + t.Fatalf("expected address='192.168.1.1:5000', got '%s'", info.ClientInfo.Address) + } + if info.ClientInfo.Transport != string(iggcon.Tcp) { + t.Fatalf("expected transport='tcp', got '%s'", info.ClientInfo.Transport) + } +} + +func TestGetConnectionInfo(t *testing.T) { + client := &IggyTcpClient{ + state: iggcon.StateConnected, + currentServerAddress: "10.0.0.1:9090", + } + + info := client.GetConnectionInfo() + if info.Protocol != iggcon.Tcp { + t.Fatalf("expected protocol=Tcp, got %s", info.Protocol) + } + if info.ServerAddress != "10.0.0.1:9090" { + t.Fatalf("expected address='10.0.0.1:9090', got '%s'", info.ServerAddress) + } +} + +func TestClose(t *testing.T) { + _, clientConn := net.Pipe() + client := &IggyTcpClient{ + conn: clientConn, + state: iggcon.StateConnected, + } + + err := client.Close() + if err != nil { + t.Fatalf("Close() unexpected error: %v", err) + } + if client.state != iggcon.StateShutdown { + t.Fatalf("expected state=Shutdown after Close, got %s", client.state) + } +} + +func TestCreatePayload(t *testing.T) { + message := []byte{0xAA, 0xBB} + result := createPayload(message, command.PingCode) + + expectedLen := uint32(len(message) + 4) + gotLen := binary.LittleEndian.Uint32(result[0:4]) + if gotLen != expectedLen { + t.Fatalf("expected message length=%d, got %d", expectedLen, gotLen) + } + + gotCmd := binary.LittleEndian.Uint32(result[4:8]) + if gotCmd != uint32(command.PingCode) { + t.Fatalf("expected command code=%d, got %d", command.PingCode, gotCmd) + } + + if result[8] != 0xAA || result[9] != 0xBB { + t.Fatalf("expected message bytes [0xAA, 0xBB], got [0x%X, 0x%X]", result[8], result[9]) + } + + if len(result) != 10 { + t.Fatalf("expected total length=10, got %d", len(result)) + } +} + +func TestGetDefaultOptions(t *testing.T) { + opts := GetDefaultOptions() + if opts.config.serverAddress != "127.0.0.1:8090" { + t.Fatalf("expected default address='127.0.0.1:8090', got '%s'", opts.config.serverAddress) + } + if opts.config.tlsEnabled { + t.Fatalf("expected TLS disabled by default") + } +} + +func TestWithServerAddress(t *testing.T) { + opts := GetDefaultOptions() + opt := WithServerAddress("10.0.0.1:9999") + opt(&opts) + + if opts.config.serverAddress != "10.0.0.1:9999" { + t.Fatalf("expected address='10.0.0.1:9999', got '%s'", opts.config.serverAddress) + } +} + +func TestWithTLS(t *testing.T) { + opts := GetDefaultOptions() + opt := WithTLS() + opt(&opts) + + if !opts.config.tlsEnabled { + t.Fatalf("expected TLS to be enabled after WithTLS()") + } +} + +func TestNewCredentials(t *testing.T) { + creds := NewUsernamePasswordCredentials("admin", "secret") + if creds.username != "admin" { + t.Fatalf("expected username='admin', got '%s'", creds.username) + } + if creds.password != "secret" { + t.Fatalf("expected password='secret', got '%s'", creds.password) + } + + tokenCreds := NewPersonalAccessTokenCredentials("my-token-123") + if tokenCreds.personalAccessToken != "my-token-123" { + t.Fatalf("expected token='my-token-123', got '%s'", tokenCreds.personalAccessToken) + } +} + +func TestCreatePersonalAccessToken(t *testing.T) { + tokenStr := "abc123def456" + tokenPayload := make([]byte, 1+len(tokenStr)) + tokenPayload[0] = byte(len(tokenStr)) + copy(tokenPayload[1:], tokenStr) + resp := buildResponse(0, tokenPayload) + client, cleanup := newTestClient(resp) + defer cleanup() + + token, err := client.CreatePersonalAccessToken("mytoken", 3600) + if err != nil { + t.Fatalf("CreatePersonalAccessToken() error: %v", err) + } + if token.Token != "abc123def456" { + t.Fatalf("expected token='abc123def456', got '%s'", token.Token) + } +} + +func TestDeletePersonalAccessToken(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + err := client.DeletePersonalAccessToken("mytoken") + if err != nil { + t.Fatalf("DeletePersonalAccessToken() unexpected error: %v", err) + } +} + +func TestGetPersonalAccessTokens(t *testing.T) { + name := "tkn" + entry := make([]byte, 1+len(name)+8) + entry[0] = byte(len(name)) + copy(entry[1:], name) + binary.LittleEndian.PutUint64(entry[1+len(name):], 5000000) + resp := buildResponse(0, entry) + client, cleanup := newTestClient(resp) + defer cleanup() + + tokens, err := client.GetPersonalAccessTokens() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(tokens) != 1 { + t.Fatalf("expected 1 token, got %d", len(tokens)) + } + if tokens[0].Name != "tkn" { + t.Fatalf("expected token name 'tkn', got '%s'", tokens[0].Name) + } +} + +func TestGetConsumerGroup(t *testing.T) { + name := "cg-1" + buf := make([]byte, 13+len(name)) + binary.LittleEndian.PutUint32(buf[0:], 5) + binary.LittleEndian.PutUint32(buf[4:], 2) + binary.LittleEndian.PutUint32(buf[8:], 0) // no members + buf[12] = byte(len(name)) + copy(buf[13:], name) + resp := buildResponse(0, buf) + client, cleanup := newTestClient(resp) + defer cleanup() + + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + gid, _ := iggcon.NewIdentifier(uint32(5)) + result, err := client.GetConsumerGroup(sid, tid, gid) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result.ConsumerGroup.Name != "cg-1" { + t.Fatalf("expected name 'cg-1', got '%s'", result.ConsumerGroup.Name) + } +} + +func TestGetConsumerGroup_EmptyBuffer(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + gid, _ := iggcon.NewIdentifier(uint32(5)) + _, err := client.GetConsumerGroup(sid, tid, gid) + if !errors.Is(err, ierror.ErrConsumerGroupIdNotFound) { + t.Fatalf("expected ErrConsumerGroupIdNotFound, got %v", err) + } +} + +func TestWithTLSValidateCertificate(t *testing.T) { + opts := GetDefaultOptions() + WithTLS(WithTLSValidateCertificate(false))(&opts) + if opts.config.tls.tlsValidateCertificate { + t.Fatalf("expected validate=false") + } + if !opts.config.tlsEnabled { + t.Fatalf("expected TLS enabled") + } +} + +func TestNewAutoLogin(t *testing.T) { + creds := NewUsernamePasswordCredentials("user", "pass") + al := NewAutoLogin(creds) + if !al.enabled { + t.Fatalf("expected enabled=true") + } + if al.credentials.username != "user" { + t.Fatalf("expected username 'user', got '%s'", al.credentials.username) + } + if al.credentials.password != "pass" { + t.Fatalf("expected password 'pass', got '%s'", al.credentials.password) + } +} + +func TestDisconnect(t *testing.T) { + _, clientConn := net.Pipe() + client := &IggyTcpClient{ + conn: clientConn, + state: iggcon.StateConnected, + } + err := client.disconnect() + if err != nil { + t.Fatalf("disconnect() unexpected error: %v", err) + } + if client.state != iggcon.StateDisconnected { + t.Fatalf("expected state Disconnected") + } +} + +func TestDisconnect_AlreadyDisconnected(t *testing.T) { + client := &IggyTcpClient{ + state: iggcon.StateDisconnected, + } + err := client.disconnect() + if err != nil { + t.Fatalf("disconnect() unexpected error: %v", err) + } +} + +func TestShutdown_AlreadyShutdown(t *testing.T) { + client := &IggyTcpClient{ + state: iggcon.StateShutdown, + } + err := client.shutdown() + if err != nil { + t.Fatalf("shutdown() unexpected error: %v", err) + } +} + +func newMultiResponseTestClient(responses [][]byte) (*IggyTcpClient, func()) { + serverConn, clientConn := net.Pipe() + + client := &IggyTcpClient{ + conn: clientConn, + state: iggcon.StateConnected, + currentServerAddress: "127.0.0.1:8090", + clientAddress: "127.0.0.1:12345", + config: defaultTcpClientConfig(), + } + + done := make(chan struct{}) + go func() { + defer close(done) + for _, resp := range responses { + lenBuf := make([]byte, 4) + if _, err := serverConn.Read(lenBuf); err != nil { + return + } + msgLen := int(binary.LittleEndian.Uint32(lenBuf)) + remaining := make([]byte, msgLen) + total := 0 + for total < msgLen { + n, err := serverConn.Read(remaining[total:]) + if err != nil { + return + } + total += n + } + if _, err := serverConn.Write(resp); err != nil { + return + } + } + }() + + cleanup := func() { + clientConn.Close() + serverConn.Close() + <-done + } + return client, cleanup +} + +func TestLoginUser(t *testing.T) { + identityPayload := make([]byte, 4) + binary.LittleEndian.PutUint32(identityPayload[0:], 42) + loginResp := buildResponse(0, identityPayload) + + // GetClusterMetadata response: nameLen(4) + name + nodesCount(4)=0 + clusterName := "c" + metaPayload := make([]byte, 4+len(clusterName)+4) + binary.LittleEndian.PutUint32(metaPayload[0:], uint32(len(clusterName))) + copy(metaPayload[4:], clusterName) + binary.LittleEndian.PutUint32(metaPayload[4+len(clusterName):], 0) + metaResp := buildResponse(0, metaPayload) + + client, cleanup := newMultiResponseTestClient([][]byte{loginResp, metaResp}) + defer cleanup() + + identity, err := client.LoginUser("admin", "password") + if err != nil { + t.Fatalf("LoginUser() unexpected error: %v", err) + } + if identity.UserId != 42 { + t.Fatalf("expected userId=42, got %d", identity.UserId) + } +} + +func TestLoginWithPersonalAccessToken(t *testing.T) { + identityPayload := make([]byte, 4) + binary.LittleEndian.PutUint32(identityPayload[0:], 7) + loginResp := buildResponse(0, identityPayload) + + clusterName := "c" + metaPayload := make([]byte, 4+len(clusterName)+4) + binary.LittleEndian.PutUint32(metaPayload[0:], uint32(len(clusterName))) + copy(metaPayload[4:], clusterName) + binary.LittleEndian.PutUint32(metaPayload[4+len(clusterName):], 0) + metaResp := buildResponse(0, metaPayload) + + client, cleanup := newMultiResponseTestClient([][]byte{loginResp, metaResp}) + defer cleanup() + + identity, err := client.LoginWithPersonalAccessToken("my-token") + if err != nil { + t.Fatalf("LoginWithPersonalAccessToken() unexpected error: %v", err) + } + if identity.UserId != 7 { + t.Fatalf("expected userId=7, got %d", identity.UserId) + } +} + +func TestGetUser(t *testing.T) { + name := "admin" + userPart := make([]byte, 14+len(name)) + binary.LittleEndian.PutUint32(userPart[0:], 1) + binary.LittleEndian.PutUint64(userPart[4:], 999) + userPart[12] = 1 // Active + userPart[13] = byte(len(name)) + copy(userPart[14:], name) + + payload := append(userPart, 0) // hasPermissions=0 + resp := buildResponse(0, payload) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + result, err := client.GetUser(uid) + if err != nil { + t.Fatalf("GetUser() unexpected error: %v", err) + } + if result.UserInfo.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) + } +} + +func TestGetUser_EmptyBuffer(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + _, err := client.GetUser(uid) + if !errors.Is(err, ierror.ErrResourceNotFound) { + t.Fatalf("expected ErrResourceNotFound, got %v", err) + } +} + +func TestGetUsers(t *testing.T) { + name := "user1" + buf := make([]byte, 14+len(name)) + binary.LittleEndian.PutUint32(buf[0:], 10) + binary.LittleEndian.PutUint64(buf[4:], 500) + buf[12] = 1 + buf[13] = byte(len(name)) + copy(buf[14:], name) + resp := buildResponse(0, buf) + client, cleanup := newTestClient(resp) + defer cleanup() + + result, err := client.GetUsers() + if err != nil { + t.Fatalf("GetUsers() unexpected error: %v", err) + } + if len(result) != 1 { + t.Fatalf("expected 1 user, got %d", len(result)) + } + if result[0].Username != "user1" { + t.Fatalf("expected 'user1', got '%s'", result[0].Username) + } +} + +func TestCreateUser(t *testing.T) { + name := "newuser" + buf := make([]byte, 14+len(name)+1) + binary.LittleEndian.PutUint32(buf[0:], 5) + binary.LittleEndian.PutUint64(buf[4:], 300) + buf[12] = 1 + buf[13] = byte(len(name)) + copy(buf[14:], name) + buf[14+len(name)] = 0 // no permissions + resp := buildResponse(0, buf) + client, cleanup := newTestClient(resp) + defer cleanup() + + result, err := client.CreateUser("newuser", "pass123", iggcon.Active, nil) + if err != nil { + t.Fatalf("CreateUser() unexpected error: %v", err) + } + if result.UserInfo.Username != "newuser" { + t.Fatalf("expected 'newuser', got '%s'", result.UserInfo.Username) + } +} + +func TestUpdateUser(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + newName := "updated" + err := client.UpdateUser(uid, &newName, nil) + if err != nil { + t.Fatalf("UpdateUser() unexpected error: %v", err) + } +} + +func TestDeleteUser(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + err := client.DeleteUser(uid) + if err != nil { + t.Fatalf("DeleteUser() unexpected error: %v", err) + } +} + +func TestUpdatePermissions(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + err := client.UpdatePermissions(uid, nil) + if err != nil { + t.Fatalf("UpdatePermissions() unexpected error: %v", err) + } +} + +func TestChangePassword(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + uid, _ := iggcon.NewIdentifier(uint32(1)) + err := client.ChangePassword(uid, "old", "new") + if err != nil { + t.Fatalf("ChangePassword() unexpected error: %v", err) + } +} + +func TestGetClusterMetadata(t *testing.T) { + // Build ClusterMetadata binary: nameLen(4) + name + nodesCount(4) + name := "cluster-1" + buf := make([]byte, 4+len(name)+4) + binary.LittleEndian.PutUint32(buf[0:], uint32(len(name))) + copy(buf[4:], name) + binary.LittleEndian.PutUint32(buf[4+len(name):], 0) // 0 nodes + resp := buildResponse(0, buf) + client, cleanup := newTestClient(resp) + defer cleanup() + + result, err := client.GetClusterMetadata() + if err != nil { + t.Fatalf("GetClusterMetadata() unexpected error: %v", err) + } + if result.Name != "cluster-1" { + t.Fatalf("expected name 'cluster-1', got '%s'", result.Name) + } + if len(result.Nodes) != 0 { + t.Fatalf("expected 0 nodes, got %d", len(result.Nodes)) + } +} + +func TestUpdateTopic(t *testing.T) { + resp := buildResponse(0, nil) + client, cleanup := newTestClient(resp) + defer cleanup() + + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + err := client.UpdateTopic(sid, tid, "newname", 0, 0, 0, nil) + if err != nil { + t.Fatalf("UpdateTopic() unexpected error: %v", err) + } +} + +func TestUpdateTopic_InvalidName(t *testing.T) { + client := &IggyTcpClient{} + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + err := client.UpdateTopic(sid, tid, "", 0, 0, 0, nil) + if !errors.Is(err, ierror.ErrInvalidTopicName) { + t.Fatalf("expected ErrInvalidTopicName, got %v", err) + } +} + +func TestUpdateTopic_InvalidReplicationFactor(t *testing.T) { + client := &IggyTcpClient{} + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + zero := uint8(0) + err := client.UpdateTopic(sid, tid, "name", 0, 0, 0, &zero) + if !errors.Is(err, ierror.ErrInvalidReplicationFactor) { + t.Fatalf("expected ErrInvalidReplicationFactor, got %v", err) + } +} + +func TestSendMessages_InvalidPartitioning(t *testing.T) { + client := &IggyTcpClient{} + sid, _ := iggcon.NewIdentifier(uint32(1)) + tid, _ := iggcon.NewIdentifier(uint32(2)) + msgs := make([]iggcon.IggyMessage, 1) + msgs[0], _ = iggcon.NewIggyMessage([]byte("data")) + + longValue := make([]byte, 256) + badPart := iggcon.Partitioning{Kind: iggcon.PartitionIdKind, Value: longValue} + err := client.SendMessages(sid, tid, badPart, msgs) + if !errors.Is(err, ierror.ErrInvalidKeyValueLength) { + t.Fatalf("expected ErrInvalidKeyValueLength, got %v", err) + } +} + +func TestWriteError(t *testing.T) { + serverConn, clientConn := net.Pipe() + client := &IggyTcpClient{ + conn: clientConn, + state: iggcon.StateConnected, + } + clientConn.Close() + serverConn.Close() + + _, err := client.sendAndFetchResponse([]byte{0}, command.PingCode) + if err == nil { + t.Fatalf("expected write error") + } +} diff --git a/foreign/go/errors/errors_gen_test.go b/foreign/go/errors/errors_gen_test.go new file mode 100644 index 0000000000..2b452eb6fa --- /dev/null +++ b/foreign/go/errors/errors_gen_test.go @@ -0,0 +1,748 @@ +// 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 ierror + +import ( + "errors" + "fmt" + "testing" +) + +func TestFromCode_ReturnsCorrectErrors(t *testing.T) { + tests := []struct { + name string + code Code + err IggyError + }{ + {"Error", ErrorCode, ErrError}, + {"InvalidConfiguration", InvalidConfigurationCode, ErrInvalidConfiguration}, + {"InvalidCommand", InvalidCommandCode, ErrInvalidCommand}, + {"InvalidFormat", InvalidFormatCode, ErrInvalidFormat}, + {"FeatureUnavailable", FeatureUnavailableCode, ErrFeatureUnavailable}, + {"InvalidIdentifier", InvalidIdentifierCode, ErrInvalidIdentifier}, + {"InvalidVersion", InvalidVersionCode, ErrInvalidVersion}, + {"Disconnected", DisconnectedCode, ErrDisconnected}, + {"CannotEstablishConnection", CannotEstablishConnectionCode, ErrCannotEstablishConnection}, + {"CannotCreateBaseDirectory", CannotCreateBaseDirectoryCode, ErrCannotCreateBaseDirectory}, + {"CannotCreateRuntimeDirectory", CannotCreateRuntimeDirectoryCode, ErrCannotCreateRuntimeDirectory}, + {"CannotRemoveRuntimeDirectory", CannotRemoveRuntimeDirectoryCode, ErrCannotRemoveRuntimeDirectory}, + {"CannotCreateStateDirectory", CannotCreateStateDirectoryCode, ErrCannotCreateStateDirectory}, + {"StateFileNotFound", StateFileNotFoundCode, ErrStateFileNotFound}, + {"StateFileCorrupted", StateFileCorruptedCode, ErrStateFileCorrupted}, + {"InvalidStateEntryChecksum", InvalidStateEntryChecksumCode, ErrInvalidStateEntryChecksum}, + {"CannotOpenDatabase", CannotOpenDatabaseCode, ErrCannotOpenDatabase}, + {"ResourceNotFound", ResourceNotFoundCode, ErrResourceNotFound}, + {"StaleClient", StaleClientCode, ErrStaleClient}, + {"TcpError", TcpErrorCode, ErrTcpError}, + {"QuicError", QuicErrorCode, ErrQuicError}, + {"InvalidServerAddress", InvalidServerAddressCode, ErrInvalidServerAddress}, + {"InvalidClientAddress", InvalidClientAddressCode, ErrInvalidClientAddress}, + {"InvalidIpAddress", InvalidIpAddressCode, ErrInvalidIpAddress}, + {"Unauthenticated", UnauthenticatedCode, ErrUnauthenticated}, + {"Unauthorized", UnauthorizedCode, ErrUnauthorized}, + {"InvalidCredentials", InvalidCredentialsCode, ErrInvalidCredentials}, + {"InvalidUsername", InvalidUsernameCode, ErrInvalidUsername}, + {"InvalidPassword", InvalidPasswordCode, ErrInvalidPassword}, + {"InvalidUserStatus", InvalidUserStatusCode, ErrInvalidUserStatus}, + {"UserAlreadyExists", UserAlreadyExistsCode, ErrUserAlreadyExists}, + {"UserInactive", UserInactiveCode, ErrUserInactive}, + {"CannotDeleteUser", CannotDeleteUserCode, ErrCannotDeleteUser}, + {"CannotChangePermissions", CannotChangePermissionsCode, ErrCannotChangePermissions}, + {"InvalidPersonalAccessTokenName", InvalidPersonalAccessTokenNameCode, ErrInvalidPersonalAccessTokenName}, + {"PersonalAccessTokenAlreadyExists", PersonalAccessTokenAlreadyExistsCode, ErrPersonalAccessTokenAlreadyExists}, + {"PersonalAccessTokensLimitReached", PersonalAccessTokensLimitReachedCode, ErrPersonalAccessTokensLimitReached}, + {"InvalidPersonalAccessToken", InvalidPersonalAccessTokenCode, ErrInvalidPersonalAccessToken}, + {"PersonalAccessTokenExpired", PersonalAccessTokenExpiredCode, ErrPersonalAccessTokenExpired}, + {"UsersLimitReached", UsersLimitReachedCode, ErrUsersLimitReached}, + {"NotConnected", NotConnectedCode, ErrNotConnected}, + {"ClientShutdown", ClientShutdownCode, ErrClientShutdown}, + {"InvalidTlsDomain", InvalidTlsDomainCode, ErrInvalidTlsDomain}, + {"InvalidTlsCertificatePath", InvalidTlsCertificatePathCode, ErrInvalidTlsCertificatePath}, + {"InvalidTlsCertificate", InvalidTlsCertificateCode, ErrInvalidTlsCertificate}, + {"FailedToAddCertificate", FailedToAddCertificateCode, ErrFailedToAddCertificate}, + {"InvalidEncryptionKey", InvalidEncryptionKeyCode, ErrInvalidEncryptionKey}, + {"CannotEncryptData", CannotEncryptDataCode, ErrCannotEncryptData}, + {"CannotDecryptData", CannotDecryptDataCode, ErrCannotDecryptData}, + {"InvalidJwtAlgorithm", InvalidJwtAlgorithmCode, ErrInvalidJwtAlgorithm}, + {"InvalidJwtSecret", InvalidJwtSecretCode, ErrInvalidJwtSecret}, + {"JwtMissing", JwtMissingCode, ErrJwtMissing}, + {"CannotGenerateJwt", CannotGenerateJwtCode, ErrCannotGenerateJwt}, + {"AccessTokenMissing", AccessTokenMissingCode, ErrAccessTokenMissing}, + {"InvalidAccessToken", InvalidAccessTokenCode, ErrInvalidAccessToken}, + {"InvalidSizeBytes", InvalidSizeBytesCode, ErrInvalidSizeBytes}, + {"InvalidUtf8", InvalidUtf8Code, ErrInvalidUtf8}, + {"InvalidNumberEncoding", InvalidNumberEncodingCode, ErrInvalidNumberEncoding}, + {"InvalidBooleanValue", InvalidBooleanValueCode, ErrInvalidBooleanValue}, + {"InvalidNumberValue", InvalidNumberValueCode, ErrInvalidNumberValue}, + {"ClientNotFound", ClientNotFoundCode, ErrClientNotFound}, + {"InvalidClientId", InvalidClientIdCode, ErrInvalidClientId}, + {"ConnectionClosed", ConnectionClosedCode, ErrConnectionClosed}, + {"CannotParseHeaderKind", CannotParseHeaderKindCode, ErrCannotParseHeaderKind}, + {"HttpResponseError", HttpResponseErrorCode, ErrHttpResponseError}, + {"InvalidHttpRequest", InvalidHttpRequestCode, ErrInvalidHttpRequest}, + {"InvalidJsonResponse", InvalidJsonResponseCode, ErrInvalidJsonResponse}, + {"InvalidBytesResponse", InvalidBytesResponseCode, ErrInvalidBytesResponse}, + {"EmptyResponse", EmptyResponseCode, ErrEmptyResponse}, + {"CannotCreateEndpoint", CannotCreateEndpointCode, ErrCannotCreateEndpoint}, + {"CannotParseUrl", CannotParseUrlCode, ErrCannotParseUrl}, + {"CannotCreateStreamsDirectory", CannotCreateStreamsDirectoryCode, ErrCannotCreateStreamsDirectory}, + {"CannotCreateStreamDirectory", CannotCreateStreamDirectoryCode, ErrCannotCreateStreamDirectory}, + {"CannotCreateStreamInfo", CannotCreateStreamInfoCode, ErrCannotCreateStreamInfo}, + {"CannotUpdateStreamInfo", CannotUpdateStreamInfoCode, ErrCannotUpdateStreamInfo}, + {"CannotOpenStreamInfo", CannotOpenStreamInfoCode, ErrCannotOpenStreamInfo}, + {"CannotReadStreamInfo", CannotReadStreamInfoCode, ErrCannotReadStreamInfo}, + {"CannotCreateStream", CannotCreateStreamCode, ErrCannotCreateStream}, + {"CannotDeleteStream", CannotDeleteStreamCode, ErrCannotDeleteStream}, + {"CannotDeleteStreamDirectory", CannotDeleteStreamDirectoryCode, ErrCannotDeleteStreamDirectory}, + {"StreamIdNotFound", StreamIdNotFoundCode, ErrStreamIdNotFound}, + {"StreamNameNotFound", StreamNameNotFoundCode, ErrStreamNameNotFound}, + {"StreamIdAlreadyExists", StreamIdAlreadyExistsCode, ErrStreamIdAlreadyExists}, + {"StreamNameAlreadyExists", StreamNameAlreadyExistsCode, ErrStreamNameAlreadyExists}, + {"InvalidStreamName", InvalidStreamNameCode, ErrInvalidStreamName}, + {"InvalidStreamId", InvalidStreamIdCode, ErrInvalidStreamId}, + {"CannotReadStreams", CannotReadStreamsCode, ErrCannotReadStreams}, + {"InvalidTopicSize", InvalidTopicSizeCode, ErrInvalidTopicSize}, + {"CannotCreateTopicsDirectory", CannotCreateTopicsDirectoryCode, ErrCannotCreateTopicsDirectory}, + {"CannotCreateTopicDirectory", CannotCreateTopicDirectoryCode, ErrCannotCreateTopicDirectory}, + {"CannotCreateTopicInfo", CannotCreateTopicInfoCode, ErrCannotCreateTopicInfo}, + {"CannotUpdateTopicInfo", CannotUpdateTopicInfoCode, ErrCannotUpdateTopicInfo}, + {"CannotOpenTopicInfo", CannotOpenTopicInfoCode, ErrCannotOpenTopicInfo}, + {"CannotReadTopicInfo", CannotReadTopicInfoCode, ErrCannotReadTopicInfo}, + {"CannotCreateTopic", CannotCreateTopicCode, ErrCannotCreateTopic}, + {"CannotDeleteTopic", CannotDeleteTopicCode, ErrCannotDeleteTopic}, + {"CannotDeleteTopicDirectory", CannotDeleteTopicDirectoryCode, ErrCannotDeleteTopicDirectory}, + {"CannotPollTopic", CannotPollTopicCode, ErrCannotPollTopic}, + {"TopicIdNotFound", TopicIdNotFoundCode, ErrTopicIdNotFound}, + {"TopicNameNotFound", TopicNameNotFoundCode, ErrTopicNameNotFound}, + {"TopicIdAlreadyExists", TopicIdAlreadyExistsCode, ErrTopicIdAlreadyExists}, + {"TopicNameAlreadyExists", TopicNameAlreadyExistsCode, ErrTopicNameAlreadyExists}, + {"InvalidTopicName", InvalidTopicNameCode, ErrInvalidTopicName}, + {"TooManyPartitions", TooManyPartitionsCode, ErrTooManyPartitions}, + {"InvalidTopicId", InvalidTopicIdCode, ErrInvalidTopicId}, + {"CannotReadTopics", CannotReadTopicsCode, ErrCannotReadTopics}, + {"InvalidReplicationFactor", InvalidReplicationFactorCode, ErrInvalidReplicationFactor}, + {"CannotCreatePartition", CannotCreatePartitionCode, ErrCannotCreatePartition}, + {"CannotCreatePartitionsDirectory", CannotCreatePartitionsDirectoryCode, ErrCannotCreatePartitionsDirectory}, + {"CannotCreatePartitionDirectory", CannotCreatePartitionDirectoryCode, ErrCannotCreatePartitionDirectory}, + {"CannotOpenPartitionLogFile", CannotOpenPartitionLogFileCode, ErrCannotOpenPartitionLogFile}, + {"CannotReadPartitions", CannotReadPartitionsCode, ErrCannotReadPartitions}, + {"CannotDeletePartition", CannotDeletePartitionCode, ErrCannotDeletePartition}, + {"CannotDeletePartitionDirectory", CannotDeletePartitionDirectoryCode, ErrCannotDeletePartitionDirectory}, + {"PartitionNotFound", PartitionNotFoundCode, ErrPartitionNotFound}, + {"NoPartitions", NoPartitionsCode, ErrNoPartitions}, + {"TopicFull", TopicFullCode, ErrTopicFull}, + {"CannotDeleteConsumerOffsetsDirectory", CannotDeleteConsumerOffsetsDirectoryCode, ErrCannotDeleteConsumerOffsetsDirectory}, + {"CannotDeleteConsumerOffsetFile", CannotDeleteConsumerOffsetFileCode, ErrCannotDeleteConsumerOffsetFile}, + {"CannotCreateConsumerOffsetsDirectory", CannotCreateConsumerOffsetsDirectoryCode, ErrCannotCreateConsumerOffsetsDirectory}, + {"CannotReadConsumerOffsets", CannotReadConsumerOffsetsCode, ErrCannotReadConsumerOffsets}, + {"ConsumerOffsetNotFound", ConsumerOffsetNotFoundCode, ErrConsumerOffsetNotFound}, + {"SegmentNotFound", SegmentNotFoundCode, ErrSegmentNotFound}, + {"SegmentClosed", SegmentClosedCode, ErrSegmentClosed}, + {"InvalidSegmentSize", InvalidSegmentSizeCode, ErrInvalidSegmentSize}, + {"CannotCreateSegmentLogFile", CannotCreateSegmentLogFileCode, ErrCannotCreateSegmentLogFile}, + {"CannotCreateSegmentIndexFile", CannotCreateSegmentIndexFileCode, ErrCannotCreateSegmentIndexFile}, + {"CannotCreateSegmentTimeIndexFile", CannotCreateSegmentTimeIndexFileCode, ErrCannotCreateSegmentTimeIndexFile}, + {"CannotSaveMessagesToSegment", CannotSaveMessagesToSegmentCode, ErrCannotSaveMessagesToSegment}, + {"CannotSaveIndexToSegment", CannotSaveIndexToSegmentCode, ErrCannotSaveIndexToSegment}, + {"CannotSaveTimeIndexToSegment", CannotSaveTimeIndexToSegmentCode, ErrCannotSaveTimeIndexToSegment}, + {"InvalidMessagesCount", InvalidMessagesCountCode, ErrInvalidMessagesCount}, + {"CannotAppendMessage", CannotAppendMessageCode, ErrCannotAppendMessage}, + {"CannotReadMessage", CannotReadMessageCode, ErrCannotReadMessage}, + {"CannotReadMessageId", CannotReadMessageIdCode, ErrCannotReadMessageId}, + {"CannotReadMessageState", CannotReadMessageStateCode, ErrCannotReadMessageState}, + {"CannotReadMessageTimestamp", CannotReadMessageTimestampCode, ErrCannotReadMessageTimestamp}, + {"CannotReadHeadersLength", CannotReadHeadersLengthCode, ErrCannotReadHeadersLength}, + {"CannotReadHeadersPayload", CannotReadHeadersPayloadCode, ErrCannotReadHeadersPayload}, + {"TooBigUserHeaders", TooBigUserHeadersCode, ErrTooBigUserHeaders}, + {"InvalidHeaderKey", InvalidHeaderKeyCode, ErrInvalidHeaderKey}, + {"InvalidHeaderValue", InvalidHeaderValueCode, ErrInvalidHeaderValue}, + {"CannotReadMessageLength", CannotReadMessageLengthCode, ErrCannotReadMessageLength}, + {"CannotReadMessagePayload", CannotReadMessagePayloadCode, ErrCannotReadMessagePayload}, + {"TooBigMessagePayload", TooBigMessagePayloadCode, ErrTooBigMessagePayload}, + {"TooManyMessages", TooManyMessagesCode, ErrTooManyMessages}, + {"EmptyMessagePayload", EmptyMessagePayloadCode, ErrEmptyMessagePayload}, + {"InvalidMessagePayloadLength", InvalidMessagePayloadLengthCode, ErrInvalidMessagePayloadLength}, + {"CannotReadMessageChecksum", CannotReadMessageChecksumCode, ErrCannotReadMessageChecksum}, + {"InvalidMessageChecksum", InvalidMessageChecksumCode, ErrInvalidMessageChecksum}, + {"InvalidKeyValueLength", InvalidKeyValueLengthCode, ErrInvalidKeyValueLength}, + {"CommandLengthError", CommandLengthErrorCode, ErrCommandLengthError}, + {"InvalidSegmentsCount", InvalidSegmentsCountCode, ErrInvalidSegmentsCount}, + {"NonZeroOffset", NonZeroOffsetCode, ErrNonZeroOffset}, + {"NonZeroTimestamp", NonZeroTimestampCode, ErrNonZeroTimestamp}, + {"MissingIndex", MissingIndexCode, ErrMissingIndex}, + {"InvalidIndexesByteSize", InvalidIndexesByteSizeCode, ErrInvalidIndexesByteSize}, + {"InvalidIndexesCount", InvalidIndexesCountCode, ErrInvalidIndexesCount}, + {"InvalidMessagesSize", InvalidMessagesSizeCode, ErrInvalidMessagesSize}, + {"TooSmallMessage", TooSmallMessageCode, ErrTooSmallMessage}, + {"CannotSendMessagesDueToClientDisconnection", CannotSendMessagesDueToClientDisconnectionCode, ErrCannotSendMessagesDueToClientDisconnection}, + {"BackgroundSendError", BackgroundSendErrorCode, ErrBackgroundSendError}, + {"BackgroundSendTimeout", BackgroundSendTimeoutCode, ErrBackgroundSendTimeout}, + {"BackgroundSendBufferFull", BackgroundSendBufferFullCode, ErrBackgroundSendBufferFull}, + {"BackgroundWorkerDisconnected", BackgroundWorkerDisconnectedCode, ErrBackgroundWorkerDisconnected}, + {"BackgroundSendBufferOverflow", BackgroundSendBufferOverflowCode, ErrBackgroundSendBufferOverflow}, + {"ProducerSendFailed", ProducerSendFailedCode, ErrProducerSendFailed}, + {"ProducerClosed", ProducerClosedCode, ErrProducerClosed}, + {"InvalidOffset", InvalidOffsetCode, ErrInvalidOffset}, + {"ConsumerGroupIdNotFound", ConsumerGroupIdNotFoundCode, ErrConsumerGroupIdNotFound}, + {"ConsumerGroupIdAlreadyExists", ConsumerGroupIdAlreadyExistsCode, ErrConsumerGroupIdAlreadyExists}, + {"InvalidConsumerGroupId", InvalidConsumerGroupIdCode, ErrInvalidConsumerGroupId}, + {"ConsumerGroupNameNotFound", ConsumerGroupNameNotFoundCode, ErrConsumerGroupNameNotFound}, + {"ConsumerGroupNameAlreadyExists", ConsumerGroupNameAlreadyExistsCode, ErrConsumerGroupNameAlreadyExists}, + {"InvalidConsumerGroupName", InvalidConsumerGroupNameCode, ErrInvalidConsumerGroupName}, + {"ConsumerGroupMemberNotFound", ConsumerGroupMemberNotFoundCode, ErrConsumerGroupMemberNotFound}, + {"CannotCreateConsumerGroupInfo", CannotCreateConsumerGroupInfoCode, ErrCannotCreateConsumerGroupInfo}, + {"CannotDeleteConsumerGroupInfo", CannotDeleteConsumerGroupInfoCode, ErrCannotDeleteConsumerGroupInfo}, + {"MissingBaseOffsetRetainedMessageBatch", MissingBaseOffsetRetainedMessageBatchCode, ErrMissingBaseOffsetRetainedMessageBatch}, + {"MissingLastOffsetDeltaRetainedMessageBatch", MissingLastOffsetDeltaRetainedMessageBatchCode, ErrMissingLastOffsetDeltaRetainedMessageBatch}, + {"MissingMaxTimestampRetainedMessageBatch", MissingMaxTimestampRetainedMessageBatchCode, ErrMissingMaxTimestampRetainedMessageBatch}, + {"MissingLengthRetainedMessageBatch", MissingLengthRetainedMessageBatchCode, ErrMissingLengthRetainedMessageBatch}, + {"MissingPayloadRetainedMessageBatch", MissingPayloadRetainedMessageBatchCode, ErrMissingPayloadRetainedMessageBatch}, + {"CannotReadBatchBaseOffset", CannotReadBatchBaseOffsetCode, ErrCannotReadBatchBaseOffset}, + {"CannotReadBatchLength", CannotReadBatchLengthCode, ErrCannotReadBatchLength}, + {"CannotReadLastOffsetDelta", CannotReadLastOffsetDeltaCode, ErrCannotReadLastOffsetDelta}, + {"CannotReadMaxTimestamp", CannotReadMaxTimestampCode, ErrCannotReadMaxTimestamp}, + {"CannotReadBatchPayload", CannotReadBatchPayloadCode, ErrCannotReadBatchPayload}, + {"InvalidConnectionString", InvalidConnectionStringCode, ErrInvalidConnectionString}, + {"SnapshotFileCompletionFailed", SnapshotFileCompletionFailedCode, ErrSnapshotFileCompletionFailed}, + {"CannotSerializeResource", CannotSerializeResourceCode, ErrCannotSerializeResource}, + {"CannotDeserializeResource", CannotDeserializeResourceCode, ErrCannotDeserializeResource}, + {"CannotReadFile", CannotReadFileCode, ErrCannotReadFile}, + {"CannotReadFileMetadata", CannotReadFileMetadataCode, ErrCannotReadFileMetadata}, + {"CannotSeekFile", CannotSeekFileCode, ErrCannotSeekFile}, + {"CannotAppendToFile", CannotAppendToFileCode, ErrCannotAppendToFile}, + {"CannotWriteToFile", CannotWriteToFileCode, ErrCannotWriteToFile}, + {"CannotOverwriteFile", CannotOverwriteFileCode, ErrCannotOverwriteFile}, + {"CannotDeleteFile", CannotDeleteFileCode, ErrCannotDeleteFile}, + {"CannotSyncFile", CannotSyncFileCode, ErrCannotSyncFile}, + {"CannotReadIndexOffset", CannotReadIndexOffsetCode, ErrCannotReadIndexOffset}, + {"CannotReadIndexPosition", CannotReadIndexPositionCode, ErrCannotReadIndexPosition}, + {"CannotReadIndexTimestamp", CannotReadIndexTimestampCode, ErrCannotReadIndexTimestamp}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FromCode(tt.code) + if got == nil { + t.Fatalf("FromCode(%d) returned nil", tt.code) + } + if got.Code() != tt.code { + t.Fatalf("FromCode(%d).Code() = %d, want %d", tt.code, got.Code(), tt.code) + } + if got.Error() == "" { + t.Fatalf("FromCode(%d).Error() returned empty string", tt.code) + } + if !errors.Is(got, tt.err) { + t.Fatalf("errors.Is(FromCode(%d), expected) = false, want true", tt.code) + } + }) + } +} + +func TestFromCode_UnknownCode(t *testing.T) { + got := FromCode(Code(99999)) + if got == nil { + t.Fatalf("FromCode(99999) returned nil, expected fallback error") + } + if got.Code() != ErrorCode { + t.Fatalf("FromCode(99999).Code() = %d, want %d (fallback to ErrError)", got.Code(), ErrorCode) + } +} + +func TestCodeString_AllCodes(t *testing.T) { + allCodes := []Code{ + ErrorCode, + InvalidConfigurationCode, + InvalidCommandCode, + InvalidFormatCode, + FeatureUnavailableCode, + InvalidIdentifierCode, + InvalidVersionCode, + DisconnectedCode, + CannotEstablishConnectionCode, + CannotCreateBaseDirectoryCode, + CannotCreateRuntimeDirectoryCode, + CannotRemoveRuntimeDirectoryCode, + CannotCreateStateDirectoryCode, + StateFileNotFoundCode, + StateFileCorruptedCode, + InvalidStateEntryChecksumCode, + CannotOpenDatabaseCode, + ResourceNotFoundCode, + StaleClientCode, + TcpErrorCode, + QuicErrorCode, + InvalidServerAddressCode, + InvalidClientAddressCode, + InvalidIpAddressCode, + UnauthenticatedCode, + UnauthorizedCode, + InvalidCredentialsCode, + InvalidUsernameCode, + InvalidPasswordCode, + InvalidUserStatusCode, + UserAlreadyExistsCode, + UserInactiveCode, + CannotDeleteUserCode, + CannotChangePermissionsCode, + InvalidPersonalAccessTokenNameCode, + PersonalAccessTokenAlreadyExistsCode, + PersonalAccessTokensLimitReachedCode, + InvalidPersonalAccessTokenCode, + PersonalAccessTokenExpiredCode, + UsersLimitReachedCode, + NotConnectedCode, + ClientShutdownCode, + InvalidTlsDomainCode, + InvalidTlsCertificatePathCode, + InvalidTlsCertificateCode, + FailedToAddCertificateCode, + InvalidEncryptionKeyCode, + CannotEncryptDataCode, + CannotDecryptDataCode, + InvalidJwtAlgorithmCode, + InvalidJwtSecretCode, + JwtMissingCode, + CannotGenerateJwtCode, + AccessTokenMissingCode, + InvalidAccessTokenCode, + InvalidSizeBytesCode, + InvalidUtf8Code, + InvalidNumberEncodingCode, + InvalidBooleanValueCode, + InvalidNumberValueCode, + ClientNotFoundCode, + InvalidClientIdCode, + ConnectionClosedCode, + CannotParseHeaderKindCode, + HttpResponseErrorCode, + InvalidHttpRequestCode, + InvalidJsonResponseCode, + InvalidBytesResponseCode, + EmptyResponseCode, + CannotCreateEndpointCode, + CannotParseUrlCode, + CannotCreateStreamsDirectoryCode, + CannotCreateStreamDirectoryCode, + CannotCreateStreamInfoCode, + CannotUpdateStreamInfoCode, + CannotOpenStreamInfoCode, + CannotReadStreamInfoCode, + CannotCreateStreamCode, + CannotDeleteStreamCode, + CannotDeleteStreamDirectoryCode, + StreamIdNotFoundCode, + StreamNameNotFoundCode, + StreamIdAlreadyExistsCode, + StreamNameAlreadyExistsCode, + InvalidStreamNameCode, + InvalidStreamIdCode, + CannotReadStreamsCode, + InvalidTopicSizeCode, + CannotCreateTopicsDirectoryCode, + CannotCreateTopicDirectoryCode, + CannotCreateTopicInfoCode, + CannotUpdateTopicInfoCode, + CannotOpenTopicInfoCode, + CannotReadTopicInfoCode, + CannotCreateTopicCode, + CannotDeleteTopicCode, + CannotDeleteTopicDirectoryCode, + CannotPollTopicCode, + TopicIdNotFoundCode, + TopicNameNotFoundCode, + TopicIdAlreadyExistsCode, + TopicNameAlreadyExistsCode, + InvalidTopicNameCode, + TooManyPartitionsCode, + InvalidTopicIdCode, + CannotReadTopicsCode, + InvalidReplicationFactorCode, + CannotCreatePartitionCode, + CannotCreatePartitionsDirectoryCode, + CannotCreatePartitionDirectoryCode, + CannotOpenPartitionLogFileCode, + CannotReadPartitionsCode, + CannotDeletePartitionCode, + CannotDeletePartitionDirectoryCode, + PartitionNotFoundCode, + NoPartitionsCode, + TopicFullCode, + CannotDeleteConsumerOffsetsDirectoryCode, + CannotDeleteConsumerOffsetFileCode, + CannotCreateConsumerOffsetsDirectoryCode, + CannotReadConsumerOffsetsCode, + ConsumerOffsetNotFoundCode, + SegmentNotFoundCode, + SegmentClosedCode, + InvalidSegmentSizeCode, + CannotCreateSegmentLogFileCode, + CannotCreateSegmentIndexFileCode, + CannotCreateSegmentTimeIndexFileCode, + CannotSaveMessagesToSegmentCode, + CannotSaveIndexToSegmentCode, + CannotSaveTimeIndexToSegmentCode, + InvalidMessagesCountCode, + CannotAppendMessageCode, + CannotReadMessageCode, + CannotReadMessageIdCode, + CannotReadMessageStateCode, + CannotReadMessageTimestampCode, + CannotReadHeadersLengthCode, + CannotReadHeadersPayloadCode, + TooBigUserHeadersCode, + InvalidHeaderKeyCode, + InvalidHeaderValueCode, + CannotReadMessageLengthCode, + CannotReadMessagePayloadCode, + TooBigMessagePayloadCode, + TooManyMessagesCode, + EmptyMessagePayloadCode, + InvalidMessagePayloadLengthCode, + CannotReadMessageChecksumCode, + InvalidMessageChecksumCode, + InvalidKeyValueLengthCode, + CommandLengthErrorCode, + InvalidSegmentsCountCode, + NonZeroOffsetCode, + NonZeroTimestampCode, + MissingIndexCode, + InvalidIndexesByteSizeCode, + InvalidIndexesCountCode, + InvalidMessagesSizeCode, + TooSmallMessageCode, + CannotSendMessagesDueToClientDisconnectionCode, + BackgroundSendErrorCode, + BackgroundSendTimeoutCode, + BackgroundSendBufferFullCode, + BackgroundWorkerDisconnectedCode, + BackgroundSendBufferOverflowCode, + ProducerSendFailedCode, + ProducerClosedCode, + InvalidOffsetCode, + ConsumerGroupIdNotFoundCode, + ConsumerGroupIdAlreadyExistsCode, + InvalidConsumerGroupIdCode, + ConsumerGroupNameNotFoundCode, + ConsumerGroupNameAlreadyExistsCode, + InvalidConsumerGroupNameCode, + ConsumerGroupMemberNotFoundCode, + CannotCreateConsumerGroupInfoCode, + CannotDeleteConsumerGroupInfoCode, + MissingBaseOffsetRetainedMessageBatchCode, + MissingLastOffsetDeltaRetainedMessageBatchCode, + MissingMaxTimestampRetainedMessageBatchCode, + MissingLengthRetainedMessageBatchCode, + MissingPayloadRetainedMessageBatchCode, + CannotReadBatchBaseOffsetCode, + CannotReadBatchLengthCode, + CannotReadLastOffsetDeltaCode, + CannotReadMaxTimestampCode, + CannotReadBatchPayloadCode, + InvalidConnectionStringCode, + SnapshotFileCompletionFailedCode, + CannotSerializeResourceCode, + CannotDeserializeResourceCode, + CannotReadFileCode, + CannotReadFileMetadataCode, + CannotSeekFileCode, + CannotAppendToFileCode, + CannotWriteToFileCode, + CannotOverwriteFileCode, + CannotDeleteFileCode, + CannotSyncFileCode, + CannotReadIndexOffsetCode, + CannotReadIndexPositionCode, + CannotReadIndexTimestampCode, + } + + for _, code := range allCodes { + s := code.String() + if s == "" { + t.Fatalf("Code(%d).String() returned empty string", code) + } + if s == "Unknown error code" { + t.Fatalf("Code(%d).String() returned %q, expected a known name", code, s) + } + } +} + +func TestCodeString_UnknownCode(t *testing.T) { + s := Code(99999).String() + if s != "Unknown error code" { + t.Fatalf("Code(99999).String() = %q, want %q", s, "Unknown error code") + } +} + +func TestErrorsIs_CrossType(t *testing.T) { + if errors.Is(ErrInvalidCommand, ErrInvalidFormat) { + t.Fatalf("errors.Is(ErrInvalidCommand, ErrInvalidFormat) = true, want false") + } + if errors.Is(ErrUnauthenticated, ErrUnauthorized) { + t.Fatalf("errors.Is(ErrUnauthenticated, ErrUnauthorized) = true, want false") + } + if errors.Is(ErrStreamIdNotFound, ErrTopicIdNotFound) { + t.Fatalf("errors.Is(ErrStreamIdNotFound, ErrTopicIdNotFound) = true, want false") + } +} + +func TestIs_DirectCall_AllTypes(t *testing.T) { + type isChecker interface { + Is(error) bool + } + + tests := []struct { + name string + err IggyError + }{ + {"Error", ErrError}, + {"InvalidConfiguration", ErrInvalidConfiguration}, + {"InvalidCommand", ErrInvalidCommand}, + {"InvalidFormat", ErrInvalidFormat}, + {"FeatureUnavailable", ErrFeatureUnavailable}, + {"InvalidIdentifier", ErrInvalidIdentifier}, + {"InvalidVersion", ErrInvalidVersion}, + {"Disconnected", ErrDisconnected}, + {"CannotEstablishConnection", ErrCannotEstablishConnection}, + {"CannotCreateBaseDirectory", ErrCannotCreateBaseDirectory}, + {"CannotCreateRuntimeDirectory", ErrCannotCreateRuntimeDirectory}, + {"CannotRemoveRuntimeDirectory", ErrCannotRemoveRuntimeDirectory}, + {"CannotCreateStateDirectory", ErrCannotCreateStateDirectory}, + {"StateFileNotFound", ErrStateFileNotFound}, + {"StateFileCorrupted", ErrStateFileCorrupted}, + {"InvalidStateEntryChecksum", ErrInvalidStateEntryChecksum}, + {"CannotOpenDatabase", ErrCannotOpenDatabase}, + {"ResourceNotFound", ErrResourceNotFound}, + {"StaleClient", ErrStaleClient}, + {"TcpError", ErrTcpError}, + {"QuicError", ErrQuicError}, + {"InvalidServerAddress", ErrInvalidServerAddress}, + {"InvalidClientAddress", ErrInvalidClientAddress}, + {"InvalidIpAddress", ErrInvalidIpAddress}, + {"Unauthenticated", ErrUnauthenticated}, + {"Unauthorized", ErrUnauthorized}, + {"InvalidCredentials", ErrInvalidCredentials}, + {"InvalidUsername", ErrInvalidUsername}, + {"InvalidPassword", ErrInvalidPassword}, + {"InvalidUserStatus", ErrInvalidUserStatus}, + {"UserAlreadyExists", ErrUserAlreadyExists}, + {"UserInactive", ErrUserInactive}, + {"CannotDeleteUser", ErrCannotDeleteUser}, + {"CannotChangePermissions", ErrCannotChangePermissions}, + {"InvalidPersonalAccessTokenName", ErrInvalidPersonalAccessTokenName}, + {"PersonalAccessTokenAlreadyExists", ErrPersonalAccessTokenAlreadyExists}, + {"PersonalAccessTokensLimitReached", ErrPersonalAccessTokensLimitReached}, + {"InvalidPersonalAccessToken", ErrInvalidPersonalAccessToken}, + {"PersonalAccessTokenExpired", ErrPersonalAccessTokenExpired}, + {"UsersLimitReached", ErrUsersLimitReached}, + {"NotConnected", ErrNotConnected}, + {"ClientShutdown", ErrClientShutdown}, + {"InvalidTlsDomain", ErrInvalidTlsDomain}, + {"InvalidTlsCertificatePath", ErrInvalidTlsCertificatePath}, + {"InvalidTlsCertificate", ErrInvalidTlsCertificate}, + {"FailedToAddCertificate", ErrFailedToAddCertificate}, + {"InvalidEncryptionKey", ErrInvalidEncryptionKey}, + {"CannotEncryptData", ErrCannotEncryptData}, + {"CannotDecryptData", ErrCannotDecryptData}, + {"InvalidJwtAlgorithm", ErrInvalidJwtAlgorithm}, + {"InvalidJwtSecret", ErrInvalidJwtSecret}, + {"JwtMissing", ErrJwtMissing}, + {"CannotGenerateJwt", ErrCannotGenerateJwt}, + {"AccessTokenMissing", ErrAccessTokenMissing}, + {"InvalidAccessToken", ErrInvalidAccessToken}, + {"InvalidSizeBytes", ErrInvalidSizeBytes}, + {"InvalidUtf8", ErrInvalidUtf8}, + {"InvalidNumberEncoding", ErrInvalidNumberEncoding}, + {"InvalidBooleanValue", ErrInvalidBooleanValue}, + {"InvalidNumberValue", ErrInvalidNumberValue}, + {"ClientNotFound", ErrClientNotFound}, + {"InvalidClientId", ErrInvalidClientId}, + {"ConnectionClosed", ErrConnectionClosed}, + {"CannotParseHeaderKind", ErrCannotParseHeaderKind}, + {"HttpResponseError", ErrHttpResponseError}, + {"InvalidHttpRequest", ErrInvalidHttpRequest}, + {"InvalidJsonResponse", ErrInvalidJsonResponse}, + {"InvalidBytesResponse", ErrInvalidBytesResponse}, + {"EmptyResponse", ErrEmptyResponse}, + {"CannotCreateEndpoint", ErrCannotCreateEndpoint}, + {"CannotParseUrl", ErrCannotParseUrl}, + {"CannotCreateStreamsDirectory", ErrCannotCreateStreamsDirectory}, + {"CannotCreateStreamDirectory", ErrCannotCreateStreamDirectory}, + {"CannotCreateStreamInfo", ErrCannotCreateStreamInfo}, + {"CannotUpdateStreamInfo", ErrCannotUpdateStreamInfo}, + {"CannotOpenStreamInfo", ErrCannotOpenStreamInfo}, + {"CannotReadStreamInfo", ErrCannotReadStreamInfo}, + {"CannotCreateStream", ErrCannotCreateStream}, + {"CannotDeleteStream", ErrCannotDeleteStream}, + {"CannotDeleteStreamDirectory", ErrCannotDeleteStreamDirectory}, + {"StreamIdNotFound", ErrStreamIdNotFound}, + {"StreamNameNotFound", ErrStreamNameNotFound}, + {"StreamIdAlreadyExists", ErrStreamIdAlreadyExists}, + {"StreamNameAlreadyExists", ErrStreamNameAlreadyExists}, + {"InvalidStreamName", ErrInvalidStreamName}, + {"InvalidStreamId", ErrInvalidStreamId}, + {"CannotReadStreams", ErrCannotReadStreams}, + {"InvalidTopicSize", ErrInvalidTopicSize}, + {"CannotCreateTopicsDirectory", ErrCannotCreateTopicsDirectory}, + {"CannotCreateTopicDirectory", ErrCannotCreateTopicDirectory}, + {"CannotCreateTopicInfo", ErrCannotCreateTopicInfo}, + {"CannotUpdateTopicInfo", ErrCannotUpdateTopicInfo}, + {"CannotOpenTopicInfo", ErrCannotOpenTopicInfo}, + {"CannotReadTopicInfo", ErrCannotReadTopicInfo}, + {"CannotCreateTopic", ErrCannotCreateTopic}, + {"CannotDeleteTopic", ErrCannotDeleteTopic}, + {"CannotDeleteTopicDirectory", ErrCannotDeleteTopicDirectory}, + {"CannotPollTopic", ErrCannotPollTopic}, + {"TopicIdNotFound", ErrTopicIdNotFound}, + {"TopicNameNotFound", ErrTopicNameNotFound}, + {"TopicIdAlreadyExists", ErrTopicIdAlreadyExists}, + {"TopicNameAlreadyExists", ErrTopicNameAlreadyExists}, + {"InvalidTopicName", ErrInvalidTopicName}, + {"TooManyPartitions", ErrTooManyPartitions}, + {"InvalidTopicId", ErrInvalidTopicId}, + {"CannotReadTopics", ErrCannotReadTopics}, + {"InvalidReplicationFactor", ErrInvalidReplicationFactor}, + {"CannotCreatePartition", ErrCannotCreatePartition}, + {"CannotCreatePartitionsDirectory", ErrCannotCreatePartitionsDirectory}, + {"CannotCreatePartitionDirectory", ErrCannotCreatePartitionDirectory}, + {"CannotOpenPartitionLogFile", ErrCannotOpenPartitionLogFile}, + {"CannotReadPartitions", ErrCannotReadPartitions}, + {"CannotDeletePartition", ErrCannotDeletePartition}, + {"CannotDeletePartitionDirectory", ErrCannotDeletePartitionDirectory}, + {"PartitionNotFound", ErrPartitionNotFound}, + {"NoPartitions", ErrNoPartitions}, + {"TopicFull", ErrTopicFull}, + {"CannotDeleteConsumerOffsetsDirectory", ErrCannotDeleteConsumerOffsetsDirectory}, + {"CannotDeleteConsumerOffsetFile", ErrCannotDeleteConsumerOffsetFile}, + {"CannotCreateConsumerOffsetsDirectory", ErrCannotCreateConsumerOffsetsDirectory}, + {"CannotReadConsumerOffsets", ErrCannotReadConsumerOffsets}, + {"ConsumerOffsetNotFound", ErrConsumerOffsetNotFound}, + {"SegmentNotFound", ErrSegmentNotFound}, + {"SegmentClosed", ErrSegmentClosed}, + {"InvalidSegmentSize", ErrInvalidSegmentSize}, + {"CannotCreateSegmentLogFile", ErrCannotCreateSegmentLogFile}, + {"CannotCreateSegmentIndexFile", ErrCannotCreateSegmentIndexFile}, + {"CannotCreateSegmentTimeIndexFile", ErrCannotCreateSegmentTimeIndexFile}, + {"CannotSaveMessagesToSegment", ErrCannotSaveMessagesToSegment}, + {"CannotSaveIndexToSegment", ErrCannotSaveIndexToSegment}, + {"CannotSaveTimeIndexToSegment", ErrCannotSaveTimeIndexToSegment}, + {"InvalidMessagesCount", ErrInvalidMessagesCount}, + {"CannotAppendMessage", ErrCannotAppendMessage}, + {"CannotReadMessage", ErrCannotReadMessage}, + {"CannotReadMessageId", ErrCannotReadMessageId}, + {"CannotReadMessageState", ErrCannotReadMessageState}, + {"CannotReadMessageTimestamp", ErrCannotReadMessageTimestamp}, + {"CannotReadHeadersLength", ErrCannotReadHeadersLength}, + {"CannotReadHeadersPayload", ErrCannotReadHeadersPayload}, + {"TooBigUserHeaders", ErrTooBigUserHeaders}, + {"InvalidHeaderKey", ErrInvalidHeaderKey}, + {"InvalidHeaderValue", ErrInvalidHeaderValue}, + {"CannotReadMessageLength", ErrCannotReadMessageLength}, + {"CannotReadMessagePayload", ErrCannotReadMessagePayload}, + {"TooBigMessagePayload", ErrTooBigMessagePayload}, + {"TooManyMessages", ErrTooManyMessages}, + {"EmptyMessagePayload", ErrEmptyMessagePayload}, + {"InvalidMessagePayloadLength", ErrInvalidMessagePayloadLength}, + {"CannotReadMessageChecksum", ErrCannotReadMessageChecksum}, + {"InvalidMessageChecksum", ErrInvalidMessageChecksum}, + {"InvalidKeyValueLength", ErrInvalidKeyValueLength}, + {"CommandLengthError", ErrCommandLengthError}, + {"InvalidSegmentsCount", ErrInvalidSegmentsCount}, + {"NonZeroOffset", ErrNonZeroOffset}, + {"NonZeroTimestamp", ErrNonZeroTimestamp}, + {"MissingIndex", ErrMissingIndex}, + {"InvalidIndexesByteSize", ErrInvalidIndexesByteSize}, + {"InvalidIndexesCount", ErrInvalidIndexesCount}, + {"InvalidMessagesSize", ErrInvalidMessagesSize}, + {"TooSmallMessage", ErrTooSmallMessage}, + {"CannotSendMessagesDueToClientDisconnection", ErrCannotSendMessagesDueToClientDisconnection}, + {"BackgroundSendError", ErrBackgroundSendError}, + {"BackgroundSendTimeout", ErrBackgroundSendTimeout}, + {"BackgroundSendBufferFull", ErrBackgroundSendBufferFull}, + {"BackgroundWorkerDisconnected", ErrBackgroundWorkerDisconnected}, + {"BackgroundSendBufferOverflow", ErrBackgroundSendBufferOverflow}, + {"ProducerSendFailed", ErrProducerSendFailed}, + {"ProducerClosed", ErrProducerClosed}, + {"InvalidOffset", ErrInvalidOffset}, + {"ConsumerGroupIdNotFound", ErrConsumerGroupIdNotFound}, + {"ConsumerGroupIdAlreadyExists", ErrConsumerGroupIdAlreadyExists}, + {"InvalidConsumerGroupId", ErrInvalidConsumerGroupId}, + {"ConsumerGroupNameNotFound", ErrConsumerGroupNameNotFound}, + {"ConsumerGroupNameAlreadyExists", ErrConsumerGroupNameAlreadyExists}, + {"InvalidConsumerGroupName", ErrInvalidConsumerGroupName}, + {"ConsumerGroupMemberNotFound", ErrConsumerGroupMemberNotFound}, + {"CannotCreateConsumerGroupInfo", ErrCannotCreateConsumerGroupInfo}, + {"CannotDeleteConsumerGroupInfo", ErrCannotDeleteConsumerGroupInfo}, + {"MissingBaseOffsetRetainedMessageBatch", ErrMissingBaseOffsetRetainedMessageBatch}, + {"MissingLastOffsetDeltaRetainedMessageBatch", ErrMissingLastOffsetDeltaRetainedMessageBatch}, + {"MissingMaxTimestampRetainedMessageBatch", ErrMissingMaxTimestampRetainedMessageBatch}, + {"MissingLengthRetainedMessageBatch", ErrMissingLengthRetainedMessageBatch}, + {"MissingPayloadRetainedMessageBatch", ErrMissingPayloadRetainedMessageBatch}, + {"CannotReadBatchBaseOffset", ErrCannotReadBatchBaseOffset}, + {"CannotReadBatchLength", ErrCannotReadBatchLength}, + {"CannotReadLastOffsetDelta", ErrCannotReadLastOffsetDelta}, + {"CannotReadMaxTimestamp", ErrCannotReadMaxTimestamp}, + {"CannotReadBatchPayload", ErrCannotReadBatchPayload}, + {"InvalidConnectionString", ErrInvalidConnectionString}, + {"SnapshotFileCompletionFailed", ErrSnapshotFileCompletionFailed}, + {"CannotSerializeResource", ErrCannotSerializeResource}, + {"CannotDeserializeResource", ErrCannotDeserializeResource}, + {"CannotReadFile", ErrCannotReadFile}, + {"CannotReadFileMetadata", ErrCannotReadFileMetadata}, + {"CannotSeekFile", ErrCannotSeekFile}, + {"CannotAppendToFile", ErrCannotAppendToFile}, + {"CannotWriteToFile", ErrCannotWriteToFile}, + {"CannotOverwriteFile", ErrCannotOverwriteFile}, + {"CannotDeleteFile", ErrCannotDeleteFile}, + {"CannotSyncFile", ErrCannotSyncFile}, + {"CannotReadIndexOffset", ErrCannotReadIndexOffset}, + {"CannotReadIndexPosition", ErrCannotReadIndexPosition}, + {"CannotReadIndexTimestamp", ErrCannotReadIndexTimestamp}, + } + + for _, tt := range tests { + t.Run(tt.name+"/self_match", func(t *testing.T) { + checker, ok := tt.err.(isChecker) + if !ok { + t.Fatalf("%s does not implement Is(error) bool", tt.name) + } + if !checker.Is(tt.err) { + t.Fatalf("%s.Is(self) = false, want true", tt.name) + } + }) + t.Run(tt.name+"/cross_type_reject", func(t *testing.T) { + checker, ok := tt.err.(isChecker) + if !ok { + t.Fatalf("%s does not implement Is(error) bool", tt.name) + } + other := fmt.Errorf("unrelated error") + if checker.Is(other) { + t.Fatalf("%s.Is(unrelated) = true, want false", tt.name) + } + }) + } +} + +func TestWrappedErrorsIs(t *testing.T) { + wrapped := fmt.Errorf("context: %w", ErrInvalidCommand) + if !errors.Is(wrapped, ErrInvalidCommand) { + t.Fatalf("errors.Is(wrapped, ErrInvalidCommand) = false, want true") + } + if errors.Is(wrapped, ErrInvalidFormat) { + t.Fatalf("errors.Is(wrapped, ErrInvalidFormat) = true for wrong type") + } +} From a34459ab7cb3483a8727d0e0a267f9154c9cb9bd Mon Sep 17 00:00:00 2001 From: Atharva Lade Date: Thu, 26 Mar 2026 13:59:02 -0500 Subject: [PATCH 3/5] fix(go): resolve golangci-lint errors and patch coverage gap --- .../binary_response_deserializer_test.go | 92 +++++++++---------- foreign/go/client/tcp/tcp_client_test.go | 40 ++++---- foreign/go/internal/command/user_test.go | 33 +++++++ 3 files changed, 99 insertions(+), 66 deletions(-) diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go index 2cd230e862..a1901b5a33 100644 --- a/foreign/go/binary_serialization/binary_response_deserializer_test.go +++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go @@ -71,20 +71,20 @@ func TestDeserializeStream(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if result.Stream.Id != 1 { - t.Fatalf("expected stream id 1, got %d", result.Stream.Id) + if result.Id != 1 { + t.Fatalf("expected stream id 1, got %d", result.Id) } - if result.Stream.Name != "stream" { - t.Fatalf("expected stream name 'stream', got '%s'", result.Stream.Name) + if result.Name != "stream" { + t.Fatalf("expected stream name 'stream', got '%s'", result.Name) } - if result.Stream.CreatedAt != 100 { - t.Fatalf("expected createdAt 100, got %d", result.Stream.CreatedAt) + if result.CreatedAt != 100 { + t.Fatalf("expected createdAt 100, got %d", result.CreatedAt) } - if result.Stream.SizeBytes != 2048 { - t.Fatalf("expected sizeBytes 2048, got %d", result.Stream.SizeBytes) + if result.SizeBytes != 2048 { + t.Fatalf("expected sizeBytes 2048, got %d", result.SizeBytes) } - if result.Stream.MessagesCount != 10 { - t.Fatalf("expected messagesCount 10, got %d", result.Stream.MessagesCount) + if result.MessagesCount != 10 { + t.Fatalf("expected messagesCount 10, got %d", result.MessagesCount) } if len(result.Topics) != 1 { t.Fatalf("expected 1 topic, got %d", len(result.Topics)) @@ -142,17 +142,17 @@ func TestDeserializeTopic(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if result.Topic.Name != "mytopic" { - t.Fatalf("expected topic name 'mytopic', got '%s'", result.Topic.Name) + if result.Name != "mytopic" { + t.Fatalf("expected topic name 'mytopic', got '%s'", result.Name) } - if result.Topic.Id != 1 { - t.Fatalf("expected topic id 1, got %d", result.Topic.Id) + if result.Id != 1 { + t.Fatalf("expected topic id 1, got %d", result.Id) } - if result.Topic.Size != 1024 { - t.Fatalf("expected topic size 1024, got %d", result.Topic.Size) + if result.Size != 1024 { + t.Fatalf("expected topic size 1024, got %d", result.Size) } - if result.Topic.MessagesCount != 5 { - t.Fatalf("expected messagesCount 5, got %d", result.Topic.MessagesCount) + if result.MessagesCount != 5 { + t.Fatalf("expected messagesCount 5, got %d", result.MessagesCount) } if len(result.Partitions) != 1 { t.Fatalf("expected 1 partition, got %d", len(result.Partitions)) @@ -197,17 +197,17 @@ func TestDeserializeConsumerGroup_WithMembers(t *testing.T) { binary.LittleEndian.PutUint32(payload[pos:], 2) result := DeserializeConsumerGroup(payload) - if result.ConsumerGroup.Name != "grp-1" { - t.Fatalf("expected group name 'grp-1', got '%s'", result.ConsumerGroup.Name) + if result.Name != "grp-1" { + t.Fatalf("expected group name 'grp-1', got '%s'", result.Name) } - if result.ConsumerGroup.Id != 1 { - t.Fatalf("expected group id 1, got %d", result.ConsumerGroup.Id) + if result.Id != 1 { + t.Fatalf("expected group id 1, got %d", result.Id) } - if result.ConsumerGroup.PartitionsCount != 2 { - t.Fatalf("expected partitionsCount 2, got %d", result.ConsumerGroup.PartitionsCount) + if result.PartitionsCount != 2 { + t.Fatalf("expected partitionsCount 2, got %d", result.PartitionsCount) } - if result.ConsumerGroup.MembersCount != 1 { - t.Fatalf("expected membersCount 1, got %d", result.ConsumerGroup.MembersCount) + if result.MembersCount != 1 { + t.Fatalf("expected membersCount 1, got %d", result.MembersCount) } if len(result.Members) != 1 { t.Fatalf("expected 1 member, got %d", len(result.Members)) @@ -256,14 +256,14 @@ func TestDeserializeUser_WithPermissions(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if result.UserInfo.Username != "admin" { - t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) + if result.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.Username) } - if result.UserInfo.Id != 42 { - t.Fatalf("expected user id 42, got %d", result.UserInfo.Id) + if result.Id != 42 { + t.Fatalf("expected user id 42, got %d", result.Id) } - if result.UserInfo.CreatedAt != 999 { - t.Fatalf("expected createdAt 999, got %d", result.UserInfo.CreatedAt) + if result.CreatedAt != 999 { + t.Fatalf("expected createdAt 999, got %d", result.CreatedAt) } if result.Permissions == nil { t.Fatalf("expected permissions to be non-nil") @@ -323,11 +323,11 @@ func TestDeserializeUser_WithoutPermissions(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if result.UserInfo.Username != "admin" { - t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) + if result.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.Username) } - if result.UserInfo.Id != 42 { - t.Fatalf("expected user id 42, got %d", result.UserInfo.Id) + if result.Id != 42 { + t.Fatalf("expected user id 42, got %d", result.Id) } if result.Permissions != nil { t.Fatalf("expected permissions to be nil, got %+v", result.Permissions) @@ -363,20 +363,20 @@ func TestDeserializeClient_WithConsumerGroups(t *testing.T) { binary.LittleEndian.PutUint32(payload[pos:], 3) result := DeserializeClient(payload) - if result.ClientInfo.ID != 1 { - t.Fatalf("expected client id 1, got %d", result.ClientInfo.ID) + if result.ID != 1 { + t.Fatalf("expected client id 1, got %d", result.ID) } - if result.ClientInfo.UserID != 2 { - t.Fatalf("expected user id 2, got %d", result.ClientInfo.UserID) + if result.UserID != 2 { + t.Fatalf("expected user id 2, got %d", result.UserID) } - if result.ClientInfo.Transport != "tcp" { - t.Fatalf("expected transport 'tcp', got '%s'", result.ClientInfo.Transport) + if result.Transport != "tcp" { + t.Fatalf("expected transport 'tcp', got '%s'", result.Transport) } - if result.ClientInfo.Address != "127.0.0.1:8090" { - t.Fatalf("expected address '127.0.0.1:8090', got '%s'", result.ClientInfo.Address) + if result.Address != "127.0.0.1:8090" { + t.Fatalf("expected address '127.0.0.1:8090', got '%s'", result.Address) } - if result.ClientInfo.ConsumerGroupsCount != 1 { - t.Fatalf("expected consumerGroupsCount 1, got %d", result.ClientInfo.ConsumerGroupsCount) + if result.ConsumerGroupsCount != 1 { + t.Fatalf("expected consumerGroupsCount 1, got %d", result.ConsumerGroupsCount) } // make([]ConsumerGroupInfo, 1) pre-fills one zero entry, then append adds the real one if len(result.ConsumerGroups) != 2 { diff --git a/foreign/go/client/tcp/tcp_client_test.go b/foreign/go/client/tcp/tcp_client_test.go index fb800aa6ea..42b78dde4c 100644 --- a/foreign/go/client/tcp/tcp_client_test.go +++ b/foreign/go/client/tcp/tcp_client_test.go @@ -67,8 +67,8 @@ func newTestClient(response []byte) (*IggyTcpClient, func()) { }() cleanup := func() { - clientConn.Close() - serverConn.Close() + _ = clientConn.Close() + _ = serverConn.Close() <-done } return client, cleanup @@ -620,17 +620,17 @@ func TestGetClient(t *testing.T) { if err != nil { t.Fatalf("GetClient() error: %v", err) } - if info.ClientInfo.ID != 42 { - t.Fatalf("expected client id=42, got %d", info.ClientInfo.ID) + if info.ID != 42 { + t.Fatalf("expected client id=42, got %d", info.ID) } - if info.ClientInfo.UserID != 7 { - t.Fatalf("expected userId=7, got %d", info.ClientInfo.UserID) + if info.UserID != 7 { + t.Fatalf("expected userId=7, got %d", info.UserID) } - if info.ClientInfo.Address != "192.168.1.1:5000" { - t.Fatalf("expected address='192.168.1.1:5000', got '%s'", info.ClientInfo.Address) + if info.Address != "192.168.1.1:5000" { + t.Fatalf("expected address='192.168.1.1:5000', got '%s'", info.Address) } - if info.ClientInfo.Transport != string(iggcon.Tcp) { - t.Fatalf("expected transport='tcp', got '%s'", info.ClientInfo.Transport) + if info.Transport != string(iggcon.Tcp) { + t.Fatalf("expected transport='tcp', got '%s'", info.Transport) } } @@ -804,8 +804,8 @@ func TestGetConsumerGroup(t *testing.T) { if err != nil { t.Fatalf("unexpected error: %v", err) } - if result.ConsumerGroup.Name != "cg-1" { - t.Fatalf("expected name 'cg-1', got '%s'", result.ConsumerGroup.Name) + if result.Name != "cg-1" { + t.Fatalf("expected name 'cg-1', got '%s'", result.Name) } } @@ -919,8 +919,8 @@ func newMultiResponseTestClient(responses [][]byte) (*IggyTcpClient, func()) { }() cleanup := func() { - clientConn.Close() - serverConn.Close() + _ = clientConn.Close() + _ = serverConn.Close() <-done } return client, cleanup @@ -994,8 +994,8 @@ func TestGetUser(t *testing.T) { if err != nil { t.Fatalf("GetUser() unexpected error: %v", err) } - if result.UserInfo.Username != "admin" { - t.Fatalf("expected username 'admin', got '%s'", result.UserInfo.Username) + if result.Username != "admin" { + t.Fatalf("expected username 'admin', got '%s'", result.Username) } } @@ -1052,8 +1052,8 @@ func TestCreateUser(t *testing.T) { if err != nil { t.Fatalf("CreateUser() unexpected error: %v", err) } - if result.UserInfo.Username != "newuser" { - t.Fatalf("expected 'newuser', got '%s'", result.UserInfo.Username) + if result.Username != "newuser" { + t.Fatalf("expected 'newuser', got '%s'", result.Username) } } @@ -1184,8 +1184,8 @@ func TestWriteError(t *testing.T) { conn: clientConn, state: iggcon.StateConnected, } - clientConn.Close() - serverConn.Close() + _ = clientConn.Close() + _ = serverConn.Close() _, err := client.sendAndFetchResponse([]byte{0}, command.PingCode) if err == nil { diff --git a/foreign/go/internal/command/user_test.go b/foreign/go/internal/command/user_test.go index 616a4a3bb8..172128045b 100644 --- a/foreign/go/internal/command/user_test.go +++ b/foreign/go/internal/command/user_test.go @@ -108,6 +108,39 @@ func TestUpdatePermissions_MarshalBinary_NilPermissions(t *testing.T) { } } +func TestUpdatePermissions_MarshalBinary_WithPermissions(t *testing.T) { + userId, _ := iggcon.NewIdentifier(uint32(1)) + + perms := &iggcon.Permissions{ + Global: iggcon.GlobalPermissions{ + ManageServers: true, + ReadServers: true, + }, + } + + request := UpdatePermissions{ + UserID: userId, + Permissions: perms, + } + + serialized, err := request.MarshalBinary() + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + if serialized[len(serialized)-1] == 0 && len(serialized) <= 8 { + t.Fatalf("expected permission bytes in output, got only %d bytes", len(serialized)) + } + + // The userId (numeric 1) is 6 bytes, then 1 byte has_permissions=1, + // then 4 bytes permission length, then the permission bytes. + idBytes, _ := userId.MarshalBinary() + pos := len(idBytes) + if serialized[pos] != 1 { + t.Fatalf("expected has_permissions=1, got %d", serialized[pos]) + } +} + func TestChangePassword_MarshalBinary(t *testing.T) { userId, _ := iggcon.NewIdentifier(uint32(1)) From e5bf0560d9f6ee02477d66b90fda139a1c264c69 Mon Sep 17 00:00:00 2001 From: Atharva Lade Date: Thu, 26 Mar 2026 15:38:37 -0500 Subject: [PATCH 4/5] Address review: add deserializer test comments, deduplicate error list --- .../binary_response_deserializer_test.go | 102 +-- foreign/go/errors/errors_gen_test.go | 847 +++++------------- 2 files changed, 268 insertions(+), 681 deletions(-) diff --git a/foreign/go/binary_serialization/binary_response_deserializer_test.go b/foreign/go/binary_serialization/binary_response_deserializer_test.go index a1901b5a33..eacdf1b468 100644 --- a/foreign/go/binary_serialization/binary_response_deserializer_test.go +++ b/foreign/go/binary_serialization/binary_response_deserializer_test.go @@ -28,44 +28,45 @@ func TestDeserializeStream(t *testing.T) { payload := make([]byte, 96) pos := 0 - // Stream header - binary.LittleEndian.PutUint32(payload[pos:], 1) + // Stream header: id(4) + createdAt(8) + topicsCount(4) + sizeBytes(8) + messagesCount(8) + nameLen(1) + name(N) + binary.LittleEndian.PutUint32(payload[pos:], 1) // id = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 100) + binary.LittleEndian.PutUint64(payload[pos:], 100) // createdAt = 100 pos += 8 - binary.LittleEndian.PutUint32(payload[pos:], 1) + binary.LittleEndian.PutUint32(payload[pos:], 1) // topicsCount = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 2048) + binary.LittleEndian.PutUint64(payload[pos:], 2048) // sizeBytes = 2048 pos += 8 - binary.LittleEndian.PutUint64(payload[pos:], 10) + binary.LittleEndian.PutUint64(payload[pos:], 10) // messagesCount = 10 pos += 8 - payload[pos] = 6 + payload[pos] = 6 // nameLen = 6 pos++ - copy(payload[pos:], "stream") + copy(payload[pos:], "stream") // name = "stream" pos += 6 - // Topic entry - binary.LittleEndian.PutUint32(payload[pos:], 1) + // Embedded topic: id(4) + createdAt(8) + partitionsCount(4) + messageExpiry(8) + + // compressionAlg(1) + maxTopicSize(8) + replicationFactor(1) + size(8) + messagesCount(8) + nameLen(1) + name(N) + binary.LittleEndian.PutUint32(payload[pos:], 1) // id = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 200) + binary.LittleEndian.PutUint64(payload[pos:], 200) // createdAt = 200 pos += 8 - binary.LittleEndian.PutUint32(payload[pos:], 2) + binary.LittleEndian.PutUint32(payload[pos:], 2) // partitionsCount = 2 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 0) + binary.LittleEndian.PutUint64(payload[pos:], 0) // messageExpiry = 0 (none) pos += 8 - payload[pos] = 0 + payload[pos] = 0 // compressionAlgorithm = 0 (none) pos++ - binary.LittleEndian.PutUint64(payload[pos:], 0) + binary.LittleEndian.PutUint64(payload[pos:], 0) // maxTopicSize = 0 (unlimited) pos += 8 - payload[pos] = 1 + payload[pos] = 1 // replicationFactor = 1 pos++ - binary.LittleEndian.PutUint64(payload[pos:], 1024) + binary.LittleEndian.PutUint64(payload[pos:], 1024) // size = 1024 pos += 8 - binary.LittleEndian.PutUint64(payload[pos:], 5) + binary.LittleEndian.PutUint64(payload[pos:], 5) // messagesCount = 5 pos += 8 - payload[pos] = 6 + payload[pos] = 6 // nameLen = 6 pos++ - copy(payload[pos:], "topic1") + copy(payload[pos:], "topic1") // name = "topic1" result, err := DeserializeStream(payload) if err != nil { @@ -101,42 +102,43 @@ func TestDeserializeTopic(t *testing.T) { payload := make([]byte, 98) pos := 0 - // Topic header - binary.LittleEndian.PutUint32(payload[pos:], 1) + // Topic header: id(4) + createdAt(8) + partitionsCount(4) + messageExpiry(8) + + // compressionAlg(1) + maxTopicSize(8) + replicationFactor(1) + size(8) + messagesCount(8) + nameLen(1) + name(N) + binary.LittleEndian.PutUint32(payload[pos:], 1) // id = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 200) + binary.LittleEndian.PutUint64(payload[pos:], 200) // createdAt = 200 pos += 8 - binary.LittleEndian.PutUint32(payload[pos:], 1) + binary.LittleEndian.PutUint32(payload[pos:], 1) // partitionsCount = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 0) + binary.LittleEndian.PutUint64(payload[pos:], 0) // messageExpiry = 0 (none) pos += 8 - payload[pos] = 0 + payload[pos] = 0 // compressionAlgorithm = 0 (none) pos++ - binary.LittleEndian.PutUint64(payload[pos:], 0) + binary.LittleEndian.PutUint64(payload[pos:], 0) // maxTopicSize = 0 (unlimited) pos += 8 - payload[pos] = 1 + payload[pos] = 1 // replicationFactor = 1 pos++ - binary.LittleEndian.PutUint64(payload[pos:], 1024) + binary.LittleEndian.PutUint64(payload[pos:], 1024) // size = 1024 pos += 8 - binary.LittleEndian.PutUint64(payload[pos:], 5) + binary.LittleEndian.PutUint64(payload[pos:], 5) // messagesCount = 5 pos += 8 - payload[pos] = 7 + payload[pos] = 7 // nameLen = 7 pos++ - copy(payload[pos:], "mytopic") + copy(payload[pos:], "mytopic") // name = "mytopic" pos += 7 - // Partition entry - binary.LittleEndian.PutUint32(payload[pos:], 1) + // Partition entry: id(4) + createdAt(8) + segmentsCount(4) + currentOffset(8) + sizeBytes(8) + messagesCount(8) + binary.LittleEndian.PutUint32(payload[pos:], 1) // id = 1 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 300) + binary.LittleEndian.PutUint64(payload[pos:], 300) // createdAt = 300 pos += 8 - binary.LittleEndian.PutUint32(payload[pos:], 3) + binary.LittleEndian.PutUint32(payload[pos:], 3) // segmentsCount = 3 pos += 4 - binary.LittleEndian.PutUint64(payload[pos:], 99) + binary.LittleEndian.PutUint64(payload[pos:], 99) // currentOffset = 99 pos += 8 - binary.LittleEndian.PutUint64(payload[pos:], 512) + binary.LittleEndian.PutUint64(payload[pos:], 512) // sizeBytes = 512 pos += 8 - binary.LittleEndian.PutUint64(payload[pos:], 7) + binary.LittleEndian.PutUint64(payload[pos:], 7) // messagesCount = 7 result, err := DeserializeTopic(payload) if err != nil { @@ -175,26 +177,26 @@ func TestDeserializeConsumerGroup_WithMembers(t *testing.T) { payload := make([]byte, 34) pos := 0 - // Consumer group header - binary.LittleEndian.PutUint32(payload[pos:], 1) + // Consumer group header: id(4) + partitionsCount(4) + membersCount(4) + nameLen(1) + name(N) + binary.LittleEndian.PutUint32(payload[pos:], 1) // id = 1 pos += 4 - binary.LittleEndian.PutUint32(payload[pos:], 2) + binary.LittleEndian.PutUint32(payload[pos:], 2) // partitionsCount = 2 pos += 4 - binary.LittleEndian.PutUint32(payload[pos:], 1) + binary.LittleEndian.PutUint32(payload[pos:], 1) // membersCount = 1 pos += 4 - payload[pos] = 5 + payload[pos] = 5 // nameLen = 5 pos++ - copy(payload[pos:], "grp-1") + copy(payload[pos:], "grp-1") // name = "grp-1" pos += 5 - // Member entry: id=10, partitionsCount=2, partitions=[1,2] - binary.LittleEndian.PutUint32(payload[pos:], 10) + // Member entry: id(4) + partitionsCount(4) + partitions(4*N) + binary.LittleEndian.PutUint32(payload[pos:], 10) // memberId = 10 pos += 4 - binary.LittleEndian.PutUint32(payload[pos:], 2) + binary.LittleEndian.PutUint32(payload[pos:], 2) // partitionsCount = 2 pos += 4 - binary.LittleEndian.PutUint32(payload[pos:], 1) + binary.LittleEndian.PutUint32(payload[pos:], 1) // partition[0] = 1 pos += 4 - binary.LittleEndian.PutUint32(payload[pos:], 2) + binary.LittleEndian.PutUint32(payload[pos:], 2) // partition[1] = 2 result := DeserializeConsumerGroup(payload) if result.Name != "grp-1" { diff --git a/foreign/go/errors/errors_gen_test.go b/foreign/go/errors/errors_gen_test.go index 2b452eb6fa..1438623693 100644 --- a/foreign/go/errors/errors_gen_test.go +++ b/foreign/go/errors/errors_gen_test.go @@ -23,218 +23,218 @@ import ( "testing" ) -func TestFromCode_ReturnsCorrectErrors(t *testing.T) { - tests := []struct { - name string - code Code - err IggyError - }{ - {"Error", ErrorCode, ErrError}, - {"InvalidConfiguration", InvalidConfigurationCode, ErrInvalidConfiguration}, - {"InvalidCommand", InvalidCommandCode, ErrInvalidCommand}, - {"InvalidFormat", InvalidFormatCode, ErrInvalidFormat}, - {"FeatureUnavailable", FeatureUnavailableCode, ErrFeatureUnavailable}, - {"InvalidIdentifier", InvalidIdentifierCode, ErrInvalidIdentifier}, - {"InvalidVersion", InvalidVersionCode, ErrInvalidVersion}, - {"Disconnected", DisconnectedCode, ErrDisconnected}, - {"CannotEstablishConnection", CannotEstablishConnectionCode, ErrCannotEstablishConnection}, - {"CannotCreateBaseDirectory", CannotCreateBaseDirectoryCode, ErrCannotCreateBaseDirectory}, - {"CannotCreateRuntimeDirectory", CannotCreateRuntimeDirectoryCode, ErrCannotCreateRuntimeDirectory}, - {"CannotRemoveRuntimeDirectory", CannotRemoveRuntimeDirectoryCode, ErrCannotRemoveRuntimeDirectory}, - {"CannotCreateStateDirectory", CannotCreateStateDirectoryCode, ErrCannotCreateStateDirectory}, - {"StateFileNotFound", StateFileNotFoundCode, ErrStateFileNotFound}, - {"StateFileCorrupted", StateFileCorruptedCode, ErrStateFileCorrupted}, - {"InvalidStateEntryChecksum", InvalidStateEntryChecksumCode, ErrInvalidStateEntryChecksum}, - {"CannotOpenDatabase", CannotOpenDatabaseCode, ErrCannotOpenDatabase}, - {"ResourceNotFound", ResourceNotFoundCode, ErrResourceNotFound}, - {"StaleClient", StaleClientCode, ErrStaleClient}, - {"TcpError", TcpErrorCode, ErrTcpError}, - {"QuicError", QuicErrorCode, ErrQuicError}, - {"InvalidServerAddress", InvalidServerAddressCode, ErrInvalidServerAddress}, - {"InvalidClientAddress", InvalidClientAddressCode, ErrInvalidClientAddress}, - {"InvalidIpAddress", InvalidIpAddressCode, ErrInvalidIpAddress}, - {"Unauthenticated", UnauthenticatedCode, ErrUnauthenticated}, - {"Unauthorized", UnauthorizedCode, ErrUnauthorized}, - {"InvalidCredentials", InvalidCredentialsCode, ErrInvalidCredentials}, - {"InvalidUsername", InvalidUsernameCode, ErrInvalidUsername}, - {"InvalidPassword", InvalidPasswordCode, ErrInvalidPassword}, - {"InvalidUserStatus", InvalidUserStatusCode, ErrInvalidUserStatus}, - {"UserAlreadyExists", UserAlreadyExistsCode, ErrUserAlreadyExists}, - {"UserInactive", UserInactiveCode, ErrUserInactive}, - {"CannotDeleteUser", CannotDeleteUserCode, ErrCannotDeleteUser}, - {"CannotChangePermissions", CannotChangePermissionsCode, ErrCannotChangePermissions}, - {"InvalidPersonalAccessTokenName", InvalidPersonalAccessTokenNameCode, ErrInvalidPersonalAccessTokenName}, - {"PersonalAccessTokenAlreadyExists", PersonalAccessTokenAlreadyExistsCode, ErrPersonalAccessTokenAlreadyExists}, - {"PersonalAccessTokensLimitReached", PersonalAccessTokensLimitReachedCode, ErrPersonalAccessTokensLimitReached}, - {"InvalidPersonalAccessToken", InvalidPersonalAccessTokenCode, ErrInvalidPersonalAccessToken}, - {"PersonalAccessTokenExpired", PersonalAccessTokenExpiredCode, ErrPersonalAccessTokenExpired}, - {"UsersLimitReached", UsersLimitReachedCode, ErrUsersLimitReached}, - {"NotConnected", NotConnectedCode, ErrNotConnected}, - {"ClientShutdown", ClientShutdownCode, ErrClientShutdown}, - {"InvalidTlsDomain", InvalidTlsDomainCode, ErrInvalidTlsDomain}, - {"InvalidTlsCertificatePath", InvalidTlsCertificatePathCode, ErrInvalidTlsCertificatePath}, - {"InvalidTlsCertificate", InvalidTlsCertificateCode, ErrInvalidTlsCertificate}, - {"FailedToAddCertificate", FailedToAddCertificateCode, ErrFailedToAddCertificate}, - {"InvalidEncryptionKey", InvalidEncryptionKeyCode, ErrInvalidEncryptionKey}, - {"CannotEncryptData", CannotEncryptDataCode, ErrCannotEncryptData}, - {"CannotDecryptData", CannotDecryptDataCode, ErrCannotDecryptData}, - {"InvalidJwtAlgorithm", InvalidJwtAlgorithmCode, ErrInvalidJwtAlgorithm}, - {"InvalidJwtSecret", InvalidJwtSecretCode, ErrInvalidJwtSecret}, - {"JwtMissing", JwtMissingCode, ErrJwtMissing}, - {"CannotGenerateJwt", CannotGenerateJwtCode, ErrCannotGenerateJwt}, - {"AccessTokenMissing", AccessTokenMissingCode, ErrAccessTokenMissing}, - {"InvalidAccessToken", InvalidAccessTokenCode, ErrInvalidAccessToken}, - {"InvalidSizeBytes", InvalidSizeBytesCode, ErrInvalidSizeBytes}, - {"InvalidUtf8", InvalidUtf8Code, ErrInvalidUtf8}, - {"InvalidNumberEncoding", InvalidNumberEncodingCode, ErrInvalidNumberEncoding}, - {"InvalidBooleanValue", InvalidBooleanValueCode, ErrInvalidBooleanValue}, - {"InvalidNumberValue", InvalidNumberValueCode, ErrInvalidNumberValue}, - {"ClientNotFound", ClientNotFoundCode, ErrClientNotFound}, - {"InvalidClientId", InvalidClientIdCode, ErrInvalidClientId}, - {"ConnectionClosed", ConnectionClosedCode, ErrConnectionClosed}, - {"CannotParseHeaderKind", CannotParseHeaderKindCode, ErrCannotParseHeaderKind}, - {"HttpResponseError", HttpResponseErrorCode, ErrHttpResponseError}, - {"InvalidHttpRequest", InvalidHttpRequestCode, ErrInvalidHttpRequest}, - {"InvalidJsonResponse", InvalidJsonResponseCode, ErrInvalidJsonResponse}, - {"InvalidBytesResponse", InvalidBytesResponseCode, ErrInvalidBytesResponse}, - {"EmptyResponse", EmptyResponseCode, ErrEmptyResponse}, - {"CannotCreateEndpoint", CannotCreateEndpointCode, ErrCannotCreateEndpoint}, - {"CannotParseUrl", CannotParseUrlCode, ErrCannotParseUrl}, - {"CannotCreateStreamsDirectory", CannotCreateStreamsDirectoryCode, ErrCannotCreateStreamsDirectory}, - {"CannotCreateStreamDirectory", CannotCreateStreamDirectoryCode, ErrCannotCreateStreamDirectory}, - {"CannotCreateStreamInfo", CannotCreateStreamInfoCode, ErrCannotCreateStreamInfo}, - {"CannotUpdateStreamInfo", CannotUpdateStreamInfoCode, ErrCannotUpdateStreamInfo}, - {"CannotOpenStreamInfo", CannotOpenStreamInfoCode, ErrCannotOpenStreamInfo}, - {"CannotReadStreamInfo", CannotReadStreamInfoCode, ErrCannotReadStreamInfo}, - {"CannotCreateStream", CannotCreateStreamCode, ErrCannotCreateStream}, - {"CannotDeleteStream", CannotDeleteStreamCode, ErrCannotDeleteStream}, - {"CannotDeleteStreamDirectory", CannotDeleteStreamDirectoryCode, ErrCannotDeleteStreamDirectory}, - {"StreamIdNotFound", StreamIdNotFoundCode, ErrStreamIdNotFound}, - {"StreamNameNotFound", StreamNameNotFoundCode, ErrStreamNameNotFound}, - {"StreamIdAlreadyExists", StreamIdAlreadyExistsCode, ErrStreamIdAlreadyExists}, - {"StreamNameAlreadyExists", StreamNameAlreadyExistsCode, ErrStreamNameAlreadyExists}, - {"InvalidStreamName", InvalidStreamNameCode, ErrInvalidStreamName}, - {"InvalidStreamId", InvalidStreamIdCode, ErrInvalidStreamId}, - {"CannotReadStreams", CannotReadStreamsCode, ErrCannotReadStreams}, - {"InvalidTopicSize", InvalidTopicSizeCode, ErrInvalidTopicSize}, - {"CannotCreateTopicsDirectory", CannotCreateTopicsDirectoryCode, ErrCannotCreateTopicsDirectory}, - {"CannotCreateTopicDirectory", CannotCreateTopicDirectoryCode, ErrCannotCreateTopicDirectory}, - {"CannotCreateTopicInfo", CannotCreateTopicInfoCode, ErrCannotCreateTopicInfo}, - {"CannotUpdateTopicInfo", CannotUpdateTopicInfoCode, ErrCannotUpdateTopicInfo}, - {"CannotOpenTopicInfo", CannotOpenTopicInfoCode, ErrCannotOpenTopicInfo}, - {"CannotReadTopicInfo", CannotReadTopicInfoCode, ErrCannotReadTopicInfo}, - {"CannotCreateTopic", CannotCreateTopicCode, ErrCannotCreateTopic}, - {"CannotDeleteTopic", CannotDeleteTopicCode, ErrCannotDeleteTopic}, - {"CannotDeleteTopicDirectory", CannotDeleteTopicDirectoryCode, ErrCannotDeleteTopicDirectory}, - {"CannotPollTopic", CannotPollTopicCode, ErrCannotPollTopic}, - {"TopicIdNotFound", TopicIdNotFoundCode, ErrTopicIdNotFound}, - {"TopicNameNotFound", TopicNameNotFoundCode, ErrTopicNameNotFound}, - {"TopicIdAlreadyExists", TopicIdAlreadyExistsCode, ErrTopicIdAlreadyExists}, - {"TopicNameAlreadyExists", TopicNameAlreadyExistsCode, ErrTopicNameAlreadyExists}, - {"InvalidTopicName", InvalidTopicNameCode, ErrInvalidTopicName}, - {"TooManyPartitions", TooManyPartitionsCode, ErrTooManyPartitions}, - {"InvalidTopicId", InvalidTopicIdCode, ErrInvalidTopicId}, - {"CannotReadTopics", CannotReadTopicsCode, ErrCannotReadTopics}, - {"InvalidReplicationFactor", InvalidReplicationFactorCode, ErrInvalidReplicationFactor}, - {"CannotCreatePartition", CannotCreatePartitionCode, ErrCannotCreatePartition}, - {"CannotCreatePartitionsDirectory", CannotCreatePartitionsDirectoryCode, ErrCannotCreatePartitionsDirectory}, - {"CannotCreatePartitionDirectory", CannotCreatePartitionDirectoryCode, ErrCannotCreatePartitionDirectory}, - {"CannotOpenPartitionLogFile", CannotOpenPartitionLogFileCode, ErrCannotOpenPartitionLogFile}, - {"CannotReadPartitions", CannotReadPartitionsCode, ErrCannotReadPartitions}, - {"CannotDeletePartition", CannotDeletePartitionCode, ErrCannotDeletePartition}, - {"CannotDeletePartitionDirectory", CannotDeletePartitionDirectoryCode, ErrCannotDeletePartitionDirectory}, - {"PartitionNotFound", PartitionNotFoundCode, ErrPartitionNotFound}, - {"NoPartitions", NoPartitionsCode, ErrNoPartitions}, - {"TopicFull", TopicFullCode, ErrTopicFull}, - {"CannotDeleteConsumerOffsetsDirectory", CannotDeleteConsumerOffsetsDirectoryCode, ErrCannotDeleteConsumerOffsetsDirectory}, - {"CannotDeleteConsumerOffsetFile", CannotDeleteConsumerOffsetFileCode, ErrCannotDeleteConsumerOffsetFile}, - {"CannotCreateConsumerOffsetsDirectory", CannotCreateConsumerOffsetsDirectoryCode, ErrCannotCreateConsumerOffsetsDirectory}, - {"CannotReadConsumerOffsets", CannotReadConsumerOffsetsCode, ErrCannotReadConsumerOffsets}, - {"ConsumerOffsetNotFound", ConsumerOffsetNotFoundCode, ErrConsumerOffsetNotFound}, - {"SegmentNotFound", SegmentNotFoundCode, ErrSegmentNotFound}, - {"SegmentClosed", SegmentClosedCode, ErrSegmentClosed}, - {"InvalidSegmentSize", InvalidSegmentSizeCode, ErrInvalidSegmentSize}, - {"CannotCreateSegmentLogFile", CannotCreateSegmentLogFileCode, ErrCannotCreateSegmentLogFile}, - {"CannotCreateSegmentIndexFile", CannotCreateSegmentIndexFileCode, ErrCannotCreateSegmentIndexFile}, - {"CannotCreateSegmentTimeIndexFile", CannotCreateSegmentTimeIndexFileCode, ErrCannotCreateSegmentTimeIndexFile}, - {"CannotSaveMessagesToSegment", CannotSaveMessagesToSegmentCode, ErrCannotSaveMessagesToSegment}, - {"CannotSaveIndexToSegment", CannotSaveIndexToSegmentCode, ErrCannotSaveIndexToSegment}, - {"CannotSaveTimeIndexToSegment", CannotSaveTimeIndexToSegmentCode, ErrCannotSaveTimeIndexToSegment}, - {"InvalidMessagesCount", InvalidMessagesCountCode, ErrInvalidMessagesCount}, - {"CannotAppendMessage", CannotAppendMessageCode, ErrCannotAppendMessage}, - {"CannotReadMessage", CannotReadMessageCode, ErrCannotReadMessage}, - {"CannotReadMessageId", CannotReadMessageIdCode, ErrCannotReadMessageId}, - {"CannotReadMessageState", CannotReadMessageStateCode, ErrCannotReadMessageState}, - {"CannotReadMessageTimestamp", CannotReadMessageTimestampCode, ErrCannotReadMessageTimestamp}, - {"CannotReadHeadersLength", CannotReadHeadersLengthCode, ErrCannotReadHeadersLength}, - {"CannotReadHeadersPayload", CannotReadHeadersPayloadCode, ErrCannotReadHeadersPayload}, - {"TooBigUserHeaders", TooBigUserHeadersCode, ErrTooBigUserHeaders}, - {"InvalidHeaderKey", InvalidHeaderKeyCode, ErrInvalidHeaderKey}, - {"InvalidHeaderValue", InvalidHeaderValueCode, ErrInvalidHeaderValue}, - {"CannotReadMessageLength", CannotReadMessageLengthCode, ErrCannotReadMessageLength}, - {"CannotReadMessagePayload", CannotReadMessagePayloadCode, ErrCannotReadMessagePayload}, - {"TooBigMessagePayload", TooBigMessagePayloadCode, ErrTooBigMessagePayload}, - {"TooManyMessages", TooManyMessagesCode, ErrTooManyMessages}, - {"EmptyMessagePayload", EmptyMessagePayloadCode, ErrEmptyMessagePayload}, - {"InvalidMessagePayloadLength", InvalidMessagePayloadLengthCode, ErrInvalidMessagePayloadLength}, - {"CannotReadMessageChecksum", CannotReadMessageChecksumCode, ErrCannotReadMessageChecksum}, - {"InvalidMessageChecksum", InvalidMessageChecksumCode, ErrInvalidMessageChecksum}, - {"InvalidKeyValueLength", InvalidKeyValueLengthCode, ErrInvalidKeyValueLength}, - {"CommandLengthError", CommandLengthErrorCode, ErrCommandLengthError}, - {"InvalidSegmentsCount", InvalidSegmentsCountCode, ErrInvalidSegmentsCount}, - {"NonZeroOffset", NonZeroOffsetCode, ErrNonZeroOffset}, - {"NonZeroTimestamp", NonZeroTimestampCode, ErrNonZeroTimestamp}, - {"MissingIndex", MissingIndexCode, ErrMissingIndex}, - {"InvalidIndexesByteSize", InvalidIndexesByteSizeCode, ErrInvalidIndexesByteSize}, - {"InvalidIndexesCount", InvalidIndexesCountCode, ErrInvalidIndexesCount}, - {"InvalidMessagesSize", InvalidMessagesSizeCode, ErrInvalidMessagesSize}, - {"TooSmallMessage", TooSmallMessageCode, ErrTooSmallMessage}, - {"CannotSendMessagesDueToClientDisconnection", CannotSendMessagesDueToClientDisconnectionCode, ErrCannotSendMessagesDueToClientDisconnection}, - {"BackgroundSendError", BackgroundSendErrorCode, ErrBackgroundSendError}, - {"BackgroundSendTimeout", BackgroundSendTimeoutCode, ErrBackgroundSendTimeout}, - {"BackgroundSendBufferFull", BackgroundSendBufferFullCode, ErrBackgroundSendBufferFull}, - {"BackgroundWorkerDisconnected", BackgroundWorkerDisconnectedCode, ErrBackgroundWorkerDisconnected}, - {"BackgroundSendBufferOverflow", BackgroundSendBufferOverflowCode, ErrBackgroundSendBufferOverflow}, - {"ProducerSendFailed", ProducerSendFailedCode, ErrProducerSendFailed}, - {"ProducerClosed", ProducerClosedCode, ErrProducerClosed}, - {"InvalidOffset", InvalidOffsetCode, ErrInvalidOffset}, - {"ConsumerGroupIdNotFound", ConsumerGroupIdNotFoundCode, ErrConsumerGroupIdNotFound}, - {"ConsumerGroupIdAlreadyExists", ConsumerGroupIdAlreadyExistsCode, ErrConsumerGroupIdAlreadyExists}, - {"InvalidConsumerGroupId", InvalidConsumerGroupIdCode, ErrInvalidConsumerGroupId}, - {"ConsumerGroupNameNotFound", ConsumerGroupNameNotFoundCode, ErrConsumerGroupNameNotFound}, - {"ConsumerGroupNameAlreadyExists", ConsumerGroupNameAlreadyExistsCode, ErrConsumerGroupNameAlreadyExists}, - {"InvalidConsumerGroupName", InvalidConsumerGroupNameCode, ErrInvalidConsumerGroupName}, - {"ConsumerGroupMemberNotFound", ConsumerGroupMemberNotFoundCode, ErrConsumerGroupMemberNotFound}, - {"CannotCreateConsumerGroupInfo", CannotCreateConsumerGroupInfoCode, ErrCannotCreateConsumerGroupInfo}, - {"CannotDeleteConsumerGroupInfo", CannotDeleteConsumerGroupInfoCode, ErrCannotDeleteConsumerGroupInfo}, - {"MissingBaseOffsetRetainedMessageBatch", MissingBaseOffsetRetainedMessageBatchCode, ErrMissingBaseOffsetRetainedMessageBatch}, - {"MissingLastOffsetDeltaRetainedMessageBatch", MissingLastOffsetDeltaRetainedMessageBatchCode, ErrMissingLastOffsetDeltaRetainedMessageBatch}, - {"MissingMaxTimestampRetainedMessageBatch", MissingMaxTimestampRetainedMessageBatchCode, ErrMissingMaxTimestampRetainedMessageBatch}, - {"MissingLengthRetainedMessageBatch", MissingLengthRetainedMessageBatchCode, ErrMissingLengthRetainedMessageBatch}, - {"MissingPayloadRetainedMessageBatch", MissingPayloadRetainedMessageBatchCode, ErrMissingPayloadRetainedMessageBatch}, - {"CannotReadBatchBaseOffset", CannotReadBatchBaseOffsetCode, ErrCannotReadBatchBaseOffset}, - {"CannotReadBatchLength", CannotReadBatchLengthCode, ErrCannotReadBatchLength}, - {"CannotReadLastOffsetDelta", CannotReadLastOffsetDeltaCode, ErrCannotReadLastOffsetDelta}, - {"CannotReadMaxTimestamp", CannotReadMaxTimestampCode, ErrCannotReadMaxTimestamp}, - {"CannotReadBatchPayload", CannotReadBatchPayloadCode, ErrCannotReadBatchPayload}, - {"InvalidConnectionString", InvalidConnectionStringCode, ErrInvalidConnectionString}, - {"SnapshotFileCompletionFailed", SnapshotFileCompletionFailedCode, ErrSnapshotFileCompletionFailed}, - {"CannotSerializeResource", CannotSerializeResourceCode, ErrCannotSerializeResource}, - {"CannotDeserializeResource", CannotDeserializeResourceCode, ErrCannotDeserializeResource}, - {"CannotReadFile", CannotReadFileCode, ErrCannotReadFile}, - {"CannotReadFileMetadata", CannotReadFileMetadataCode, ErrCannotReadFileMetadata}, - {"CannotSeekFile", CannotSeekFileCode, ErrCannotSeekFile}, - {"CannotAppendToFile", CannotAppendToFileCode, ErrCannotAppendToFile}, - {"CannotWriteToFile", CannotWriteToFileCode, ErrCannotWriteToFile}, - {"CannotOverwriteFile", CannotOverwriteFileCode, ErrCannotOverwriteFile}, - {"CannotDeleteFile", CannotDeleteFileCode, ErrCannotDeleteFile}, - {"CannotSyncFile", CannotSyncFileCode, ErrCannotSyncFile}, - {"CannotReadIndexOffset", CannotReadIndexOffsetCode, ErrCannotReadIndexOffset}, - {"CannotReadIndexPosition", CannotReadIndexPositionCode, ErrCannotReadIndexPosition}, - {"CannotReadIndexTimestamp", CannotReadIndexTimestampCode, ErrCannotReadIndexTimestamp}, - } +var allErrors = []struct { + name string + code Code + err IggyError +}{ + {"Error", ErrorCode, ErrError}, + {"InvalidConfiguration", InvalidConfigurationCode, ErrInvalidConfiguration}, + {"InvalidCommand", InvalidCommandCode, ErrInvalidCommand}, + {"InvalidFormat", InvalidFormatCode, ErrInvalidFormat}, + {"FeatureUnavailable", FeatureUnavailableCode, ErrFeatureUnavailable}, + {"InvalidIdentifier", InvalidIdentifierCode, ErrInvalidIdentifier}, + {"InvalidVersion", InvalidVersionCode, ErrInvalidVersion}, + {"Disconnected", DisconnectedCode, ErrDisconnected}, + {"CannotEstablishConnection", CannotEstablishConnectionCode, ErrCannotEstablishConnection}, + {"CannotCreateBaseDirectory", CannotCreateBaseDirectoryCode, ErrCannotCreateBaseDirectory}, + {"CannotCreateRuntimeDirectory", CannotCreateRuntimeDirectoryCode, ErrCannotCreateRuntimeDirectory}, + {"CannotRemoveRuntimeDirectory", CannotRemoveRuntimeDirectoryCode, ErrCannotRemoveRuntimeDirectory}, + {"CannotCreateStateDirectory", CannotCreateStateDirectoryCode, ErrCannotCreateStateDirectory}, + {"StateFileNotFound", StateFileNotFoundCode, ErrStateFileNotFound}, + {"StateFileCorrupted", StateFileCorruptedCode, ErrStateFileCorrupted}, + {"InvalidStateEntryChecksum", InvalidStateEntryChecksumCode, ErrInvalidStateEntryChecksum}, + {"CannotOpenDatabase", CannotOpenDatabaseCode, ErrCannotOpenDatabase}, + {"ResourceNotFound", ResourceNotFoundCode, ErrResourceNotFound}, + {"StaleClient", StaleClientCode, ErrStaleClient}, + {"TcpError", TcpErrorCode, ErrTcpError}, + {"QuicError", QuicErrorCode, ErrQuicError}, + {"InvalidServerAddress", InvalidServerAddressCode, ErrInvalidServerAddress}, + {"InvalidClientAddress", InvalidClientAddressCode, ErrInvalidClientAddress}, + {"InvalidIpAddress", InvalidIpAddressCode, ErrInvalidIpAddress}, + {"Unauthenticated", UnauthenticatedCode, ErrUnauthenticated}, + {"Unauthorized", UnauthorizedCode, ErrUnauthorized}, + {"InvalidCredentials", InvalidCredentialsCode, ErrInvalidCredentials}, + {"InvalidUsername", InvalidUsernameCode, ErrInvalidUsername}, + {"InvalidPassword", InvalidPasswordCode, ErrInvalidPassword}, + {"InvalidUserStatus", InvalidUserStatusCode, ErrInvalidUserStatus}, + {"UserAlreadyExists", UserAlreadyExistsCode, ErrUserAlreadyExists}, + {"UserInactive", UserInactiveCode, ErrUserInactive}, + {"CannotDeleteUser", CannotDeleteUserCode, ErrCannotDeleteUser}, + {"CannotChangePermissions", CannotChangePermissionsCode, ErrCannotChangePermissions}, + {"InvalidPersonalAccessTokenName", InvalidPersonalAccessTokenNameCode, ErrInvalidPersonalAccessTokenName}, + {"PersonalAccessTokenAlreadyExists", PersonalAccessTokenAlreadyExistsCode, ErrPersonalAccessTokenAlreadyExists}, + {"PersonalAccessTokensLimitReached", PersonalAccessTokensLimitReachedCode, ErrPersonalAccessTokensLimitReached}, + {"InvalidPersonalAccessToken", InvalidPersonalAccessTokenCode, ErrInvalidPersonalAccessToken}, + {"PersonalAccessTokenExpired", PersonalAccessTokenExpiredCode, ErrPersonalAccessTokenExpired}, + {"UsersLimitReached", UsersLimitReachedCode, ErrUsersLimitReached}, + {"NotConnected", NotConnectedCode, ErrNotConnected}, + {"ClientShutdown", ClientShutdownCode, ErrClientShutdown}, + {"InvalidTlsDomain", InvalidTlsDomainCode, ErrInvalidTlsDomain}, + {"InvalidTlsCertificatePath", InvalidTlsCertificatePathCode, ErrInvalidTlsCertificatePath}, + {"InvalidTlsCertificate", InvalidTlsCertificateCode, ErrInvalidTlsCertificate}, + {"FailedToAddCertificate", FailedToAddCertificateCode, ErrFailedToAddCertificate}, + {"InvalidEncryptionKey", InvalidEncryptionKeyCode, ErrInvalidEncryptionKey}, + {"CannotEncryptData", CannotEncryptDataCode, ErrCannotEncryptData}, + {"CannotDecryptData", CannotDecryptDataCode, ErrCannotDecryptData}, + {"InvalidJwtAlgorithm", InvalidJwtAlgorithmCode, ErrInvalidJwtAlgorithm}, + {"InvalidJwtSecret", InvalidJwtSecretCode, ErrInvalidJwtSecret}, + {"JwtMissing", JwtMissingCode, ErrJwtMissing}, + {"CannotGenerateJwt", CannotGenerateJwtCode, ErrCannotGenerateJwt}, + {"AccessTokenMissing", AccessTokenMissingCode, ErrAccessTokenMissing}, + {"InvalidAccessToken", InvalidAccessTokenCode, ErrInvalidAccessToken}, + {"InvalidSizeBytes", InvalidSizeBytesCode, ErrInvalidSizeBytes}, + {"InvalidUtf8", InvalidUtf8Code, ErrInvalidUtf8}, + {"InvalidNumberEncoding", InvalidNumberEncodingCode, ErrInvalidNumberEncoding}, + {"InvalidBooleanValue", InvalidBooleanValueCode, ErrInvalidBooleanValue}, + {"InvalidNumberValue", InvalidNumberValueCode, ErrInvalidNumberValue}, + {"ClientNotFound", ClientNotFoundCode, ErrClientNotFound}, + {"InvalidClientId", InvalidClientIdCode, ErrInvalidClientId}, + {"ConnectionClosed", ConnectionClosedCode, ErrConnectionClosed}, + {"CannotParseHeaderKind", CannotParseHeaderKindCode, ErrCannotParseHeaderKind}, + {"HttpResponseError", HttpResponseErrorCode, ErrHttpResponseError}, + {"InvalidHttpRequest", InvalidHttpRequestCode, ErrInvalidHttpRequest}, + {"InvalidJsonResponse", InvalidJsonResponseCode, ErrInvalidJsonResponse}, + {"InvalidBytesResponse", InvalidBytesResponseCode, ErrInvalidBytesResponse}, + {"EmptyResponse", EmptyResponseCode, ErrEmptyResponse}, + {"CannotCreateEndpoint", CannotCreateEndpointCode, ErrCannotCreateEndpoint}, + {"CannotParseUrl", CannotParseUrlCode, ErrCannotParseUrl}, + {"CannotCreateStreamsDirectory", CannotCreateStreamsDirectoryCode, ErrCannotCreateStreamsDirectory}, + {"CannotCreateStreamDirectory", CannotCreateStreamDirectoryCode, ErrCannotCreateStreamDirectory}, + {"CannotCreateStreamInfo", CannotCreateStreamInfoCode, ErrCannotCreateStreamInfo}, + {"CannotUpdateStreamInfo", CannotUpdateStreamInfoCode, ErrCannotUpdateStreamInfo}, + {"CannotOpenStreamInfo", CannotOpenStreamInfoCode, ErrCannotOpenStreamInfo}, + {"CannotReadStreamInfo", CannotReadStreamInfoCode, ErrCannotReadStreamInfo}, + {"CannotCreateStream", CannotCreateStreamCode, ErrCannotCreateStream}, + {"CannotDeleteStream", CannotDeleteStreamCode, ErrCannotDeleteStream}, + {"CannotDeleteStreamDirectory", CannotDeleteStreamDirectoryCode, ErrCannotDeleteStreamDirectory}, + {"StreamIdNotFound", StreamIdNotFoundCode, ErrStreamIdNotFound}, + {"StreamNameNotFound", StreamNameNotFoundCode, ErrStreamNameNotFound}, + {"StreamIdAlreadyExists", StreamIdAlreadyExistsCode, ErrStreamIdAlreadyExists}, + {"StreamNameAlreadyExists", StreamNameAlreadyExistsCode, ErrStreamNameAlreadyExists}, + {"InvalidStreamName", InvalidStreamNameCode, ErrInvalidStreamName}, + {"InvalidStreamId", InvalidStreamIdCode, ErrInvalidStreamId}, + {"CannotReadStreams", CannotReadStreamsCode, ErrCannotReadStreams}, + {"InvalidTopicSize", InvalidTopicSizeCode, ErrInvalidTopicSize}, + {"CannotCreateTopicsDirectory", CannotCreateTopicsDirectoryCode, ErrCannotCreateTopicsDirectory}, + {"CannotCreateTopicDirectory", CannotCreateTopicDirectoryCode, ErrCannotCreateTopicDirectory}, + {"CannotCreateTopicInfo", CannotCreateTopicInfoCode, ErrCannotCreateTopicInfo}, + {"CannotUpdateTopicInfo", CannotUpdateTopicInfoCode, ErrCannotUpdateTopicInfo}, + {"CannotOpenTopicInfo", CannotOpenTopicInfoCode, ErrCannotOpenTopicInfo}, + {"CannotReadTopicInfo", CannotReadTopicInfoCode, ErrCannotReadTopicInfo}, + {"CannotCreateTopic", CannotCreateTopicCode, ErrCannotCreateTopic}, + {"CannotDeleteTopic", CannotDeleteTopicCode, ErrCannotDeleteTopic}, + {"CannotDeleteTopicDirectory", CannotDeleteTopicDirectoryCode, ErrCannotDeleteTopicDirectory}, + {"CannotPollTopic", CannotPollTopicCode, ErrCannotPollTopic}, + {"TopicIdNotFound", TopicIdNotFoundCode, ErrTopicIdNotFound}, + {"TopicNameNotFound", TopicNameNotFoundCode, ErrTopicNameNotFound}, + {"TopicIdAlreadyExists", TopicIdAlreadyExistsCode, ErrTopicIdAlreadyExists}, + {"TopicNameAlreadyExists", TopicNameAlreadyExistsCode, ErrTopicNameAlreadyExists}, + {"InvalidTopicName", InvalidTopicNameCode, ErrInvalidTopicName}, + {"TooManyPartitions", TooManyPartitionsCode, ErrTooManyPartitions}, + {"InvalidTopicId", InvalidTopicIdCode, ErrInvalidTopicId}, + {"CannotReadTopics", CannotReadTopicsCode, ErrCannotReadTopics}, + {"InvalidReplicationFactor", InvalidReplicationFactorCode, ErrInvalidReplicationFactor}, + {"CannotCreatePartition", CannotCreatePartitionCode, ErrCannotCreatePartition}, + {"CannotCreatePartitionsDirectory", CannotCreatePartitionsDirectoryCode, ErrCannotCreatePartitionsDirectory}, + {"CannotCreatePartitionDirectory", CannotCreatePartitionDirectoryCode, ErrCannotCreatePartitionDirectory}, + {"CannotOpenPartitionLogFile", CannotOpenPartitionLogFileCode, ErrCannotOpenPartitionLogFile}, + {"CannotReadPartitions", CannotReadPartitionsCode, ErrCannotReadPartitions}, + {"CannotDeletePartition", CannotDeletePartitionCode, ErrCannotDeletePartition}, + {"CannotDeletePartitionDirectory", CannotDeletePartitionDirectoryCode, ErrCannotDeletePartitionDirectory}, + {"PartitionNotFound", PartitionNotFoundCode, ErrPartitionNotFound}, + {"NoPartitions", NoPartitionsCode, ErrNoPartitions}, + {"TopicFull", TopicFullCode, ErrTopicFull}, + {"CannotDeleteConsumerOffsetsDirectory", CannotDeleteConsumerOffsetsDirectoryCode, ErrCannotDeleteConsumerOffsetsDirectory}, + {"CannotDeleteConsumerOffsetFile", CannotDeleteConsumerOffsetFileCode, ErrCannotDeleteConsumerOffsetFile}, + {"CannotCreateConsumerOffsetsDirectory", CannotCreateConsumerOffsetsDirectoryCode, ErrCannotCreateConsumerOffsetsDirectory}, + {"CannotReadConsumerOffsets", CannotReadConsumerOffsetsCode, ErrCannotReadConsumerOffsets}, + {"ConsumerOffsetNotFound", ConsumerOffsetNotFoundCode, ErrConsumerOffsetNotFound}, + {"SegmentNotFound", SegmentNotFoundCode, ErrSegmentNotFound}, + {"SegmentClosed", SegmentClosedCode, ErrSegmentClosed}, + {"InvalidSegmentSize", InvalidSegmentSizeCode, ErrInvalidSegmentSize}, + {"CannotCreateSegmentLogFile", CannotCreateSegmentLogFileCode, ErrCannotCreateSegmentLogFile}, + {"CannotCreateSegmentIndexFile", CannotCreateSegmentIndexFileCode, ErrCannotCreateSegmentIndexFile}, + {"CannotCreateSegmentTimeIndexFile", CannotCreateSegmentTimeIndexFileCode, ErrCannotCreateSegmentTimeIndexFile}, + {"CannotSaveMessagesToSegment", CannotSaveMessagesToSegmentCode, ErrCannotSaveMessagesToSegment}, + {"CannotSaveIndexToSegment", CannotSaveIndexToSegmentCode, ErrCannotSaveIndexToSegment}, + {"CannotSaveTimeIndexToSegment", CannotSaveTimeIndexToSegmentCode, ErrCannotSaveTimeIndexToSegment}, + {"InvalidMessagesCount", InvalidMessagesCountCode, ErrInvalidMessagesCount}, + {"CannotAppendMessage", CannotAppendMessageCode, ErrCannotAppendMessage}, + {"CannotReadMessage", CannotReadMessageCode, ErrCannotReadMessage}, + {"CannotReadMessageId", CannotReadMessageIdCode, ErrCannotReadMessageId}, + {"CannotReadMessageState", CannotReadMessageStateCode, ErrCannotReadMessageState}, + {"CannotReadMessageTimestamp", CannotReadMessageTimestampCode, ErrCannotReadMessageTimestamp}, + {"CannotReadHeadersLength", CannotReadHeadersLengthCode, ErrCannotReadHeadersLength}, + {"CannotReadHeadersPayload", CannotReadHeadersPayloadCode, ErrCannotReadHeadersPayload}, + {"TooBigUserHeaders", TooBigUserHeadersCode, ErrTooBigUserHeaders}, + {"InvalidHeaderKey", InvalidHeaderKeyCode, ErrInvalidHeaderKey}, + {"InvalidHeaderValue", InvalidHeaderValueCode, ErrInvalidHeaderValue}, + {"CannotReadMessageLength", CannotReadMessageLengthCode, ErrCannotReadMessageLength}, + {"CannotReadMessagePayload", CannotReadMessagePayloadCode, ErrCannotReadMessagePayload}, + {"TooBigMessagePayload", TooBigMessagePayloadCode, ErrTooBigMessagePayload}, + {"TooManyMessages", TooManyMessagesCode, ErrTooManyMessages}, + {"EmptyMessagePayload", EmptyMessagePayloadCode, ErrEmptyMessagePayload}, + {"InvalidMessagePayloadLength", InvalidMessagePayloadLengthCode, ErrInvalidMessagePayloadLength}, + {"CannotReadMessageChecksum", CannotReadMessageChecksumCode, ErrCannotReadMessageChecksum}, + {"InvalidMessageChecksum", InvalidMessageChecksumCode, ErrInvalidMessageChecksum}, + {"InvalidKeyValueLength", InvalidKeyValueLengthCode, ErrInvalidKeyValueLength}, + {"CommandLengthError", CommandLengthErrorCode, ErrCommandLengthError}, + {"InvalidSegmentsCount", InvalidSegmentsCountCode, ErrInvalidSegmentsCount}, + {"NonZeroOffset", NonZeroOffsetCode, ErrNonZeroOffset}, + {"NonZeroTimestamp", NonZeroTimestampCode, ErrNonZeroTimestamp}, + {"MissingIndex", MissingIndexCode, ErrMissingIndex}, + {"InvalidIndexesByteSize", InvalidIndexesByteSizeCode, ErrInvalidIndexesByteSize}, + {"InvalidIndexesCount", InvalidIndexesCountCode, ErrInvalidIndexesCount}, + {"InvalidMessagesSize", InvalidMessagesSizeCode, ErrInvalidMessagesSize}, + {"TooSmallMessage", TooSmallMessageCode, ErrTooSmallMessage}, + {"CannotSendMessagesDueToClientDisconnection", CannotSendMessagesDueToClientDisconnectionCode, ErrCannotSendMessagesDueToClientDisconnection}, + {"BackgroundSendError", BackgroundSendErrorCode, ErrBackgroundSendError}, + {"BackgroundSendTimeout", BackgroundSendTimeoutCode, ErrBackgroundSendTimeout}, + {"BackgroundSendBufferFull", BackgroundSendBufferFullCode, ErrBackgroundSendBufferFull}, + {"BackgroundWorkerDisconnected", BackgroundWorkerDisconnectedCode, ErrBackgroundWorkerDisconnected}, + {"BackgroundSendBufferOverflow", BackgroundSendBufferOverflowCode, ErrBackgroundSendBufferOverflow}, + {"ProducerSendFailed", ProducerSendFailedCode, ErrProducerSendFailed}, + {"ProducerClosed", ProducerClosedCode, ErrProducerClosed}, + {"InvalidOffset", InvalidOffsetCode, ErrInvalidOffset}, + {"ConsumerGroupIdNotFound", ConsumerGroupIdNotFoundCode, ErrConsumerGroupIdNotFound}, + {"ConsumerGroupIdAlreadyExists", ConsumerGroupIdAlreadyExistsCode, ErrConsumerGroupIdAlreadyExists}, + {"InvalidConsumerGroupId", InvalidConsumerGroupIdCode, ErrInvalidConsumerGroupId}, + {"ConsumerGroupNameNotFound", ConsumerGroupNameNotFoundCode, ErrConsumerGroupNameNotFound}, + {"ConsumerGroupNameAlreadyExists", ConsumerGroupNameAlreadyExistsCode, ErrConsumerGroupNameAlreadyExists}, + {"InvalidConsumerGroupName", InvalidConsumerGroupNameCode, ErrInvalidConsumerGroupName}, + {"ConsumerGroupMemberNotFound", ConsumerGroupMemberNotFoundCode, ErrConsumerGroupMemberNotFound}, + {"CannotCreateConsumerGroupInfo", CannotCreateConsumerGroupInfoCode, ErrCannotCreateConsumerGroupInfo}, + {"CannotDeleteConsumerGroupInfo", CannotDeleteConsumerGroupInfoCode, ErrCannotDeleteConsumerGroupInfo}, + {"MissingBaseOffsetRetainedMessageBatch", MissingBaseOffsetRetainedMessageBatchCode, ErrMissingBaseOffsetRetainedMessageBatch}, + {"MissingLastOffsetDeltaRetainedMessageBatch", MissingLastOffsetDeltaRetainedMessageBatchCode, ErrMissingLastOffsetDeltaRetainedMessageBatch}, + {"MissingMaxTimestampRetainedMessageBatch", MissingMaxTimestampRetainedMessageBatchCode, ErrMissingMaxTimestampRetainedMessageBatch}, + {"MissingLengthRetainedMessageBatch", MissingLengthRetainedMessageBatchCode, ErrMissingLengthRetainedMessageBatch}, + {"MissingPayloadRetainedMessageBatch", MissingPayloadRetainedMessageBatchCode, ErrMissingPayloadRetainedMessageBatch}, + {"CannotReadBatchBaseOffset", CannotReadBatchBaseOffsetCode, ErrCannotReadBatchBaseOffset}, + {"CannotReadBatchLength", CannotReadBatchLengthCode, ErrCannotReadBatchLength}, + {"CannotReadLastOffsetDelta", CannotReadLastOffsetDeltaCode, ErrCannotReadLastOffsetDelta}, + {"CannotReadMaxTimestamp", CannotReadMaxTimestampCode, ErrCannotReadMaxTimestamp}, + {"CannotReadBatchPayload", CannotReadBatchPayloadCode, ErrCannotReadBatchPayload}, + {"InvalidConnectionString", InvalidConnectionStringCode, ErrInvalidConnectionString}, + {"SnapshotFileCompletionFailed", SnapshotFileCompletionFailedCode, ErrSnapshotFileCompletionFailed}, + {"CannotSerializeResource", CannotSerializeResourceCode, ErrCannotSerializeResource}, + {"CannotDeserializeResource", CannotDeserializeResourceCode, ErrCannotDeserializeResource}, + {"CannotReadFile", CannotReadFileCode, ErrCannotReadFile}, + {"CannotReadFileMetadata", CannotReadFileMetadataCode, ErrCannotReadFileMetadata}, + {"CannotSeekFile", CannotSeekFileCode, ErrCannotSeekFile}, + {"CannotAppendToFile", CannotAppendToFileCode, ErrCannotAppendToFile}, + {"CannotWriteToFile", CannotWriteToFileCode, ErrCannotWriteToFile}, + {"CannotOverwriteFile", CannotOverwriteFileCode, ErrCannotOverwriteFile}, + {"CannotDeleteFile", CannotDeleteFileCode, ErrCannotDeleteFile}, + {"CannotSyncFile", CannotSyncFileCode, ErrCannotSyncFile}, + {"CannotReadIndexOffset", CannotReadIndexOffsetCode, ErrCannotReadIndexOffset}, + {"CannotReadIndexPosition", CannotReadIndexPositionCode, ErrCannotReadIndexPosition}, + {"CannotReadIndexTimestamp", CannotReadIndexTimestampCode, ErrCannotReadIndexTimestamp}, +} - for _, tt := range tests { +func TestFromCode_ReturnsCorrectErrors(t *testing.T) { + for _, tt := range allErrors { t.Run(tt.name, func(t *testing.T) { got := FromCode(tt.code) if got == nil { @@ -264,219 +264,13 @@ func TestFromCode_UnknownCode(t *testing.T) { } func TestCodeString_AllCodes(t *testing.T) { - allCodes := []Code{ - ErrorCode, - InvalidConfigurationCode, - InvalidCommandCode, - InvalidFormatCode, - FeatureUnavailableCode, - InvalidIdentifierCode, - InvalidVersionCode, - DisconnectedCode, - CannotEstablishConnectionCode, - CannotCreateBaseDirectoryCode, - CannotCreateRuntimeDirectoryCode, - CannotRemoveRuntimeDirectoryCode, - CannotCreateStateDirectoryCode, - StateFileNotFoundCode, - StateFileCorruptedCode, - InvalidStateEntryChecksumCode, - CannotOpenDatabaseCode, - ResourceNotFoundCode, - StaleClientCode, - TcpErrorCode, - QuicErrorCode, - InvalidServerAddressCode, - InvalidClientAddressCode, - InvalidIpAddressCode, - UnauthenticatedCode, - UnauthorizedCode, - InvalidCredentialsCode, - InvalidUsernameCode, - InvalidPasswordCode, - InvalidUserStatusCode, - UserAlreadyExistsCode, - UserInactiveCode, - CannotDeleteUserCode, - CannotChangePermissionsCode, - InvalidPersonalAccessTokenNameCode, - PersonalAccessTokenAlreadyExistsCode, - PersonalAccessTokensLimitReachedCode, - InvalidPersonalAccessTokenCode, - PersonalAccessTokenExpiredCode, - UsersLimitReachedCode, - NotConnectedCode, - ClientShutdownCode, - InvalidTlsDomainCode, - InvalidTlsCertificatePathCode, - InvalidTlsCertificateCode, - FailedToAddCertificateCode, - InvalidEncryptionKeyCode, - CannotEncryptDataCode, - CannotDecryptDataCode, - InvalidJwtAlgorithmCode, - InvalidJwtSecretCode, - JwtMissingCode, - CannotGenerateJwtCode, - AccessTokenMissingCode, - InvalidAccessTokenCode, - InvalidSizeBytesCode, - InvalidUtf8Code, - InvalidNumberEncodingCode, - InvalidBooleanValueCode, - InvalidNumberValueCode, - ClientNotFoundCode, - InvalidClientIdCode, - ConnectionClosedCode, - CannotParseHeaderKindCode, - HttpResponseErrorCode, - InvalidHttpRequestCode, - InvalidJsonResponseCode, - InvalidBytesResponseCode, - EmptyResponseCode, - CannotCreateEndpointCode, - CannotParseUrlCode, - CannotCreateStreamsDirectoryCode, - CannotCreateStreamDirectoryCode, - CannotCreateStreamInfoCode, - CannotUpdateStreamInfoCode, - CannotOpenStreamInfoCode, - CannotReadStreamInfoCode, - CannotCreateStreamCode, - CannotDeleteStreamCode, - CannotDeleteStreamDirectoryCode, - StreamIdNotFoundCode, - StreamNameNotFoundCode, - StreamIdAlreadyExistsCode, - StreamNameAlreadyExistsCode, - InvalidStreamNameCode, - InvalidStreamIdCode, - CannotReadStreamsCode, - InvalidTopicSizeCode, - CannotCreateTopicsDirectoryCode, - CannotCreateTopicDirectoryCode, - CannotCreateTopicInfoCode, - CannotUpdateTopicInfoCode, - CannotOpenTopicInfoCode, - CannotReadTopicInfoCode, - CannotCreateTopicCode, - CannotDeleteTopicCode, - CannotDeleteTopicDirectoryCode, - CannotPollTopicCode, - TopicIdNotFoundCode, - TopicNameNotFoundCode, - TopicIdAlreadyExistsCode, - TopicNameAlreadyExistsCode, - InvalidTopicNameCode, - TooManyPartitionsCode, - InvalidTopicIdCode, - CannotReadTopicsCode, - InvalidReplicationFactorCode, - CannotCreatePartitionCode, - CannotCreatePartitionsDirectoryCode, - CannotCreatePartitionDirectoryCode, - CannotOpenPartitionLogFileCode, - CannotReadPartitionsCode, - CannotDeletePartitionCode, - CannotDeletePartitionDirectoryCode, - PartitionNotFoundCode, - NoPartitionsCode, - TopicFullCode, - CannotDeleteConsumerOffsetsDirectoryCode, - CannotDeleteConsumerOffsetFileCode, - CannotCreateConsumerOffsetsDirectoryCode, - CannotReadConsumerOffsetsCode, - ConsumerOffsetNotFoundCode, - SegmentNotFoundCode, - SegmentClosedCode, - InvalidSegmentSizeCode, - CannotCreateSegmentLogFileCode, - CannotCreateSegmentIndexFileCode, - CannotCreateSegmentTimeIndexFileCode, - CannotSaveMessagesToSegmentCode, - CannotSaveIndexToSegmentCode, - CannotSaveTimeIndexToSegmentCode, - InvalidMessagesCountCode, - CannotAppendMessageCode, - CannotReadMessageCode, - CannotReadMessageIdCode, - CannotReadMessageStateCode, - CannotReadMessageTimestampCode, - CannotReadHeadersLengthCode, - CannotReadHeadersPayloadCode, - TooBigUserHeadersCode, - InvalidHeaderKeyCode, - InvalidHeaderValueCode, - CannotReadMessageLengthCode, - CannotReadMessagePayloadCode, - TooBigMessagePayloadCode, - TooManyMessagesCode, - EmptyMessagePayloadCode, - InvalidMessagePayloadLengthCode, - CannotReadMessageChecksumCode, - InvalidMessageChecksumCode, - InvalidKeyValueLengthCode, - CommandLengthErrorCode, - InvalidSegmentsCountCode, - NonZeroOffsetCode, - NonZeroTimestampCode, - MissingIndexCode, - InvalidIndexesByteSizeCode, - InvalidIndexesCountCode, - InvalidMessagesSizeCode, - TooSmallMessageCode, - CannotSendMessagesDueToClientDisconnectionCode, - BackgroundSendErrorCode, - BackgroundSendTimeoutCode, - BackgroundSendBufferFullCode, - BackgroundWorkerDisconnectedCode, - BackgroundSendBufferOverflowCode, - ProducerSendFailedCode, - ProducerClosedCode, - InvalidOffsetCode, - ConsumerGroupIdNotFoundCode, - ConsumerGroupIdAlreadyExistsCode, - InvalidConsumerGroupIdCode, - ConsumerGroupNameNotFoundCode, - ConsumerGroupNameAlreadyExistsCode, - InvalidConsumerGroupNameCode, - ConsumerGroupMemberNotFoundCode, - CannotCreateConsumerGroupInfoCode, - CannotDeleteConsumerGroupInfoCode, - MissingBaseOffsetRetainedMessageBatchCode, - MissingLastOffsetDeltaRetainedMessageBatchCode, - MissingMaxTimestampRetainedMessageBatchCode, - MissingLengthRetainedMessageBatchCode, - MissingPayloadRetainedMessageBatchCode, - CannotReadBatchBaseOffsetCode, - CannotReadBatchLengthCode, - CannotReadLastOffsetDeltaCode, - CannotReadMaxTimestampCode, - CannotReadBatchPayloadCode, - InvalidConnectionStringCode, - SnapshotFileCompletionFailedCode, - CannotSerializeResourceCode, - CannotDeserializeResourceCode, - CannotReadFileCode, - CannotReadFileMetadataCode, - CannotSeekFileCode, - CannotAppendToFileCode, - CannotWriteToFileCode, - CannotOverwriteFileCode, - CannotDeleteFileCode, - CannotSyncFileCode, - CannotReadIndexOffsetCode, - CannotReadIndexPositionCode, - CannotReadIndexTimestampCode, - } - - for _, code := range allCodes { - s := code.String() + for _, tt := range allErrors { + s := tt.code.String() if s == "" { - t.Fatalf("Code(%d).String() returned empty string", code) + t.Fatalf("Code(%d).String() returned empty string", tt.code) } if s == "Unknown error code" { - t.Fatalf("Code(%d).String() returned %q, expected a known name", code, s) + t.Fatalf("Code(%d).String() returned %q, expected a known name", tt.code, s) } } } @@ -505,216 +299,7 @@ func TestIs_DirectCall_AllTypes(t *testing.T) { Is(error) bool } - tests := []struct { - name string - err IggyError - }{ - {"Error", ErrError}, - {"InvalidConfiguration", ErrInvalidConfiguration}, - {"InvalidCommand", ErrInvalidCommand}, - {"InvalidFormat", ErrInvalidFormat}, - {"FeatureUnavailable", ErrFeatureUnavailable}, - {"InvalidIdentifier", ErrInvalidIdentifier}, - {"InvalidVersion", ErrInvalidVersion}, - {"Disconnected", ErrDisconnected}, - {"CannotEstablishConnection", ErrCannotEstablishConnection}, - {"CannotCreateBaseDirectory", ErrCannotCreateBaseDirectory}, - {"CannotCreateRuntimeDirectory", ErrCannotCreateRuntimeDirectory}, - {"CannotRemoveRuntimeDirectory", ErrCannotRemoveRuntimeDirectory}, - {"CannotCreateStateDirectory", ErrCannotCreateStateDirectory}, - {"StateFileNotFound", ErrStateFileNotFound}, - {"StateFileCorrupted", ErrStateFileCorrupted}, - {"InvalidStateEntryChecksum", ErrInvalidStateEntryChecksum}, - {"CannotOpenDatabase", ErrCannotOpenDatabase}, - {"ResourceNotFound", ErrResourceNotFound}, - {"StaleClient", ErrStaleClient}, - {"TcpError", ErrTcpError}, - {"QuicError", ErrQuicError}, - {"InvalidServerAddress", ErrInvalidServerAddress}, - {"InvalidClientAddress", ErrInvalidClientAddress}, - {"InvalidIpAddress", ErrInvalidIpAddress}, - {"Unauthenticated", ErrUnauthenticated}, - {"Unauthorized", ErrUnauthorized}, - {"InvalidCredentials", ErrInvalidCredentials}, - {"InvalidUsername", ErrInvalidUsername}, - {"InvalidPassword", ErrInvalidPassword}, - {"InvalidUserStatus", ErrInvalidUserStatus}, - {"UserAlreadyExists", ErrUserAlreadyExists}, - {"UserInactive", ErrUserInactive}, - {"CannotDeleteUser", ErrCannotDeleteUser}, - {"CannotChangePermissions", ErrCannotChangePermissions}, - {"InvalidPersonalAccessTokenName", ErrInvalidPersonalAccessTokenName}, - {"PersonalAccessTokenAlreadyExists", ErrPersonalAccessTokenAlreadyExists}, - {"PersonalAccessTokensLimitReached", ErrPersonalAccessTokensLimitReached}, - {"InvalidPersonalAccessToken", ErrInvalidPersonalAccessToken}, - {"PersonalAccessTokenExpired", ErrPersonalAccessTokenExpired}, - {"UsersLimitReached", ErrUsersLimitReached}, - {"NotConnected", ErrNotConnected}, - {"ClientShutdown", ErrClientShutdown}, - {"InvalidTlsDomain", ErrInvalidTlsDomain}, - {"InvalidTlsCertificatePath", ErrInvalidTlsCertificatePath}, - {"InvalidTlsCertificate", ErrInvalidTlsCertificate}, - {"FailedToAddCertificate", ErrFailedToAddCertificate}, - {"InvalidEncryptionKey", ErrInvalidEncryptionKey}, - {"CannotEncryptData", ErrCannotEncryptData}, - {"CannotDecryptData", ErrCannotDecryptData}, - {"InvalidJwtAlgorithm", ErrInvalidJwtAlgorithm}, - {"InvalidJwtSecret", ErrInvalidJwtSecret}, - {"JwtMissing", ErrJwtMissing}, - {"CannotGenerateJwt", ErrCannotGenerateJwt}, - {"AccessTokenMissing", ErrAccessTokenMissing}, - {"InvalidAccessToken", ErrInvalidAccessToken}, - {"InvalidSizeBytes", ErrInvalidSizeBytes}, - {"InvalidUtf8", ErrInvalidUtf8}, - {"InvalidNumberEncoding", ErrInvalidNumberEncoding}, - {"InvalidBooleanValue", ErrInvalidBooleanValue}, - {"InvalidNumberValue", ErrInvalidNumberValue}, - {"ClientNotFound", ErrClientNotFound}, - {"InvalidClientId", ErrInvalidClientId}, - {"ConnectionClosed", ErrConnectionClosed}, - {"CannotParseHeaderKind", ErrCannotParseHeaderKind}, - {"HttpResponseError", ErrHttpResponseError}, - {"InvalidHttpRequest", ErrInvalidHttpRequest}, - {"InvalidJsonResponse", ErrInvalidJsonResponse}, - {"InvalidBytesResponse", ErrInvalidBytesResponse}, - {"EmptyResponse", ErrEmptyResponse}, - {"CannotCreateEndpoint", ErrCannotCreateEndpoint}, - {"CannotParseUrl", ErrCannotParseUrl}, - {"CannotCreateStreamsDirectory", ErrCannotCreateStreamsDirectory}, - {"CannotCreateStreamDirectory", ErrCannotCreateStreamDirectory}, - {"CannotCreateStreamInfo", ErrCannotCreateStreamInfo}, - {"CannotUpdateStreamInfo", ErrCannotUpdateStreamInfo}, - {"CannotOpenStreamInfo", ErrCannotOpenStreamInfo}, - {"CannotReadStreamInfo", ErrCannotReadStreamInfo}, - {"CannotCreateStream", ErrCannotCreateStream}, - {"CannotDeleteStream", ErrCannotDeleteStream}, - {"CannotDeleteStreamDirectory", ErrCannotDeleteStreamDirectory}, - {"StreamIdNotFound", ErrStreamIdNotFound}, - {"StreamNameNotFound", ErrStreamNameNotFound}, - {"StreamIdAlreadyExists", ErrStreamIdAlreadyExists}, - {"StreamNameAlreadyExists", ErrStreamNameAlreadyExists}, - {"InvalidStreamName", ErrInvalidStreamName}, - {"InvalidStreamId", ErrInvalidStreamId}, - {"CannotReadStreams", ErrCannotReadStreams}, - {"InvalidTopicSize", ErrInvalidTopicSize}, - {"CannotCreateTopicsDirectory", ErrCannotCreateTopicsDirectory}, - {"CannotCreateTopicDirectory", ErrCannotCreateTopicDirectory}, - {"CannotCreateTopicInfo", ErrCannotCreateTopicInfo}, - {"CannotUpdateTopicInfo", ErrCannotUpdateTopicInfo}, - {"CannotOpenTopicInfo", ErrCannotOpenTopicInfo}, - {"CannotReadTopicInfo", ErrCannotReadTopicInfo}, - {"CannotCreateTopic", ErrCannotCreateTopic}, - {"CannotDeleteTopic", ErrCannotDeleteTopic}, - {"CannotDeleteTopicDirectory", ErrCannotDeleteTopicDirectory}, - {"CannotPollTopic", ErrCannotPollTopic}, - {"TopicIdNotFound", ErrTopicIdNotFound}, - {"TopicNameNotFound", ErrTopicNameNotFound}, - {"TopicIdAlreadyExists", ErrTopicIdAlreadyExists}, - {"TopicNameAlreadyExists", ErrTopicNameAlreadyExists}, - {"InvalidTopicName", ErrInvalidTopicName}, - {"TooManyPartitions", ErrTooManyPartitions}, - {"InvalidTopicId", ErrInvalidTopicId}, - {"CannotReadTopics", ErrCannotReadTopics}, - {"InvalidReplicationFactor", ErrInvalidReplicationFactor}, - {"CannotCreatePartition", ErrCannotCreatePartition}, - {"CannotCreatePartitionsDirectory", ErrCannotCreatePartitionsDirectory}, - {"CannotCreatePartitionDirectory", ErrCannotCreatePartitionDirectory}, - {"CannotOpenPartitionLogFile", ErrCannotOpenPartitionLogFile}, - {"CannotReadPartitions", ErrCannotReadPartitions}, - {"CannotDeletePartition", ErrCannotDeletePartition}, - {"CannotDeletePartitionDirectory", ErrCannotDeletePartitionDirectory}, - {"PartitionNotFound", ErrPartitionNotFound}, - {"NoPartitions", ErrNoPartitions}, - {"TopicFull", ErrTopicFull}, - {"CannotDeleteConsumerOffsetsDirectory", ErrCannotDeleteConsumerOffsetsDirectory}, - {"CannotDeleteConsumerOffsetFile", ErrCannotDeleteConsumerOffsetFile}, - {"CannotCreateConsumerOffsetsDirectory", ErrCannotCreateConsumerOffsetsDirectory}, - {"CannotReadConsumerOffsets", ErrCannotReadConsumerOffsets}, - {"ConsumerOffsetNotFound", ErrConsumerOffsetNotFound}, - {"SegmentNotFound", ErrSegmentNotFound}, - {"SegmentClosed", ErrSegmentClosed}, - {"InvalidSegmentSize", ErrInvalidSegmentSize}, - {"CannotCreateSegmentLogFile", ErrCannotCreateSegmentLogFile}, - {"CannotCreateSegmentIndexFile", ErrCannotCreateSegmentIndexFile}, - {"CannotCreateSegmentTimeIndexFile", ErrCannotCreateSegmentTimeIndexFile}, - {"CannotSaveMessagesToSegment", ErrCannotSaveMessagesToSegment}, - {"CannotSaveIndexToSegment", ErrCannotSaveIndexToSegment}, - {"CannotSaveTimeIndexToSegment", ErrCannotSaveTimeIndexToSegment}, - {"InvalidMessagesCount", ErrInvalidMessagesCount}, - {"CannotAppendMessage", ErrCannotAppendMessage}, - {"CannotReadMessage", ErrCannotReadMessage}, - {"CannotReadMessageId", ErrCannotReadMessageId}, - {"CannotReadMessageState", ErrCannotReadMessageState}, - {"CannotReadMessageTimestamp", ErrCannotReadMessageTimestamp}, - {"CannotReadHeadersLength", ErrCannotReadHeadersLength}, - {"CannotReadHeadersPayload", ErrCannotReadHeadersPayload}, - {"TooBigUserHeaders", ErrTooBigUserHeaders}, - {"InvalidHeaderKey", ErrInvalidHeaderKey}, - {"InvalidHeaderValue", ErrInvalidHeaderValue}, - {"CannotReadMessageLength", ErrCannotReadMessageLength}, - {"CannotReadMessagePayload", ErrCannotReadMessagePayload}, - {"TooBigMessagePayload", ErrTooBigMessagePayload}, - {"TooManyMessages", ErrTooManyMessages}, - {"EmptyMessagePayload", ErrEmptyMessagePayload}, - {"InvalidMessagePayloadLength", ErrInvalidMessagePayloadLength}, - {"CannotReadMessageChecksum", ErrCannotReadMessageChecksum}, - {"InvalidMessageChecksum", ErrInvalidMessageChecksum}, - {"InvalidKeyValueLength", ErrInvalidKeyValueLength}, - {"CommandLengthError", ErrCommandLengthError}, - {"InvalidSegmentsCount", ErrInvalidSegmentsCount}, - {"NonZeroOffset", ErrNonZeroOffset}, - {"NonZeroTimestamp", ErrNonZeroTimestamp}, - {"MissingIndex", ErrMissingIndex}, - {"InvalidIndexesByteSize", ErrInvalidIndexesByteSize}, - {"InvalidIndexesCount", ErrInvalidIndexesCount}, - {"InvalidMessagesSize", ErrInvalidMessagesSize}, - {"TooSmallMessage", ErrTooSmallMessage}, - {"CannotSendMessagesDueToClientDisconnection", ErrCannotSendMessagesDueToClientDisconnection}, - {"BackgroundSendError", ErrBackgroundSendError}, - {"BackgroundSendTimeout", ErrBackgroundSendTimeout}, - {"BackgroundSendBufferFull", ErrBackgroundSendBufferFull}, - {"BackgroundWorkerDisconnected", ErrBackgroundWorkerDisconnected}, - {"BackgroundSendBufferOverflow", ErrBackgroundSendBufferOverflow}, - {"ProducerSendFailed", ErrProducerSendFailed}, - {"ProducerClosed", ErrProducerClosed}, - {"InvalidOffset", ErrInvalidOffset}, - {"ConsumerGroupIdNotFound", ErrConsumerGroupIdNotFound}, - {"ConsumerGroupIdAlreadyExists", ErrConsumerGroupIdAlreadyExists}, - {"InvalidConsumerGroupId", ErrInvalidConsumerGroupId}, - {"ConsumerGroupNameNotFound", ErrConsumerGroupNameNotFound}, - {"ConsumerGroupNameAlreadyExists", ErrConsumerGroupNameAlreadyExists}, - {"InvalidConsumerGroupName", ErrInvalidConsumerGroupName}, - {"ConsumerGroupMemberNotFound", ErrConsumerGroupMemberNotFound}, - {"CannotCreateConsumerGroupInfo", ErrCannotCreateConsumerGroupInfo}, - {"CannotDeleteConsumerGroupInfo", ErrCannotDeleteConsumerGroupInfo}, - {"MissingBaseOffsetRetainedMessageBatch", ErrMissingBaseOffsetRetainedMessageBatch}, - {"MissingLastOffsetDeltaRetainedMessageBatch", ErrMissingLastOffsetDeltaRetainedMessageBatch}, - {"MissingMaxTimestampRetainedMessageBatch", ErrMissingMaxTimestampRetainedMessageBatch}, - {"MissingLengthRetainedMessageBatch", ErrMissingLengthRetainedMessageBatch}, - {"MissingPayloadRetainedMessageBatch", ErrMissingPayloadRetainedMessageBatch}, - {"CannotReadBatchBaseOffset", ErrCannotReadBatchBaseOffset}, - {"CannotReadBatchLength", ErrCannotReadBatchLength}, - {"CannotReadLastOffsetDelta", ErrCannotReadLastOffsetDelta}, - {"CannotReadMaxTimestamp", ErrCannotReadMaxTimestamp}, - {"CannotReadBatchPayload", ErrCannotReadBatchPayload}, - {"InvalidConnectionString", ErrInvalidConnectionString}, - {"SnapshotFileCompletionFailed", ErrSnapshotFileCompletionFailed}, - {"CannotSerializeResource", ErrCannotSerializeResource}, - {"CannotDeserializeResource", ErrCannotDeserializeResource}, - {"CannotReadFile", ErrCannotReadFile}, - {"CannotReadFileMetadata", ErrCannotReadFileMetadata}, - {"CannotSeekFile", ErrCannotSeekFile}, - {"CannotAppendToFile", ErrCannotAppendToFile}, - {"CannotWriteToFile", ErrCannotWriteToFile}, - {"CannotOverwriteFile", ErrCannotOverwriteFile}, - {"CannotDeleteFile", ErrCannotDeleteFile}, - {"CannotSyncFile", ErrCannotSyncFile}, - {"CannotReadIndexOffset", ErrCannotReadIndexOffset}, - {"CannotReadIndexPosition", ErrCannotReadIndexPosition}, - {"CannotReadIndexTimestamp", ErrCannotReadIndexTimestamp}, - } - - for _, tt := range tests { + for _, tt := range allErrors { t.Run(tt.name+"/self_match", func(t *testing.T) { checker, ok := tt.err.(isChecker) if !ok { From 46793c972de8a3079f556a71ad86b2bae84a31ec Mon Sep 17 00:00:00 2001 From: Atharva Lade Date: Fri, 27 Mar 2026 12:13:52 -0500 Subject: [PATCH 5/5] Retry CI